source ⟩ app ⟩
custom-date-format.js
const englishWeekdayNames = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
];
const dateMatcherString = "GyMdE";
const dateMatcherArray = ["era", "year", "month", "day", "weekday"];
const lettersToParts = {
G: "era",
y: "year",
M: "month",
L: "standaloneMonth",
d: "day",
E: "weekday",
h: "hour",
H: "periodHour",
m: "minute",
s: "second",
B: "period",
v: "timeZone"
};
const customDateFormat = (parts, intlFallback, numHelpers) => {
const numeric =
numHelpers?.generic ??
new Intl.NumberFormat(intlFallback, { useGrouping: false });
const twoDigits =
numHelpers?.twoDigits ??
new Intl.NumberFormat(intlFallback, {
minimumIntegerDigits: 2
});
const twoDigitsDecimal =
numHelpers?.twoDigitsDecimal ??
(dp =>
new Intl.NumberFormat(intlFallback, {
minimumIntegerDigits: 2,
minimumFractionDigits: dp
}));
return class {
constructor(options) {
this.options = {
hour12: options?.hour12,
timeZone: options?.timeZone ?? "Europe/London"
};
if ("dateStyle" in options) {
this.options.dateStyle = options.dateStyle;
this.options.year = "numeric";
this.options.month =
options.dateStyle == "short"
? "2-digit"
: options.dateStyle == "medium"
? "short"
: "long";
this.options.day = "2-digit";
this.options.weekday =
options.dateStyle == "full" ? "long" : undefined;
}
if ("timeStyle" in options) {
this.options.hour = "2-digit";
this.options.minute = "2-digit";
this.options.second =
options.timeStyle == "short" ? undefined : "2-digit";
this.options.timeZoneName =
options.timeStyle == "full"
? "long"
: options.timeStyle == "long"
? "short"
: undefined;
}
Object.assign(this.options, options);
this.matchedFormat = {
date: undefined,
time: undefined,
joiner: undefined
};
if (dateMatcherArray.some(el => this.options?.[el])) {
this.matchedFormat.date = dateMatcherString.slice(
dateMatcherArray.findIndex(el => this.options?.[el]),
dateMatcherArray.findLastIndex(el => this.options?.[el]) + 1
);
}
if (this.options?.hour) {
this.matchedFormat.time = "h";
if (this.options?.minute) {
this.matchedFormat.time += "m";
if (this.options?.second) this.matchedFormat.time += "s";
}
if (this.options?.hour12) this.matchedFormat.time += "B";
if (this.options?.timeZoneName) this.matchedFormat.time += "v";
}
if (this.matchedFormat?.date && this.matchedFormat?.time) {
this.matchedFormat.joiner =
this.options?.dateStyle ?? this.options?.month == "long"
? "long"
: "short";
}
this.intlReferenceFormat = new Intl.DateTimeFormat("en-GB", {
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric",
weekday: "long",
month: "numeric",
year: "numeric",
era: "short",
fractionalSecondDigits: this.options.fractionalSecondDigits
});
}
getPart(letter, refParts) {
let keyToSearch, value;
switch (letter) {
case "G":
return (
parts.texts.era?.[this.options?.era]?.[value] ??
parts.texts.era.short[value]
);
case "B":
return parts.texts.hour12[+(refParts.hour >= 12)];
case "H":
value = refParts.hour - 12 || 12;
return this.options?.hour == "numeric"
? numeric.format(value)
: twoDigits.format(value);
case "y":
return this.options?.year == "2-digits"
? twoDigits.format(refParts.year)
: numeric.format(refParts.year);
case "s":
return this.options.fractionalSecondDigits
? twoDigitsDecimal.format(refParts.second)
: this.options?.second == "numeric"
? numeric.format(refParts.second)
: twoDigits.format(refParts.second);
case "d":
case "h":
case "m":
keyToSearch = lettersToParts[letter];
value = refParts[keyToSearch];
return this.options?.[keyToSearch] == "numeric"
? numeric.format(value)
: twoDigits.format(value);
case "M":
case "L":
keyToSearch =
letter == "L" && "standaloneMonth" in parts.texts
? "standaloneMonth"
: "month";
value = refParts.month - 1;
return this.options?.month == "numeric"
? numeric.format(value)
: this.options?.month == "2-digits"
? twoDigits.format(value)
: parts.texts[keyToSearch]?.[this.options?.month]?.[
value
] ?? parts.texts[keyToSearch].long[value];
case "E":
value = refParts.weekday;
return (
parts.texts.weekday?.[this.options?.weekday]?.[value] ??
parts.texts.weekday.long[value]
);
case "v":
return this.options.timeZone;
default:
break;
}
}
format(date) {
const intlFormattedParts = Object.fromEntries(
this.intlReferenceFormat
.formatToParts(date)
.filter(el => el.type != "literal")
.map(el => [
el.type,
el.type == "weekday"
? englishWeekdayNames.indexOf(el.value)
: el.type == "era"
? !el.value.startsWith("B")
: +el.value
])
);
const dateTemplate = this.matchedFormat?.date
? parts.date[this.matchedFormat.date]
: null;
const timeTemplate = this.matchedFormat?.time
? parts.time[this.matchedFormat.time]
: null;
const formatTemplate = this.matchedFormat?.joiner
? parts.joiner[this.matchedFormat.joiner]
.replace("{1}", dateTemplate)
.replace("{0}", timeTemplate)
: dateTemplate || timeTemplate;
return formatTemplate.replace(
/\{([GyMLdEhHmsBv])\}/g,
(_, letter) => this.getPart(letter, intlFormattedParts)
);
}
};
};
export default customDateFormat;