Система форматирования в Luxon основана на шаблонах токенов — специальных последовательностях символов, описывающих способ представления даты и времени. Стандартный набор токенов покрывает большинство задач: форматирование дней, месяцев, часов, временных зон, миллисекунд и других компонентов. Однако в крупных приложениях часто возникает необходимость создавать собственные соглашения форматирования и переиспользовать их как единый стандарт.
Под регистрацией пользовательских токенов обычно понимают создание слоя абстракции над стандартными форматами Luxon: пользовательские маски, словари токенов, фабрики форматирования и механизмы централизованного преобразования шаблонов.
Luxon не предоставляет API наподобие:
DateTime.registerToken(...)
или:
Settings.addFormatToken(...)
Новые токены напрямую в движок Luxon добавить нельзя. Библиотека использует фиксированный набор шаблонов, совместимых с ICU-форматированием.
Например:
DateTime.now().toFormat("dd.MM.yyyy")
Поддерживаемые токены интерпретируются самим Luxon.
Попытка использовать неизвестный токен:
DateTime.now().toFormat("QQQ")
не создаёт новый форматтер — строка будет обработана как литерал или приведёт к неожиданному результату.
Поэтому пользовательские токены реализуются через:
DateTime.Наиболее распространённый подход — хранение собственных токенов в объекте-конфигурации.
import { DateTime } from "luxon";
const customTokens = {
SHORT_DATE: "dd.MM.yyyy",
SHORT_TIME: "HH:mm",
FULL_DATE: "dd LLLL yyyy",
ISO_DATE: "yyyy-MM-dd"
};
function format(date, token) {
return date.toFormat(customTokens[token]);
}
const now = DateTime.now();
console.log(format(now, "SHORT_DATE"));
console.log(format(now, "FULL_DATE"));
Такой подход создаёт единый слой стандартизации.
Изменение формата производится в одном месте:
const customTokens = {
SHORT_DATE: "yyyy/MM/dd"
};
Все вызовы автоматически используют новую маску.
Без пользовательских токенов одинаковые строки форматирования дублируются:
date.toFormat("dd.MM.yyyy")
Во множестве файлов.
С пользовательскими токенами:
format(date, "SHORT_DATE")
Особенно важно в:
Более продвинутый вариант — преобразование пользовательских обозначений в стандартные Luxon-форматы.
import { DateTime } from "luxon";
const aliases = {
YYYY: "yyyy",
DD: "dd",
MM: "LL",
HH: "HH",
mm: "mm"
};
function compileFormat(format) {
let compiled = format;
for (const token in aliases) {
compiled = compiled.replaceAll(token, aliases[token]);
}
return compiled;
}
function formatDate(date, format) {
return date.toFormat(compileFormat(format));
}
const now = DateTime.now();
console.log(formatDate(now, "DD.MM.YYYY"));
console.log(formatDate(now, "YYYY/MM/DD HH:mm"));
При миграции с Moment.js часто возникает проблема несовместимости токенов.
Например:
| Moment.js | Luxon |
|---|---|
| YYYY | yyyy |
| DD | dd |
| dddd | cccc |
| A | a |
Создание промежуточного слоя позволяет сохранить старые форматы.
const momentToLuxon = {
YYYY: "yyyy",
YY: "yy",
DD: "dd",
D: "d",
MM: "LL",
MMMM: "LLLL",
dddd: "cccc",
HH: "HH",
mm: "mm",
ss: "ss"
};
function convertMomentFormat(format) {
let result = format;
Object.entries(momentToLuxon).forEach(([key, value]) => {
result = result.replaceAll(key, value);
});
return result;
}
При замене токенов необходимо учитывать порядок обработки.
{
YY: "yy",
YYYY: "yyyy"
}
Если сначала заменить YY, формат:
YYYY-MM-DD
может превратиться в:
yyyyyy-MM-DD
Сначала заменяются длинные токены.
const orderedTokens = [
["YYYY", "yyyy"],
["YY", "yy"]
];
function compile(format) {
const tokens = [
["YYYY", "yyyy"],
["YY", "yy"],
["DD", "dd"],
["MM", "LL"]
];
let result = format;
for (const [from, to] of tokens) {
result = result.replaceAll(from, to);
}
return result;
}
В крупных проектах форматирование обычно инкапсулируется.
import { DateTime } from "luxon";
class DateFormatter {
constructor() {
this.tokens = new Map();
}
register(name, format) {
this.tokens.set(name, format);
}
format(date, token) {
const format = this.tokens.get(token);
if (!format) {
throw new Error(`Unknown token: ${token}`);
}
return date.toFormat(format);
}
}
const formatter = new DateFormatter();
formatter.register("API_DATE", "yyyy-MM-dd");
formatter.register("UI_DATE", "dd.MM.yyyy");
const now = DateTime.now();
console.log(formatter.format(now, "UI_DATE"));
Токены могут загружаться:
{
"DATE_SHORT": "dd.MM.yyyy",
"DATE_LONG": "dd LLLL yyyy",
"TIME_SHORT": "HH:mm"
}
import config from "./formats.json";
class Formatter {
constructor(tokens) {
this.tokens = tokens;
}
format(date, token) {
return date.toFormat(this.tokens[token]);
}
}
const formatter = new Formatter(config);
Один и тот же пользовательский токен может иметь разные форматы в зависимости от локали.
const localeFormats = {
ru: {
SHORT: "dd.MM.yyyy"
},
en: {
SHORT: "MM/dd/yyyy"
}
};
function format(date, token, locale) {
const formatString = localeFormats[locale][token];
return date
.setLocale(locale)
.toFormat(formatString);
}
Пользовательские токены особенно полезны при работе с часовыми поясами.
const formats = {
LOG_TIME: "yyyy-MM-dd HH:mm:ss ZZZZ",
UTC_TIME: "yyyy-MM-dd'T'HH:mm:ss'Z'"
};
const now = DateTime.now().setZone("UTC");
console.log(now.toFormat(formats.LOG_TIME));
Luxon предоставляет встроенные пресеты:
DateTime.DATE_SHORT
DateTime.DATE_MED
DateTime.DATE_FULL
Но часто требуется собственный набор.
const presets = {
REPORT: {
year: "numeric",
month: "long",
day: "2-digit"
},
COMPACT: {
year: "2-digit",
month: "2-digit",
day: "2-digit"
}
};
function formatPreset(date, preset) {
return date.toLocaleString(presets[preset]);
}
Токены могут состоять из других токенов.
const tokens = {
DATE: "dd.MM.yyyy",
TIME: "HH:mm:ss",
DATETIME: "{DATE} {TIME}"
};
function resolve(format, dictionary) {
return format.replace(/\{(.*?)\}/g, (_, token) => {
return dictionary[token];
});
}
const result = resolve(tokens.DATETIME, tokens);
console.log(result);
Получится:
dd.MM.yyyy HH:mm:ss
Для сложных систем требуется рекурсивная обработка.
function resolveRecursive(format, dictionary) {
return format.replace(/\{(.*?)\}/g, (_, token) => {
return resolveRecursive(dictionary[token], dictionary);
});
}
Постоянная компиляция форматов может создавать лишние накладные расходы.
class TokenCompiler {
constructor() {
this.cache = new Map();
}
compile(format) {
if (this.cache.has(format)) {
return this.cache.get(format);
}
const compiled = format
.replaceAll("YYYY", "yyyy")
.replaceAll("DD", "dd");
this.cache.set(format, compiled);
return compiled;
}
}
Ошибочные токены могут приводить к некорректному выводу.
function validateToken(name, dictionary) {
if (!dictionary[name]) {
throw new Error(`Token "${name}" is not registered`);
}
}
При рекурсивных токенах возможны циклы.
const tokens = {
A: "{B}",
B: "{A}"
};
function resolve(format, dictionary, visited = new Set()) {
return format.replace(/\{(.*?)\}/g, (_, token) => {
if (visited.has(token)) {
throw new Error("Circular token reference");
}
visited.add(token);
return resolve(dictionary[token], dictionary, visited);
});
}
TypeScript позволяет типизировать пользовательские токены.
type FormatToken =
| "SHORT_DATE"
| "LONG_DATE"
| "TIME";
function format(
date: DateTime,
token: FormatToken
) {
return date.toFormat(formats[token]);
}
enum Tokens {
SHORT = "SHORT",
LONG = "LONG"
}
const formats: Record<Tokens, string> = {
[Tokens.SHORT]: "dd.MM.yyyy",
[Tokens.LONG]: "dd LLLL yyyy"
};
Хотя напрямую расширять DateTime не рекомендуется, можно
создавать собственные адаптеры.
class AppDate {
constructor(date) {
this.date = date;
}
format(token) {
return this.date.toFormat(customTokens[token]);
}
}
Современный JavaScript позволяет создавать динамические API.
const formats = {
short: "dd.MM.yyyy",
time: "HH:mm"
};
const formatter = new Proxy(formats, {
get(target, prop) {
return DateTime.now().toFormat(target[prop]);
}
});
console.log(formatter.short);
В серверном рендеринге важно:
function createFormatter(locale) {
return {
format(date, token) {
return date
.setLocale(locale)
.toFormat(tokens[token]);
}
};
}
export const formats = {
CARD_DATE: "dd.MM.yyyy",
CARD_TIME: "HH:mm"
};
function DateLabel({ value }) {
return (
<span>
{value.toFormat(formats.CARD_DATE)}
</span>
);
}
const logFormats = {
FILE: "yyyy-MM-dd HH:mm:ss",
CONSOLE: "HH:mm:ss"
};
function log(message) {
const timestamp = DateTime.now()
.toFormat(logFormats.FILE);
console.log(`[${timestamp}] ${message}`);
}
В больших проектах токены обычно разделяются по доменам.
formats/
ui.js
api.js
reports.js
logs.js
export const uiFormats = {
SHORT_DATE: "dd.MM.yyyy",
TIME: "HH:mm"
};
export const apiFormats = {
ISO: "yyyy-MM-dd'T'HH:mm:ss"
};
Плохо:
DATE1
FORMAT2
Хорошо:
USER_PROFILE_DATE
INVOICE_TIMESTAMP
LOG_ENTRY_TIME
Нежелательно:
"dd.MM.yyyy"
во множестве мест проекта.
UI-форматы ориентированы на читаемость:
dd.MM.yyyy
API-форматы — на стандартизацию:
yyyy-MM-dd'T'HH:mm:ss
Luxon предоставляет готовые методы:
toISO()
toISODate()
toISOTime()
Они надёжнее пользовательских шаблонов для обмена данными.
Ошибка:
DateTime.now().toFormat("YYYY-MM-DD")
Правильно:
DateTime.now().toFormat("yyyy-LL-dd")
Некоторые сокращённые обозначения могут вести себя неоднозначно в разных локалях.
Хаотичные строки форматирования усложняют поддержку приложения.
Нежелательно:
date.setLocale("ru")
внутри универсальных утилит.
Локаль должна передаваться извне.
class FormatRegistry {
constructor() {
this.formats = new Map();
}
register(name, pattern) {
this.formats.set(name, pattern);
}
get(name) {
return this.formats.get(name);
}
format(date, token) {
const pattern = this.get(token);
if (!pattern) {
throw new Error(`Unknown token: ${token}`);
}
return date.toFormat(pattern);
}
}
const registry = new FormatRegistry();
registry.register(
"USER_DATE",
"dd.MM.yyyy"
);
registry.register(
"ADMIN_TIMESTAMP",
"yyyy-MM-dd HH:mm:ss"
);
const result = registry.format(
DateTime.now(),
"ADMIN_TIMESTAMP"
);
console.log(result);