UNPKG

@thi.ng/date

Version:

Datetime types, relative dates, math, iterators, composable formatters, locales

272 lines (271 loc) 7.44 kB
import { isFunction } from "@thi.ng/checks/is-function"; import { isString } from "@thi.ng/checks/is-string"; import { Z2, Z3, Z4 } from "@thi.ng/strings/pad-left"; import { DAY, HOUR, MINUTE, MONTH, SECOND, YEAR } from "./api.js"; import { ensureDate, ensureEpoch } from "./checks.js"; import { decomposeDuration } from "./duration.js"; import { LOCALE, tense, units, unitsLessThan } from "./i18n.js"; import { __idToPrecision, __precisionToID } from "./internal/precision.js"; import { decomposeDifference, difference } from "./relative.js"; import { weekInYear } from "./units.js"; const FORMATTERS = { /** * Full year (4 digits) */ yyyy: (d) => Z4(d.getFullYear()), /** * Short year (2 digits, e.g. `2020 % 100` => 20) */ yy: (d) => Z2(d.getFullYear() % 100), /** * Month name, using current {@link LOCALE} (e.g. `Feb`) */ MMM: (d) => LOCALE.months[d.getMonth()], /** * Zero-padded 2-digit month */ MM: (d) => Z2(d.getMonth() + 1), /** * Unpadded month */ M: (d) => String(d.getMonth() + 1), /** * Zero-padded 2-digit day of month */ dd: (d) => Z2(d.getDate()), /** * Unpadded day of month */ d: (d) => String(d.getDate()), /** * Weekday name, using current {@link LOCALE} (e.g. `Mon`) */ E: (d) => LOCALE.days[d.getDay()], /** * Zero-padded 2-digit ISO week number. */ ww: (d) => Z2(FORMATTERS.w(d, false)), /** * Unpadded ISO week number. */ w: (d) => String(weekInYear(d.getFullYear(), d.getMonth(), d.getDate())), /** * Unpadded quarter: * * - 1 = Jan - Mar * - 2 = Apr - Jun * - 3 = Jul - Sep * - 4 = Oct - Dec */ q: (d) => String((d.getMonth() / 3 | 0) + 1), /** * Zero-padded 2-digit hour of day (0-23) */ HH: (d) => Z2(d.getHours()), /** * Unpadded our of day (0-23) */ H: (d) => String(d.getHours()), /** * Zero-padded hour of day (1-12) */ hh: (d) => { const h = d.getHours() % 12; return Z2(h > 0 ? h : 12); }, /** * Unpadded hour of day (1-12) */ h: (d) => { const h = d.getHours() % 12; return String(h > 0 ? h : 12); }, /** * Zero-padded 2-digit minute of hour */ mm: (d) => Z2(d.getMinutes()), /** * Unpadded minute of hour */ m: (d) => String(d.getMinutes()), /** * Zero-padded 2-digit second of minute */ ss: (d) => Z2(d.getSeconds()), /** * Unpadded second of minute */ s: (d) => String(d.getSeconds()), /** * Zero-padded 3-digit millisecond of second */ SS: (d) => Z3(d.getMilliseconds()), /** * Unpadded millisecond of second */ S: (d) => String(d.getMilliseconds()), /** * 12-hour AM/PM marker (uppercase) */ A: (d) => String(d.getHours() < 12 ? "AM" : "PM"), /** * 12-hour am/pm marker (lowercase) */ a: (d) => String(d.getHours() < 12 ? "am" : "pm"), /** * Timezone offset in signed `HH:mm` format */ Z: (d, utc = false) => { const z = utc ? 0 : d.getTimezoneOffset(); const za = Math.abs(z); return `${z < 0 ? "+" : "-"}${Z2(za / 60 | 0)}:${Z2(za % 60)}`; }, /** * Returns literal `"Z"` iff timezone offset is zero (UTC), else the same as * `Z` formatter. * * @param d - */ ZZ: (d, utc = false) => utc ? "Z" : FORMATTERS.Z(d, utc), /** * Current {@link LOCALE}'s day-month separator. */ "/DM": () => LOCALE.sepDM, /** * Current {@link LOCALE}'s weekday-day separator. */ "/ED": () => LOCALE.sepED, /** * Current {@link LOCALE}'s hour-minute separator. */ "/HM": () => LOCALE.sepHM, /** * Current {@link LOCALE}'s month-year separator. */ "/MY": () => LOCALE.sepMY }; const defFormat = (fmt) => (x = Date.now(), utc = false) => { let d = ensureDate(x); utc && (d = new Date(d.getTime() + d.getTimezoneOffset() * MINUTE)); return fmt.map( (x2) => isString(x2) ? x2.startsWith("\\") ? x2.substring(1) : FORMATTERS[x2]?.(d, utc) ?? x2 : isFunction(x2) ? x2(d, utc) : x2 ).join(""); }; const FMT_yyyyMMdd = defFormat(["yyyy", "-", "MM", "-", "dd"]); const FMT_yyyyMMdd_ALT = defFormat(["yyyy", "MM", "dd"]); const FMT_Mdyyyy = defFormat(["M", "/", "d", "/", "yyyy"]); const FMT_MMMdyyyy = defFormat(["MMM", " ", "d", " ", "yyyy"]); const FMT_dMyyyy = defFormat(["d", "~", "M", "~", "yyyy"]); const FMT_dMMMyyyy = defFormat(["d", "~~", "MMM", " ", "yyyy"]); const FMT_HHmm = defFormat(["HH", ":", "mm"]); const FMT_hm = defFormat(["h", ":", "mm", " ", "A"]); const FMT_HHmmss = defFormat(["HH", ":", "mm", ":", "ss"]); const FMT_HHmmss_ALT = defFormat(["HH", "mm", "ss"]); const FMT_hms = defFormat(["h", ":", "mm", ":", "ss", " ", "A"]); const FMT_yyyyMMdd_HHmmss = defFormat( ["yyyy", "MM", "dd", "-", "HH", "mm", "ss"] ); const FMT_ISO_SHORT = defFormat( ["yyyy", "-", "MM", "-", "dd", "T", "HH", ":", "mm", ":", "ss", "ZZ"] ); const FMT_ISO = defFormat( ["yyyy", "-", "MM", "-", "dd", "T", "HH", ":", "mm", ":", "ss", ".", "SS", "ZZ"] ); const FMT_yyyy = defFormat(["yyyy"]); const FMT_MM = defFormat(["MM"]); const FMT_ww = defFormat(["ww"]); const FMT_dd = defFormat(["dd"]); const FMT_HH = defFormat(["HH"]); const FMT_mm = defFormat(["mm"]); const FMT_ss = defFormat(["ss"]); const formatRelative = (date, base = /* @__PURE__ */ new Date(), prec = 0, eps = 100) => { const delta = difference(date, base); if (Math.abs(delta) < eps) return LOCALE.now; let abs = Math.abs(delta); let unit; if (abs < SECOND) { unit = "t"; } else if (abs < MINUTE) { abs /= SECOND; unit = "s"; } else if (abs < HOUR) { abs /= MINUTE; unit = "m"; } else if (abs < DAY) { abs /= HOUR; unit = "h"; } else if (abs < MONTH) { abs /= DAY; unit = "d"; } else if (abs < YEAR) { abs /= MONTH; unit = "M"; } else { abs /= YEAR; unit = "y"; } const exp = 10 ** -prec; abs = Math.round(abs / exp) * exp; return tense(delta, `${abs.toFixed(prec)} ${units(abs, unit, true, true)}`); }; const formatRelativeParts = (date, base = Date.now(), prec = "s", eps = 1e3) => { date = ensureEpoch(date); base = ensureEpoch(base); if (Math.abs(date - base) < eps) return LOCALE.now; const [sign, ...parts] = decomposeDifference(date, base); return tense(sign, formatDurationParts(parts, prec)); }; const formatDuration = (dur, prec = "s") => formatDurationParts(decomposeDuration(dur), prec); const formatDurationParts = (parts, prec = "s") => { const precID = __precisionToID(prec); let maxID = precID; while (!parts[maxID] && maxID > 0) maxID--; let minID = parts.findIndex((x) => x > 0); minID < 0 && (minID = maxID); maxID = Math.min(Math.max(maxID, minID), precID); if (minID <= precID && precID < 6) { parts[maxID] = Math.round( parts[maxID] + parts[maxID + 1] / [12, 31, 24, 60, 60, 1e3][maxID] ); } return parts.slice(0, maxID + 1).map((x, i) => { let unit = LOCALE.units[__idToPrecision(i)]; return x > 0 ? units(x, unit, true) : i === maxID && maxID < 6 ? unitsLessThan(1, unit, true) : ""; }).filter((x) => !!x).join(", "); }; export { FMT_HH, FMT_HHmm, FMT_HHmmss, FMT_HHmmss_ALT, FMT_ISO, FMT_ISO_SHORT, FMT_MM, FMT_MMMdyyyy, FMT_Mdyyyy, FMT_dMMMyyyy, FMT_dMyyyy, FMT_dd, FMT_hm, FMT_hms, FMT_mm, FMT_ss, FMT_ww, FMT_yyyy, FMT_yyyyMMdd, FMT_yyyyMMdd_ALT, FMT_yyyyMMdd_HHmmss, FORMATTERS, defFormat, formatDuration, formatDurationParts, formatRelative, formatRelativeParts };