Альтернативные календари

Библиотека Js-joda построена вокруг стандарта ISO-8601, однако экосистема Java Time API изначально поддерживает и другие календарные системы. Концепция альтернативных календарей особенно важна при разработке международных приложений, систем документооборота, архивов, финансовых платформ и государственных сервисов, где дата может быть представлена не только в григорианском календаре.

В Js-joda основная библиотека ориентирована на ISO-календарь, но архитектура Temporal API и подходы Java Time позволяют моделировать альтернативные хронологии через:

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

Ключевая идея — отделение:

  • момента времени;
  • локальной даты;
  • системы календарного представления.

Один и тот же момент времени может быть отображён разными календарями.


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

Большинство разработчиков неявно предполагают, что:

2025-05-01

— универсальная дата. На практике это лишь представление даты в ISO-календаре.

В разных странах и исторических периодах используются:

  • японский императорский календарь;
  • буддийский календарь;
  • исламский календарь;
  • еврейский календарь;
  • юлианский календарь;
  • индийский национальный календарь.

При этом:

  • длина года может отличаться;
  • месяцы могут быть лунными;
  • начало года может смещаться;
  • эпохи могут определяться правителями или религиозными событиями.

Архитектура даты в Js-joda

Основной тип локальной даты:

const { LocalDate } = require('@js-joda/core')

const date = LocalDate.of(2025, 5, 10)

LocalDate всегда использует ISO-8601.

Внутри:

  • год;
  • месяц;
  • день;
  • правила ISO-календаря.

Это означает, что Js-joda не предоставляет полноценные альтернативные хронологии «из коробки», как Java SE (JapaneseDate, HijrahDate, ThaiBuddhistDate), однако библиотека позволяет строить подобную логику поверх существующей модели.


Эпохи как основа альтернативных календарей

Во многих календарях год отсчитывается не от рождения Христа.

Например:

Календарь Начало эпохи
Буддийский 543 год до н.э.
Японский Начало правления императора
Исламский Хиджра
Еврейский Сотворение мира

Js-joda предоставляет тип Era.


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

const { IsoEra } = require('@js-joda/core')

console.log(IsoEra.CE)
console.log(IsoEra.BCE)

Результат:

CE
BCE

Эпохи особенно важны при:

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

Работа с годами до нашей эры

const { LocalDate } = require('@js-joda/core')

const caesarDeath = LocalDate.of(-44, 3, 15)

console.log(caesarDeath.toString())

Результат:

-0044-03-15

Здесь используется астрономическая система нумерации:

Исторический год Js-joda
1 до н.э. 0
2 до н.э. -1
44 до н.э. -43

Это важно учитывать при интеграции с историческими источниками.


Пролептический календарь

Js-joda использует пролептический ISO-календарь.

Это означает:

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

Например:

const date = LocalDate.of(1400, 10, 10)

Дата будет валидной даже несмотря на то, что в тот период использовался юлианский календарь.


Моделирование альтернативных календарей

Подход через преобразование года

Самый простой вариант — преобразовывать ISO-даты в альтернативное представление.

Буддийский календарь

Буддийский календарь:

BE = CE + 543

Пример:

const { LocalDate } = require('@js-joda/core')

function toBuddhist(date) {
    return {
        year: date.year() + 543,
        month: date.monthValue(),
        day: date.dayOfMonth()
    }
}

const date = LocalDate.of(2025, 5, 10)

console.log(toBuddhist(date))

Результат:

{
  year: 2568,
  month: 5,
  day: 10
}

Японский императорский календарь

В японской системе годы отсчитываются по эпохам императоров.

Эпоха Начало
Reiwa 2019
Heisei 1989
Showa 1926

Реализация японских эпох

const ERAS = [
    {
        name: 'Reiwa',
        start: 2019
    },
    {
        name: 'Heisei',
        start: 1989
    },
    {
        name: 'Showa',
        start: 1926
    }
]

