Класс Period в библиотеке js-joda
хранит период в виде трёх независимых компонентов:
years)months)days)В отличие от длительности (Duration), период работает с
календарными величинами, а не с точным количеством секунд или
миллисекунд.
Период может содержать значения, выходящие за привычные диапазоны:
const { Period } = require('@js-joda/core')
const period = Period.of(1, 15, 40)
console.log(period.toString())
// P1Y15M40D
Здесь:
15M — это 15 месяцев40D — это 40 днейПодобные значения допустимы, однако в ряде случаев требуется привести период к более компактному и нормализованному виду.
normalized()Для нормализации используется метод normalized().
Он преобразует месяцы в годы, если количество месяцев выходит за
диапазон 0–11.
const { Period } = require('@js-joda/core')
const period = Period.of(1, 15, 10)
const normalized = period.normalized()
console.log(normalized.toString())
// P2Y3M10D
Результат:
| Исходное значение | После нормализации |
|---|---|
| 1 год | 2 года |
| 15 месяцев | 3 месяца |
| 10 дней | 10 дней |
Алгоритм крайне простой:
общие месяцы = years * 12 + months
новые годы = общие месяцы / 12
новые месяцы = остаток от деления на 12
Дни при этом не изменяются.
const { Period } = require('@js-joda/core')
const period = Period.of(0, 28, 5)
console.log(period.normalized().toString())
// P2Y4M5D
Разбор:
28 месяцев = 2 года и 4 месяца
const { Period } = require('@js-joda/core')
const period = Period.of(3, -15, 0)
console.log(period.normalized().toString())
// P1Y9M
Расчёт:
3 года = 36 месяцев
36 - 15 = 21 месяц
21 месяц = 1 год 9 месяцев
Метод normalized() работает только с комбинацией:
Поле дней не преобразуется.
const { Period } = require('@js-joda/core')
const period = Period.of(0, 0, 75)
console.log(period.normalized().toString())
// P75D
Даже если количество дней значительно превышает длину месяца, преобразования не произойдёт.
Причина заключается в том, что месяц имеет переменную длину:
Автоматически определить эквивалентное количество месяцев невозможно без конкретной даты.
Рассмотрим пример:
30 дней
Это может означать:
Результат зависит от контекста календаря.
Например:
const { LocalDate, Period } = require('@js-joda/core')
const date = LocalDate.parse('2025-01-31')
console.log(date.plusDays(30).toString())
// 2025-03-02
Тридцать дней после 31 января — это уже март, а не конец февраля.
Поэтому библиотека сознательно не пытается автоматически переводить дни в месяцы.
Метод корректно работает и с полностью отрицательными значениями.
const { Period } = require('@js-joda/core')
const period = Period.of(-1, -14, 0)
console.log(period.normalized().toString())
// P-2Y-2M
Расчёт:
-1 год = -12 месяцев
-12 - 14 = -26 месяцев
-26 месяцев = -2 года -2 месяца
Период может содержать компоненты с разными знаками:
const { Period } = require('@js-joda/core')
const period = Period.of(1, -25, 0)
console.log(period.normalized().toString())
// P-1Y-1M
Разбор:
1 год = 12 месяцев
12 - 25 = -13 месяцев
-13 месяцев = -1 год -1 месяц
Это важная особенность: после нормализации знак периода может измениться.
Period является immutable-типом.
Метод normalized() не изменяет исходный объект.
const { Period } = require('@js-joda/core')
const original = Period.of(1, 15, 0)
const normalized = original.normalized()
console.log(original.toString())
// P1Y15M
console.log(normalized.toString())
// P2Y3M
Исходный объект остаётся прежним.
Перед сохранением периода в JSON или базу данных часто требуется компактное представление.
const period = Period.of(0, 36, 0)
console.log(period.normalized().toString())
// P3Y
Без нормализации одинаковые интервалы могут выглядеть по-разному.
const p1 = Period.of(1, 12, 0)
const p2 = Period.of(2, 0, 0)
Логически это одинаковые значения:
24 месяца
После нормализации:
console.log(p1.normalized().toString())
// P2Y
console.log(p2.normalized().toString())
// P2Y
Нормализованные периоды легче воспринимаются человеком.
Сравнение:
P5Y27M
и
P7Y3M
Второй вариант значительно понятнее.
После сложения или вычитания периодов часто появляются избыточные месяцы.
const { Period } = require('@js-joda/core')
const p1 = Period.of(1, 8, 0)
const p2 = Period.of(0, 9, 0)
const result = p1.plus(p2)
console.log(result.toString())
// P1Y17M
console.log(result.normalized().toString())
// P2Y5M
const { Period } = require('@js-joda/core')
const p1 = Period.of(5, 2, 0)
const p2 = Period.of(1, 8, 0)
const result = p1.minus(p2)
console.log(result.toString())
// P4Y-6M
console.log(result.normalized().toString())
// P3Y6M
normalized() от ручного пересчётаИногда выполняют пересчёт вручную:
const totalMonths = years * 12 + months
Однако normalized():
Строковое представление периода соответствует стандарту ISO-8601.
До нормализации:
P1Y15M
После нормализации:
P2Y3M
Обе записи валидны с точки зрения стандарта.
Нормализация влияет только на удобство представления.
const { Period } = require('@js-joda/core')
const period = Period.of(0, 0, 0)
console.log(period.normalized().toString())
// P0D
Нулевой период остаётся неизменным.
const { Period } = require('@js-joda/core')
const period = Period.of(0, 250, 0)
console.log(period.normalized().toString())
// P20Y10M
Нормализация особенно полезна при накопительных расчётах.
const { Period } = require('@js-joda/core')
const subscription = Period.of(2, 18, 0)
console.log(subscription.toString())
// P2Y18M
console.log(subscription.normalized().toString())
// P3Y6M
Нормализованное представление значительно лучше подходит для отображения в интерфейсе.
const { Period } = require('@js-joda/core')
const quarterly = Period.ofMonths(3)
let total = Period.ZERO
for (let i = 0; i < 10; i++) {
total = total.plus(quarterly)
}
console.log(total.toString())
// P30M
console.log(total.normalized().toString())
// P2Y6M
Без нормализации значение постепенно становится менее читаемым.
normalized()Метод не выполняет:
Для подобных задач требуется работа с LocalDate.
LocalDateПериод может вести себя по-разному в зависимости от даты применения.
const { LocalDate, Period } = require('@js-joda/core')
const period = Period.ofMonths(1)
const d1 = LocalDate.parse('2025-01-31')
const d2 = LocalDate.parse('2025-02-01')
console.log(d1.plus(period).toString())
// 2025-02-28
console.log(d2.plus(period).toString())
// 2025-03-01
Поэтому нормализация периода не пытается выполнять календарные преобразования дней.
Иногда полезно определить, содержит ли период избыточные месяцы.
const { Period } = require('@js-joda/core')
const period = Period.of(0, 18, 0)
const needsNormalization =
Math.abs(period.months()) >= 12
console.log(needsNormalization)
// true
const result =
p1.plus(p2).normalized()
console.log(period.normalized().toString())
Неправильно:
75 дней ≈ 2 месяца
Правильно:
LocalDateНеправильно:
Period.ofDays(40).normalized()
Ожидание:
P1M10D
Фактический результат:
P40D
Period.of(1, 12, 0)
Period.of(2, 0, 0)
Строки отличаются, хотя интервалы эквивалентны.
normalized() меняет объектНеправильно:
period.normalized()
console.log(period)
Метод возвращает новый объект.
Внутри Period хранит:
years
months
days
Нормализация не создаёт новую модель данных и не переводит период в абсолютную величину времени.
Это исключительно преобразование структуры хранения месяцев и лет.
DurationDuration нормализуется иначе.
Для Duration:
90 секунд = 1 минута 30 секунд
Для Period:
40 дней != 1 месяц 10 дней
Причина — различная природа календарного и точного времени.
normalized()| Компонент | Обрабатывается |
|---|---|
| Годы | Да |
| Месяцы | Да |
| Дни | Нет |
| Часы | Нет |
| Минуты | Нет |
| Секунды | Нет |
=years+months
years=,months=totalMonths