Работа со старыми календарями

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

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

Библиотека Luxon не реализует полноценную поддержку альтернативных календарных систем, но предоставляет инструменты для работы с временными зонами, ISO-форматами, локалями и вычислениями, позволяющими моделировать поведение старых календарей.


Юлианский и григорианский календари

Юлианский календарь

Юлианский календарь был введён Юлием Цезарем в 45 году до н.э. Основная особенность — високосный год каждые 4 года без исключений.

Средняя длина года:

365.25 суток

Из-за накопления ошибки календарь постепенно начал расходиться с астрономическим временем.


Григорианский календарь

В 1582 году папа Григорий XIII провёл реформу:

  • годы, кратные 100, перестали быть високосными;
  • исключение — годы, кратные 400.

Средняя длина года:

365.2425 суток

После реформы произошло удаление нескольких дней из календаря.


Проблема отсутствующих дат

После перехода на григорианский календарь часть дат была пропущена.

Пример для Италии:

4 октября 1582 → 15 октября 1582

Между этими датами не существовало дней:

5–14 октября 1582

Разные страны переходили на новый календарь в разные годы.

Страна Переход
Италия 1582
Франция 1582
Германия 1700
Британия 1752
Россия 1918

Ограничения Luxon при работе со старыми датами

Luxon использует:

  • объект Date;
  • ISO-8601;
  • proleptic Gregorian calendar.

Proleptic Gregorian Calendar

Это расширение современного григорианского календаря в прошлое, даже в эпохи, когда он ещё не существовал.

Следовательно:

DateTime.fromISO("1400-01-01")

не создаёт «реальную» историческую дату 1400 года, а создаёт дату по современным правилам григорианского календаря.


Создание исторических дат

Базовое создание

import { DateTime } from "luxon";

const date = DateTime.fromObject({
  year: 1453,
  month: 5,
  day: 29
});

console.log(date.toISODate());

Результат:

1453-05-29

Luxon спокойно работает с древними датами.


Работа с отрицательными годами

ISO-8601 поддерживает годы до нашей эры.

const caesar = DateTime.fromObject({
  year: -44,
  month: 3,
  day: 15
});

console.log(caesar.toISODate());

Результат:

-000044-03-15

Это соответствует 44 году до н.э.


Особенности исторического летоисчисления

Отсутствие нулевого года

Историческая система:

1 год до н.э. → 1 год н.э.

ISO-система:

0 = 1 год до н.э.
-1 = 2 год до н.э.

Поэтому необходимо учитывать смещение.


Моделирование юлианского календаря

Luxon не умеет переключаться на юлианский календарь автоматически, поэтому приходится использовать ручные вычисления.


Разница между календарями

После 1582 года разница постепенно увеличивалась.

Период Разница
1582 10 дней
1700 11 дней
1800 12 дней
1900 13 дней

Простое преобразование юлианской даты

function julianToGregorian(date) {
  return date.plus({ days: 10 });
}

Использование:

const oldDate = DateTime.fromISO("1582-10-04");

const converted = julianToGregorian(oldDate);

console.log(converted.toISODate());

Более точный алгоритм

Для исторических вычислений требуется учитывать вековые поправки.

function getJulianDifference(year) {
  if (year >= 2100) return 14;
  if (year >= 1900) return 13;
  if (year >= 1800) return 12;
  if (year >= 1700) return 11;
  return 10;
}

function julianToGregorian(date) {
  const diff = getJulianDifference(date.year);

  return date.plus({ days: diff });
}

Работа с историческими диапазонами

Luxon хорошо подходит для вычисления интервалов.

const start = DateTime.fromISO("1200-01-01");
const end = DateTime.fromISO("1500-01-01");

const diff = end.diff(start, "years");

console.log(diff.years);

Исторические интервалы и точность

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

  • високосные реформы;
  • разные системы календарей;
  • локальные переходы;
  • отсутствие стандартизации.

Luxon считает интервалы математически, а не исторически.


Использование ISO-форматов для архивов

Хранение исторических дат

Рекомендуется сохранять:

YYYY-MM-DD

Пример:

const archiveDate = DateTime.fromObject({
  year: 1812,
  month: 9,
  day: 14
});

console.log(archiveDate.toISODate());

Указание календарной системы

Поскольку Luxon не хранит информацию о календаре, полезно сохранять её отдельно.

const event = {
  calendar: "julian",
  date: "1700-02-10"
};

Конвертация старых архивных записей

Старый стиль и новый стиль

Во многих документах встречаются пометки:

O.S. — Old Style
N.S. — New Style

Пример:

const record = {
  style: "OS",
  date: "1752-09-02"
};

Британская календарная реформа

В Британии переход произошёл в 1752 году.

После:

2 сентября 1752

сразу наступило:

14 сентября 1752

Проверка несуществующих дат

Можно реализовать собственный валидатор.

function isMissingBritishDate(date) {
  return (
    date.year === 1752 &&
    date.month === 9 &&
    date.day >= 3 &&
    date.day <= 13
  );
}

Использование:

const date = DateTime.fromISO("1752-09-10");

console.log(isMissingBritishDate(date));

Исторические временные зоны

До появления стандартизированных часовых поясов использовалось локальное солнечное время.

Luxon опирается на базу IANA timezone database, которая:

  • не покрывает всю древнюю историю;
  • содержит приближённые данные;
  • ориентирована на современную эпоху.