function toJapanese(date) {
    const year = date.year()

    const era = ERAS.find(e => year >= e.start)

    return {
        era: era.name,
        year: year - era.start + 1,
        month: date.monthValue(),
        day: date.dayOfMonth()
    }
}

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

const { LocalDate } = require('@js-joda/core')

const date = LocalDate.of(2025, 5, 10)

console.log(toJapanese(date))

Результат:

{
  era: 'Reiwa',
  year: 7,
  month: 5,
  day: 10
}

Проблемы альтернативных календарей

Неодинаковая длина месяцев

В лунных календарях:

  • месяцы могут содержать 29 или 30 дней;
  • високосные правила сложнее;
  • могут существовать вставочные месяцы.

Разные начала года

ISO:

1 января

Исламский календарь:

  • зависит от лунных циклов;
  • начало года смещается.

Еврейский календарь:

  • начинается осенью.

Исторические реформы

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

Например:

Страна Переход
Италия 1582
Россия 1918
Греция 1923

Из-за этого одна и та же историческая дата может иметь разные представления.


Преобразование через Epoch Day

Наиболее надёжный способ работы с альтернативными календарями — хранение даты через абсолютное количество дней.

Js-joda предоставляет:

toEpochDay()

Получение абсолютного дня

const { LocalDate } = require('@js-joda/core')

const date = LocalDate.of(2025, 5, 10)

console.log(date.toEpochDay())

Результат:

20218

Это количество дней от:

1970-01-01

Почему Epoch Day важен

Абсолютный номер дня:

  • не зависит от календаря;
  • не зависит от эпохи;
  • не зависит от локализации.

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

ISO дата
↓
epoch day
↓
альтернативный календарь

— наиболее безопасная схема преобразования.


Создание собственного календарного адаптера

Базовая структура

class CustomCalendarDate {

    constructor(epochDay) {
        this.epochDay = epochDay
    }

    static fromIso(date) {
        return new CustomCalendarDate(
            date.toEpochDay()
        )
    }

    toIso() {
        return LocalDate.ofEpochDay(
            this.epochDay
        )
    }
}

Такой подход позволяет:

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

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

const { LocalDate } = require('@js-joda/core')

const date = LocalDate.ofEpochDay(0)

console.log(date.toString())

Результат:

1970-01-01

Адаптация исламского календаря

Полная реализация исламского календаря крайне сложна:

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

Поэтому обычно используется:

  • внешний пакет;
  • API ICU;
  • Intl;
  • серверные преобразования.

Интеграция с Intl

JavaScript Intl.DateTimeFormat поддерживает альтернативные календари.


Японский календарь

const formatter = new Intl.DateTimeFormat(
    'ja-JP-u-ca-japanese',
    {
        year: 'numeric',
        month: 'long',
        day: 'numeric'
    }
)

console.log(
    formatter.format(new Date())
)

Возможный результат:

令和7年5月10日

Использование Js-joda вместе с Intl

Наиболее практичная архитектура:

Js-joda
↓
точные вычисления
↓
Date
↓
Intl
↓
локализованное представление

Пример интеграции

const { LocalDate } = require('@js-joda/core')

const date = LocalDate.of(2025, 5, 10)

const jsDate = new Date(
    date.year(),
    date.monthValue() - 1,
    date.dayOfMonth()
)

const formatter = new Intl.DateTimeFormat(
    'th-TH-u-ca-buddhist'
)

console.log(
    formatter.format(jsDate)
)

Буддийский календарь через Intl

const formatter = new Intl.DateTimeFormat(
    'th-TH-u-ca-buddhist',
    {
        year: 'numeric',
        month: 'long',
        day: 'numeric'
    }
)

Поддерживаемые календарные системы Intl

Код Календарь
buddhist Буддийский
chinese Китайский
coptic Коптский
ethiopic Эфиопский
hebrew Еврейский
indian Индийский
islamic Исламский
japanese Японский
persian Персидский

Проверка поддержки календаря

console.log(
    Intl.supportedValuesOf('calendar')
)

