@thi.ng/date
Version:
Datetime types, relative dates, math, iterators, composable formatters, locales
272 lines (271 loc) • 7.44 kB
JavaScript
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
};