Ограничения исторических timezone

Пример:

DateTime.fromObject(
  {
    year: 1500,
    month: 1,
    day: 1
  },
  {
    zone: "Europe/Moscow"
  }
);

Результат технически существует, но не отражает реальную систему времени XV века.


Работа с юлианским днём

Для астрономических расчётов часто используется Julian Day Number.

Luxon напрямую его не поддерживает, но позволяет выполнять преобразования.


Получение timestamp

const date = DateTime.fromISO("1900-01-01");

console.log(date.toMillis());

Проблемы Unix timestamp

Unix timestamp начинается с:

1970-01-01

Для древних дат появляются:

  • отрицательные значения;
  • ограничения точности;
  • различия между движками JavaScript.

Проверка валидности древних дат

const ancient = DateTime.fromObject({
  year: -5000,
  month: 1,
  day: 1
});

console.log(ancient.isValid);

Границы JavaScript Date

Внутренний объект Date имеет ограничения:

±8.64e15 миллисекунд

Это примерно:

±275000 лет

Локализация исторических дат

Luxon поддерживает локали через Intl API.

const date = DateTime.fromISO("1812-09-14")
  .setLocale("ru");

console.log(date.toLocaleString(DateTime.DATE_FULL));

Исторические названия месяцев

Некоторые старые документы используют:

  • латинские названия;
  • церковные обозначения;
  • региональные варианты.

Luxon работает только с современными локалями.


Пользовательские исторические форматы

Разбор нестандартных дат

const date = DateTime.fromFormat(
  "14 Sep 1752",
  "dd LLL yyyy"
);

console.log(date.toISODate());

Работа с римскими датами

Luxon не поддерживает:

  • календы;
  • ноны;
  • иды.

Требуется отдельный парсер.


Создание адаптера для старых календарей

Практичный подход — создание обёртки.

class HistoricalDate {
  constructor(date, calendar = "gregorian") {
    this.date = date;
    this.calendar = calendar;
  }

  toGregorian() {
    if (this.calendar === "julian") {
      return this.date.plus({ days: 13 });
    }

    return this.date;
  }
}

Использование immutable-подхода

Luxon использует неизменяемые объекты.

const original = DateTime.fromISO("1700-01-01");

const modified = original.plus({ days: 11 });

console.log(original.toISODate());
console.log(modified.toISODate());

Это особенно важно для исторических вычислений, где исходные данные должны оставаться неизменными.


Исторические базы данных

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

Поле Назначение
original_date Исходная запись
normalized_date Нормализованная дата
calendar Тип календаря
precision Точность
source Источник

Частичная точность дат

Во многих документах известен только:

  • год;
  • месяц и год;
  • приблизительный период.

Пример структуры:

const dateInfo = {
  year: 1348,
  month: null,
  day: null
};

Исторические диапазоны вместо точных дат

const plaguePeriod = Interval.fromDateTimes(
  DateTime.fromISO("1347-01-01"),
  DateTime.fromISO("1353-12-31")
);

Работа с интервалами

import { Interval } from "luxon";

const medieval = Interval.fromDateTimes(
  DateTime.fromISO("1000-01-01"),
  DateTime.fromISO("1500-01-01")
);

console.log(medieval.length("years"));

Проверка принадлежности эпохе

function isMiddleAges(date) {
  return date.year >= 476 && date.year <= 1492;
}

Исторические события и сортировка

Luxon упрощает сортировку дат.

const events = [
  DateTime.fromISO("1917-11-07"),
  DateTime.fromISO("1812-09-14"),
  DateTime.fromISO("1945-05-09")
];

events.sort((a, b) => a - b);

Сериализация исторических данных

JSON

const event = {
  title: "Battle",
  date: DateTime.fromISO("1415-10-25").toISO()
};

console.log(JSON.stringify(event));

Потеря календарного контекста

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

  • юлианский стиль;
  • регион;
  • реформа;
  • оригинальная запись.

Поэтому необходимо хранить метаданные отдельно.


Использование Luxon вместе со специализированными библиотеками

Для полноценной исторической точности Luxon часто комбинируют с:

  • астрономическими библиотеками;
  • системами генеалогии;
  • библиотеками юлианских вычислений.

Luxon в таких системах отвечает за:

  • форматирование;
  • интервалы;
  • локализацию;
  • временные зоны;
  • сериализацию.

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

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

  • избегать повторного парсинга;
  • кэшировать преобразования;
  • минимизировать fromFormat;
  • использовать ISO-строки.

Кэширование преобразований

const cache = new Map();

function convert(date) {
  if (cache.has(date)) {
    return cache.get(date);
  }

  const result = DateTime.fromISO(date)
    .plus({ days: 13 });

  cache.set(date, result);

  return result;
}

Тестирование исторических дат

Особое внимание уделяется:

  • високосным годам;
  • переходам между календарями;
  • отсутствующим датам;
  • BCE-датам;
  • timezone-аномалиям.

Пример набора тестов

describe("Historical dates", () => {
  test("british missing dates", () => {
    const date = DateTime.fromISO("1752-09-10");

    expect(isMissingBritishDate(date))
      .toBe(true);
  });

  test("julian conversion", () => {
    const date = DateTime.fromISO("1700-01-01");

    expect(
      julianToGregorian(date).day
    ).toBe(12);
  });
});