core/core.js

import { add, subtract } from '../modules/arithmetic.js';
import { format } from '../modules/format.js';
import { isSame, isBefore, isAfter, isBetween } from '../modules/compare.js';
import { setTimezone, getTimezoneOffset } from '../modules/timezone.js';
import { interval as createInterval } from '../modules/interval.js';
import { duration as createDuration } from '../modules/duration.js';
import { diff } from '../utils/calendar.js';
import {
    now as createNow,
    fromTimestamp as createFromTimestamp,
    fromDate as createFromDate,
    fromISO as createFromISO,
    fromComponents as createFromComponents,
} from './factory.js';

/**
 * Основной модуль, содержащий класс {@link OzTime}.
 *
 * @module core/core
 */

/**
 * Строка с идентификатором часового пояса в формате IANA.
 *
 * @typedef {string} TimezoneString
 */

/**
 * Строка локали, совместимая с Intl API.
 *
 * @typedef {string} LocaleString
 */

/**
 * Формат включённости границ диапазона.
 *
 * @typedef {'[]'|'[)'|'(]'|'()'} Inclusivity
 */

/**
 * Поддерживаемая единица времени.
 *
 * @typedef {'millisecond'|'second'|'minute'|'hour'|'day'|'month'|'year'} TimeUnit
 */

/**
 * Проверяет корректность timestamp.
 *
 * @private
 * @param {number} timestamp - Unix timestamp в миллисекундах.
 * @throws {TypeError} Выбрасывается, если timestamp не является корректным числом.
 * @returns {void}
 */
function assertValidTimestamp(timestamp) {
    if (typeof timestamp !== 'number' || Number.isNaN(timestamp)) {
        throw new TypeError('OzTime: timestamp must be a valid number');
    }
}

/**
 * Проверяет корректность строки часового пояса.
 *
 * @private
 * @param {TimezoneString} timezone - Идентификатор часового пояса.
 * @throws {TypeError} Выбрасывается, если timezone пустой или не является строкой.
 * @returns {void}
 */
function assertValidTimezone(timezone) {
    if (typeof timezone !== 'string' || timezone.trim() === '') {
        throw new TypeError('OzTime: timezone must be a non-empty string');
    }
}

/**
 * Проверяет корректность строки локали.
 *
 * @private
 * @param {LocaleString} locale - Локаль форматирования.
 * @throws {TypeError} Выбрасывается, если locale пустая или не является строкой.
 * @returns {void}
 */
function assertValidLocale(locale) {
    if (typeof locale !== 'string' || locale.trim() === '') {
        throw new TypeError('OzTime: locale must be a non-empty string');
    }
}

/**
 * Неизменяемый объект даты и времени на основе UTC timestamp
 * с дополнительными метаданными о часовом поясе и локали.
 *
 * Класс поддерживает как создание экземпляров через конструктор,
 * так и через статические фабричные методы.
 *
 * @class
 * @example
 * import { OzTime } from '@alexstukovnikov/oz-time';
 *
 * const time = OzTime.fromISO('2024-05-25T12:00:00Z', 'UTC', 'ru-RU');
 * console.log(time.toISOString()); // ожидаемый результат: 2024-05-25T12:00:00.000Z
 *
 * @example
 * import { OzTime } from '@alexstukovnikov/oz-time';
 *
 * const result = OzTime
 *   .fromISO('2024-05-25T12:00:00Z', 'UTC', 'ru-RU')
 *   .add(1, 'day')
 *   .add(2, 'hour')
 *   .subtract(30, 'minute')
 *   .setTimezone('Europe/Moscow')
 *   .format('DD.MM.YYYY HH:mm:ss');
 *
 * console.log(result);
 */
export class OzTime {
    /**
     * Создаёт новый экземпляр OzTime.
     *
     * @param {number} timestamp - Unix timestamp в миллисекундах.
     * @param {TimezoneString} [timezone='UTC'] - Часовой пояс в формате IANA.
     * @param {LocaleString} [locale='en-US'] - Локаль, используемая для форматирования.
     * @throws {TypeError} Выбрасывается, если timestamp, timezone или locale некорректны.
     */
    constructor(timestamp, timezone = 'UTC', locale = 'en-US') {
        assertValidTimestamp(timestamp);
        assertValidTimezone(timezone);
        assertValidLocale(locale);

        this._timestamp = timestamp;
        this._timezone = timezone;
        this._locale = locale;
    }

