@ethisyscore/core-utils 1.87.0 → 1.88.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,260 @@
1
+ import { parseISO, isValid, format } from 'date-fns';
2
+
3
+ // src/date/constants.ts
4
+ var DATE_FORMAT = "yyyy-MM-dd";
5
+ var MS_PER_MINUTE = 6e4;
6
+
7
+ // src/date/format.ts
8
+ var relativeTimePhrase = (diffMinutes) => {
9
+ if (diffMinutes < 1) return "Just now";
10
+ if (diffMinutes < 60) return `${diffMinutes}m ago`;
11
+ const hours = Math.floor(diffMinutes / 60);
12
+ if (hours < 24) return `${hours}h ago`;
13
+ const days = Math.floor(hours / 24);
14
+ if (days < 7) return `${days}d ago`;
15
+ return null;
16
+ };
17
+ var formatDateSafe = (date, formatStr = DATE_FORMAT, fallback = "\u2014") => {
18
+ if (!date) return fallback;
19
+ try {
20
+ const dateObj = typeof date === "string" ? parseISO(date) : date;
21
+ if (!isValid(dateObj)) return fallback;
22
+ return format(dateObj, formatStr);
23
+ } catch {
24
+ return fallback;
25
+ }
26
+ };
27
+
28
+ // src/date/formatValidation.ts
29
+ var SEPARATOR_CHARS = " ,./-:()";
30
+ var DAY_TOKENS = /* @__PURE__ */ new Set(["d", "dd", "do"]);
31
+ var WEEKDAY_TOKENS = /* @__PURE__ */ new Set(["EEE", "EEEE"]);
32
+ var MONTH_TOKENS = /* @__PURE__ */ new Set(["M", "MM", "MMM", "MMMM"]);
33
+ var YEAR_TOKENS = /* @__PURE__ */ new Set(["yy", "yyyy"]);
34
+ var HOUR12_TOKENS = /* @__PURE__ */ new Set(["h", "hh"]);
35
+ var HOUR24_TOKENS = /* @__PURE__ */ new Set(["H", "HH"]);
36
+ var MINUTE_TOKENS = /* @__PURE__ */ new Set(["m", "mm"]);
37
+ var SECOND_TOKENS = /* @__PURE__ */ new Set(["s", "ss"]);
38
+ var MERIDIEM_TOKEN = "a";
39
+ var DATE_HINTS = {
40
+ D: "d",
41
+ DD: "dd",
42
+ Y: "yyyy",
43
+ YY: "yy",
44
+ YYYY: "yyyy",
45
+ E: "EEE",
46
+ EE: "EEE",
47
+ m: "M",
48
+ mm: "MM"
49
+ };
50
+ var TIME_HINTS = {
51
+ t: "a",
52
+ tt: "a",
53
+ A: "a",
54
+ M: "m",
55
+ MM: "mm"
56
+ };
57
+ var invalid = (error) => ({ valid: false, error });
58
+ var isLetter = (c) => /[a-zA-Z]/.test(c);
59
+ function tokenize(value, isKnownToken, hints) {
60
+ const tokens = [];
61
+ let i = 0;
62
+ while (i < value.length) {
63
+ const c = value[i];
64
+ if (c === "'") {
65
+ i++;
66
+ let closed = false;
67
+ while (i < value.length) {
68
+ if (value[i] === "'") {
69
+ if (i + 1 < value.length && value[i + 1] === "'") {
70
+ i += 2;
71
+ continue;
72
+ }
73
+ closed = true;
74
+ i++;
75
+ break;
76
+ }
77
+ i++;
78
+ }
79
+ if (!closed) return { result: invalid("Unterminated quoted literal."), tokens };
80
+ continue;
81
+ }
82
+ if (isLetter(c)) {
83
+ const start = i;
84
+ while (i < value.length && value[i] === c) i++;
85
+ let run = value.slice(start, i);
86
+ if (run === "d" && value[i] === "o") {
87
+ run = "do";
88
+ i++;
89
+ }
90
+ if (!isKnownToken(run)) {
91
+ const hint = hints[run];
92
+ return {
93
+ result: invalid(hint ? `Unknown token '${run}' \u2014 use '${hint}' instead.` : `Unknown token '${run}'.`),
94
+ tokens
95
+ };
96
+ }
97
+ tokens.push(run);
98
+ continue;
99
+ }
100
+ if (SEPARATOR_CHARS.includes(c)) {
101
+ i++;
102
+ continue;
103
+ }
104
+ return { result: invalid(`Unsupported character '${c}'.`), tokens };
105
+ }
106
+ return { result: { valid: true }, tokens };
107
+ }
108
+ function validateDateFormat(value) {
109
+ if (!value) return { valid: true };
110
+ const { result, tokens } = tokenize(
111
+ value,
112
+ (t) => DAY_TOKENS.has(t) || WEEKDAY_TOKENS.has(t) || MONTH_TOKENS.has(t) || YEAR_TOKENS.has(t),
113
+ DATE_HINTS
114
+ );
115
+ if (!result.valid) return result;
116
+ if (!tokens.some((t) => DAY_TOKENS.has(t))) return invalid("Date format must include a day token (d, dd or do).");
117
+ if (!tokens.some((t) => MONTH_TOKENS.has(t))) {
118
+ return invalid("Date format must include a month token (M, MM, MMM or MMMM).");
119
+ }
120
+ if (!tokens.some((t) => YEAR_TOKENS.has(t))) return invalid("Date format must include a year token (yy or yyyy).");
121
+ return { valid: true };
122
+ }
123
+ function validateTimeFormat(value) {
124
+ if (!value) return { valid: true };
125
+ const { result, tokens } = tokenize(
126
+ value,
127
+ (t) => HOUR12_TOKENS.has(t) || HOUR24_TOKENS.has(t) || MINUTE_TOKENS.has(t) || SECOND_TOKENS.has(t) || t === MERIDIEM_TOKEN,
128
+ TIME_HINTS
129
+ );
130
+ if (!result.valid) return result;
131
+ const hasHour12 = tokens.some((t) => HOUR12_TOKENS.has(t));
132
+ const hasHour24 = tokens.some((t) => HOUR24_TOKENS.has(t));
133
+ if (!hasHour12 && !hasHour24) return invalid("Time format must include an hour token (h, hh, H or HH).");
134
+ if (!tokens.some((t) => MINUTE_TOKENS.has(t))) return invalid("Time format must include a minute token (m or mm).");
135
+ const hasMeridiem = tokens.includes(MERIDIEM_TOKEN);
136
+ if (hasHour12 && !hasMeridiem) return invalid("12-hour time format requires the meridiem token 'a'.");
137
+ if (hasMeridiem && !hasHour12) return invalid("Meridiem token 'a' requires a 12-hour hour token (h or hh).");
138
+ return { valid: true };
139
+ }
140
+
141
+ // src/date/orgFormat.ts
142
+ var ORG_DATE_FALLBACK_FORMAT = "dd/MM/yyyy";
143
+ var ORG_TIME_FALLBACK_FORMAT = "HH:mm";
144
+ var EM_DASH = "\u2014";
145
+ var orgDateFormatRef = null;
146
+ var orgTimeFormatRef = null;
147
+ function getOrgDateFormat() {
148
+ return orgDateFormatRef;
149
+ }
150
+ function getOrgTimeFormat() {
151
+ return orgTimeFormatRef;
152
+ }
153
+ function setOrgFormats(dateFormat, timeFormat) {
154
+ orgDateFormatRef = dateFormat;
155
+ orgTimeFormatRef = timeFormat;
156
+ }
157
+ function resolveOrgDateFormat() {
158
+ const fmt = getOrgDateFormat();
159
+ return fmt && validateDateFormat(fmt).valid ? fmt : ORG_DATE_FALLBACK_FORMAT;
160
+ }
161
+ function resolveOrgTimeFormat() {
162
+ const fmt = getOrgTimeFormat();
163
+ return fmt && validateTimeFormat(fmt).valid ? fmt : ORG_TIME_FALLBACK_FORMAT;
164
+ }
165
+ function formatDate2(dateStr) {
166
+ return formatDateSafe(dateStr, resolveOrgDateFormat(), EM_DASH);
167
+ }
168
+ function formatDateTime(dateStr) {
169
+ const datePart = formatDateSafe(dateStr, resolveOrgDateFormat(), EM_DASH);
170
+ if (datePart === EM_DASH) return EM_DASH;
171
+ return `${datePart} ${formatDateSafe(dateStr, resolveOrgTimeFormat(), EM_DASH)}`;
172
+ }
173
+ function formatTime(timeStr) {
174
+ if (!timeStr) return EM_DASH;
175
+ if (timeStr.includes("T")) {
176
+ return formatDateSafe(timeStr, resolveOrgTimeFormat(), EM_DASH);
177
+ }
178
+ const trimmed = timeStr.trim();
179
+ const parts = /^(\d{2}):(\d{2})(:\d{2})?$/.exec(trimmed);
180
+ if (!parts) return timeStr;
181
+ const anchored = `2000-01-01T${parts[3] ? trimmed : `${trimmed}:00`}`;
182
+ return formatDateSafe(anchored, resolveOrgTimeFormat(), trimmed);
183
+ }
184
+ function formatDateOrPresent(dateStr) {
185
+ if (!dateStr) return "Present";
186
+ return formatDateSafe(dateStr, resolveOrgDateFormat(), "Present");
187
+ }
188
+ function formatRelativeTime(dateStr, now = /* @__PURE__ */ new Date()) {
189
+ if (!dateStr) return EM_DASH;
190
+ const then = new Date(dateStr).getTime();
191
+ if (Number.isNaN(then)) return EM_DASH;
192
+ const diffMinutes = Math.floor((now.getTime() - then) / MS_PER_MINUTE);
193
+ return relativeTimePhrase(diffMinutes) ?? formatDateTime(dateStr);
194
+ }
195
+ function convertTokenRun(run) {
196
+ switch (run[0]) {
197
+ case "y":
198
+ return "Y".repeat(run.length);
199
+ // yyyy -> YYYY, yy -> YY
200
+ case "d":
201
+ return "D".repeat(run.length);
202
+ // dd -> DD, d -> D
203
+ case "a":
204
+ return "A";
205
+ // date-fns AM/PM -> dayjs AM/PM
206
+ case "E":
207
+ return run.length >= 4 ? "dddd" : "ddd";
208
+ default:
209
+ return run;
210
+ }
211
+ }
212
+ function dateFnsToDayjsFormat(fmt) {
213
+ let out = "";
214
+ let i = 0;
215
+ while (i < fmt.length) {
216
+ const ch = fmt[i];
217
+ if (ch === "'") {
218
+ i++;
219
+ let literal = "";
220
+ while (i < fmt.length) {
221
+ if (fmt[i] === "'") {
222
+ if (fmt[i + 1] === "'") {
223
+ literal += "'";
224
+ i += 2;
225
+ continue;
226
+ }
227
+ i++;
228
+ break;
229
+ }
230
+ literal += fmt[i];
231
+ i++;
232
+ }
233
+ out += literal === "" ? "[']" : `[${literal}]`;
234
+ continue;
235
+ }
236
+ if (/[a-zA-Z]/.test(ch)) {
237
+ let run = ch;
238
+ while (i + 1 < fmt.length && fmt[i + 1] === ch) {
239
+ run += fmt[i + 1];
240
+ i++;
241
+ }
242
+ out += convertTokenRun(run);
243
+ i++;
244
+ continue;
245
+ }
246
+ out += ch;
247
+ i++;
248
+ }
249
+ return out;
250
+ }
251
+ function getOrgDateInputFormat() {
252
+ return dateFnsToDayjsFormat(resolveOrgDateFormat());
253
+ }
254
+ function getOrgDateTimeInputFormat() {
255
+ return `${getOrgDateInputFormat()} ${dateFnsToDayjsFormat(resolveOrgTimeFormat())}`;
256
+ }
257
+
258
+ export { ORG_DATE_FALLBACK_FORMAT, ORG_TIME_FALLBACK_FORMAT, dateFnsToDayjsFormat, formatDate2 as formatDate, formatDateOrPresent, formatDateTime, formatRelativeTime, formatTime, getOrgDateFormat, getOrgDateInputFormat, getOrgDateTimeInputFormat, getOrgTimeFormat, resolveOrgDateFormat, resolveOrgTimeFormat, setOrgFormats };
259
+ //# sourceMappingURL=org-format.js.map
260
+ //# sourceMappingURL=org-format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/date/constants.ts","../../src/date/format.ts","../../src/date/formatValidation.ts","../../src/date/orgFormat.ts"],"names":["isValid","format","formatDate"],"mappings":"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","file":"org-format.js","sourcesContent":["/**\n * Shared date/time format tokens (date-fns syntax).\n *\n * These are the canonical formats used across the EthisysCore monolith and\n * plugins. Change a value here for a global locale requirement rather than\n * hardcoding a format string at a call site.\n */\n\n/** ISO wire format `yyyy-MM-dd` — the standard date-only format throughout the app. */\nexport const DATE_FORMAT = \"yyyy-MM-dd\";\n\n/** Audit-log timestamp, e.g. `5 March 2026, 14:30`. */\nexport const AUDIT_DATE_FORMAT = \"d MMMM yyyy, HH:mm\";\n\n/** User-facing date display, e.g. `5 Mar 2026`. */\nexport const DISPLAY_DATE_FORMAT = \"d MMM yyyy\";\n\n/** Weekday + day + month, e.g. `Monday, 5 March`. */\nexport const DAY_DATE_FORMAT = \"EEEE, d MMMM\";\n\n/** Abbreviated day + month, e.g. `Mar 5`. */\nexport const DAY_MONTH_FORMAT = \"MMM d\";\n\n/** Abbreviated day + month + year, e.g. `Mar 5, 2026`. */\nexport const DAY_MONTH_YEAR_FORMAT = \"MMM d, yyyy\";\n\n/** 12-hour time, e.g. `2:30 PM`. */\nexport const TIME_FORMAT = \"h:mm a\";\n\n/** Short datetime, e.g. `Mar 5, 2:30 PM`. */\nexport const SHORT_DATETIME_FORMAT = \"MMM d, h:mm a\";\n\n/** Milliseconds in a day — whole-day duration / diff math (`daysBetweenIso`, axis geometry). */\nexport const MS_PER_DAY = 86_400_000;\n\n/** Milliseconds in an hour — duration math without inline `1000 * 60 * 60`. */\nexport const MS_PER_HOUR = 3_600_000;\n\n/** Milliseconds in a minute — duration / relative-time math without inline `1000 * 60`. */\nexport const MS_PER_MINUTE = 60_000;\n\n/** Fixed numeric UK date, e.g. `05/03/2026`. Deliberately NOT org-driven — form-field / compact contexts. */\nexport const DISPLAY_DATE_SLASH_FORMAT = \"dd/MM/yyyy\";\n\n/** Fixed numeric UK datetime, e.g. `05/03/2026 14:30`. Deliberately NOT org-driven. */\nexport const DISPLAY_DATETIME_SLASH_FORMAT = \"dd/MM/yyyy HH:mm\";\n\n/** Weekday + day + abbreviated month, e.g. `Wed 5 Mar`. */\nexport const DISPLAY_SHORT_DATE_FORMAT = \"EEE d MMM\";\n","import { format, isValid, parseISO } from \"date-fns\";\n\nimport {\n DATE_FORMAT,\n DAY_MONTH_FORMAT,\n DISPLAY_DATE_SLASH_FORMAT,\n DISPLAY_DATETIME_SLASH_FORMAT,\n DISPLAY_SHORT_DATE_FORMAT,\n MS_PER_MINUTE,\n} from \"./constants\";\nimport { formatDate, isDateOnlyString, parseIsoDateLocal } from \"./iso\";\n\n/**\n * FALLBACK CONVENTION: a display formatter given falsy/unparseable input returns\n * the em-dash placeholder {@link EM_DASH} so callers can render unconditionally.\n * (Wire/parse helpers in `iso.ts` / `wire.ts` return `\"\"`/`undefined` instead so\n * the value is omitted rather than shown; `formatDuration` likewise uses the\n * em-dash for missing/invalid input, but `0m` for a real zero.) The one exception\n * is {@link formatDateWithOrdinal}, whose \"Not specified\" / \"Invalid date\" are\n * deliberate, richer domain messages.\n */\nexport const EM_DASH = \"—\";\n\n/**\n * Shared body for the fixed (not org-driven) datetime formatters below: parse an\n * ISO string with the native `Date`, and render it with `fmt` or the em-dash for\n * falsy/invalid input. Not exported — the named formatters are the public surface.\n */\nconst formatFixedDateTime = (value: string | null | undefined, fmt: string): string => {\n if (!value) return EM_DASH;\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? EM_DASH : format(date, fmt);\n};\n\n/**\n * Shared relative-time ladder: maps a whole-minute age to a short phrase\n * (\"Just now\", \"5m ago\", \"3h ago\", \"2d ago\"), or `null` for anything a week or\n * older so the caller can substitute an absolute date. Backs both\n * {@link formatTimeAgo} here and `formatRelativeTime` in the org-format module.\n */\nexport const relativeTimePhrase = (diffMinutes: number): string | null => {\n if (diffMinutes < 1) return \"Just now\";\n if (diffMinutes < 60) return `${diffMinutes}m ago`;\n const hours = Math.floor(diffMinutes / 60);\n if (hours < 24) return `${hours}h ago`;\n const days = Math.floor(hours / 24);\n if (days < 7) return `${days}d ago`;\n return null;\n};\n\n/**\n * Formats a date-only `yyyy-MM-dd` string in the user's LOCAL timezone as the fixed\n * numeric UK date `dd/MM/yyyy`. Parses via {@link parseIsoDateLocal} to avoid the\n * `new Date(\"yyyy-MM-dd\")` UTC-midnight parse that renders as the previous day west\n * of UTC. Returns the original string when it is not a valid date-only value.\n * Deliberately fixed (not org-driven) — for form-field / compact contexts.\n */\nexport const formatSlashDate = (dateString: string): string => {\n const parsed = parseIsoDateLocal(dateString);\n return parsed ? format(parsed, DISPLAY_DATE_SLASH_FORMAT) : dateString;\n};\n\n/**\n * Formats an ISO datetime string as the fixed numeric UK datetime `dd/MM/yyyy HH:mm`\n * for compact display contexts such as table columns. Returns em-dash for missing or\n * invalid values. Deliberately fixed (not org-driven) — distinct from the org-aware\n * `formatDateTime` in `@ethisyscore/core-utils/date/org-format`.\n */\nexport const formatCompactDateTime = (value?: string | null): string =>\n formatFixedDateTime(value, DISPLAY_DATETIME_SLASH_FORMAT);\n\n/**\n * Formats an ISO date/datetime string as a short weekday date `EEE d MMM` (e.g. `Wed 5 Mar`).\n * Returns em-dash for missing or invalid values. Deliberately fixed (not org-driven).\n */\nexport const formatShortDate = (value?: string | null): string =>\n formatFixedDateTime(value, DISPLAY_SHORT_DATE_FORMAT);\n\n/**\n * Formats an ISO date/datetime string as an abbreviated month-day `MMM d` (e.g. `Mar 5`).\n * Intended for chart axis ticks where vertical space is limited. Returns em-dash for\n * missing or invalid values. Deliberately fixed (not org-driven).\n */\nexport const formatMonthDay = (value?: string | null): string =>\n formatFixedDateTime(value, DAY_MONTH_FORMAT);\n\n/**\n * Safely formats a date string or `Date`. Handles null, undefined, and invalid\n * dates by returning a fallback string.\n * @param date - The date to format (string, Date, null, or undefined)\n * @param formatStr - The desired output format (defaults to `DATE_FORMAT`)\n * @param fallback - Returned when the date is invalid or missing (defaults to `\"—\"`)\n */\nexport const formatDateSafe = (\n date: string | Date | null | undefined,\n formatStr: string = DATE_FORMAT,\n fallback: string = \"—\",\n): string => {\n if (!date) return fallback;\n try {\n const dateObj = typeof date === \"string\" ? parseISO(date) : date;\n if (!isValid(dateObj)) return fallback;\n return format(dateObj, formatStr);\n } catch {\n return fallback;\n }\n};\n\n/** Returns the ordinal suffix for a day of month (`st`, `nd`, `rd`, `th`). */\nconst getOrdinalSuffix = (day: number): string => {\n if (day > 3 && day < 21) return \"th\";\n switch (day % 10) {\n case 1:\n return \"st\";\n case 2:\n return \"nd\";\n case 3:\n return \"rd\";\n default:\n return \"th\";\n }\n};\n\n/**\n * Formats a date string or `Date` to a format like `31st Aug 2025`.\n * Handles date-only strings (`yyyy-MM-dd`) as local dates to avoid timezone shifts.\n * @param date - Date string (`yyyy-MM-dd`) or `Date`\n * @returns Formatted date string with ordinal suffix\n */\nexport const formatDateWithOrdinal = (date: string | Date | null | undefined): string => {\n if (!date) return \"Not specified\";\n\n let dateObj: Date;\n if (typeof date === \"string\") {\n if (isDateOnlyString(date)) {\n const [year, month, day] = date.split(\"-\").map(Number);\n dateObj = new Date(year, month - 1, day);\n } else {\n dateObj = parseISO(date);\n }\n } else {\n dateObj = date;\n }\n\n if (!isValid(dateObj)) return \"Invalid date\";\n\n const day = dateObj.getDate();\n const month = format(dateObj, \"MMM\");\n const year = dateObj.getFullYear();\n return `${day}${getOrdinalSuffix(day)} ${month} ${year}`;\n};\n\n/**\n * Formats a date as a relative time string (e.g. `2h ago`, `Just now`).\n * Optimised for short labels in dropdowns; anything older than a week falls\n * back to the standard date format.\n * @param date - ISO string or `Date`\n */\nexport const formatTimeAgo = (date: string | Date | null | undefined): string => {\n if (!date) return \"\";\n\n const dateObj = typeof date === \"string\" ? parseISO(date) : date;\n if (!isValid(dateObj)) return \"\";\n\n const diffMinutes = Math.floor((new Date().getTime() - dateObj.getTime()) / MS_PER_MINUTE);\n // A week or older falls back to the plain ISO date (this helper's fixed contract);\n // the org-aware sibling `formatRelativeTime` falls back to the org datetime instead.\n return relativeTimePhrase(diffMinutes) ?? formatDate(dateObj);\n};\n","/**\n * Structural (token-grammar) validator for date/time format strings — a framework-agnostic\n * mirror of the CoreConnect API's `DateFnsFormatValidator` (CoreConnect.Application.Common).\n * Accepts any combination of the supported date-fns tokens (a deliberate subset — see the token\n * sets below), separators and quoted literals (so users can define custom formats), while unknown\n * tokens — incl. .NET-style DD/YYYY/tt, which date-fns either throws on or silently renders as\n * garbage — are rejected with a targeted hint.\n *\n * PARITY CONTRACT: the canonical test vectors in `__tests__/formatValidation.test.ts` are\n * duplicated verbatim in the API's `DateFnsFormatValidatorTests.cs`. Error message strings are\n * part of the contract — change them in lockstep across both repos.\n */\n\nexport interface FormatValidationResult {\n valid: boolean;\n error?: string;\n}\n\nconst SEPARATOR_CHARS = \" ,./-:()\";\n\nconst DAY_TOKENS = new Set([\"d\", \"dd\", \"do\"]);\nconst WEEKDAY_TOKENS = new Set([\"EEE\", \"EEEE\"]);\nconst MONTH_TOKENS = new Set([\"M\", \"MM\", \"MMM\", \"MMMM\"]);\nconst YEAR_TOKENS = new Set([\"yy\", \"yyyy\"]);\nconst HOUR12_TOKENS = new Set([\"h\", \"hh\"]);\nconst HOUR24_TOKENS = new Set([\"H\", \"HH\"]);\nconst MINUTE_TOKENS = new Set([\"m\", \"mm\"]);\nconst SECOND_TOKENS = new Set([\"s\", \"ss\"]);\nconst MERIDIEM_TOKEN = \"a\";\n\n/** Common wrong-token → right-token hints for DATE formats (.NET / minute-vs-month mixups). */\nconst DATE_HINTS: Record<string, string> = {\n D: \"d\",\n DD: \"dd\",\n Y: \"yyyy\",\n YY: \"yy\",\n YYYY: \"yyyy\",\n E: \"EEE\",\n EE: \"EEE\",\n m: \"M\",\n mm: \"MM\",\n};\n\n/** Common wrong-token → right-token hints for TIME formats (.NET meridiem / month-vs-minute mixups). */\nconst TIME_HINTS: Record<string, string> = {\n t: \"a\",\n tt: \"a\",\n A: \"a\",\n M: \"m\",\n MM: \"mm\",\n};\n\nconst invalid = (error: string): FormatValidationResult => ({ valid: false, error });\n\nconst isLetter = (c: string): boolean => /[a-zA-Z]/.test(c);\n\n/**\n * Walks the format string collecting letter-run tokens. Quoted literals ('...', with '' as the\n * escaped apostrophe) and separator characters are skipped; any other character, or a letter run\n * not in `isKnownToken`, fails with the canonical error (with a hint when the mixup is known).\n */\nfunction tokenize(\n value: string,\n isKnownToken: (token: string) => boolean,\n hints: Record<string, string>,\n): { result: FormatValidationResult; tokens: string[] } {\n const tokens: string[] = [];\n let i = 0;\n\n while (i < value.length) {\n const c = value[i];\n\n if (c === \"'\") {\n i++;\n let closed = false;\n while (i < value.length) {\n if (value[i] === \"'\") {\n if (i + 1 < value.length && value[i + 1] === \"'\") {\n i += 2; // '' inside a literal = escaped apostrophe\n continue;\n }\n closed = true;\n i++;\n break;\n }\n i++;\n }\n if (!closed) return { result: invalid(\"Unterminated quoted literal.\"), tokens };\n continue;\n }\n\n if (isLetter(c)) {\n const start = i;\n while (i < value.length && value[i] === c) i++;\n let run = value.slice(start, i);\n\n // date-fns ordinal day-of-month is the two-letter token 'do' — the only\n // mixed-letter token the grammar supports.\n if (run === \"d\" && value[i] === \"o\") {\n run = \"do\";\n i++;\n }\n\n if (!isKnownToken(run)) {\n const hint = hints[run];\n return {\n result: invalid(hint ? `Unknown token '${run}' — use '${hint}' instead.` : `Unknown token '${run}'.`),\n tokens,\n };\n }\n\n tokens.push(run);\n continue;\n }\n\n if (SEPARATOR_CHARS.includes(c)) {\n i++;\n continue;\n }\n\n return { result: invalid(`Unsupported character '${c}'.`), tokens };\n }\n\n return { result: { valid: true }, tokens };\n}\n\n/**\n * Validates a DATE format string: only supported date-fns date tokens, separators and quoted\n * literals; must contain a day, a month and a year token. Nullish or empty input is valid\n * (means \"unset / use the caller's default\"), mirroring the API's `string?` overload — a\n * non-empty whitespace-only string is NOT treated as empty and fails the day-token check, matching\n * the server's `string.IsNullOrEmpty` semantics.\n */\nexport function validateDateFormat(value: string | null | undefined): FormatValidationResult {\n if (!value) return { valid: true };\n\n const { result, tokens } = tokenize(\n value,\n (t) => DAY_TOKENS.has(t) || WEEKDAY_TOKENS.has(t) || MONTH_TOKENS.has(t) || YEAR_TOKENS.has(t),\n DATE_HINTS,\n );\n if (!result.valid) return result;\n\n if (!tokens.some((t) => DAY_TOKENS.has(t))) return invalid(\"Date format must include a day token (d, dd or do).\");\n if (!tokens.some((t) => MONTH_TOKENS.has(t))) {\n return invalid(\"Date format must include a month token (M, MM, MMM or MMMM).\");\n }\n if (!tokens.some((t) => YEAR_TOKENS.has(t))) return invalid(\"Date format must include a year token (yy or yyyy).\");\n\n return { valid: true };\n}\n\n/**\n * Validates a TIME format string: only supported date-fns time tokens, separators and quoted\n * literals; must contain an hour and a minute token; 12-hour tokens require the meridiem token\n * 'a' (and vice versa) so an ambiguous 12h-without-AM/PM can never be stored. Nullish or empty\n * input is valid (means \"unset / use the caller's default\"), mirroring the API's `string?`\n * overload; a non-empty whitespace-only string is NOT treated as empty and fails the hour-token check.\n */\nexport function validateTimeFormat(value: string | null | undefined): FormatValidationResult {\n if (!value) return { valid: true };\n\n const { result, tokens } = tokenize(\n value,\n (t) =>\n HOUR12_TOKENS.has(t) ||\n HOUR24_TOKENS.has(t) ||\n MINUTE_TOKENS.has(t) ||\n SECOND_TOKENS.has(t) ||\n t === MERIDIEM_TOKEN,\n TIME_HINTS,\n );\n if (!result.valid) return result;\n\n const hasHour12 = tokens.some((t) => HOUR12_TOKENS.has(t));\n const hasHour24 = tokens.some((t) => HOUR24_TOKENS.has(t));\n\n if (!hasHour12 && !hasHour24) return invalid(\"Time format must include an hour token (h, hh, H or HH).\");\n if (!tokens.some((t) => MINUTE_TOKENS.has(t))) return invalid(\"Time format must include a minute token (m or mm).\");\n\n const hasMeridiem = tokens.includes(MERIDIEM_TOKEN);\n if (hasHour12 && !hasMeridiem) return invalid(\"12-hour time format requires the meridiem token 'a'.\");\n if (hasMeridiem && !hasHour12) return invalid(\"Meridiem token 'a' requires a 12-hour hour token (h or hh).\");\n\n return { valid: true };\n}\n","/**\n * Organisation-driven date/time DISPLAY formatting.\n *\n * Plugin (and host) surfaces render dates in the organisation's configured\n * format. This module holds the module-level format refs, the org-aware display\n * formatters, and the MUI X (dayjs) input-mask derivations. It is pure — no React,\n * no dayjs — so it lives in core-utils behind the\n * `@ethisyscore/core-utils/date/org-format` sub-path rather than the main barrel,\n * whose `formatDate` is the ISO `(date: Date) => string` helper, a different\n * contract from the org-aware `formatDate` exported here.\n *\n * The formatters do NOT re-implement parse/format/fallback: they resolve the\n * effective format (the org ref when grammar-valid, else the UK fallback) and\n * delegate to the shared `formatDateSafe` primitive. The refs are stamped by the\n * surface sync hook — the SDK `useOrgFormatSync` reads the\n * `ethisys:organisation-settings` reserved resource and calls `setOrgFormats`.\n * When no org format is set (or a stored value fails the shared token grammar)\n * formatters fall back to the deterministic UK default, NOT the OS locale, so\n * output is machine-stable regardless of the host.\n *\n * Consolidated from the per-plugin `adapter/shared/{orgFormatContextValue,dates}`\n * copies and the monolith's `dateUtils` mask helpers, which re-export from here\n * so every existing import path keeps working.\n */\nimport { MS_PER_MINUTE } from \"./constants\";\nimport { formatDateSafe, relativeTimePhrase } from \"./format\";\nimport { validateDateFormat, validateTimeFormat } from \"./formatValidation\";\n\n/** UK display fallback (date-fns tokens) — numeric `dd/MM/yyyy`. */\nexport const ORG_DATE_FALLBACK_FORMAT = \"dd/MM/yyyy\";\n/** UK time display fallback (date-fns tokens) — 24-hour `HH:mm`. */\nexport const ORG_TIME_FALLBACK_FORMAT = \"HH:mm\";\n\nconst EM_DASH = \"—\";\n\n// ---------------------------------------------------------------------------\n// Module-level format refs\n// ---------------------------------------------------------------------------\n\nlet orgDateFormatRef: string | null = null;\nlet orgTimeFormatRef: string | null = null;\n\n/** The org's preferred date-fns date format, or null to fall back to the UK default. */\nexport function getOrgDateFormat(): string | null {\n return orgDateFormatRef;\n}\n\n/** The org's preferred date-fns time format, or null to fall back to the UK default. */\nexport function getOrgTimeFormat(): string | null {\n return orgTimeFormatRef;\n}\n\n/**\n * Stamps the module-level date/time format refs. Called by the surface sync hook\n * each time organisation settings resolve. Pass `null` for either argument to\n * clear that format and fall back to the deterministic UK default.\n */\nexport function setOrgFormats(dateFormat: string | null, timeFormat: string | null): void {\n orgDateFormatRef = dateFormat;\n orgTimeFormatRef = timeFormat;\n}\n\n// ---------------------------------------------------------------------------\n// Effective format resolution\n//\n// The grammar gate (validateDateFormat/validateTimeFormat) is what catches the\n// `tt` gotcha — `tt` is date-fns's seconds-timestamp token, not .NET AM/PM, so it\n// renders garbage rather than throwing — before a bad stored format reaches\n// date-fns. A non-grammar (legacy .NET) token therefore degrades to the UK default.\n// ---------------------------------------------------------------------------\n\n/** The org's effective date format: the configured override when grammar-valid, else the UK `dd/MM/yyyy` fallback. */\nexport function resolveOrgDateFormat(): string {\n const fmt = getOrgDateFormat();\n return fmt && validateDateFormat(fmt).valid ? fmt : ORG_DATE_FALLBACK_FORMAT;\n}\n\n/** The org's effective time format: the configured override when grammar-valid, else the UK `HH:mm` fallback. */\nexport function resolveOrgTimeFormat(): string {\n const fmt = getOrgTimeFormat();\n return fmt && validateTimeFormat(fmt).valid ? fmt : ORG_TIME_FALLBACK_FORMAT;\n}\n\n// ---------------------------------------------------------------------------\n// Org-aware display formatters (delegate to formatDateSafe)\n// ---------------------------------------------------------------------------\n\n/**\n * Formats a date/ISO string for display in the org's dateFormat (UK `dd/MM/yyyy`\n * fallback) — never the OS locale. Returns em-dash for falsy/invalid.\n */\nexport function formatDate(dateStr?: string | null): string {\n return formatDateSafe(dateStr, resolveOrgDateFormat(), EM_DASH);\n}\n\n/**\n * Formats a date/ISO string with time in the org's date+time formats (UK\n * `dd/MM/yyyy HH:mm` fallback). Returns em-dash for falsy/invalid.\n */\nexport function formatDateTime(dateStr?: string | null): string {\n const datePart = formatDateSafe(dateStr, resolveOrgDateFormat(), EM_DASH);\n if (datePart === EM_DASH) return EM_DASH;\n return `${datePart} ${formatDateSafe(dateStr, resolveOrgTimeFormat(), EM_DASH)}`;\n}\n\n/**\n * Extracts the time portion from an ISO datetime string in the org's timeFormat\n * (UK `HH:mm` fallback). A bare `HH:mm`/`HH:mm:ss` value (no date component) is\n * anchored to a fixed date and rendered in the org's time format too — a 12-hour\n * org turns `14:30` into `2:30 PM`, it is NOT passed straight through. Returns\n * em-dash for falsy values, and the original string for anything that is neither\n * ISO nor a bare `HH:mm(:ss)`.\n */\nexport function formatTime(timeStr?: string | null): string {\n if (!timeStr) return EM_DASH;\n if (timeStr.includes(\"T\")) {\n return formatDateSafe(timeStr, resolveOrgTimeFormat(), EM_DASH);\n }\n // A bare \"HH:mm\" / \"HH:mm:ss\" carries no date, so anchor it to a fixed date and\n // render it in the org's time format — a 12-hour org format must still turn\n // \"14:30\" into \"2:30 PM\", not pass the 24-hour value straight through. Match,\n // anchor and fall back on ONE trimmed value so a padded input (\" 14:30 \") is\n // converted too rather than building an invalid anchored string that silently\n // bypasses the org conversion.\n const trimmed = timeStr.trim();\n const parts = /^(\\d{2}):(\\d{2})(:\\d{2})?$/.exec(trimmed);\n if (!parts) return timeStr;\n const anchored = `2000-01-01T${parts[3] ? trimmed : `${trimmed}:00`}`;\n return formatDateSafe(anchored, resolveOrgTimeFormat(), trimmed);\n}\n\n/**\n * Formats a date/ISO string in the org's dateFormat (UK fallback), or returns\n * `\"Present\"` for falsy/invalid values. Use for open-ended ranges where a missing\n * end date means \"still active\" — distinct from {@link formatDate} which returns em-dash.\n */\nexport function formatDateOrPresent(dateStr?: string | null): string {\n if (!dateStr) return \"Present\";\n return formatDateSafe(dateStr, resolveOrgDateFormat(), \"Present\");\n}\n\n/**\n * Returns a short relative-time phrase — \"Just now\", \"5m ago\", \"3h ago\", \"2d ago\" —\n * or the absolute org-formatted datetime for anything older than a week. Returns\n * em-dash for falsy/unparseable values. `now` is injectable for tests.\n *\n * The org-aware sibling of core-utils `formatTimeAgo`, which shares the tier ladder\n * but falls back to the ISO date (not the org format) and returns \"\" for empty.\n */\nexport function formatRelativeTime(dateStr?: string | null, now: Date = new Date()): string {\n if (!dateStr) return EM_DASH;\n const then = new Date(dateStr).getTime();\n if (Number.isNaN(then)) return EM_DASH;\n const diffMinutes = Math.floor((now.getTime() - then) / MS_PER_MINUTE);\n // Shares the tier ladder with core-utils `formatTimeAgo`; the only difference is\n // the older-than-a-week fallback (org datetime here vs the plain ISO date there).\n return relativeTimePhrase(diffMinutes) ?? formatDateTime(dateStr);\n}\n\n// ---------------------------------------------------------------------------\n// MUI X (dayjs) input-mask derivations\n// ---------------------------------------------------------------------------\n\n/** Converts one letter-run token from date-fns to dayjs (families that differ; others pass through). */\nfunction convertTokenRun(run: string): string {\n switch (run[0]) {\n case \"y\":\n return \"Y\".repeat(run.length); // yyyy -> YYYY, yy -> YY\n case \"d\":\n return \"D\".repeat(run.length); // dd -> DD, d -> D\n case \"a\":\n return \"A\"; // date-fns AM/PM -> dayjs AM/PM\n case \"E\":\n // date-fns weekday widths -> dayjs: full (EEEE) -> dddd, short (E-EEE) -> ddd.\n return run.length >= 4 ? \"dddd\" : \"ddd\";\n default:\n return run; // M, H, h, m, s and any other letter run are identical\n }\n}\n\n/**\n * Converts a date-fns format string to the equivalent dayjs tokens. Only four token\n * families differ — year (`y`->`Y`), day-of-month (`d`->`D`), weekday (`E`->`d…`)\n * and meridiem (`a`->`A`); month, hour, minute and second are identical.\n *\n * Quoted literals are handled properly rather than blindly regex-replaced: date-fns\n * escapes literal text with single quotes (`dd 'day' MMM` renders a literal \"day\"),\n * which must NOT have its letters rewritten and must become a dayjs bracket literal\n * (`[day]`); `''` is a literal apostrophe.\n */\nexport function dateFnsToDayjsFormat(fmt: string): string {\n let out = \"\";\n let i = 0;\n while (i < fmt.length) {\n const ch = fmt[i];\n if (ch === \"'\") {\n // Consume a quoted literal span; '' inside it is an escaped apostrophe.\n i++;\n let literal = \"\";\n while (i < fmt.length) {\n if (fmt[i] === \"'\") {\n if (fmt[i + 1] === \"'\") {\n literal += \"'\";\n i += 2;\n continue;\n }\n i++;\n break;\n }\n literal += fmt[i];\n i++;\n }\n out += literal === \"\" ? \"[']\" : `[${literal}]`;\n continue;\n }\n if (/[a-zA-Z]/.test(ch)) {\n let run = ch;\n while (i + 1 < fmt.length && fmt[i + 1] === ch) {\n run += fmt[i + 1];\n i++;\n }\n out += convertTokenRun(run);\n i++;\n continue;\n }\n out += ch;\n i++;\n }\n return out;\n}\n\n/**\n * The org's date mask as dayjs tokens for a MUI X date picker — the effective\n * date format converted to dayjs tokens, so the UK fallback yields `DD/MM/YYYY`.\n * Mirrors {@link formatDate} so an editable input shows the read-only format.\n */\nexport function getOrgDateInputFormat(): string {\n return dateFnsToDayjsFormat(resolveOrgDateFormat());\n}\n\n/**\n * The org's date+time mask as dayjs tokens for a datetime picker, combining\n * {@link getOrgDateInputFormat} with the effective time format (UK fallback yields `HH:mm`).\n */\nexport function getOrgDateTimeInputFormat(): string {\n return `${getOrgDateInputFormat()} ${dateFnsToDayjsFormat(resolveOrgTimeFormat())}`;\n}\n"]}
package/dist/index.cjs CHANGED
@@ -12,6 +12,12 @@ var DAY_MONTH_FORMAT = "MMM d";
12
12
  var DAY_MONTH_YEAR_FORMAT = "MMM d, yyyy";
