@formatjs/intl-datetimeformat
Version:
Intl.DateTimeFormat polyfill
192 lines (191 loc) • 6.02 kB
JavaScript
import { TimeClip, createMemoizedNumberFormat } from "@formatjs/ecma402-abstract";
import Decimal from "decimal.js";
import { ToLocalTime } from "./ToLocalTime.js";
import { DATE_TIME_PROPS } from "./utils.js";
function pad(n) {
if (n < 10) {
return `0${n}`;
}
return String(n);
}
function offsetToGmtString(gmtFormat, hourFormat, offsetInMs, style) {
const offsetInMinutes = Math.floor(offsetInMs / 6e4);
const mins = Math.abs(offsetInMinutes) % 60;
const hours = Math.floor(Math.abs(offsetInMinutes) / 60);
const [positivePattern, negativePattern] = hourFormat.split(";");
let offsetStr = "";
let pattern = offsetInMs < 0 ? negativePattern : positivePattern;
if (style === "long") {
offsetStr = pattern.replace("HH", pad(hours)).replace("H", String(hours)).replace("mm", pad(mins)).replace("m", String(mins));
} else if (mins || hours) {
if (!mins) {
pattern = pattern.replace(/:?m+/, "");
}
offsetStr = pattern.replace(/H+/, String(hours)).replace(/m+/, String(mins));
}
return gmtFormat.replace("{0}", offsetStr);
}
/**
* https://tc39.es/ecma402/#sec-partitiondatetimepattern
* @param dtf
* @param x
*/
export function FormatDateTimePattern(dtf, patternParts, x, { getInternalSlots, localeData, getDefaultTimeZone, tzData }) {
x = TimeClip(x);
/** IMPL START */
const internalSlots = getInternalSlots(dtf);
const dataLocale = internalSlots.dataLocale;
const dataLocaleData = localeData[dataLocale];
/** IMPL END */
const locale = internalSlots.locale;
const nfOptions = Object.create(null);
nfOptions.useGrouping = false;
const nf = createMemoizedNumberFormat(locale, nfOptions);
const nf2Options = Object.create(null);
nf2Options.minimumIntegerDigits = 2;
nf2Options.useGrouping = false;
const nf2 = createMemoizedNumberFormat(locale, nf2Options);
const fractionalSecondDigits = internalSlots.fractionalSecondDigits;
let nf3;
if (fractionalSecondDigits !== undefined) {
const nf3Options = Object.create(null);
nf3Options.minimumIntegerDigits = fractionalSecondDigits;
nf3Options.useGrouping = false;
nf3 = createMemoizedNumberFormat(locale, nf3Options);
}
const tm = ToLocalTime(
x,
// @ts-ignore
internalSlots.calendar,
internalSlots.timeZone,
{ tzData }
);
const result = [];
// Check if month is stand-alone (no other date fields like day, year, weekday)
const hasMonth = patternParts.some((part) => part.type === "month");
const hasOtherDateFields = patternParts.some((part) => part.type === "day" || part.type === "year" || part.type === "weekday" || part.type === "era");
const isMonthStandalone = hasMonth && !hasOtherDateFields;
for (const patternPart of patternParts) {
const p = patternPart.type;
if (p === "literal") {
result.push({
type: "literal",
value: patternPart.value
});
} else if (p === "fractionalSecondDigits") {
const v = new Decimal(tm.millisecond).times(10).pow((fractionalSecondDigits || 0) - 3).floor().toNumber();
result.push({
type: "fractionalSecond",
value: nf3.format(v)
});
} else if (p === "dayPeriod") {
const f = internalSlots.dayPeriod;
// @ts-ignore
const fv = tm[f];
result.push({
type: p,
value: fv
});
} else if (p === "timeZoneName") {
const f = internalSlots.timeZoneName;
let fv;
const { timeZoneName, gmtFormat, hourFormat } = dataLocaleData;
const timeZone = internalSlots.timeZone || getDefaultTimeZone();
const timeZoneData = timeZoneName[timeZone];
if (timeZoneData && timeZoneData[f]) {
const names = timeZoneData[f];
// GH #5114: If in DST and both standard/daylight names are the same,
// fall back to GMT offset format (matches native browser behavior).
// This handles locales where CLDR doesn't provide a daylight name.
// NOTE: This is a formatjs implementation detail - ECMA-402 doesn't
// explicitly specify behavior for missing DST names in locale data.
if (tm.inDST && names.length >= 2 && names[0] === names[1]) {
fv = offsetToGmtString(gmtFormat, hourFormat, tm.timeZoneOffset, f);
} else {
fv = names[+tm.inDST];
}
} else {
// Fallback to gmtFormat
fv = offsetToGmtString(gmtFormat, hourFormat, tm.timeZoneOffset, f);
}
result.push({
type: p,
value: fv
});
} else if (DATE_TIME_PROPS.indexOf(p) > -1) {
let fv = "";
const f = internalSlots[p];
// @ts-ignore
let v = tm[p];
if (p === "year" && v <= 0) {
v = 1 - v;
}
if (p === "month") {
v++;
}
const hourCycle = internalSlots.hourCycle;
if (p === "hour" && (hourCycle === "h11" || hourCycle === "h12")) {
v = v % 12;
if (v === 0 && hourCycle === "h12") {
v = 12;
}
}
if (p === "hour" && hourCycle === "h24") {
if (v === 0) {
v = 24;
}
}
if (f === "numeric") {
fv = nf.format(v);
} else if (f === "2-digit") {
fv = nf2.format(v);
if (fv.length > 2) {
fv = fv.slice(fv.length - 2, fv.length);
}
} else if (f === "narrow" || f === "short" || f === "long") {
if (p === "era") {
fv = dataLocaleData[p][f][v];
} else if (p === "month") {
// Use stand-alone month form if available and month is displayed alone
const monthData = isMonthStandalone && dataLocaleData.monthStandalone ? dataLocaleData.monthStandalone : dataLocaleData.month;
fv = monthData[f][v - 1];
} else {
fv = dataLocaleData[p][f][v];
}
}
result.push({
type: p,
value: fv
});
} else if (p === "ampm") {
const v = tm.hour;
let fv;
if (v > 11) {
fv = dataLocaleData.pm;
} else {
fv = dataLocaleData.am;
}
result.push({
type: "dayPeriod",
value: fv
});
} else if (p === "relatedYear") {
const v = tm.relatedYear;
// @ts-ignore
const fv = nf.format(v);
result.push({
type: "relatedYear",
value: fv
});
} else if (p === "yearName") {
const v = tm.yearName;
// @ts-ignore
const fv = nf.format(v);
result.push({
type: "yearName",
value: fv
});
}
}
return result;
}