Сложности локализации

Дата включает:

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

Например:

'ja-JP-u-ca-japanese'

означает:

Часть Значение
ja японский язык
JP Япония
u-ca-japanese японский календарь

Форматирование альтернативных календарей

Еврейский календарь

const formatter = new Intl.DateTimeFormat(
    'he-IL-u-ca-hebrew',
    {
        dateStyle: 'full'
    }
)

Исламский календарь

const formatter = new Intl.DateTimeFormat(
    'ar-SA-u-ca-islamic',
    {
        dateStyle: 'full'
    }
)

Разделение вычислений и отображения

Ключевой архитектурный принцип:

Задача Инструмент
Арифметика дат Js-joda
Хранение ISO / epoch
Локализация Intl
Альтернативные календари Intl / адаптер

Почему вычисления должны оставаться в ISO

ISO-календарь:

  • предсказуем;
  • стабилен;
  • поддерживается всеми API;
  • удобен для сериализации;
  • хорошо подходит для БД.

Альтернативные календари часто используются только как:

  • пользовательское отображение;
  • культурная локализация;
  • интерфейсный слой.

Ошибки при работе с альтернативными календарями

Ошибка хранения локализованной даты

Плохо:

"令和7年5月10日"

Хорошо:

2025-05-10

Ошибка вычислений в пользовательском календаре

Плохо:

2568 + 1

Хорошо:

ISO date + 1 year
↓
конвертация

Смешивание календарей

Опасный код:

if (year === 2568)

Без понимания календарной системы это приводит к ошибкам бизнес-логики.


Рекомендованная архитектура

Внутренний слой

Используется:

LocalDate
Instant
ZonedDateTime

Транспортный слой

Используется:

ISO-8601
epoch milliseconds
epoch day

UI-слой

Используется:

Intl.DateTimeFormat

или собственный адаптер календаря.


Реализация универсального календарного преобразователя

class CalendarConverter {

    static toBuddhist(date) {
        return {
            year: date.year() + 543,
            month: date.monthValue(),
            day: date.dayOfMonth()
        }
    }

    static toJapanese(date) {

        const eras = [
            ['Reiwa', 2019],
            ['Heisei', 1989]
        ]

        for (const [name, start] of eras) {

            if (date.year() >= start) {

                return {
                    era: name,
                    year: date.year() - start + 1,
                    month: date.monthValue(),
                    day: date.dayOfMonth()
                }
            }
        }
    }
}

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

Преобразования календарей могут быть дорогими:

  • астрономические вычисления;
  • таблицы эпох;
  • локализация;
  • ICU.

Поэтому:

  • вычисления выполняются в ISO;
  • конвертация производится только на UI-слое;
  • форматтеры кэшируются.

Кэширование Intl.DateTimeFormat

Создание форматтера дорогостоящее.

Плохо:

function format(date) {
    return new Intl.DateTimeFormat(
        'ja-JP-u-ca-japanese'
    ).format(date)
}

Хорошо:

const formatter =
    new Intl.DateTimeFormat(
        'ja-JP-u-ca-japanese'
    )

function format(date) {
    return formatter.format(date)
}

Совместимость платформ

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

  • браузера;
  • версии ICU;
  • Node.js;
  • окружения исполнения.

Например:

  • старые Node.js могут не поддерживать некоторые календари;
  • embedded-системы часто используют урезанный ICU;
  • мобильные WebView могут отличаться по возможностям.

Проверка поддержки локали

console.log(
    Intl.DateTimeFormat.supportedLocalesOf([
        'th-TH-u-ca-buddhist'
    ])
)

Когда необходимы полноценные альтернативные календари

Полная реализация нужна в:

  • государственных системах;
  • религиозных приложениях;
  • исторических архивах;
  • астрономическом ПО;
  • международных ERP;
  • юридических системах.

Во многих остальных случаях достаточно:

  • ISO-хранения;
  • UI-конвертации;
  • локализованного форматирования.