import { OzTime } from '../core/core.js';
/**
* Модуль форматирования экземпляров {@link OzTime}.
*
* @module modules/format
*/
/**
* Проверяет, является ли значение экземпляром OzTime.
*
* @private
* @param {*} value - Проверяемое значение.
* @throws {TypeError} Выбрасывается, если значение не является экземпляром OzTime.
* @returns {void}
*/
function assertOzTime(value) {
if (!(value instanceof OzTime)) {
throw new TypeError('format: first argument must be OzTime');
}
}
/**
* Дополняет значение ведущими нулями до нужной длины.
*
* @private
* @param {string|number} value - Исходное значение.
* @param {number} [length=2] - Итоговая длина строки.
* @returns {string} Строка, дополненная ведущими нулями.
*/
function pad(value, length = 2) {
return String(value).padStart(length, '0');
}
/**
* Возвращает числовые части даты для форматирования.
*
* @private
* @param {OzTime} time - Экземпляр времени.
* @param {string} locale - Локаль форматирования.
* @returns {Object.<string, string>} Объект с числовыми частями даты и времени.
*/
function getNumericParts(time, locale) {
const formatter = new Intl.DateTimeFormat(locale, {
timeZone: time.getTimezone(),
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: false,
});
const parts = formatter.formatToParts(new Date(time.getTimestamp()));
return Object.fromEntries(parts.map((part) => [part.type, part.value]));
}
/**
* Возвращает название месяца в нужном формате.
*
* @private
* @param {OzTime} time - Экземпляр времени.
* @param {string} locale - Локаль форматирования.
* @param {'long'|'short'|'narrow'} length - Длина названия месяца.
* @returns {string} Название месяца.
*/
function getMonthName(time, locale, length) {
return new Intl.DateTimeFormat(locale, {
timeZone: time.getTimezone(),
month: length,
}).format(new Date(time.getTimestamp()));
}
/**
* Возвращает название дня недели в нужном формате.
*
* @private
* @param {OzTime} time - Экземпляр времени.
* @param {string} locale - Локаль форматирования.
* @param {'long'|'short'|'narrow'} length - Длина названия дня недели.
* @returns {string} Название дня недели.
*/
function getWeekdayName(time, locale, length) {
return new Intl.DateTimeFormat(locale, {
timeZone: time.getTimezone(),
weekday: length,
}).format(new Date(time.getTimestamp()));
}
/**
* Возвращает строковое представление экземпляра {@link OzTime}
* по заданному шаблону с токенами.
*
* Поддерживаются токены `YYYY`, `YY`, `MMMM`, `MMM`, `MM`, `M`, `dddd`, `ddd`,
* `DD`, `D`, `HH`, `H`, `hh`, `h`, `mm`, `ss`, `SSS` и `A`.
*
* @param {OzTime} time - Экземпляр времени для форматирования.
* @param {string} template - Шаблон форматирования.
* @param {string} [locale] - Необязательное переопределение локали.
* @throws {TypeError} Выбрасывается, если первый аргумент не является экземпляром OzTime или template некорректен.
* @returns {string} Отформатированная строка.
* @example
* import { format, fromISO } from '@alexstukovnikov/oz-time';
*
* const time = fromISO('2024-05-25T12:00:00Z', 'UTC', 'ru-RU');
* console.log(format(time, 'DD.MM.YYYY HH:mm')); // ожидаемый результат: 25.05.2024 12:00
*/
export function format(time, template, locale) {
assertOzTime(time);
if (typeof template !== 'string' || template.trim() === '') {
throw new TypeError('format: template must be a non-empty string');
}
const usedLocale = locale ?? time.getLocale();
const parts = getNumericParts(time, usedLocale);
const year = Number(parts.year);
const month = Number(parts.month);
const day = Number(parts.day);
const hour24 = Number(parts.hour);
const minute = Number(parts.minute);
const second = Number(parts.second);
const millisecond = new Date(time.getTimestamp()).getUTCMilliseconds();
const hour12base = hour24 % 12;
const hour12 = hour12base === 0 ? 12 : hour12base;
const meridiem = hour24 >= 12 ? 'PM' : 'AM';
const tokens = {
YYYY: String(year),
YY: String(year).slice(-2),
MMMM: getMonthName(time, usedLocale, 'long'),
MMM: getMonthName(time, usedLocale, 'short'),
MM: pad(month),
M: String(month),
dddd: getWeekdayName(time, usedLocale, 'long'),
ddd: getWeekdayName(time, usedLocale, 'short'),
DD: pad(day),
D: String(day),
HH: pad(hour24),
H: String(hour24),
hh: pad(hour12),
h: String(hour12),
mm: pad(minute),
ss: pad(second),
SSS: pad(millisecond, 3),
A: meridiem,
};
const tokenPattern = /YYYY|MMMM|MMM|MM|M|dddd|ddd|DD|D|HH|H|hh|h|mm|ss|SSS|YY|A/g;
return template.replace(tokenPattern, (token) => tokens[token] ?? token);
}