NodaTime для C# (.NET): полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

NodaTime для C# (.NET): полное руководство с примерами

Работа с датой и временем — одна из самых сложных и подверженных ошибкам задач в разработке программного обеспечения. Встроенные типы DateTime и DateTimeOffset в .NET имеют ряд фундаментальных проблем: путаница между местным и UTC временем, отсутствие явного разделения между "моментом во времени" и "календарной датой", неоднозначность при работе с часовыми поясами и летним временем. Библиотека NodaTime создана для решения этих проблем, предлагая разработчикам безопасный, выразительный и типобезопасный API для работы со временем.

NodaTime — это альтернативная библиотека для работы с датой и временем в .NET, вдохновленная библиотекой Joda-Time для Java. Она предоставляет четкое разделение между различными концепциями времени: Instant (момент во времени), LocalDateTime (дата и время без часового пояса), ZonedDateTime (дата и время с часовым поясом), Duration (промежуток времени в наносекундах) и Period (промежуток времени в календарных единицах). Библиотека использует данные часовых поясов от IANA (база tzdata), что делает её более точной и актуальной, чем встроенные механизмы Windows.

Основная цель NodaTime — сделать неверные операции с временем невозможными на уровне компиляции. Например, вы не сможете случайно сложить LocalDateTime с Duration (потому что это не имеет смысла без часового пояса), или сравнить Instant с LocalDate. Библиотека также предоставляет мощные возможности для форматирования, парсинга, работы с часовыми поясами и календарными системами (включая юлианский, исламский и еврейский календари).

Установка

Для установки NodaTime в проект .NET используйте NuGet Package Manager. Выполните одну из следующих команд:

# Через .NET CLI (рекомендуется)
dotnet add package NodaTime

# Через Package Manager Console (Visual Studio)
Install-Package NodaTime

# Или через NuGet Package Manager UI: ищите "NodaTime"

После установки добавьте директиву using NodaTime; в начало вашего файла. Для работы с часовыми поясами также может понадобиться using NodaTime.TimeZones;.

Быстрый старт

Минимальный рабочий пример, демонстрирующий основные возможности NodaTime:

using NodaTime;
using NodaTime.Text;

// 1. Текущий момент времени в UTC
Instant now = SystemClock.Instance.GetCurrentInstant();
Console.WriteLine($"Текущий момент (UTC): {now}");

// 2. Конвертация в местное время (Москва)
DateTimeZone moscowZone = DateTimeZoneProviders.Tzdb["Europe/Moscow"];
ZonedDateTime moscowTime = now.InZone(moscowZone);
Console.WriteLine($"Время в Москве: {moscowTime}");

// 3. Создание конкретной даты и времени
LocalDateTime localDate = new LocalDateTime(2024, 12, 25, 10, 30, 0);
Console.WriteLine($"Локальная дата: {localDate}");

// 4. Парсинг строки
var pattern = LocalDateTimePattern.CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss");
LocalDateTime parsed = pattern.Parse("2024-12-25 10:30:00").Value;
Console.WriteLine($"Распарсено: {parsed}");

// 5. Работа с длительностями
Duration duration = Duration.FromHours(2.5);
Instant future = now + duration;
Console.WriteLine($"Через 2.5 часа: {future.InZone(moscowZone)}");

Основные классы и методы

1. Instant — момент во времени

Сигнатура: public readonly struct Instant : IEquatable<Instant>, IComparable<Instant>

Что делает: Представляет точку на временной шкале, измеряемую в наносекундах от эпохи Unix (1 января 1970 года, 00:00:00 UTC). Не зависит от часовых поясов.

Instant epoch = Instant.FromUnixTimeTicks(0); // Эпоха Unix
Instant now = SystemClock.Instance.GetCurrentInstant();
Instant fromTicks = Instant.FromUnixTimeTicks(DateTimeOffset.UtcNow.Ticks);

// Арифметика
Duration oneHour = Duration.FromHours(1);
Instant later = now + oneHour;
TimeSpan diff = later - now; // Duration можно конвертировать в TimeSpan
Console.WriteLine($"Разница: {diff.TotalMinutes} минут");

2. LocalDateTime — локальная дата и время

Сигнатура: public readonly struct LocalDateTime : IEquatable<LocalDateTime>, IComparable<LocalDateTime>

