@ethisyscore/core-utils 1.94.1 → 1.95.0

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.
Files changed (57) hide show
  1. package/dist/chunk-2F5FENXB.js +84 -0
  2. package/dist/chunk-2F5FENXB.js.map +1 -0
  3. package/dist/chunk-3LIIUXBE.js +14 -0
  4. package/dist/chunk-3LIIUXBE.js.map +1 -0
  5. package/dist/chunk-G2XGZH7P.cjs +16 -0
  6. package/dist/chunk-G2XGZH7P.cjs.map +1 -0
  7. package/dist/chunk-GAZLFZEL.js +105 -0
  8. package/dist/chunk-GAZLFZEL.js.map +1 -0
  9. package/dist/chunk-LT7I4JVK.cjs +121 -0
  10. package/dist/chunk-LT7I4JVK.cjs.map +1 -0
  11. package/dist/chunk-OEDAX6WN.js +121 -0
  12. package/dist/chunk-OEDAX6WN.js.map +1 -0
  13. package/dist/chunk-P5FSVRZO.cjs +127 -0
  14. package/dist/chunk-P5FSVRZO.cjs.map +1 -0
  15. package/dist/chunk-RLPXYKSM.cjs +23 -0
  16. package/dist/chunk-RLPXYKSM.cjs.map +1 -0
  17. package/dist/chunk-TN6DXBVD.js +252 -0
  18. package/dist/chunk-TN6DXBVD.js.map +1 -0
  19. package/dist/chunk-YNBKRMK3.cjs +290 -0
  20. package/dist/chunk-YNBKRMK3.cjs.map +1 -0
  21. package/dist/chunk-ZQCGDT7D.js +19 -0
  22. package/dist/chunk-ZQCGDT7D.js.map +1 -0
  23. package/dist/chunk-ZR7TABGT.cjs +94 -0
  24. package/dist/chunk-ZR7TABGT.cjs.map +1 -0
  25. package/dist/date/index.cjs +210 -397
  26. package/dist/date/index.cjs.map +1 -1
  27. package/dist/date/index.js +2 -349
  28. package/dist/date/index.js.map +1 -1
  29. package/dist/date/org-format.cjs +13 -151
  30. package/dist/date/org-format.cjs.map +1 -1
  31. package/dist/date/org-format.js +3 -141
  32. package/dist/date/org-format.js.map +1 -1
  33. package/dist/index.cjs +286 -600
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +6 -539
  38. package/dist/index.js.map +1 -1
  39. package/dist/list/index.cjs +7 -11
  40. package/dist/list/index.cjs.map +1 -1
  41. package/dist/list/index.js +1 -12
  42. package/dist/list/index.js.map +1 -1
  43. package/dist/money/index.cjs +39 -51
  44. package/dist/money/index.cjs.map +1 -1
  45. package/dist/money/index.d.cts +129 -8
  46. package/dist/money/index.d.ts +129 -8
  47. package/dist/money/index.js +1 -49
  48. package/dist/money/index.js.map +1 -1
  49. package/dist/number/index.cjs +15 -18
  50. package/dist/number/index.cjs.map +1 -1
  51. package/dist/number/index.js +1 -17
  52. package/dist/number/index.js.map +1 -1
  53. package/dist/patch/index.cjs +22 -121
  54. package/dist/patch/index.cjs.map +1 -1
  55. package/dist/patch/index.js +1 -119
  56. package/dist/patch/index.js.map +1 -1
  57. package/package.json +1 -1
@@ -1,144 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var dateFns = require('date-fns');
4
-
5
- // src/date/constants.ts
6
- var DATE_FORMAT = "yyyy-MM-dd";
7
- var MS_PER_MINUTE = 6e4;
8
-
9
- // src/date/format.ts
10
- var relativeTimePhrase = (diffMinutes) => {
11
- if (diffMinutes < 1) return "Just now";
12
- if (diffMinutes < 60) return `${diffMinutes}m ago`;
13
- const hours = Math.floor(diffMinutes / 60);
14
- if (hours < 24) return `${hours}h ago`;
15
- const days = Math.floor(hours / 24);
16
- if (days < 7) return `${days}d ago`;
17
- return null;
18
- };
19
- var formatDateSafe = (date, formatStr = DATE_FORMAT, fallback = "\u2014") => {
20
- if (!date) return fallback;
21
- try {
22
- const dateObj = typeof date === "string" ? dateFns.parseISO(date) : date;
23
- if (!dateFns.isValid(dateObj)) return fallback;
24
- return dateFns.format(dateObj, formatStr);
25
- } catch {
26
- return fallback;
27
- }
28
- };
29
-
30
- // src/date/formatValidation.ts
31
- var SEPARATOR_CHARS = " ,./-:()";
32
- var DAY_TOKENS = /* @__PURE__ */ new Set(["d", "dd", "do"]);
33
- var WEEKDAY_TOKENS = /* @__PURE__ */ new Set(["EEE", "EEEE"]);
34
- var MONTH_TOKENS = /* @__PURE__ */ new Set(["M", "MM", "MMM", "MMMM"]);
35
- var YEAR_TOKENS = /* @__PURE__ */ new Set(["yy", "yyyy"]);
36
- var HOUR12_TOKENS = /* @__PURE__ */ new Set(["h", "hh"]);
37
- var HOUR24_TOKENS = /* @__PURE__ */ new Set(["H", "HH"]);
38
- var MINUTE_TOKENS = /* @__PURE__ */ new Set(["m", "mm"]);
39
- var SECOND_TOKENS = /* @__PURE__ */ new Set(["s", "ss"]);
40
- var MERIDIEM_TOKEN = "a";
41
- var DATE_HINTS = {
42
- D: "d",
43
- DD: "dd",
44
- Y: "yyyy",
45
- YY: "yy",
46
- YYYY: "yyyy",
47
- E: "EEE",
48
- EE: "EEE",
49
- m: "M",
50
- mm: "MM"
51
- };
52
- var TIME_HINTS = {
53
- t: "a",
54
- tt: "a",
55
- A: "a",
56
- M: "m",
57
- MM: "mm"
58
- };
59
- var invalid = (error) => ({ valid: false, error });
60
- var isLetter = (c) => /[a-zA-Z]/.test(c);
61
- function tokenize(value, isKnownToken, hints) {
62
- const tokens = [];
63
- let i = 0;
64
- while (i < value.length) {
65
- const c = value[i];
66
- if (c === "'") {
67
- i++;
68
- let closed = false;
69
- while (i < value.length) {
70
- if (value[i] === "'") {
71
- if (i + 1 < value.length && value[i + 1] === "'") {
72
- i += 2;
73
- continue;
74
- }
75
- closed = true;
76
- i++;
77
- break;
78
- }
79
- i++;
80
- }
81
- if (!closed) return { result: invalid("Unterminated quoted literal."), tokens };
82
- continue;
83
- }
84
- if (isLetter(c)) {
85
- const start = i;
86
- while (i < value.length && value[i] === c) i++;
87
- let run = value.slice(start, i);
88
- if (run === "d" && value[i] === "o") {
89
- run = "do";
90
- i++;
91
- }
92
- if (!isKnownToken(run)) {
93
- const hint = hints[run];
94
- return {
95
- result: invalid(hint ? `Unknown token '${run}' \u2014 use '${hint}' instead.` : `Unknown token '${run}'.`),
96
- tokens
97
- };
98
- }
99
- tokens.push(run);
100
- continue;
101
- }
102
- if (SEPARATOR_CHARS.includes(c)) {
103
- i++;
104
- continue;
105
- }
106
- return { result: invalid(`Unsupported character '${c}'.`), tokens };
107
- }
108
- return { result: { valid: true }, tokens };
109
- }
110
- function validateDateFormat(value) {
111
- if (!value) return { valid: true };
112
- const { result, tokens } = tokenize(
113
- value,
114
- (t) => DAY_TOKENS.has(t) || WEEKDAY_TOKENS.has(t) || MONTH_TOKENS.has(t) || YEAR_TOKENS.has(t),
115
- DATE_HINTS
116
- );
117
- if (!result.valid) return result;
118
- if (!tokens.some((t) => DAY_TOKENS.has(t))) return invalid("Date format must include a day token (d, dd or do).");
119
- if (!tokens.some((t) => MONTH_TOKENS.has(t))) {
120
- return invalid("Date format must include a month token (M, MM, MMM or MMMM).");
121
- }
122
- if (!tokens.some((t) => YEAR_TOKENS.has(t))) return invalid("Date format must include a year token (yy or yyyy).");
123
- return { valid: true };
124
- }
125
- function validateTimeFormat(value) {
126
- if (!value) return { valid: true };
127
- const { result, tokens } = tokenize(
128
- value,
129
- (t) => HOUR12_TOKENS.has(t) || HOUR24_TOKENS.has(t) || MINUTE_TOKENS.has(t) || SECOND_TOKENS.has(t) || t === MERIDIEM_TOKEN,
130
- TIME_HINTS
131
- );
132
- if (!result.valid) return result;
133
- const hasHour12 = tokens.some((t) => HOUR12_TOKENS.has(t));
134
- const hasHour24 = tokens.some((t) => HOUR24_TOKENS.has(t));
135
- if (!hasHour12 && !hasHour24) return invalid("Time format must include an hour token (h, hh, H or HH).");
136
- if (!tokens.some((t) => MINUTE_TOKENS.has(t))) return invalid("Time format must include a minute token (m or mm).");
137
- const hasMeridiem = tokens.includes(MERIDIEM_TOKEN);
138
- if (hasHour12 && !hasMeridiem) return invalid("12-hour time format requires the meridiem token 'a'.");
139
- if (hasMeridiem && !hasHour12) return invalid("Meridiem token 'a' requires a 12-hour hour token (h or hh).");
140
- return { valid: true };
141
- }
3
+ var chunkYNBKRMK3_cjs = require('../chunk-YNBKRMK3.cjs');
142
4
 
