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