    /**
     * Создаёт экземпляр {@link OzTime} для текущего момента времени.
     *
     * @param {TimezoneString} [timezone='UTC'] - Часовой пояс в формате IANA.
     * @param {LocaleString} [locale='en-US'] - Локаль форматирования.
     * @returns {OzTime} Экземпляр с текущим временем.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const current = OzTime.now('Europe/Moscow', 'ru-RU');
     * console.log(current.getTimezone()); // ожидаемый результат: Europe/Moscow
     */
    static now(timezone = 'UTC', locale = 'en-US') {
        return createNow(timezone, locale);
    }

    /**
     * Создаёт экземпляр {@link OzTime} из Unix timestamp в миллисекундах.
     *
     * @param {number} timestamp - Unix timestamp в миллисекундах.
     * @param {TimezoneString} [timezone='UTC'] - Часовой пояс в формате IANA.
     * @param {LocaleString} [locale='en-US'] - Локаль форматирования.
     * @returns {OzTime} Экземпляр времени.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const time = OzTime.fromTimestamp(1716638400000, 'UTC', 'ru-RU');
     * console.log(time.toISOString()); // ожидаемый результат: 2024-05-25T12:00:00.000Z
     */
    static fromTimestamp(timestamp, timezone = 'UTC', locale = 'en-US') {
        return createFromTimestamp(timestamp, timezone, locale);
    }

    /**
     * Создаёт экземпляр {@link OzTime} из объекта {@link Date}.
     *
     * @param {Date} date - Нативный объект Date.
     * @param {TimezoneString} [timezone='UTC'] - Часовой пояс в формате IANA.
     * @param {LocaleString} [locale='en-US'] - Локаль форматирования.
     * @returns {OzTime} Экземпляр времени.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const time = OzTime.fromDate(new Date('2024-05-25T12:00:00Z'), 'UTC', 'ru-RU');
     * console.log(time.toTimestamp()); // ожидаемый результат: 1716638400000
     */
    static fromDate(date, timezone = 'UTC', locale = 'en-US') {
        return createFromDate(date, timezone, locale);
    }

    /**
     * Создаёт экземпляр {@link OzTime} из ISO-строки.
     *
     * @param {string} isoString - Строка даты и времени в формате ISO 8601.
     * @param {TimezoneString} [timezone='UTC'] - Часовой пояс в формате IANA.
     * @param {LocaleString} [locale='en-US'] - Локаль форматирования.
     * @returns {OzTime} Экземпляр времени.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const time = OzTime.fromISO('2024-05-25T12:00:00Z', 'UTC', 'ru-RU');
     * console.log(time.format('DD.MM.YYYY HH:mm')); // ожидаемый результат: 25.05.2024 12:00
     */
    static fromISO(isoString, timezone = 'UTC', locale = 'en-US') {
        return createFromISO(isoString, timezone, locale);
    }

    /**
     * Создаёт экземпляр {@link OzTime} из отдельных компонентов даты и времени.
     *
     * @param {number} year - Год.
     * @param {number} month - Месяц от 1 до 12.
     * @param {number} day - День месяца.
     * @param {number} [hour=0] - Час от 0 до 23.
     * @param {number} [minute=0] - Минута от 0 до 59.
     * @param {number} [second=0] - Секунда от 0 до 59.
     * @param {number} [ms=0] - Миллисекунда от 0 до 999.
     * @param {TimezoneString} [timezone='UTC'] - Часовой пояс в формате IANA.
     * @param {LocaleString} [locale='en-US'] - Локаль форматирования.
     * @returns {OzTime} Экземпляр времени.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const time = OzTime.fromComponents(2024, 5, 25, 12, 0, 0, 0, 'UTC', 'ru-RU');
     * console.log(time.toISOString()); // ожидаемый результат: 2024-05-25T12:00:00.000Z
     */
    static fromComponents(year, month, day, hour = 0, minute = 0, second = 0, ms = 0, timezone = 'UTC', locale = 'en-US') {
        return createFromComponents(year, month, day, hour, minute, second, ms, timezone, locale);
    }

    /**
     * Создаёт новый интервал между двумя значениями {@link OzTime}.
     *
     * @param {OzTime} start - Начало интервала.
     * @param {OzTime} end - Конец интервала.
     * @returns {Interval} Экземпляр интервала.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const start = OzTime.fromISO('2024-05-25T10:00:00Z');
     * const end = OzTime.fromISO('2024-05-25T12:00:00Z');
     * const range = OzTime.interval(start, end);
     *
     * console.log(range.duration('hour')); // ожидаемый результат: 2
     */
    static interval(start, end) {
        return createInterval(start, end);
    }