Что делает: Представляет дату и время без привязки к часовому поясу. Идеально подходит для "календарных" событий, например, "встреча в 15:00 25 декабря".

// Создание
LocalDateTime meeting = new LocalDateTime(2024, 12, 25, 15, 0, 0);
LocalDateTime today = LocalDateTime.FromDateTime(DateTime.Now); // Из DateTime

// Компоненты
int year = meeting.Year;
int month = meeting.Month;
int day = meeting.Day;
int hour = meeting.Hour;
DayOfWeek dayOfWeek = meeting.DayOfWeek;

// Арифметика (Period)
Period oneWeek = Period.FromWeeks(1);
LocalDateTime nextWeek = meeting + oneWeek;
Console.WriteLine($"Следующая неделя: {nextWeek}");

// Сравнение
bool isBefore = meeting < nextWeek; // true

3. ZonedDateTime — дата и время с часовым поясом

Сигнатура: public readonly struct ZonedDateTime : IEquatable<ZonedDateTime>

Что делает: Объединяет LocalDateTime с DateTimeZone, представляя точный момент во времени с учётом часового пояса и правил перехода на летнее/зимнее время.

DateTimeZone zone = DateTimeZoneProviders.Tzdb["America/New_York"];
ZonedDateTime nyTime = new ZonedDateTime(
    new LocalDateTime(2024, 3, 10, 2, 30, 0), // Переход на летнее время
    zone,
    Offset.FromHours(-5) // Явное указание смещения для разрешения неоднозначности
);

// Конвертация в другой часовой пояс
DateTimeZone londonZone = DateTimeZoneProviders.Tzdb["Europe/London"];
ZonedDateTime londonTime = nyTime.WithZone(londonZone);

// Получение Instant
Instant instant = nyTime.ToInstant();

// Обработка неоднозначного времени (переход на летнее время)
ZonedDateTime ambiguous = zone.AtStrictly(new LocalDateTime(2024, 11, 3, 1, 30, 0)); // Выбросит исключение
ZonedDateTime resolved = zone.AtLeniently(new LocalDateTime(2024, 11, 3, 1, 30, 0)); // Выберет раннее смещение

4. Duration — длительность в наносекундах

Сигнатура: public readonly struct Duration : IEquatable<Duration>, IComparable<Duration>

Что делает: Представляет фиксированный промежуток времени, измеряемый в наносекундах. Не зависит от календаря (1 час всегда равен 3600 секундам).

Duration twoHours = Duration.FromHours(2);
Duration thirtyMinutes = Duration.FromMinutes(30);
Duration total = twoHours + thirtyMinutes; // 2.5 часа

// Конвертация
TimeSpan ts = total.ToTimeSpan();
double totalSeconds = total.TotalSeconds; // 9000
long totalNanoseconds = total.TotalNanoseconds; // 9000000000000

// Сравнение с Period
Duration oneDayDuration = Duration.FromDays(1); // Всегда 24 часа
Period oneDayPeriod = Period.FromDays(1); // Может быть 23, 24 или 25 часов при переходе на летнее время

5. Period — период в календарных единицах

Сигнатура: public sealed class Period : IEquatable<Period>

Что делает: Представляет промежуток времени в "человеческих" единицах: годах, месяцах, днях, часах, минутах и секундах. Учитывает календарные особенности (разную длину месяцев, високосные годы).

Period oneMonth = Period.FromMonths(1);
Period twoYearsThreeMonths = new PeriodBuilder { Years = 2, Months = 3 }.Build();

// Применение к LocalDateTime
LocalDateTime start = new LocalDateTime(2024, 1, 31, 0, 0, 0);
LocalDateTime result = start + oneMonth; // 2024-02-29 (високосный год!)

// Разница между двумя датами
LocalDateTime end = new LocalDateTime(2024, 12, 25, 0, 0, 0);
Period difference = Period.Between(start, end, PeriodUnits.YearMonthDay);
Console.WriteLine($"Разница: {difference.Years} лет, {difference.Months} месяцев, {difference.Days} дней");

6. DateTimeZone — часовой пояс

Сигнатура: public abstract class DateTimeZone : IEquatable<DateTimeZone>

Что делает: Представляет часовой пояс на основе базы данных IANA (tzdata). Позволяет получать смещения, обрабатывать переходы на летнее время и конвертировать между поясами.

