core/factory.js

import { OzTime } from './core.js';
import { daysInMonth } from '../utils/calendar.js';

/**
 * Фабричные функции для создания экземпляров {@link OzTime}.
 *
 * @module core/factory
 */

/**
 * Проверяет корректность 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('fromTimestamp: timestamp must be a valid number');
    }
}

/**
 * Проверяет корректность объекта Date.
 *
 * @private
 * @param {Date} date - Проверяемый объект Date.
 * @throws {TypeError} Выбрасывается, если date некорректен.
 * @returns {void}
 */
function assertValidDate(date) {
    if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
        throw new TypeError('fromDate: date must be a valid Date');
    }
}

/**
 * Проверяет, что значение является целым числом.
 *
 * @private
 * @param {number} value - Проверяемое значение.
 * @param {string} name - Имя параметра.
 * @throws {TypeError} Выбрасывается, если значение не является целым числом.
 * @returns {void}
 */
function assertInteger(value, name) {
    if (!Number.isInteger(value)) {
        throw new TypeError(`${name} must be an integer`);
    }
}

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

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

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

/**
 * Создаёт и возвращает экземпляр {@link OzTime} на основе ISO-строки.
 *
 * @param {string} isoString - Строка даты и времени в формате ISO 8601.
 * @param {string} [timezone='UTC'] - Часовой пояс в формате IANA.
 * @param {string} [locale='en-US'] - Локаль форматирования.
 * @throws {TypeError} Выбрасывается, если строка пустая или не является строкой.
 * @throws {Error} Выбрасывается, если строку не удалось распарсить.
 * @returns {OzTime} Экземпляр времени.
 * @example
 * import { fromISO } from '@alexstukovnikov/oz-time';
 *
 * const time = fromISO('2024-05-25T12:00:00Z', 'UTC', 'ru-RU');
 * console.log(time.format('DD.MM.YYYY HH:mm')); // ожидаемый результат: 25.05.2024 12:00
 */
export function fromISO(isoString, timezone = 'UTC', locale = 'en-US') {
    if (typeof isoString !== 'string' || isoString.trim() === '') {
        throw new TypeError('fromISO: isoString must be a non-empty string');
    }

    const ts = Date.parse(isoString);

    if (Number.isNaN(ts)) {
        throw new Error(`Invalid ISO date string: ${isoString}`);
    }

    return new OzTime(ts, 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 {string} [timezone='UTC'] - Часовой пояс в формате IANA.
 * @param {string} [locale='en-US'] - Локаль форматирования.
 * @throws {TypeError} Выбрасывается, если любой числовой параметр не является целым числом.
 * @throws {RangeError} Выбрасывается, если любой компонент даты или времени вне допустимого диапазона.
 * @returns {OzTime} Экземпляр времени.
 * @example
 * import { fromComponents } from '@alexstukovnikov/oz-time';
 *
 * const time = fromComponents(2024, 5, 25, 12, 0, 0, 0, 'UTC', 'ru-RU');
 * console.log(time.toISOString()); // ожидаемый результат: 2024-05-25T12:00:00.000Z
 */
export function fromComponents(year, month, day, hour = 0, minute = 0, second = 0, ms = 0, timezone = 'UTC', locale = 'en-US') {
    assertInteger(year, 'year');
    assertInteger(month, 'month');
    assertInteger(day, 'day');
    assertInteger(hour, 'hour');
    assertInteger(minute, 'minute');
    assertInteger(second, 'second');
    assertInteger(ms, 'millisecond');

    if (month < 1 || month > 12) {
        throw new RangeError('month must be between 1 and 12');
    }

    if (hour < 0 || hour > 23) {
        throw new RangeError('hour must be between 0 and 23');
    }

    if (minute < 0 || minute > 59) {
        throw new RangeError('minute must be between 0 and 59');
    }

    if (second < 0 || second > 59) {
        throw new RangeError('second must be between 0 and 59');
    }

    if (ms < 0 || ms > 999) {
        throw new RangeError('millisecond must be between 0 and 999');
    }

    const maxDay = daysInMonth(year, month);

    if (day < 1 || day > maxDay) {
        throw new RangeError(`day must be between 1 and ${maxDay} for ${year}-${month}`);
    }

    const ts = Date.UTC(year, month - 1, day, hour, minute, second, ms);
    return new OzTime(ts, timezone, locale);
}