    /**
     * Создаёт новую длительность из фиксированной единицы времени.
     *
     * @param {number} amount - Количество единиц времени.
     * @param {TimeUnit|string} unit - Единица времени.
     * @returns {Duration} Экземпляр длительности.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const value = OzTime.duration(2, 'hour');
     * console.log(value.asMinutes()); // ожидаемый результат: 120
     */
    static duration(amount, unit) {
        return createDuration(amount, unit);
    }

    /**
     * Возвращает внутренний Unix timestamp экземпляра в миллисекундах.
     *
     * @returns {number} Unix timestamp в миллисекундах.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const time = OzTime.fromISO('2024-05-25T12:00:00Z', 'UTC', 'ru-RU');
     * console.log(time.getTimestamp()); // ожидаемый результат: 1716638400000
     */
    getTimestamp() {
        return this._timestamp;
    }

    /**
     * Возвращает текущий часовой пояс экземпляра.
     *
     * @returns {TimezoneString} Идентификатор часового пояса.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const time = OzTime.fromISO('2024-05-25T12:00:00Z', 'Europe/Moscow', 'ru-RU');
     * console.log(time.getTimezone()); // ожидаемый результат: Europe/Moscow
     */
    getTimezone() {
        return this._timezone;
    }

    /**
     * Возвращает текущую локаль экземпляра.
     *
     * @returns {LocaleString} Строка локали.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const time = OzTime.fromISO('2024-05-25T12:00:00Z', 'UTC', 'ru-RU');
     * console.log(time.getLocale()); // ожидаемый результат: ru-RU
     */
    getLocale() {
        return this._locale;
    }

    /**
     * Возвращает внутренний Unix timestamp экземпляра в миллисекундах.
     *
     * @returns {number} Unix timestamp в миллисекундах.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const time = OzTime.fromISO('2024-05-25T12:00:00Z', 'UTC', 'ru-RU');
     * console.log(time.toTimestamp()); // ожидаемый результат: 1716638400000
     */
    toTimestamp() {
        return this._timestamp;
    }

    /**
     * Преобразует текущее значение времени в строку формата ISO 8601.
     *
     * @returns {string} Строковое представление даты и времени в формате ISO 8601.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const time = OzTime.fromComponents(2024, 5, 25, 12, 0, 0, 0, 'UTC', 'ru-RU');
     * console.log(time.toISOString()); // ожидаемый результат: 2024-05-25T12:00:00.000Z
     */
    toISOString() {
        return new Date(this._timestamp).toISOString();
    }

    /**
     * Возвращает новый экземпляр OzTime, у которого timestamp увеличен
     * на указанное количество единиц времени.
     *
     * Исходный экземпляр не изменяется.
     *
     * @param {number} amount - Количество единиц времени.
     * @param {TimeUnit|string} unit - Единица времени.
     * @returns {OzTime} Новый экземпляр OzTime с timestamp, сдвинутым вперёд.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const nextDay = OzTime
     *   .fromISO('2024-05-25T12:00:00Z')
     *   .add(1, 'day');
     *
     * console.log(nextDay.toISOString()); // ожидаемый результат: 2024-05-26T12:00:00.000Z
     */
    add(amount, unit) {
        return add(this, amount, unit);
    }

    /**
     * Возвращает новый экземпляр OzTime, у которого timestamp уменьшен
     * на указанное количество единиц времени.
     *
     * Исходный экземпляр не изменяется.
     *
     * @param {number} amount - Количество единиц времени.
     * @param {TimeUnit|string} unit - Единица времени.
     * @returns {OzTime} Новый экземпляр OzTime с timestamp, сдвинутым назад.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const prevHour = OzTime
     *   .fromISO('2024-05-25T12:00:00Z')
     *   .subtract(1, 'hour');
     *
     * console.log(prevHour.toISOString()); // ожидаемый результат: 2024-05-25T11:00:00.000Z
     */
    subtract(amount, unit) {
        return subtract(this, amount, unit);
    }

    /**
     * Форматирует текущее значение времени по заданному шаблону.
     *
     * @param {string} template - Строка шаблона форматирования.
     * @param {LocaleString} [locale] - Локаль, которая временно переопределяет локаль экземпляра.
     * @returns {string} Отформатированная строка.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const value = OzTime.fromISO('2024-05-25T12:00:00Z', 'UTC', 'ru-RU');
     * console.log(value.format('DD.MM.YYYY HH:mm')); // ожидаемый результат: 25.05.2024 12:00
     */
    format(template, locale) {
        return format(this, template, locale);
    }