// Получение всех доступных поясов
var allZones = DateTimeZoneProviders.Tzdb.GetAllZones();

// Поиск пояса по городу
DateTimeZone tokyo = DateTimeZoneProviders.Tzdb["Asia/Tokyo"];

// Получение смещения для конкретного момента
Instant now = SystemClock.Instance.GetCurrentInstant();
Offset offset = tokyo.GetUtcOffset(now);
Console.WriteLine($"Текущее смещение Токио: {offset}");

// Проверка, действует ли летнее время
bool isDaylight = tokyo.IsDaylightSavingTime(now);

// История изменений смещения
var intervals = tokyo.GetZoneIntervals(
    new Interval(
        Instant.FromUnixTimeTicks(0),
        SystemClock.Instance.GetCurrentInstant()
    )
);
foreach (var interval in intervals)
{
    Console.WriteLine($"{interval.Start} - {interval.End}: {interval.StandardOffset} / {interval.WallOffset}");
}

7. Offset — смещение от UTC

Сигнатура: public readonly struct Offset : IEquatable<Offset>, IComparable<Offset>

Что делает: Представляет фиксированное смещение от UTC в часах, минутах и секундах. Используется внутри ZonedDateTime и OffsetDateTime.

Offset utc = Offset.Zero;
Offset moscowWinter = Offset.FromHours(3);
Offset nepal = Offset.FromHoursAndMinutes(5, 45); // UTC+5:45

// Создание OffsetDateTime
OffsetDateTime odt = new OffsetDateTime(
    new LocalDateTime(2024, 12, 25, 10, 0, 0),
    moscowWinter
);

// Конвертация
Instant instant = odt.ToInstant();
DateTimeOffset dto = odt.ToDateTimeOffset();

// Сравнение
bool isEqual = moscowWinter == Offset.FromHours(3); // true

8. DateInterval — интервал дат

Сигнатура: public sealed class DateInterval : IEquatable<DateInterval>

Что делает: Представляет диапазон между двумя датами (включительно с обеих сторон). Полезен для проверки попадания даты в диапазон и итерации по дням.

LocalDate start = new LocalDate(2024, 1, 1);
LocalDate end = new LocalDate(2024, 12, 31);
DateInterval year2024 = new DateInterval(start, end);

// Проверка вхождения
LocalDate testDate = new LocalDate(2024, 6, 15);
bool contains = year2024.Contains(testDate); // true

// Итерация по дням
foreach (LocalDate day in year2024)
{
    if (day.DayOfWeek == IsoDayOfWeek.Saturday || day.DayOfWeek == IsoDayOfWeek.Sunday)
    {
        Console.WriteLine($"Выходной: {day}");
    }
    // Осторожно: 366 итераций для високосного года!
}

// Длина интервала
int days = year2024.Length; // 366 (високосный год)

9. LocalDate — только дата

Сигнатура: public readonly struct LocalDate : IEquatable<LocalDate>, IComparable<LocalDate>

Что делает: Представляет дату без времени и часового пояса. Идеально для дней рождения, праздников и других "календарных" событий.

LocalDate today = LocalDate.FromDateTime(DateTime.Today);
LocalDate birthday = new LocalDate(1990, 5, 15);

// Сравнение
bool isBirthday = today == birthday.WithYear(today.Year);

// Добавление месяцев/лет
LocalDate nextMonth = today.PlusMonths(1);
LocalDate sameDayNextYear = today.PlusYears(1);

// День недели
IsoDayOfWeek dayOfWeek = today.DayOfWeek;

// Последний день месяца
LocalDate lastDayOfMonth = today.With(DateAdjusters.EndOfMonth);

10. LocalTime — только время

Сигнатура: public readonly struct LocalTime : IEquatable<LocalTime>, IComparable<LocalTime>

Что делает: Представляет время суток без даты. Полезен для расписаний, графиков работы и других повторяющихся событий.

LocalTime opening = new LocalTime(9, 0, 0); // 09:00:00
LocalTime closing = new LocalTime(18, 0, 0); // 18:00:00

// Проверка рабочего времени
LocalTime now = LocalTime.FromDateTime(DateTime.Now);
bool isOpen = now >= opening && now < closing;

// Добавление времени
LocalTime lunchBreak = opening.PlusHours(4); // 13:00

// Сравнение
bool isMorning = now < new LocalTime(12, 0, 0                    

Рекомендаций