13
13
  var TIME_FORMAT = "h:mm a";
14
14
  var SHORT_DATETIME_FORMAT = "MMM d, h:mm a";
15
+ var MS_PER_DAY = 864e5;
16
+ var MS_PER_HOUR = 36e5;
17
+ var MS_PER_MINUTE = 6e4;
18
+ var DISPLAY_DATE_SLASH_FORMAT = "dd/MM/yyyy";
19
+ var DISPLAY_DATETIME_SLASH_FORMAT = "dd/MM/yyyy HH:mm";
20
+ var DISPLAY_SHORT_DATE_FORMAT = "EEE d MMM";
15
21
  var isDateOnlyString = (value) => {
16
22
  return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value);
17
23
  };
@@ -21,6 +27,19 @@ var formatDate = (date, dateFormat = DATE_FORMAT) => {
21
27
  var getTodayIsoDate = () => {
22
28
  return formatDate(/* @__PURE__ */ new Date());
23
29
  };
30
+ var toIsoDate = (date) => formatDate(date);
31
+ var startOfMonthOffset = (date, months) => {
32
+ if (!date || !dateFns.isValid(date)) {
33
+ return /* @__PURE__ */ new Date(NaN);
34
+ }
35
+ return new Date(date.getFullYear(), date.getMonth() + months, 1);
36
+ };
37
+ var daysBetweenIso = (fromIso, toIso) => {
38
+ const from = parseIsoDateLocal(fromIso);
39
+ const to = parseIsoDateLocal(toIso);
40
+ if (!from || !to) return NaN;
41
+ return Math.round((to.getTime() - from.getTime()) / MS_PER_DAY);
42
+ };
24
43
  var getMonthStartIso = (date) => formatDate(new Date(date.getFullYear(), date.getMonth(), 1));
25
44
  var getMonthEndIso = (date) => formatDate(new Date(date.getFullYear(), date.getMonth() + 1, 0));
26
45
  var parseIsoDateLocal = (value) => {
@@ -35,17 +54,39 @@ var addDaysToIsoDate = (isoDate, days) => {
35
54
  base.setDate(base.getDate() + days);
36
55
  return formatDate(base);
37
56
  };
57
+ var addYearsToIsoDate = (isoDate, years) => {
58
+ const base = parseIsoDateLocal(isoDate);
59
+ if (!base) return isoDate;
60
+ base.setFullYear(base.getFullYear() + years);
61
+ return formatDate(base);
62
+ };
38
63
  var toDateOnlyString = (value) => {
39
64
  if (!value) return "";
40
65
  const datepart = value.split("T")[0];
41
66
  return isDateOnlyString(datepart) ? datepart : "";
42
67
  };
43
- var formatDateString = (date, dateFormat = DATE_FORMAT) => {
44
- if (!date) return "-";
45
- const parsed = dateFns.parseISO(date);
46
- if (!dateFns.isValid(parsed)) return "-";
47
- return dateFns.format(parsed, dateFormat);
68
+ var EM_DASH = "\u2014";
69
+ var formatFixedDateTime = (value, fmt) => {
70
+ if (!value) return EM_DASH;
71
+ const date = new Date(value);
72
+ return Number.isNaN(date.getTime()) ? EM_DASH : dateFns.format(date, fmt);
73
+ };
74
+ var relativeTimePhrase = (diffMinutes) => {
75
+ if (diffMinutes < 1) return "Just now";
76
+ if (diffMinutes < 60) return `${diffMinutes}m ago`;
77
+ const hours = Math.floor(diffMinutes / 60);
78
+ if (hours < 24) return `${hours}h ago`;
79
+ const days = Math.floor(hours / 24);
80
+ if (days < 7) return `${days}d ago`;
81
+ return null;
82
+ };
83
+ var formatSlashDate = (dateString) => {
84
+ const parsed = parseIsoDateLocal(dateString);
85
+ return parsed ? dateFns.format(parsed, DISPLAY_DATE_SLASH_FORMAT) : dateString;
48
86
  };
87
+ var formatCompactDateTime = (value) => formatFixedDateTime(value, DISPLAY_DATETIME_SLASH_FORMAT);
88
+ var formatShortDate = (value) => formatFixedDateTime(value, DISPLAY_SHORT_DATE_FORMAT);
89
+ var formatMonthDay = (value) => formatFixedDateTime(value, DAY_MONTH_FORMAT);
49
90
  var formatDateSafe = (date, formatStr = DATE_FORMAT, fallback = "\u2014") => {
50
91
  if (!date) return fallback;
51
92
  try {
@@ -92,16 +133,8 @@ var formatTimeAgo = (date) => {
92
133
  if (!date) return "";
93
134
  const dateObj = typeof date === "string" ? dateFns.parseISO(date) : date;
94
135
  if (!dateFns.isValid(dateObj)) return "";
95
- const now = /* @__PURE__ */ new Date();
96
- const seconds = Math.floor((now.getTime() - dateObj.getTime()) / 1e3);
97
- if (seconds < 60) return "Just now";
98
- const minutes = Math.floor(seconds / 60);
99
- if (minutes < 60) return `${minutes}m ago`;
100
- const hours = Math.floor(minutes / 60);
101
- if (hours < 24) return `${hours}h ago`;
102
- const days = Math.floor(hours / 24);
103
- if (days < 7) return `${days}d ago`;
104
- return formatDate(dateObj);
136
+ const diffMinutes = Math.floor(((/* @__PURE__ */ new Date()).getTime() - dateObj.getTime()) / MS_PER_MINUTE);
137
+ return relativeTimePhrase(diffMinutes) ?? formatDate(dateObj);
105
138
  };
106
139
 
107
140
  // src/date/formatValidation.ts
@@ -226,42 +259,19 @@ var timespanToMilliseconds = (timespan) => {
226
259
  const seconds = parseInt(s || "0", 10);
227
260
  return (isNaN(hours) ? 0 : hours) * 60 * 60 * 1e3 + (isNaN(minutes) ? 0 : minutes) * 60 * 1e3 + (isNaN(seconds) ? 0 : seconds) * 1e3;
228
261
  };
229
- var millisecondsToHours = (ms) => ms / (1e3 * 60 * 60);
230
- var hoursToMilliseconds = (hours) => hours * (1e3 * 60 * 60);
231
- var formatMillisecondsAsTimeSpent = (ms) => {
232
- if (!ms || ms < 0) return "0h 0min";
233
- const totalMinutes = Math.floor(ms / (1e3 * 60));
234
- const hours = Math.floor(totalMinutes / 60);
235
- const minutes = totalMinutes % 60;
236
- let result = "";
237
- if (hours > 0) result += `${hours}h`;
238
- if (minutes > 0 || hours === 0) result += (hours > 0 ? " " : "") + `${minutes}min`;
239
- return result;
240
- };
241
- function formatDuration(duration) {
242
- if (!duration) return "-";
243
- const [hours, minutes, seconds] = duration.split(":").map(Number);
244
- let result = "";
245
- if (hours) result += `${hours}h`;
246
- if (minutes) result += (result ? " " : "") + `${minutes}m`;
247
- if (seconds) result += (result ? " " : "") + `${seconds}s`;
248
- return result || "0m";
249
- }
250
- var formatDurationNumber = (durationHours) => {
251
- if (durationHours == null || durationHours === 0) return "\u2014";
252
- if (durationHours > 8760) return "Unknown";
253
- if (durationHours < 0) return "Error";
254
- const hours = Math.floor(durationHours);
255
- const minutes = Math.floor(durationHours % 1 * 60);
256
- if (durationHours < 1) return `${minutes}m`;
257
- if (hours < 24) {
258
- if (minutes === 0) return `${hours}h`;
259
- return `${hours}h ${minutes}m`;
260
- }
261
- const days = Math.floor(hours / 24);
262
- const remainingHours = hours % 24;
263
- if (remainingHours === 0) return `${days}d`;
264
- return `${days}d ${remainingHours}h`;
262
+ var millisecondsToHours = (ms) => ms / MS_PER_HOUR;
263
+ var hoursToMilliseconds = (hours) => hours * MS_PER_HOUR;
264
+ var formatDuration = (ms) => {
265
+ if (ms == null || Number.isNaN(ms) || ms < 0) return "\u2014";
266
+ if (ms < MS_PER_MINUTE) return "0m";
267
+ const days = Math.floor(ms / MS_PER_DAY);
268
+ const hours = Math.floor(ms % MS_PER_DAY / MS_PER_HOUR);
269
+ const minutes = Math.floor(ms % MS_PER_HOUR / MS_PER_MINUTE);
270
+ const parts = [];
271
+ if (days) parts.push(`${days}d`);
272
+ if (hours) parts.push(`${hours}h`);
273
+ if (minutes) parts.push(`${minutes}m`);
274
+ return parts.join(" ");
265
275
  };
266
276
  function toHHmm(value, fallback) {
267
277
  if (!value || value.length < 5) return fallback;
@@ -272,19 +282,6 @@ function toHHmmss(value) {
272
282
  if (!trimmed) return null;
273
283
  return trimmed.length === 5 ? `${trimmed}:00` : trimmed;
274
284
  }
275
- var isoToDateInput = (isoString) => {
276
- if (!isoString) return "";
277
- try {
278
- return new Date(isoString).toISOString().split("T")[0];
279
- } catch {
280
- return "";
281
- }
282
- };
283
- var dateInputToIso = (dateString) => {
284
- if (!dateString) return "";
285
- const parsed = /* @__PURE__ */ new Date(dateString + "T00:00:00Z");
286
- return Number.isNaN(parsed.getTime()) ? "" : parsed.toISOString();
287
- };
288
285
  function dateOnlyToIsoUtc(value) {
289
286
  if (!value) return void 0;
290
287
  const parsed = value.includes("T") ? new Date(value) : /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
@@ -482,24 +479,33 @@ exports.DATE_FORMAT = DATE_FORMAT;
482
479
  exports.DAY_DATE_FORMAT = DAY_DATE_FORMAT;
483
480
  exports.DAY_MONTH_FORMAT = DAY_MONTH_FORMAT;
484
481
  exports.DAY_MONTH_YEAR_FORMAT = DAY_MONTH_YEAR_FORMAT;
482
+ exports.DISPLAY_DATETIME_SLASH_FORMAT = DISPLAY_DATETIME_SLASH_FORMAT;
485
483
  exports.DISPLAY_DATE_FORMAT = DISPLAY_DATE_FORMAT;
484
+ exports.DISPLAY_DATE_SLASH_FORMAT = DISPLAY_DATE_SLASH_FORMAT;
485
+ exports.DISPLAY_SHORT_DATE_FORMAT = DISPLAY_SHORT_DATE_FORMAT;
486
+ exports.EM_DASH = EM_DASH;
487
+ exports.MS_PER_DAY = MS_PER_DAY;
488
+ exports.MS_PER_HOUR = MS_PER_HOUR;
489
+ exports.MS_PER_MINUTE = MS_PER_MINUTE;
486
490
  exports.SHORT_DATETIME_FORMAT = SHORT_DATETIME_FORMAT;
487
491
  exports.TIME_FORMAT = TIME_FORMAT;
488
492
  exports.addDaysToIsoDate = addDaysToIsoDate;
493
+ exports.addYearsToIsoDate = addYearsToIsoDate;
489
494
  exports.applyJsonPatch = applyJsonPatch;
490
495
  exports.computeDateRange = computeDateRange;
491
496
  exports.currentMonthRangeUtc = currentMonthRangeUtc;
492
- exports.dateInputToIso = dateInputToIso;
493
497
  exports.dateOnlyToIsoUtc = dateOnlyToIsoUtc;
498
+ exports.daysBetweenIso = daysBetweenIso;
494
499
  exports.deepClone = deepClone;
495
500
  exports.ensureUtcIso = ensureUtcIso;
501
+ exports.formatCompactDateTime = formatCompactDateTime;
496
502
  exports.formatDate = formatDate;
497
503
  exports.formatDateSafe = formatDateSafe;
498
- exports.formatDateString = formatDateString;
499
504
  exports.formatDateWithOrdinal = formatDateWithOrdinal;
500
505
  exports.formatDuration = formatDuration;
501
- exports.formatDurationNumber = formatDurationNumber;
502
- exports.formatMillisecondsAsTimeSpent = formatMillisecondsAsTimeSpent;
506
+ exports.formatMonthDay = formatMonthDay;
507
+ exports.formatShortDate = formatShortDate;
508
+ exports.formatSlashDate = formatSlashDate;
503
509
  exports.formatTimeAgo = formatTimeAgo;
504
510
  exports.generateJsonPatch = generateJsonPatch;
505
511
  exports.getCurrentOffsetMinutes = getCurrentOffsetMinutes;
@@ -511,16 +517,18 @@ exports.hasPatches = hasPatches;
511
517
  exports.hoursToMilliseconds = hoursToMilliseconds;
512
518
  exports.isDateOnlyString = isDateOnlyString;
513
519
  exports.isNullOrEmpty = isNullOrEmpty;
514
- exports.isoToDateInput = isoToDateInput;
515
520
  exports.isoUtcToLocalDateTimeInput = isoUtcToLocalDateTimeInput;
516
521
  exports.localDateTimeInputToIsoUtc = localDateTimeInputToIsoUtc;
517
522
  exports.millisecondsToHours = millisecondsToHours;
518
523
  exports.nowLocalDateTimeInputValue = nowLocalDateTimeInputValue;
519
524
  exports.parseIsoDateLocal = parseIsoDateLocal;
525
+ exports.relativeTimePhrase = relativeTimePhrase;
526
+ exports.startOfMonthOffset = startOfMonthOffset;
520
527
  exports.timespanToMilliseconds = timespanToMilliseconds;
521
528
  exports.toDateOnlyString = toDateOnlyString;
522
529
  exports.toHHmm = toHHmm;
523
530
  exports.toHHmmss = toHHmmss;
531
+ exports.toIsoDate = toIsoDate;
524
532
  exports.unwrapList = unwrapList;
525
533
  exports.validateDateFormat = validateDateFormat;
526
534
  exports.validateTimeFormat = validateTimeFormat;