Нормализация Duration

В Luxon Duration представляет собой структуру для работы с длительностями времени, которая отличается от абсолютных моментов времени (DateTime) тем, что оперирует относительными единицами: годами, месяцами, днями, часами, минутами, секундами и миллисекундами. Ключевая сложность при работе с Duration заключается в неоднородности единиц измерения и их неоднозначной взаимной конвертации.

Внутреннее представление Duration в Luxon основано на наборе полей:

  • years
  • months
  • weeks
  • days
  • hours
  • minutes
  • seconds
  • milliseconds

Эти единицы не являются взаимно однозначно конвертируемыми. Например, месяц может содержать 28, 29, 30 или 31 день, а год — 365 или 366 дней. Из-за этого Duration хранит значения раздельно, не приводя их автоматически к единому масштабу.

Проблема возникает при необходимости:

  • сравнения длительностей
  • сложения и вычитания
  • сериализации
  • преобразования в человекочитаемую форму
  • приведения к единой шкале измерения

Для этого используется нормализация.

Суть нормализации Duration

Нормализация Duration — это процесс приведения разрозненных единиц к согласованному виду по правилам Luxon. Основная идея заключается в перераспределении значений между полями так, чтобы:

  • меньшие единицы корректно «перетекали» в большие и наоборот
  • не возникало переполнений (например, 90 секунд → 1 минута 30 секунд)
  • сохранялась согласованность внутреннего состояния

Важный момент: нормализация не означает преобразование всего Duration в одну единицу (например, в миллисекунды). Это именно структурное выравнивание полей.

Метод normalize()

Метод normalize() выполняет приведение Duration к каноническому виду, перераспределяя значения между полями в пределах их естественных диапазонов.

Пример базового поведения:

import { Duration } from "luxon";

const d = Duration.fromObject({
  hours: 1,
  minutes: 120,
  seconds: 90
});

const normalized = d.normalize();

После нормализации:

  • 120 minutes → 2 hours
  • 90 seconds → 1 minute 30 seconds

Итоговая структура становится:

{
  hours: 3,
  minutes: 1,
  seconds: 30
}

normalize() не изменяет смысл Duration, но приводит его к компактному и согласованному виду.

Принципы перераспределения значений

Нормализация в Luxon выполняется по каскадному принципу:

  1. Миллисекунды переносятся в секунды
  2. Секунды в минуты
  3. Минуты в часы
  4. Часы в дни
  5. Дни могут переходить в недели
  6. Недели в месяцы (в зависимости от контекста календаря)

Однако важно учитывать: переходы через месяцы и годы не всегда выполняются линейно, поскольку длина месяца и года зависит от конкретной даты, если она задана в контексте DateTime.

Ограниченность полной конверсии

Duration не всегда может быть полностью нормализован в строгом математическом смысле. Например:

  • 1 месяц ≠ фиксированное количество дней
  • 1 год ≠ фиксированное количество дней

Поэтому Luxon разделяет:

  • фиксированную нормализацию (seconds → minutes → hours)
  • календарную нормализацию (months → years, days → months)

Календарная нормализация требует привязки к конкретной временной точке.

shiftTo и отличие от normalize

Метод shiftTo выполняет более агрессивную трансформацию: он приводит Duration к заданному набору единиц, перераспределяя всё в указанные поля.

const d = Duration.fromObject({
  hours: 2,
  minutes: 120,
  seconds: 90
});

const shifted = d.shiftTo("hours", "minutes", "seconds");

В отличие от normalize():

  • normalize сохраняет структуру и исправляет переполнения
  • shiftTo принудительно перестраивает представление под выбранные единицы

Если normalize — это «очистка», то shiftTo — «пересборка».

rescale и плотная нормализация

Метод rescale применяется для приведения Duration к минимальному набору единиц без избыточных полей.

const d = Duration.fromObject({
  hours: 1,
  minutes: 120,
  seconds: 90
});

const rescaled = d.rescale();

После rescale результат может быть представлен, например, так:

{
  hours: 3,
  minutes: 1,
  seconds: 30
}

Отличие от normalize заключается в том, что rescale стремится максимально «сжать» представление, устраняя избыточные единицы.

Нормализация отрицательных значений

Duration может содержать отрицательные значения:

const d = Duration.fromObject({
  minutes: -90,
  seconds: -30
});

При нормализации возникает необходимость согласованного переноса отрицательных значений между единицами. Luxon обрабатывает такие случаи по правилам:

  • отрицательные секунды заимствуют из минут
  • отрицательные минуты заимствуют из часов
  • знак сохраняется консистентно

Результатом становится структурно корректная отрицательная длительность, а не набор разрозненных отрицательных полей.

Влияние floating-point и погрешностей

При работе с миллисекундами и преобразованиями между единицами может возникать накопление ошибок IEEE 754:

  • 0.1 + 0.2 ≠ 0.3
  • дробные секунды могут давать остатки в миллисекундах

Нормализация частично компенсирует такие эффекты, перераспределяя остатки в младшие единицы, но не устраняет фундаментальные ограничения числового представления.

Нормализация и toHuman()

Метод toHuman() использует нормализованное представление Duration для формирования строки:

Duration.fromObject({
  minutes: 90
}).normalize().toHuman();

Без нормализации результат может быть менее предсказуемым, поскольку поля могут содержать переполненные значения.

Сравнение Duration без нормализации

Сравнение двух Duration без предварительной нормализации может давать некорректные результаты:

const a = Duration.fromObject({ hours: 1, minutes: 60 });
const b = Duration.fromObject({ hours: 2 });

a.equals(b); // может быть false

После normalize:

a.normalize().equals(b.normalize()); // true

Причина в том, что внутреннее представление не гарантирует каноническую форму.

Практические сценарии применения нормализации

Нормализация становится критичной в следующих задачах:

  • агрегация временных интервалов (логирование, аналитика)
  • построение таймеров и расписаний
  • преобразование пользовательского ввода (например, «90 минут»)
  • сериализация в API
  • визуализация длительностей

Особенно важна нормализация при смешанных единицах, где пользовательские данные могут поступать в произвольной форме.

Особенности поведения при календарных единицах

При наличии месяцев и лет нормализация зависит от контекста DateTime:

  • 1 месяц может быть интерпретирован по-разному
  • добавление месяцев требует знания базовой даты
  • normalize не всегда выполняет «полное» сведение в дни

Это ограничение принципиально связано с календарной природой времени.

Структурная роль нормализации

Normalize выполняет функцию приведения Duration к стабильному внутреннему виду, пригодному для:

  • детерминированных вычислений
  • корректного отображения
  • безопасного сравнения
  • повторного преобразования между форматами

Без нормализации Duration остаётся «сырой» структурой, чувствительной к способу конструирования и порядку операций.