    /**
     * Проверяет, совпадает ли текущее значение с другим временем
     * на заданной точности.
     *
     * @param {OzTime} other - Второе значение для сравнения.
     * @param {TimeUnit|string} [unit='millisecond'] - Точность сравнения.
     * @returns {boolean} `true`, если значения совпадают.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const a = OzTime.fromISO('2024-05-25T12:00:00.100Z');
     * const b = OzTime.fromISO('2024-05-25T12:00:00.900Z');
     * console.log(a.isSame(b, 'second')); // ожидаемый результат: true
     */
    isSame(other, unit = 'millisecond') {
        return isSame(this, other, unit);
    }

    /**
     * Проверяет, находится ли текущее значение раньше другого времени.
     *
     * @param {OzTime} other - Второе значение для сравнения.
     * @param {TimeUnit|string} [unit='millisecond'] - Точность сравнения.
     * @returns {boolean} `true`, если текущее значение меньше.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const a = OzTime.fromISO('2024-05-25T12:00:00Z');
     * const b = a.add(1, 'day');
     * console.log(a.isBefore(b)); // ожидаемый результат: true
     */
    isBefore(other, unit = 'millisecond') {
        return isBefore(this, other, unit);
    }

    /**
     * Проверяет, находится ли текущее значение позже другого времени.
     *
     * @param {OzTime} other - Второе значение для сравнения.
     * @param {TimeUnit|string} [unit='millisecond'] - Точность сравнения.
     * @returns {boolean} `true`, если текущее значение больше.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const a = OzTime.fromISO('2024-05-26T12:00:00Z');
     * const b = OzTime.fromISO('2024-05-25T12:00:00Z');
     * console.log(a.isAfter(b)); // ожидаемый результат: true
     */
    isAfter(other, unit = 'millisecond') {
        return isAfter(this, other, unit);
    }

    /**
     * Проверяет, попадает ли текущее значение в диапазон между двумя датами.
     *
     * @param {OzTime} start - Левая граница диапазона.
     * @param {OzTime} end - Правая граница диапазона.
     * @param {TimeUnit|string} [unit='millisecond'] - Точность сравнения.
     * @param {Inclusivity} [inclusivity='[]'] - Формат включённости границ.
     * @returns {boolean} `true`, если значение находится внутри диапазона.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const current = OzTime.fromISO('2024-05-25T12:00:00Z');
     * const start = current.subtract(1, 'day');
     * const end = current.add(1, 'day');
     * console.log(current.isBetween(start, end)); // ожидаемый результат: true
     */
    isBetween(start, end, unit = 'millisecond', inclusivity = '[]') {
        return isBetween(this, start, end, unit, inclusivity);
    }

    /**
     * Возвращает новый экземпляр с тем же timestamp, но другим часовым поясом.
     *
     * @param {TimezoneString} timezone - Новый часовой пояс.
     * @returns {OzTime} Новый экземпляр с обновлённым часовым поясом.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const moscowTime = OzTime
     *   .fromISO('2024-05-25T12:00:00Z', 'UTC', 'ru-RU')
     *   .setTimezone('Europe/Moscow');
     *
     * console.log(moscowTime.getTimezone()); // ожидаемый результат: Europe/Moscow
     */
    setTimezone(timezone) {
        return setTimezone(this, timezone);
    }

    /**
     * Возвращает смещение текущего часового пояса относительно UTC в минутах.
     *
     * @returns {number} Смещение в минутах.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const time = OzTime.fromISO('2024-05-25T12:00:00Z', 'Europe/Moscow', 'ru-RU');
     * console.log(time.getTimezoneOffset()); // ожидаемый результат: 180
     */
    getTimezoneOffset() {
        return getTimezoneOffset(this);
    }

    /**
     * Вычисляет разницу между текущим значением и другим временем.
     *
     * @param {OzTime} other - Второе значение для сравнения.
     * @param {TimeUnit|string} [unit='millisecond'] - Единица измерения разницы.
     * @returns {number} Разница между двумя значениями.
     * @example
     * import { OzTime } from '@alexstukovnikov/oz-time';
     *
     * const start = OzTime.fromISO('2024-05-25T12:00:00Z');
     * const end = start.add(2, 'hour');
     * console.log(end.diff(start, 'hour')); // ожидаемый результат: 2
     */
    diff(other, unit = 'millisecond') {
        return diff(this, other, unit);
    }
}