143
5
  // src/date/orgFormat.ts
144
6
  var ORG_DATE_FALLBACK_FORMAT = "dd/MM/yyyy";
@@ -158,41 +20,41 @@ function setOrgFormats(dateFormat, timeFormat) {
158
20
  }
159
21
  function resolveOrgDateFormat() {
160
22
  const fmt = getOrgDateFormat();
161
- return fmt && validateDateFormat(fmt).valid ? fmt : ORG_DATE_FALLBACK_FORMAT;
23
+ return fmt && chunkYNBKRMK3_cjs.validateDateFormat(fmt).valid ? fmt : ORG_DATE_FALLBACK_FORMAT;
162
24
  }
163
25
  function resolveOrgTimeFormat() {
164
26
  const fmt = getOrgTimeFormat();
165
- return fmt && validateTimeFormat(fmt).valid ? fmt : ORG_TIME_FALLBACK_FORMAT;
27
+ return fmt && chunkYNBKRMK3_cjs.validateTimeFormat(fmt).valid ? fmt : ORG_TIME_FALLBACK_FORMAT;
166
28
  }
167
- function formatDate2(dateStr) {
168
- return formatDateSafe(dateStr, resolveOrgDateFormat(), EM_DASH);
29
+ function formatDate(dateStr) {
30
+ return chunkYNBKRMK3_cjs.formatDateSafe(dateStr, resolveOrgDateFormat(), EM_DASH);
169
31
  }
170
32
  function formatDateTime(dateStr) {
171
- const datePart = formatDateSafe(dateStr, resolveOrgDateFormat(), EM_DASH);
33
+ const datePart = chunkYNBKRMK3_cjs.formatDateSafe(dateStr, resolveOrgDateFormat(), EM_DASH);
172
34
  if (datePart === EM_DASH) return EM_DASH;
173
- return `${datePart} ${formatDateSafe(dateStr, resolveOrgTimeFormat(), EM_DASH)}`;
35
+ return `${datePart} ${chunkYNBKRMK3_cjs.formatDateSafe(dateStr, resolveOrgTimeFormat(), EM_DASH)}`;
174
36
  }
175
37
  function formatTime(timeStr) {
176
38
  if (!timeStr) return EM_DASH;
177
39
  if (timeStr.includes("T")) {
178
- return formatDateSafe(timeStr, resolveOrgTimeFormat(), EM_DASH);
40
+ return chunkYNBKRMK3_cjs.formatDateSafe(timeStr, resolveOrgTimeFormat(), EM_DASH);
179
41
  }
180
42
  const trimmed = timeStr.trim();
181
43
  const parts = /^(\d{2}):(\d{2})(:\d{2})?$/.exec(trimmed);
182
44
  if (!parts) return timeStr;
183
45
  const anchored = `2000-01-01T${parts[3] ? trimmed : `${trimmed}:00`}`;
184
- return formatDateSafe(anchored, resolveOrgTimeFormat(), trimmed);
46
+ return chunkYNBKRMK3_cjs.formatDateSafe(anchored, resolveOrgTimeFormat(), trimmed);
185
47
  }
186
48
  function formatDateOrPresent(dateStr) {
187
49
  if (!dateStr) return "Present";
188
- return formatDateSafe(dateStr, resolveOrgDateFormat(), "Present");
50
+ return chunkYNBKRMK3_cjs.formatDateSafe(dateStr, resolveOrgDateFormat(), "Present");
189
51
  }
190
52
  function formatRelativeTime(dateStr, now = /* @__PURE__ */ new Date()) {
191
53
  if (!dateStr) return EM_DASH;
192
54
  const then = new Date(dateStr).getTime();
193
55
  if (Number.isNaN(then)) return EM_DASH;
194
- const diffMinutes = Math.floor((now.getTime() - then) / MS_PER_MINUTE);
195
- return relativeTimePhrase(diffMinutes) ?? formatDateTime(dateStr);
56
+ const diffMinutes = Math.floor((now.getTime() - then) / chunkYNBKRMK3_cjs.MS_PER_MINUTE);
57
+ return chunkYNBKRMK3_cjs.relativeTimePhrase(diffMinutes) ?? formatDateTime(dateStr);
196
58
  }
197
59
  function convertTokenRun(run) {
198
60
  switch (run[0]) {
@@ -260,7 +122,7 @@ function getOrgDateTimeInputFormat() {
260
122
  exports.ORG_DATE_FALLBACK_FORMAT = ORG_DATE_FALLBACK_FORMAT;
261
123
  exports.ORG_TIME_FALLBACK_FORMAT = ORG_TIME_FALLBACK_FORMAT;
262
124
  exports.dateFnsToDayjsFormat = dateFnsToDayjsFormat;
263
- exports.formatDate = formatDate2;
125
+ exports.formatDate = formatDate;
264
126
  exports.formatDateOrPresent = formatDateOrPresent;
265
127
  exports.formatDateTime = formatDateTime;
266
128
  exports.formatRelativeTime = formatRelativeTime;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/date/constants.ts","../../src/date/format.ts","../../src/date/formatValidation.ts","../../src/date/orgFormat.ts"],"names":["parseISO","isValid","format","formatDate"],"mappings":";;;;;AASO,IAAM,WAAA,GAAc,YAAA;AA8BpB,IAAM,aAAA,GAAgB,GAAA;;;ACCtB,IAAM,kBAAA,GAAqB,CAAC,WAAA,KAAuC;AACxE,EAAA,IAAI,WAAA,GAAc,GAAG,OAAO,UAAA;AAC5B,EAAA,IAAI,WAAA,GAAc,EAAA,EAAI,OAAO,CAAA,EAAG,WAAW,CAAA,KAAA,CAAA;AAC3C,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,WAAA,GAAc,EAAE,CAAA;AACzC,EAAA,IAAI,KAAA,GAAQ,EAAA,EAAI,OAAO,CAAA,EAAG,KAAK,CAAA,KAAA,CAAA;AAC/B,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,KAAA,GAAQ,EAAE,CAAA;AAClC,EAAA,IAAI,IAAA,GAAO,CAAA,EAAG,OAAO,CAAA,EAAG,IAAI,CAAA,KAAA,CAAA;AAC5B,EAAA,OAAO,IAAA;AACT,CAAA;AA6CO,IAAM,iBAAiB,CAC5B,IAAA,EACA,SAAA,GAAoB,WAAA,EACpB,WAAmB,QAAA,KACR;AACX,EAAA,IAAI,CAAC,MAAM,OAAO,QAAA;AAClB,EAAA,IAAI;AACF,IAAA,MAAM,UAAU,OAAO,IAAA,KAAS,QAAA,GAAWA,gBAAA,CAAS,IAAI,CAAA,GAAI,IAAA;AAC5D,IAAA,IAAI,CAACC,eAAAA,CAAQ,OAAO,CAAA,EAAG,OAAO,QAAA;AAC9B,IAAA,OAAOC,cAAAA,CAAO,SAAS,SAAS,CAAA;AAAA,EAClC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,QAAA;AAAA,EACT;AACF,CAAA;;;ACxFA,IAAM,eAAA,GAAkB,UAAA;AAExB,IAAM,6BAAa,IAAI,GAAA,CAAI,CAAC,GAAA,EAAK,IAAA,EAAM,IAAI,CAAC,CAAA;AAC5C,IAAM,iCAAiB,IAAI,GAAA,CAAI,CAAC,KAAA,EAAO,MAAM,CAAC,CAAA;AAC9C,IAAM,YAAA,uBAAmB,GAAA,CAAI,CAAC,KAAK,IAAA,EAAM,KAAA,EAAO,MAAM,CAAC,CAAA;AACvD,IAAM,8BAAc,IAAI,GAAA,CAAI,CAAC,IAAA,EAAM,MAAM,CAAC,CAAA;AAC1C,IAAM,gCAAgB,IAAI,GAAA,CAAI,CAAC,GAAA,EAAK,IAAI,CAAC,CAAA;AACzC,IAAM,gCAAgB,IAAI,GAAA,CAAI,CAAC,GAAA,EAAK,IAAI,CAAC,CAAA;AACzC,IAAM,gCAAgB,IAAI,GAAA,CAAI,CAAC,GAAA,EAAK,IAAI,CAAC,CAAA;AACzC,IAAM,gCAAgB,IAAI,GAAA,CAAI,CAAC,GAAA,EAAK,IAAI,CAAC,CAAA;AACzC,IAAM,cAAA,GAAiB,GAAA;AAGvB,IAAM,UAAA,GAAqC;AAAA,EACzC,CAAA,EAAG,GAAA;AAAA,EACH,EAAA,EAAI,IAAA;AAAA,EACJ,CAAA,EAAG,MAAA;AAAA,EACH,EAAA,EAAI,IAAA;AAAA,EACJ,IAAA,EAAM,MAAA;AAAA,EACN,CAAA,EAAG,KAAA;AAAA,EACH,EAAA,EAAI,KAAA;AAAA,EACJ,CAAA,EAAG,GAAA;AAAA,EACH,EAAA,EAAI;AACN,CAAA;AAGA,IAAM,UAAA,GAAqC;AAAA,EACzC,CAAA,EAAG,GAAA;AAAA,EACH,EAAA,EAAI,GAAA;AAAA,EACJ,CAAA,EAAG,GAAA;AAAA,EACH,CAAA,EAAG,GAAA;AAAA,EACH,EAAA,EAAI;AACN,CAAA;AAEA,IAAM,UAAU,CAAC,KAAA,MAA2C,EAAE,KAAA,EAAO,OAAO,KAAA,EAAM,CAAA;AAElF,IAAM,QAAA,GAAW,CAAC,CAAA,KAAuB,UAAA,CAAW,KAAK,CAAC,CAAA;AAO1D,SAAS,QAAA,CACP,KAAA,EACA,YAAA,EACA,KAAA,EACsD;AACtD,EAAA,MAAM,SAAmB,EAAC;AAC1B,EAAA,IAAI,CAAA,GAAI,CAAA;AAER,EAAA,OAAO,CAAA,GAAI,MAAM,MAAA,EAAQ;AACvB,IAAA,MAAM,CAAA,GAAI,MAAM,CAAC,CAAA;AAEjB,IAAA,IAAI,MAAM,GAAA,EAAK;AACb,MAAA,CAAA,EAAA;AACA,MAAA,IAAI,MAAA,GAAS,KAAA;AACb,MAAA,OAAO,CAAA,GAAI,MAAM,MAAA,EAAQ;AACvB,QAAA,IAAI,KAAA,CAAM,CAAC,CAAA,KAAM,GAAA,EAAK;AACpB,UAAA,IAAI,CAAA,GAAI,IAAI,KAAA,CAAM,MAAA,IAAU,MAAM,CAAA,GAAI,CAAC,MAAM,GAAA,EAAK;AAChD,YAAA,CAAA,IAAK,CAAA;AACL,YAAA;AAAA,UACF;AACA,UAAA,MAAA,GAAS,IAAA;AACT,UAAA,CAAA,EAAA;AACA,UAAA;AAAA,QACF;AACA,QAAA,CAAA,EAAA;AAAA,MACF;AACA,MAAA,IAAI,CAAC,QAAQ,OAAO,EAAE,QAAQ,OAAA,CAAQ,8BAA8B,GAAG,MAAA,EAAO;AAC9E,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,QAAA,CAAS,CAAC,CAAA,EAAG;AACf,MAAA,MAAM,KAAA,GAAQ,CAAA;AACd,MAAA,OAAO,IAAI,KAAA,CAAM,MAAA,IAAU,KAAA,CAAM,CAAC,MAAM,CAAA,EAAG,CAAA,EAAA;AAC3C,MAAA,IAAI,GAAA,GAAM,KAAA,CAAM,KAAA,CAAM,KAAA,EAAO,CAAC,CAAA;AAI9B,MAAA,IAAI,GAAA,KAAQ,GAAA,IAAO,KAAA,CAAM,CAAC,MAAM,GAAA,EAAK;AACnC,QAAA,GAAA,GAAM,IAAA;AACN,QAAA,CAAA,EAAA;AAAA,MACF;AAEA,MAAA,IAAI,CAAC,YAAA,CAAa,GAAG,CAAA,EAAG;AACtB,QAAA,MAAM,IAAA,GAAO,MAAM,GAAG,CAAA;AACtB,QAAA,OAAO;AAAA,UACL,MAAA,EAAQ,OAAA,CAAQ,IAAA,GAAO,CAAA,eAAA,EAAkB,GAAG,iBAAY,IAAI,CAAA,UAAA,CAAA,GAAe,CAAA,eAAA,EAAkB,GAAG,CAAA,EAAA,CAAI,CAAA;AAAA,UACpG;AAAA,SACF;AAAA,MACF;AAEA,MAAA,MAAA,CAAO,KAAK,GAAG,CAAA;AACf,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,eAAA,CAAgB,QAAA,CAAS,CAAC,CAAA,EAAG;AAC/B,MAAA,CAAA,EAAA;AACA,MAAA;AAAA,IACF;AAEA,IAAA,OAAO,EAAE,MAAA,EAAQ,OAAA,CAAQ,0BAA0B,CAAC,CAAA,EAAA,CAAI,GAAG,MAAA,EAAO;AAAA,EACpE;AAEA,EAAA,OAAO,EAAE,MAAA,EAAQ,EAAE,KAAA,EAAO,IAAA,IAAQ,MAAA,EAAO;AAC3C;AASO,SAAS,mBAAmB,KAAA,EAA0D;AAC3F,EAAA,IAAI,CAAC,KAAA,EAAO,OAAO,EAAE,OAAO,IAAA,EAAK;AAEjC,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAO,GAAI,QAAA;AAAA,IACzB,KAAA;AAAA,IACA,CAAC,CAAA,KAAM,UAAA,CAAW,GAAA,CAAI,CAAC,KAAK,cAAA,CAAe,GAAA,CAAI,CAAC,CAAA,IAAK,aAAa,GAAA,CAAI,CAAC,CAAA,IAAK,WAAA,CAAY,IAAI,CAAC,CAAA;AAAA,IAC7F;AAAA,GACF;AACA,EAAA,IAAI,CAAC,MAAA,CAAO,KAAA,EAAO,OAAO,MAAA;AAE1B,EAAA,IAAI,CAAC,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,KAAM,UAAA,CAAW,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,OAAO,OAAA,CAAQ,qDAAqD,CAAA;AAChH,EAAA,IAAI,CAAC,OAAO,IAAA,CAAK,CAAC,MAAM,YAAA,CAAa,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG;AAC5C,IAAA,OAAO,QAAQ,8DAA8D,CAAA;AAAA,EAC/E;AACA,EAAA,IAAI,CAAC,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,KAAM,WAAA,CAAY,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,OAAO,OAAA,CAAQ,qDAAqD,CAAA;AAEjH,EAAA,OAAO,EAAE,OAAO,IAAA,EAAK;AACvB;AASO,SAAS,mBAAmB,KAAA,EAA0D;AAC3F,EAAA,IAAI,CAAC,KAAA,EAAO,OAAO,EAAE,OAAO,IAAA,EAAK;AAEjC,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAO,GAAI,QAAA;AAAA,IACzB,KAAA;AAAA,IACA,CAAC,CAAA,KACC,aAAA,CAAc,IAAI,CAAC,CAAA,IACnB,cAAc,GAAA,CAAI,CAAC,CAAA,IACnB,aAAA,CAAc,IAAI,CAAC,CAAA,IACnB,cAAc,GAAA,CAAI,CAAC,KACnB,CAAA,KAAM,cAAA;AAAA,IACR;AAAA,GACF;AACA,EAAA,IAAI,CAAC,MAAA,CAAO,KAAA,EAAO,OAAO,MAAA;AAE1B,EAAA,MAAM,SAAA,GAAY,OAAO,IAAA,CAAK,CAAC,MAAM,aAAA,CAAc,GAAA,CAAI,CAAC,CAAC,CAAA;AACzD,EAAA,MAAM,SAAA,GAAY,OAAO,IAAA,CAAK,CAAC,MAAM,aAAA,CAAc,GAAA,CAAI,CAAC,CAAC,CAAA;AAEzD,EAAA,IAAI,CAAC,SAAA,IAAa,CAAC,SAAA,EAAW,OAAO,QAAQ,0DAA0D,CAAA;AACvG,EAAA,IAAI,CAAC,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,KAAM,aAAA,CAAc,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,OAAO,OAAA,CAAQ,oDAAoD,CAAA;AAElH,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,QAAA,CAAS,cAAc,CAAA;AAClD,EAAA,IAAI,SAAA,IAAa,CAAC,WAAA,EAAa,OAAO,QAAQ,sDAAsD,CAAA;AACpG,EAAA,IAAI,WAAA,IAAe,CAAC,SAAA,EAAW,OAAO,QAAQ,6DAA6D,CAAA;AAE3G,EAAA,OAAO,EAAE,OAAO,IAAA,EAAK;AACvB;;;AC5JO,IAAM,wBAAA,GAA2B;AAEjC,IAAM,wBAAA,GAA2B;AAExC,IAAM,OAAA,GAAU,QAAA;AAMhB,IAAI,gBAAA,GAAkC,IAAA;AACtC,IAAI,gBAAA,GAAkC,IAAA;AAG/B,SAAS,gBAAA,GAAkC;AAChD,EAAA,OAAO,gBAAA;AACT;AAGO,SAAS,gBAAA,GAAkC;AAChD,EAAA,OAAO,gBAAA;AACT;AAOO,SAAS,aAAA,CAAc,YAA2B,UAAA,EAAiC;AACxF,EAAA,gBAAA,GAAmB,UAAA;AACnB,EAAA,gBAAA,GAAmB,UAAA;AACrB;AAYO,SAAS,oBAAA,GAA+B;AAC7C,EAAA,MAAM,MAAM,gBAAA,EAAiB;AAC7B,EAAA,OAAO,GAAA,IAAO,kBAAA,CAAmB,GAAG,CAAA,CAAE,QAAQ,GAAA,GAAM,wBAAA;AACtD;AAGO,SAAS,oBAAA,GAA+B;AAC7C,EAAA,MAAM,MAAM,gBAAA,EAAiB;AAC7B,EAAA,OAAO,GAAA,IAAO,kBAAA,CAAmB,GAAG,CAAA,CAAE,QAAQ,GAAA,GAAM,wBAAA;AACtD;AAUO,SAASC,YAAW,OAAA,EAAiC;AAC1D,EAAA,OAAO,cAAA,CAAe,OAAA,EAAS,oBAAA,EAAqB,EAAG,OAAO,CAAA;AAChE;AAMO,SAAS,eAAe,OAAA,EAAiC;AAC9D,EAAA,MAAM,QAAA,GAAW,cAAA,CAAe,OAAA,EAAS,oBAAA,IAAwB,OAAO,CAAA;AACxE,EAAA,IAAI,QAAA,KAAa,SAAS,OAAO,OAAA;AACjC,EAAA,OAAO,CAAA,EAAG,QAAQ,CAAA,CAAA,EAAI,cAAA,CAAe,SAAS,oBAAA,EAAqB,EAAG,OAAO,CAAC,CAAA,CAAA;AAChF;AAUO,SAAS,WAAW,OAAA,EAAiC;AAC1D,EAAA,IAAI,CAAC,SAAS,OAAO,OAAA;AACrB,EAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,GAAG,CAAA,EAAG;AACzB,IAAA,OAAO,cAAA,CAAe,OAAA,EAAS,oBAAA,EAAqB,EAAG,OAAO,CAAA;AAAA,EAChE;AAOA,EAAA,MAAM,OAAA,GAAU,QAAQ,IAAA,EAAK;AAC7B,EAAA,MAAM,KAAA,GAAQ,4BAAA,CAA6B,IAAA,CAAK,OAAO,CAAA;AACvD,EAAA,IAAI,CAAC,OAAO,OAAO,OAAA;AACnB,EAAA,MAAM,QAAA,GAAW,cAAc,KAAA,CAAM,CAAC,IAAI,OAAA,GAAU,CAAA,EAAG,OAAO,CAAA,GAAA,CAAK,CAAA,CAAA;AACnE,EAAA,OAAO,cAAA,CAAe,QAAA,EAAU,oBAAA,EAAqB,EAAG,OAAO,CAAA;AACjE;AAOO,SAAS,oBAAoB,OAAA,EAAiC;AACnE,EAAA,IAAI,CAAC,SAAS,OAAO,SAAA;AACrB,EAAA,OAAO,cAAA,CAAe,OAAA,EAAS,oBAAA,EAAqB,EAAG,SAAS,CAAA;AAClE;AAUO,SAAS,kBAAA,CAAmB,OAAA,EAAyB,GAAA,mBAAY,IAAI,MAAK,EAAW;AAC1F,EAAA,IAAI,CAAC,SAAS,OAAO,OAAA;AACrB,EAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,OAAO,EAAE,OAAA,EAAQ;AACvC,EAAA,IAAI,MAAA,CAAO,KAAA,CAAM,IAAI,CAAA,EAAG,OAAO,OAAA;AAC/B,EAAA,MAAM,cAAc,IAAA,CAAK,KAAA,CAAA,CAAO,IAAI,OAAA,EAAQ,GAAI,QAAQ,aAAa,CAAA;AAGrE,EAAA,OAAO,kBAAA,CAAmB,WAAW,CAAA,IAAK,cAAA,CAAe,OAAO,CAAA;AAClE;AAOA,SAAS,gBAAgB,GAAA,EAAqB;AAC5C,EAAA,QAAQ,GAAA,CAAI,CAAC,CAAA;AAAG,IACd,KAAK,GAAA;AACH,MAAA,OAAO,GAAA,CAAI,MAAA,CAAO,GAAA,CAAI,MAAM,CAAA;AAAA;AAAA,IAC9B,KAAK,GAAA;AACH,MAAA,OAAO,GAAA,CAAI,MAAA,CAAO,GAAA,CAAI,MAAM,CAAA;AAAA;AAAA,IAC9B,KAAK,GAAA;AACH,MAAA,OAAO,GAAA;AAAA;AAAA,IACT,KAAK,GAAA;AAEH,MAAA,OAAO,GAAA,CAAI,MAAA,IAAU,CAAA,GAAI,MAAA,GAAS,KAAA;AAAA,IACpC;AACE,MAAA,OAAO,GAAA;AAAA;AAEb;AAYO,SAAS,qBAAqB,GAAA,EAAqB;AACxD,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,OAAO,CAAA,GAAI,IAAI,MAAA,EAAQ;AACrB,IAAA,MAAM,EAAA,GAAK,IAAI,CAAC,CAAA;AAChB,IAAA,IAAI,OAAO,GAAA,EAAK;AAEd,MAAA,CAAA,EAAA;AACA,MAAA,IAAI,OAAA,GAAU,EAAA;AACd,MAAA,OAAO,CAAA,GAAI,IAAI,MAAA,EAAQ;AACrB,QAAA,IAAI,GAAA,CAAI,CAAC,CAAA,KAAM,GAAA,EAAK;AAClB,UAAA,IAAI,GAAA,CAAI,CAAA,GAAI,CAAC,CAAA,KAAM,GAAA,EAAK;AACtB,YAAA,OAAA,IAAW,GAAA;AACX,YAAA,CAAA,IAAK,CAAA;AACL,YAAA;AAAA,UACF;AACA,UAAA,CAAA,EAAA;AACA,UAAA;AAAA,QACF;AACA,QAAA,OAAA,IAAW,IAAI,CAAC,CAAA;AAChB,QAAA,CAAA,EAAA;AAAA,MACF;AACA,MAAA,GAAA,IAAO,OAAA,KAAY,EAAA,GAAK,KAAA,GAAQ,CAAA,CAAA,EAAI,OAAO,CAAA,CAAA,CAAA;AAC3C,MAAA;AAAA,IACF;AACA,IAAA,IAAI,UAAA,CAAW,IAAA,CAAK,EAAE,CAAA,EAAG;AACvB,MAAA,IAAI,GAAA,GAAM,EAAA;AACV,MAAA,OAAO,CAAA,GAAI,IAAI,GAAA,CAAI,MAAA,IAAU,IAAI,CAAA,GAAI,CAAC,MAAM,EAAA,EAAI;AAC9C,QAAA,GAAA,IAAO,GAAA,CAAI,IAAI,CAAC,CAAA;AAChB,QAAA,CAAA,EAAA;AAAA,MACF;AACA,MAAA,GAAA,IAAO,gBAAgB,GAAG,CAAA;AAC1B,MAAA,CAAA,EAAA;AACA,MAAA;AAAA,IACF;AACA,IAAA,GAAA,IAAO,EAAA;AACP,IAAA,CAAA,EAAA;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT;AAOO,SAAS,qBAAA,GAAgC;AAC9C,EAAA,OAAO,oBAAA,CAAqB,sBAAsB,CAAA;AACpD;AAMO,SAAS,yBAAA,GAAoC;AAClD,EAAA,OAAO,GAAG,qBAAA,EAAuB,IAAI,oBAAA,CAAqB,oBAAA,EAAsB,CAAC,CAAA,CAAA;AACnF","file":"org-format.cjs","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"]}
1
+ {"version":3,"sources":["../../src/date/orgFormat.ts"],"names":["validateDateFormat","validateTimeFormat","formatDateSafe","MS_PER_MINUTE","relativeTimePhrase"],"mappings":"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","file":"org-format.cjs","sourcesContent":["/**\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"]}
@@ -1,142 +1,4 @@
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
- }
1
+ import { validateDateFormat, validateTimeFormat, formatDateSafe, MS_PER_MINUTE, relativeTimePhrase } from '../chunk-TN6DXBVD.js';
140
2
 
141
3
  // src/date/orgFormat.ts
142
4
  var ORG_DATE_FALLBACK_FORMAT = "dd/MM/yyyy";
@@ -162,7 +24,7 @@ function resolveOrgTimeFormat() {
162
24
  const fmt = getOrgTimeFormat();
163
25
  return fmt && validateTimeFormat(fmt).valid ? fmt : ORG_TIME_FALLBACK_FORMAT;
164
26
  }
165
- function formatDate2(dateStr) {
27
+ function formatDate(dateStr) {
166
28
  return formatDateSafe(dateStr, resolveOrgDateFormat(), EM_DASH);
167
29
  }
168
30
  function formatDateTime(dateStr) {
@@ -255,6 +117,6 @@ function getOrgDateTimeInputFormat() {
255
117
  return `${getOrgDateInputFormat()} ${dateFnsToDayjsFormat(resolveOrgTimeFormat())}`;
256
118
  }
257
119
 
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 };
120
+ export { ORG_DATE_FALLBACK_FORMAT, ORG_TIME_FALLBACK_FORMAT, dateFnsToDayjsFormat, formatDate, formatDateOrPresent, formatDateTime, formatRelativeTime, formatTime, getOrgDateFormat, getOrgDateInputFormat, getOrgDateTimeInputFormat, getOrgTimeFormat, resolveOrgDateFormat, resolveOrgTimeFormat, setOrgFormats };
259
121
  //# sourceMappingURL=org-format.js.map
260
122
  //# sourceMappingURL=org-format.js.map
@@ -1 +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":";;;AASO,IAAM,WAAA,GAAc,YAAA;AA8BpB,IAAM,aAAA,GAAgB,GAAA;;;ACCtB,IAAM,kBAAA,GAAqB,CAAC,WAAA,KAAuC;AACxE,EAAA,IAAI,WAAA,GAAc,GAAG,OAAO,UAAA;AAC5B,EAAA,IAAI,WAAA,GAAc,EAAA,EAAI,OAAO,CAAA,EAAG,WAAW,CAAA,KAAA,CAAA;AAC3C,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,WAAA,GAAc,EAAE,CAAA;AACzC,EAAA,IAAI,KAAA,GAAQ,EAAA,EAAI,OAAO,CAAA,EAAG,KAAK,CAAA,KAAA,CAAA;AAC/B,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,KAAA,GAAQ,EAAE,CAAA;AAClC,EAAA,IAAI,IAAA,GAAO,CAAA,EAAG,OAAO,CAAA,EAAG,IAAI,CAAA,KAAA,CAAA;AAC5B,EAAA,OAAO,IAAA;AACT,CAAA;AA6CO,IAAM,iBAAiB,CAC5B,IAAA,EACA,SAAA,GAAoB,WAAA,EACpB,WAAmB,QAAA,KACR;AACX,EAAA,IAAI,CAAC,MAAM,OAAO,QAAA;AAClB,EAAA,IAAI;AACF,IAAA,MAAM,UAAU,OAAO,IAAA,KAAS,QAAA,GAAW,QAAA,CAAS,IAAI,CAAA,GAAI,IAAA;AAC5D,IAAA,IAAI,CAACA,OAAAA,CAAQ,OAAO,CAAA,EAAG,OAAO,QAAA;AAC9B,IAAA,OAAOC,MAAAA,CAAO,SAAS,SAAS,CAAA;AAAA,EAClC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,QAAA;AAAA,EACT;AACF,CAAA;;;ACxFA,IAAM,eAAA,GAAkB,UAAA;AAExB,IAAM,6BAAa,IAAI,GAAA,CAAI,CAAC,GAAA,EAAK,IAAA,EAAM,IAAI,CAAC,CAAA;AAC5C,IAAM,iCAAiB,IAAI,GAAA,CAAI,CAAC,KAAA,EAAO,MAAM,CAAC,CAAA;AAC9C,IAAM,YAAA,uBAAmB,GAAA,CAAI,CAAC,KAAK,IAAA,EAAM,KAAA,EAAO,MAAM,CAAC,CAAA;AACvD,IAAM,8BAAc,IAAI,GAAA,CAAI,CAAC,IAAA,EAAM,MAAM,CAAC,CAAA;AAC1C,IAAM,gCAAgB,IAAI,GAAA,CAAI,CAAC,GAAA,EAAK,IAAI,CAAC,CAAA;AACzC,IAAM,gCAAgB,IAAI,GAAA,CAAI,CAAC,GAAA,EAAK,IAAI,CAAC,CAAA;AACzC,IAAM,gCAAgB,IAAI,GAAA,CAAI,CAAC,GAAA,EAAK,IAAI,CAAC,CAAA;AACzC,IAAM,gCAAgB,IAAI,GAAA,CAAI,CAAC,GAAA,EAAK,IAAI,CAAC,CAAA;AACzC,IAAM,cAAA,GAAiB,GAAA;AAGvB,IAAM,UAAA,GAAqC;AAAA,EACzC,CAAA,EAAG,GAAA;AAAA,EACH,EAAA,EAAI,IAAA;AAAA,EACJ,CAAA,EAAG,MAAA;AAAA,EACH,EAAA,EAAI,IAAA;AAAA,EACJ,IAAA,EAAM,MAAA;AAAA,EACN,CAAA,EAAG,KAAA;AAAA,EACH,EAAA,EAAI,KAAA;AAAA,EACJ,CAAA,EAAG,GAAA;AAAA,EACH,EAAA,EAAI;AACN,CAAA;AAGA,IAAM,UAAA,GAAqC;AAAA,EACzC,CAAA,EAAG,GAAA;AAAA,EACH,EAAA,EAAI,GAAA;AAAA,EACJ,CAAA,EAAG,GAAA;AAAA,EACH,CAAA,EAAG,GAAA;AAAA,EACH,EAAA,EAAI;AACN,CAAA;AAEA,IAAM,UAAU,CAAC,KAAA,MAA2C,EAAE,KAAA,EAAO,OAAO,KAAA,EAAM,CAAA;AAElF,IAAM,QAAA,GAAW,CAAC,CAAA,KAAuB,UAAA,CAAW,KAAK,CAAC,CAAA;AAO1D,SAAS,QAAA,CACP,KAAA,EACA,YAAA,EACA,KAAA,EACsD;AACtD,EAAA,MAAM,SAAmB,EAAC;AAC1B,EAAA,IAAI,CAAA,GAAI,CAAA;AAER,EAAA,OAAO,CAAA,GAAI,MAAM,MAAA,EAAQ;AACvB,IAAA,MAAM,CAAA,GAAI,MAAM,CAAC,CAAA;AAEjB,IAAA,IAAI,MAAM,GAAA,EAAK;AACb,MAAA,CAAA,EAAA;AACA,MAAA,IAAI,MAAA,GAAS,KAAA;AACb,MAAA,OAAO,CAAA,GAAI,MAAM,MAAA,EAAQ;AACvB,QAAA,IAAI,KAAA,CAAM,CAAC,CAAA,KAAM,GAAA,EAAK;AACpB,UAAA,IAAI,CAAA,GAAI,IAAI,KAAA,CAAM,MAAA,IAAU,MAAM,CAAA,GAAI,CAAC,MAAM,GAAA,EAAK;AAChD,YAAA,CAAA,IAAK,CAAA;AACL,YAAA;AAAA,UACF;AACA,UAAA,MAAA,GAAS,IAAA;AACT,UAAA,CAAA,EAAA;AACA,UAAA;AAAA,QACF;AACA,QAAA,CAAA,EAAA;AAAA,MACF;AACA,MAAA,IAAI,CAAC,QAAQ,OAAO,EAAE,QAAQ,OAAA,CAAQ,8BAA8B,GAAG,MAAA,EAAO;AAC9E,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,QAAA,CAAS,CAAC,CAAA,EAAG;AACf,MAAA,MAAM,KAAA,GAAQ,CAAA;AACd,MAAA,OAAO,IAAI,KAAA,CAAM,MAAA,IAAU,KAAA,CAAM,CAAC,MAAM,CAAA,EAAG,CAAA,EAAA;AAC3C,MAAA,IAAI,GAAA,GAAM,KAAA,CAAM,KAAA,CAAM,KAAA,EAAO,CAAC,CAAA;AAI9B,MAAA,IAAI,GAAA,KAAQ,GAAA,IAAO,KAAA,CAAM,CAAC,MAAM,GAAA,EAAK;AACnC,QAAA,GAAA,GAAM,IAAA;AACN,QAAA,CAAA,EAAA;AAAA,MACF;AAEA,MAAA,IAAI,CAAC,YAAA,CAAa,GAAG,CAAA,EAAG;AACtB,QAAA,MAAM,IAAA,GAAO,MAAM,GAAG,CAAA;AACtB,QAAA,OAAO;AAAA,UACL,MAAA,EAAQ,OAAA,CAAQ,IAAA,GAAO,CAAA,eAAA,EAAkB,GAAG,iBAAY,IAAI,CAAA,UAAA,CAAA,GAAe,CAAA,eAAA,EAAkB,GAAG,CAAA,EAAA,CAAI,CAAA;AAAA,UACpG;AAAA,SACF;AAAA,MACF;AAEA,MAAA,MAAA,CAAO,KAAK,GAAG,CAAA;AACf,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,eAAA,CAAgB,QAAA,CAAS,CAAC,CAAA,EAAG;AAC/B,MAAA,CAAA,EAAA;AACA,MAAA;AAAA,IACF;AAEA,IAAA,OAAO,EAAE,MAAA,EAAQ,OAAA,CAAQ,0BAA0B,CAAC,CAAA,EAAA,CAAI,GAAG,MAAA,EAAO;AAAA,EACpE;AAEA,EAAA,OAAO,EAAE,MAAA,EAAQ,EAAE,KAAA,EAAO,IAAA,IAAQ,MAAA,EAAO;AAC3C;AASO,SAAS,mBAAmB,KAAA,EAA0D;AAC3F,EAAA,IAAI,CAAC,KAAA,EAAO,OAAO,EAAE,OAAO,IAAA,EAAK;AAEjC,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAO,GAAI,QAAA;AAAA,IACzB,KAAA;AAAA,IACA,CAAC,CAAA,KAAM,UAAA,CAAW,GAAA,CAAI,CAAC,KAAK,cAAA,CAAe,GAAA,CAAI,CAAC,CAAA,IAAK,aAAa,GAAA,CAAI,CAAC,CAAA,IAAK,WAAA,CAAY,IAAI,CAAC,CAAA;AAAA,IAC7F;AAAA,GACF;AACA,EAAA,IAAI,CAAC,MAAA,CAAO,KAAA,EAAO,OAAO,MAAA;AAE1B,EAAA,IAAI,CAAC,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,KAAM,UAAA,CAAW,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,OAAO,OAAA,CAAQ,qDAAqD,CAAA;AAChH,EAAA,IAAI,CAAC,OAAO,IAAA,CAAK,CAAC,MAAM,YAAA,CAAa,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG;AAC5C,IAAA,OAAO,QAAQ,8DAA8D,CAAA;AAAA,EAC/E;AACA,EAAA,IAAI,CAAC,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,KAAM,WAAA,CAAY,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,OAAO,OAAA,CAAQ,qDAAqD,CAAA;AAEjH,EAAA,OAAO,EAAE,OAAO,IAAA,EAAK;AACvB;AASO,SAAS,mBAAmB,KAAA,EAA0D;AAC3F,EAAA,IAAI,CAAC,KAAA,EAAO,OAAO,EAAE,OAAO,IAAA,EAAK;AAEjC,EAAA,MAAM,EAAE,MAAA,EAAQ,MAAA,EAAO,GAAI,QAAA;AAAA,IACzB,KAAA;AAAA,IACA,CAAC,CAAA,KACC,aAAA,CAAc,IAAI,CAAC,CAAA,IACnB,cAAc,GAAA,CAAI,CAAC,CAAA,IACnB,aAAA,CAAc,IAAI,CAAC,CAAA,IACnB,cAAc,GAAA,CAAI,CAAC,KACnB,CAAA,KAAM,cAAA;AAAA,IACR;AAAA,GACF;AACA,EAAA,IAAI,CAAC,MAAA,CAAO,KAAA,EAAO,OAAO,MAAA;AAE1B,EAAA,MAAM,SAAA,GAAY,OAAO,IAAA,CAAK,CAAC,MAAM,aAAA,CAAc,GAAA,CAAI,CAAC,CAAC,CAAA;AACzD,EAAA,MAAM,SAAA,GAAY,OAAO,IAAA,CAAK,CAAC,MAAM,aAAA,CAAc,GAAA,CAAI,CAAC,CAAC,CAAA;AAEzD,EAAA,IAAI,CAAC,SAAA,IAAa,CAAC,SAAA,EAAW,OAAO,QAAQ,0DAA0D,CAAA;AACvG,EAAA,IAAI,CAAC,MAAA,CAAO,IAAA,CAAK,CAAC,CAAA,KAAM,aAAA,CAAc,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,OAAO,OAAA,CAAQ,oDAAoD,CAAA;AAElH,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,QAAA,CAAS,cAAc,CAAA;AAClD,EAAA,IAAI,SAAA,IAAa,CAAC,WAAA,EAAa,OAAO,QAAQ,sDAAsD,CAAA;AACpG,EAAA,IAAI,WAAA,IAAe,CAAC,SAAA,EAAW,OAAO,QAAQ,6DAA6D,CAAA;AAE3G,EAAA,OAAO,EAAE,OAAO,IAAA,EAAK;AACvB;;;AC5JO,IAAM,wBAAA,GAA2B;AAEjC,IAAM,wBAAA,GAA2B;AAExC,IAAM,OAAA,GAAU,QAAA;AAMhB,IAAI,gBAAA,GAAkC,IAAA;AACtC,IAAI,gBAAA,GAAkC,IAAA;AAG/B,SAAS,gBAAA,GAAkC;AAChD,EAAA,OAAO,gBAAA;AACT;AAGO,SAAS,gBAAA,GAAkC;AAChD,EAAA,OAAO,gBAAA;AACT;AAOO,SAAS,aAAA,CAAc,YAA2B,UAAA,EAAiC;AACxF,EAAA,gBAAA,GAAmB,UAAA;AACnB,EAAA,gBAAA,GAAmB,UAAA;AACrB;AAYO,SAAS,oBAAA,GAA+B;AAC7C,EAAA,MAAM,MAAM,gBAAA,EAAiB;AAC7B,EAAA,OAAO,GAAA,IAAO,kBAAA,CAAmB,GAAG,CAAA,CAAE,QAAQ,GAAA,GAAM,wBAAA;AACtD;AAGO,SAAS,oBAAA,GAA+B;AAC7C,EAAA,MAAM,MAAM,gBAAA,EAAiB;AAC7B,EAAA,OAAO,GAAA,IAAO,kBAAA,CAAmB,GAAG,CAAA,CAAE,QAAQ,GAAA,GAAM,wBAAA;AACtD;AAUO,SAASC,YAAW,OAAA,EAAiC;AAC1D,EAAA,OAAO,cAAA,CAAe,OAAA,EAAS,oBAAA,EAAqB,EAAG,OAAO,CAAA;AAChE;AAMO,SAAS,eAAe,OAAA,EAAiC;AAC9D,EAAA,MAAM,QAAA,GAAW,cAAA,CAAe,OAAA,EAAS,oBAAA,IAAwB,OAAO,CAAA;AACxE,EAAA,IAAI,QAAA,KAAa,SAAS,OAAO,OAAA;AACjC,EAAA,OAAO,CAAA,EAAG,QAAQ,CAAA,CAAA,EAAI,cAAA,CAAe,SAAS,oBAAA,EAAqB,EAAG,OAAO,CAAC,CAAA,CAAA;AAChF;AAUO,SAAS,WAAW,OAAA,EAAiC;AAC1D,EAAA,IAAI,CAAC,SAAS,OAAO,OAAA;AACrB,EAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,GAAG,CAAA,EAAG;AACzB,IAAA,OAAO,cAAA,CAAe,OAAA,EAAS,oBAAA,EAAqB,EAAG,OAAO,CAAA;AAAA,EAChE;AAOA,EAAA,MAAM,OAAA,GAAU,QAAQ,IAAA,EAAK;AAC7B,EAAA,MAAM,KAAA,GAAQ,4BAAA,CAA6B,IAAA,CAAK,OAAO,CAAA;AACvD,EAAA,IAAI,CAAC,OAAO,OAAO,OAAA;AACnB,EAAA,MAAM,QAAA,GAAW,cAAc,KAAA,CAAM,CAAC,IAAI,OAAA,GAAU,CAAA,EAAG,OAAO,CAAA,GAAA,CAAK,CAAA,CAAA;AACnE,EAAA,OAAO,cAAA,CAAe,QAAA,EAAU,oBAAA,EAAqB,EAAG,OAAO,CAAA;AACjE;AAOO,SAAS,oBAAoB,OAAA,EAAiC;AACnE,EAAA,IAAI,CAAC,SAAS,OAAO,SAAA;AACrB,EAAA,OAAO,cAAA,CAAe,OAAA,EAAS,oBAAA,EAAqB,EAAG,SAAS,CAAA;AAClE;AAUO,SAAS,kBAAA,CAAmB,OAAA,EAAyB,GAAA,mBAAY,IAAI,MAAK,EAAW;AAC1F,EAAA,IAAI,CAAC,SAAS,OAAO,OAAA;AACrB,EAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,OAAO,EAAE,OAAA,EAAQ;AACvC,EAAA,IAAI,MAAA,CAAO,KAAA,CAAM,IAAI,CAAA,EAAG,OAAO,OAAA;AAC/B,EAAA,MAAM,cAAc,IAAA,CAAK,KAAA,CAAA,CAAO,IAAI,OAAA,EAAQ,GAAI,QAAQ,aAAa,CAAA;AAGrE,EAAA,OAAO,kBAAA,CAAmB,WAAW,CAAA,IAAK,cAAA,CAAe,OAAO,CAAA;AAClE;AAOA,SAAS,gBAAgB,GAAA,EAAqB;AAC5C,EAAA,QAAQ,GAAA,CAAI,CAAC,CAAA;AAAG,IACd,KAAK,GAAA;AACH,MAAA,OAAO,GAAA,CAAI,MAAA,CAAO,GAAA,CAAI,MAAM,CAAA;AAAA;AAAA,IAC9B,KAAK,GAAA;AACH,MAAA,OAAO,GAAA,CAAI,MAAA,CAAO,GAAA,CAAI,MAAM,CAAA;AAAA;AAAA,IAC9B,KAAK,GAAA;AACH,MAAA,OAAO,GAAA;AAAA;AAAA,IACT,KAAK,GAAA;AAEH,MAAA,OAAO,GAAA,CAAI,MAAA,IAAU,CAAA,GAAI,MAAA,GAAS,KAAA;AAAA,IACpC;AACE,MAAA,OAAO,GAAA;AAAA;AAEb;AAYO,SAAS,qBAAqB,GAAA,EAAqB;AACxD,EAAA,IAAI,GAAA,GAAM,EAAA;AACV,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,OAAO,CAAA,GAAI,IAAI,MAAA,EAAQ;AACrB,IAAA,MAAM,EAAA,GAAK,IAAI,CAAC,CAAA;AAChB,IAAA,IAAI,OAAO,GAAA,EAAK;AAEd,MAAA,CAAA,EAAA;AACA,MAAA,IAAI,OAAA,GAAU,EAAA;AACd,MAAA,OAAO,CAAA,GAAI,IAAI,MAAA,EAAQ;AACrB,QAAA,IAAI,GAAA,CAAI,CAAC,CAAA,KAAM,GAAA,EAAK;AAClB,UAAA,IAAI,GAAA,CAAI,CAAA,GAAI,CAAC,CAAA,KAAM,GAAA,EAAK;AACtB,YAAA,OAAA,IAAW,GAAA;AACX,YAAA,CAAA,IAAK,CAAA;AACL,YAAA;AAAA,UACF;AACA,UAAA,CAAA,EAAA;AACA,UAAA;AAAA,QACF;AACA,QAAA,OAAA,IAAW,IAAI,CAAC,CAAA;AAChB,QAAA,CAAA,EAAA;AAAA,MACF;AACA,MAAA,GAAA,IAAO,OAAA,KAAY,EAAA,GAAK,KAAA,GAAQ,CAAA,CAAA,EAAI,OAAO,CAAA,CAAA,CAAA;AAC3C,MAAA;AAAA,IACF;AACA,IAAA,IAAI,UAAA,CAAW,IAAA,CAAK,EAAE,CAAA,EAAG;AACvB,MAAA,IAAI,GAAA,GAAM,EAAA;AACV,MAAA,OAAO,CAAA,GAAI,IAAI,GAAA,CAAI,MAAA,IAAU,IAAI,CAAA,GAAI,CAAC,MAAM,EAAA,EAAI;AAC9C,QAAA,GAAA,IAAO,GAAA,CAAI,IAAI,CAAC,CAAA;AAChB,QAAA,CAAA,EAAA;AAAA,MACF;AACA,MAAA,GAAA,IAAO,gBAAgB,GAAG,CAAA;AAC1B,MAAA,CAAA,EAAA;AACA,MAAA;AAAA,IACF;AACA,IAAA,GAAA,IAAO,EAAA;AACP,IAAA,CAAA,EAAA;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT;AAOO,SAAS,qBAAA,GAAgC;AAC9C,EAAA,OAAO,oBAAA,CAAqB,sBAAsB,CAAA;AACpD;AAMO,SAAS,yBAAA,GAAoC;AAClD,EAAA,OAAO,GAAG,qBAAA,EAAuB,IAAI,oBAAA,CAAqB,oBAAA,EAAsB,CAAC,CAAA,CAAA;AACnF","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"]}
1
+ {"version":3,"sources":["../../src/date/orgFormat.ts"],"names":[],"mappings":"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","file":"org-format.js","sourcesContent":["/**\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"]}