Работа с датами в JavaScript традиционно сопровождается множеством скрытых проблем. Одна из самых опасных категорий ошибок связана с високосными годами. Неправильная обработка 29 февраля приводит к сбоям в расчётах периодов, возраста, дедлайнов, подписок, финансовых отчётов и расписаний.
Библиотека js-joda реализует API, вдохновлённый Java Time API из Java 8, и предоставляет строгую, предсказуемую и безопасную модель работы с календарными датами.
В григорианском календаре:
Примеры:
| Год | Високосный |
|---|---|
| 2024 | Да |
| 2025 | Нет |
| 1900 | Нет |
| 2000 | Да |
В стандартном Date JavaScript подобные детали часто
скрыты внутри встроенной реализации браузера или движка Node.js. В
js-joda календарные вычисления работают строго и
прозрачно.
Для работы с календарными правилами используется класс
Year.
const { Year } = require('@js-joda/core');
console.log(Year.of(2024).isLeap()); // true
console.log(Year.of(2025).isLeap()); // false
console.log(Year.of(1900).isLeap()); // false
console.log(Year.of(2000).isLeap()); // true
Метод isLeap() реализует все правила григорианского
календаря.
Если год високосный, дата создаётся корректно:
const { LocalDate } = require('@js-joda/core');
const date = LocalDate.of(2024, 2, 29);
console.log(date.toString());
Результат:
2024-02-29
Попытка создать 29 февраля в невисокосном году вызывает исключение.
const { LocalDate } = require('@js-joda/core');
const date = LocalDate.of(2023, 2, 29);
Ошибка:
DateTimeException: Invalid date 'February 29' as '2023' is not a leap year
Это важное отличие от встроенного Date, который может
автоматически “перепрыгнуть” на март и скрыть проблему.
Стандартный Jav * aScript:
const date = new Date(2023, 1, 29);
console.log(date);
Результат:
Wed Mar 01 2023
Ошибка остаётся незамеченной.
js-joda использует строгую модель:
Одна из самых сложных ситуаций — перенос даты на следующий год.
const { LocalDate } = require('@js-joda/core');
const date = LocalDate.of(2024, 2, 29);
const nextYear = date.plusYears(1);
console.log(nextYear.toString());
Результат:
2025-02-28
js-joda автоматически корректирует дату до последнего
допустимого дня месяца.
plusYears()Правила работы метода:
| Исходная дата | Результат |
|---|---|
| 2024-02-29 + 1 год | 2025-02-28 |
| 2020-02-29 + 4 года | 2024-02-29 |
| 2024-02-29 - 1 год | 2023-02-28 |
Пример:
const leap = LocalDate.of(2020, 2, 29);
console.log(leap.plusYears(4).toString());
console.log(leap.plusYears(1).toString());
console.log(leap.minusYears(1).toString());
Метод plusMonths() также учитывает календарные
ограничения.
const date = LocalDate.of(2024, 1, 31);
console.log(date.plusMonths(1).toString());
Результат:
2024-02-29
Для невисокосного года:
const date = LocalDate.of(2023, 1, 31);
console.log(date.plusMonths(1).toString());
Результат:
2023-02-28
Определение количества дней в месяце особенно важно в финансовых системах и системах бронирования.
const { YearMonth } = require('@js-joda/core');
console.log(YearMonth.of(2024, 2).lengthOfMonth());
console.log(YearMonth.of(2023, 2).lengthOfMonth());
Результат:
29
28
const { Year } = require('@js-joda/core');
console.log(Year.of(2024).length());
console.log(Year.of(2023).length());
Результат:
366
365
Иногда дата приходит извне:
Безопасная проверка:
const { LocalDate } = require('@js-joda/core');
function isValidDate(year, month, day) {
try {
LocalDate.of(year, month, day);
return true;
} catch {
return false;
}
}
console.log(isValidDate(2024, 2, 29));
console.log(isValidDate(2023, 2, 29));
Это одна из самых известных проблем календарной логики.
const { LocalDate, Period } = require('@js-joda/core');
const birthDate = LocalDate.of(2000, 2, 29);
const currentDate = LocalDate.of(2025, 2, 28);
const age = Period.between(birthDate, currentDate);
console.log(age.years());
Результат:
24
На следующий день:
const currentDate = LocalDate.of(2025, 3, 1);
Возраст станет:
25
Нельзя считать год равным фиксированному количеству дней.
Ошибка:
const days = 365 * 5;
Проблема:
Правильный подход:
const start = LocalDate.of(2020, 1, 1);
const end = start.plusYears(5);
console.log(end.toString());
Period и DurationВисокосные годы особенно хорошо демонстрируют различие между календарными и временными интервалами.
PeriodРаботает с:
Учитывает календарь.
const period = Period.ofYears(1);
DurationРаботает с:
Не учитывает календарные особенности.
const { Duration } = require('@js-joda/core');
const duration = Duration.ofDays(365);
Duration вместо Periodconst {
LocalDateTime,
Duration
} = require('@js-joda/core');
const date = LocalDateTime.parse('2024-02-29T10:00');
const result = date.plus(Duration.ofDays(365));
console.log(result.toString());
Результат:
2025-02-28T10:00
Но:
console.log(
date.plusYears(1).toString()
);
Результат:
2025-02-28T10:00
В данном случае результат совпадает, но в более длинных интервалах различия становятся критичными.
Неверная реализация:
function isLeap(year) {
return year % 4 === 0;
}
Ошибка:
console.log(isLeap(1900));
Результат:
true
Правильная реализация:
function isLeap(year) {
return (
(year % 4 === 0 && year % 100 !== 0) ||
year % 400 === 0
);
}
Однако в js-joda нет необходимости писать такую логику
вручную.
TemporalAdjustersЧастая задача — получить последний день февраля независимо от года.
const {
LocalDate,
TemporalAdjusters
} = require('@js-joda/core');
const date = LocalDate.of(2024, 2, 10);
const lastDay = date.with(
TemporalAdjusters.lastDayOfMonth()
);
console.log(lastDay.toString());
Результат:
2024-02-29
Для 2023:
2023-02-28
Поскольку LocalDate является immutable-объектом и
использует строгую календарную модель, сортировка выполняется
корректно.
const dates = [
LocalDate.of(2024, 2, 29),
LocalDate.of(2023, 2, 28),
LocalDate.of(2025, 1, 1)
];
dates.sort((a, b) => a.compareTo(b));
console.log(dates);
const {
LocalDate,
ChronoUnit
} = require('@js-joda/core');
const start = LocalDate.of(2024, 1, 1);
const end = LocalDate.of(2025, 1, 1);
const days = ChronoUnit.DAYS.between(start, end);
console.log(days);
Результат:
366
Для невисокосного года:
365
Проблемная логика:
const expiration = created.plusDays(365);
Ошибка:
Правильный подход:
const expiration = created.plusYears(1);
Проблемы часто возникают в системах:
Пример:
const paymentDate = LocalDate.of(2024, 2, 29);
console.log(
paymentDate.plusMonths(1).toString()
);
Результат:
2024-03-29
Но:
console.log(
paymentDate.plusYears(1).toString()
);
Результат:
2025-02-28
Все объекты js-joda неизменяемы.
const original = LocalDate.of(2024, 2, 29);
const modified = original.plusYears(1);
console.log(original.toString());
console.log(modified.toString());
Результат:
2024-02-29
2025-02-28
Исходный объект не изменяется.
LocalDate для календарных датconst date = LocalDate.of(2024, 2, 29);
Плохо:
365 * years
Хорошо:
date.plusYears(years)
Date для сложной бизнес-логикиПричины:
Плохо:
year % 4 === 0
Хорошо:
Year.of(year).isLeap()
| Тип | Назначение |
|---|---|
Period |
Годы, месяцы, дни |
Duration |
Секунды и время |
| Ошибка | Последствие |
|---|---|
plusDays(365) вместо plusYears(1) |
Смещение даты |
| Ручная проверка високосности | Логические ошибки |
Использование Date |
Скрытая коррекция |
| Игнорирование 29 февраля | Сбой расчётов |
| Хранение года как 365 дней | Накопление ошибки |
| Использование timestamp для календарных вычислений | Потеря точности |
const {
LocalDate
} = require('@js-joda/core');
function renewSubscription(startDate) {
return startDate.plusYears(1);
}
const created = LocalDate.of(2024, 2, 29);
const renewed = renewSubscription(created);
console.log(renewed.toString());
Результат:
2025-02-28
| Задача | Date |
js-joda |
|---|---|---|
| Создание 2023-02-29 | Автокоррекция | Исключение |
| Проверка високосности | Вручную | isLeap() |
| Immutable API | Нет | Да |
| Календарная арифметика | Нестабильна | Предсказуема |
| Работа с периодами | Ограничена | Полноценная |
| Безопасность бизнес-логики | Низкая | Высокая |