import { parse, add, sub, type Locale } from 'date-fns'; import { toZonedTime, fromZonedTime, formatInTimeZone } from 'date-fns-tz'; import { SYSTEM_TZ, UNITS_MAP, type IntlDateDisplayFormatEnum, type IntlDateSourceFormatEnum } from './constants'; import { getLocale, createConfig } from './lib'; import type { IntlDateConfig, IntlDateInstanceInterface, IntlDateMethods, IntlDateLocaleMap, IntlDateLanguage, InitIntlDateServiceConfig, } from './lib'; class IntlDateService { private _locale: Locale | undefined; private _date: Date; private _config: IntlDateConfig; private _logger?: Console; constructor(config: IntlDateConfig, date: Date, logger?: Console) { this._config = config; this._logger = logger; const initialLang = config.localeMap[config.language]; this._locale = initialLang?.locale || (initialLang ? getLocale(initialLang.code) : undefined); this._date = fromZonedTime(date, 'UTC'); } _call(date: Date): this { this._date = fromZonedTime(date, 'UTC'); return this; } public setSupportedLanguages(supportedLanguages: IntlDateLocaleMap): void { this._logger?.log('Set supported languages: ', supportedLanguages); this._config.localeMap = supportedLanguages; } public setLocaleGlobally(lang: IntlDateLanguage): void { const language = this._config.localeMap[lang]; if (!language) { this._logger?.error('Language is not supported. Add it to the supported list.'); return; } this._locale = language.locale ?? getLocale(language.code); this._config.language = lang; this._logger?.log('Set global language: ', lang); } private _getSourceFormat(fmt: IntlDateSourceFormatEnum): string { return this._config.sourceFormat[fmt]; } private _getDisplayFormat(fmt: IntlDateDisplayFormatEnum): string { const localeFormat = this._config.localeMap[this._config.language]?.displayFormats?.[fmt]; if (localeFormat) return localeFormat; return this._config.displayFormat[fmt]; } private _parseDate(dateStr: string, formatStr: string, options?: { strict?: boolean; lang?: IntlDateLanguage }): Date { if (formatStr === 'UNIVERSAL_ISO') { return new Date(dateStr); } const locale = this._config.localeMap[options?.lang ?? this._config.language]?.locale; const parsedLocal = parse(dateStr, formatStr, new Date(), { locale: locale, }); return fromZonedTime(parsedLocal, SYSTEM_TZ); } private _formatDate(date: Date, pattern: string, displayLocal?: boolean): string { if (pattern === 'UNIVERSAL_ISO') { return displayLocal ? toZonedTime(date, SYSTEM_TZ).toISOString() : date.toISOString(); } if (!displayLocal) { return formatInTimeZone(date, 'UTC', pattern, { locale: this._locale, }); } return formatInTimeZone(date, SYSTEM_TZ, pattern, { locale: this._locale, }); } public parseSource: IntlDateMethods['parseSource'] = (date, fmt, options) => { this._date = this._parseDate(date, this._getSourceFormat(fmt), options); return this; }; public parseDisplay: IntlDateMethods['parseDisplay'] = (date, fmt, options) => { this._date = this._parseDate(date, this._getDisplayFormat(fmt), options); return this; }; public formatToDisplay: IntlDateMethods['formatToDisplay'] = (fmt, options) => { try { const pattern = this._getDisplayFormat(fmt); return this._formatDate(this._date, pattern, options?.displayLocal); } catch { return 'Invalid date'; } }; public formatToSource: IntlDateMethods['formatToSource'] = (fmt, options) => { try { const pattern = this._getSourceFormat(fmt); return this._formatDate(this._date, pattern, options?.displayLocal); } catch { return 'Invalid date'; } }; public add: IntlDateMethods['add'] = (value, unit) => { const duration = Object.assign(UNITS_MAP, { [unit]: value }); const result = add(this._date, duration); this._date = fromZonedTime(result, 'UTC'); return this; }; public subtract: IntlDateMethods['subtract'] = (value, unit) => { const duration = Object.assign(UNITS_MAP, { [unit]: value }); const result = sub(this._date, duration); this._date = fromZonedTime(result, 'UTC'); return this; }; public toDate = () => this._date; public setNow = () => { this._date = fromZonedTime(new Date(), 'UTC'); return this; }; public getMonthShort = (index: number): string => { const d = new Date(Date.UTC(2020, index, 1)); return formatInTimeZone(d, 'UTC', 'MMM', { locale: this._locale }); }; public getMonthLong = (index: number): string => { const d = new Date(Date.UTC(2020, index, 1)); return formatInTimeZone(d, 'UTC', 'MMMM', { locale: this._locale }); }; } export const intlDateService = (config?: InitIntlDateServiceConfig, logger?: Console): IntlDateInstanceInterface => { const instance = new IntlDateService(createConfig(config), new Date(), logger); const methods: IntlDateMethods = { parseSource: instance.parseSource.bind(instance), parseDisplay: instance.parseDisplay.bind(instance), formatToDisplay: instance.formatToDisplay.bind(instance), formatToSource: instance.formatToSource.bind(instance), toDate: instance.toDate.bind(instance), subtract: instance.subtract.bind(instance), add: instance.add.bind(instance), setNow: instance.setNow.bind(instance), setSupportedLanguages: instance.setSupportedLanguages.bind(instance), setLocaleGlobally: instance.setLocaleGlobally.bind(instance), getMonthShort: instance.getMonthShort.bind(instance), getMonthLong: instance.getMonthLong.bind(instance), }; return Object.assign((date: Date) => instance._call(date), methods); };