Выбор единиц измерения

В библиотеке Luxon выбор единиц измерения определяется тем, как именно представляется время: как абсолютное количество миллисекунд или как календарная величина, зависящая от контекста. Архитектура работы с временем в Luxon строится вокруг двух уровней — машинного времени и календарных единиц, и различие между ними критично для корректного моделирования дат и длительностей.

В основе временных вычислений лежат абсолютные единицы, которые однозначно интерпретируются в любой системе отсчёта:

  • миллисекунды (ms)
  • секунды (s)
  • минуты (m)
  • часы (h)
  • дни (d)

Эти единицы линейны и не зависят от календаря. Например, 1 день всегда равен 86 400 000 миллисекунд в абстрактном времени, если не учитывать переходы на летнее время.

Противоположностью являются календарные единицы:

  • недели (w)
  • месяцы (M)
  • годы (y)

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

Представление длительностей

В Luxon длительности представлены через объект Duration, который позволяет явно задавать единицы измерения:

import { Duration } from 'luxon';

const d1 = Duration.fromObject({ hours: 2, minutes: 30 });
const d2 = Duration.fromObject({ days: 1, hours: 6 });
const d3 = Duration.fromObject({ months: 2, weeks: 1 });

Каждый объект хранит набор полей-единиц, а не единую числовую величину. Это фундаментальное отличие от простого хранения времени в миллисекундах.

Приоритет единиц и нормализация

При работе с несколькими единицами Luxon выполняет нормализацию, если требуется привести длительность к каноническому виду:

const dur = Duration.fromObject({
  seconds: 90,
  minutes: 2
}).normalize();

Результат нормализации:

  • 90 секунд → 1 минута 30 секунд
  • итоговая структура становится согласованной

Нормализация важна при смешивании единиц разного уровня, особенно при последующих операциях сравнения или форматирования.

Преобразование единиц через shiftTo

Метод shiftTo позволяет пересчитать длительность в выбранные единицы, перераспределяя значение:

const d = Duration.fromObject({ seconds: 3661 });

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

Здесь происходит разложение:

  • 3661 секунда → 1 час, 1 минута, 1 секунда

Важно, что shiftTo не изменяет смысл времени, а лишь перераспределяет его между единицами.

Получение значения в конкретной единице

Для извлечения числового значения используется метод as:

const d = Duration.fromObject({ minutes: 3 });

d.as('seconds'); // 180
d.as('hours');    // 0.05

as возвращает эквивалент в выбранной единице, приводя все внутренние поля к единой шкале.

Потеря точности при преобразованиях

При переходе от крупных единиц к мелким и обратно возникает эффект округления:

  • месяцы → дни: зависит от конкретного месяца
  • годы → дни: зависит от високосности
  • недели → дни: фиксированное соотношение

Поэтому преобразования через календарные единицы всегда контекстно зависимы.

Выбор единиц в операциях diff

Метод diff в Luxon возвращает разницу между датами с указанием единиц:

import { DateTime } from 'luxon';

const a = DateTime.fromISO('2024-01-01');
const b = DateTime.fromISO('2024-02-01');

b.diff(a, 'days').days;
b.diff(a, 'months').months;

Выбор единицы влияет на результат:

  • days — точная разница в сутках
  • months — календарная разница, зависящая от структуры месяцев

При множественных единицах:

b.diff(a, ['days', 'hours']);

формируется составная длительность.

Миллисекунды как базовый слой

Любая длительность в конечном счёте может быть сведена к миллисекундам:

const d = Duration.fromObject({ minutes: 1 });

d.toMillis(); // 60000

Однако обратное преобразование из миллисекунд в календарные единицы неоднозначно:

  • 2 592 000 000 ms могут означать 30 или 31 день в зависимости от контекста

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

  • абсолютные единицы → точные преобразования
  • календарные единицы → контекстные вычисления

ISO-формат и единицы измерения

При сериализации используется ISO 8601:

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

d.toISO(); // PT1H15M

ISO-формат сохраняет структуру единиц, а не их числовое сведение. Это позволяет избежать неоднозначности при восстановлении объекта.

Смешение единиц и порядок приоритетов

При создании длительности порядок единиц не влияет на результат:

Duration.fromObject({ minutes: 1, seconds: 30 });
Duration.fromObject({ seconds: 30, minutes: 1 });

Обе конструкции идентичны, так как данные хранятся в виде словаря единиц.

Однако при нормализации происходит перераспределение от меньших единиц к большим, если это возможно.

Работа с неделями, месяцами и годами

Календарные единицы требуют учёта даты отсчёта:

const d = Duration.fromObject({ months: 1 });

d.shiftTo('days').days;

Результат зависит от базовой даты, поскольку:

  • февраль даёт 28/29 дней
  • март — 31 день
  • переход через границы месяцев влияет на итог

Это делает такие единицы зависимыми от временного контекста DateTime.

Практическая модель выбора единиц

Выбор единиц в Luxon определяется типом задачи:

  • события и таймеры → секунды, минуты, миллисекунды
  • расписания и интервалы → дни, недели
  • бизнес-логика с датами → месяцы, годы
  • высокоточные измерения → миллисекунды

Чем более детерминированной должна быть логика, тем ближе единицы к абсолютной шкале времени.

Проблема неоднозначности календарных единиц

Месяцы и годы создают неоднозначные ситуации:

  • добавление 1 месяца к 31 января
  • пересечение високосного года
  • переходы на летнее и зимнее время

В таких случаях Luxon не может использовать фиксированное преобразование, и результат зависит от начальной точки времени.

Комбинированные представления

Длительности могут одновременно содержать несколько уровней:

const d = Duration.fromObject({
  days: 2,
  hours: 5,
  minutes: 10
});

Такая структура позволяет сохранить семантику времени без потери информации при преобразованиях между единицами.

При этом внутреннее представление остаётся числовым только на уровне миллисекунд, а логическая структура сохраняется отдельно.