UNPKG

@formatjs/intl-datetimeformat

Version:
192 lines (191 loc) 6.02 kB
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; }