@ethisyscore/core-utils 1.87.0 → 1.88.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,12 @@ var DAY_MONTH_FORMAT = "MMM d";
11
11
  var DAY_MONTH_YEAR_FORMAT = "MMM d, yyyy";
12
12
  var TIME_FORMAT = "h:mm a";
13
13
  var SHORT_DATETIME_FORMAT = "MMM d, h:mm a";
14
+ var MS_PER_DAY = 864e5;
15
+ var MS_PER_HOUR = 36e5;
16
+ var MS_PER_MINUTE = 6e4;
17
+ var DISPLAY_DATE_SLASH_FORMAT = "dd/MM/yyyy";
18
+ var DISPLAY_DATETIME_SLASH_FORMAT = "dd/MM/yyyy HH:mm";
19
+ var DISPLAY_SHORT_DATE_FORMAT = "EEE d MMM";
14
20
  var isDateOnlyString = (value) => {
15
21
  return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value);
16
22
  };
@@ -20,6 +26,19 @@ var formatDate = (date, dateFormat = DATE_FORMAT) => {
20
26
  var getTodayIsoDate = () => {
21
27
  return formatDate(/* @__PURE__ */ new Date());
22
28
  };
29
+ var toIsoDate = (date) => formatDate(date);
30
+ var startOfMonthOffset = (date, months) => {
31
+ if (!date || !dateFns.isValid(date)) {
32
+ return /* @__PURE__ */ new Date(NaN);
33
+ }
34
+ return new Date(date.getFullYear(), date.getMonth() + months, 1);
35
+ };
36
+ var daysBetweenIso = (fromIso, toIso) => {
37
+ const from = parseIsoDateLocal(fromIso);
38
+ const to = parseIsoDateLocal(toIso);
39
+ if (!from || !to) return NaN;
40
+ return Math.round((to.getTime() - from.getTime()) / MS_PER_DAY);
41
+ };
23
42
  var getMonthStartIso = (date) => formatDate(new Date(date.getFullYear(), date.getMonth(), 1));
24
43
  var getMonthEndIso = (date) => formatDate(new Date(date.getFullYear(), date.getMonth() + 1, 0));
25
44
  var parseIsoDateLocal = (value) => {
@@ -34,17 +53,39 @@ var addDaysToIsoDate = (isoDate, days) => {
34
53
  base.setDate(base.getDate() + days);
35
54
  return formatDate(base);
36
55
  };
56
+ var addYearsToIsoDate = (isoDate, years) => {
57
+ const base = parseIsoDateLocal(isoDate);
58
+ if (!base) return isoDate;
59
+ base.setFullYear(base.getFullYear() + years);
60
+ return formatDate(base);
61
+ };
37
62
  var toDateOnlyString = (value) => {
38
63
  if (!value) return "";
39
64
  const datepart = value.split("T")[0];
40
65
  return isDateOnlyString(datepart) ? datepart : "";
41
66
  };
42
- var formatDateString = (date, dateFormat = DATE_FORMAT) => {
43
- if (!date) return "-";
44
- const parsed = dateFns.parseISO(date);
45
- if (!dateFns.isValid(parsed)) return "-";
46
- return dateFns.format(parsed, dateFormat);
67
+ var EM_DASH = "\u2014";
68
+ var formatFixedDateTime = (value, fmt) => {
69
+ if (!value) return EM_DASH;
70
+ const date = new Date(value);
71
+ return Number.isNaN(date.getTime()) ? EM_DASH : dateFns.format(date, fmt);
72
+ };
73
+ var relativeTimePhrase = (diffMinutes) => {
74
+ if (diffMinutes < 1) return "Just now";
75
+ if (diffMinutes < 60) return `${diffMinutes}m ago`;
76
+ const hours = Math.floor(diffMinutes / 60);
77
+ if (hours < 24) return `${hours}h ago`;
78
+ const days = Math.floor(hours / 24);
79
+ if (days < 7) return `${days}d ago`;
80
+ return null;
81
+ };
82
+ var formatSlashDate = (dateString) => {
83
+ const parsed = parseIsoDateLocal(dateString);
84
+ return parsed ? dateFns.format(parsed, DISPLAY_DATE_SLASH_FORMAT) : dateString;
47
85
  };
86
+ var formatCompactDateTime = (value) => formatFixedDateTime(value, DISPLAY_DATETIME_SLASH_FORMAT);
87
+ var formatShortDate = (value) => formatFixedDateTime(value, DISPLAY_SHORT_DATE_FORMAT);
88
+ var formatMonthDay = (value) => formatFixedDateTime(value, DAY_MONTH_FORMAT);
48
89
  var formatDateSafe = (date, formatStr = DATE_FORMAT, fallback = "\u2014") => {
49
90
  if (!date) return fallback;
50
91
  try {
@@ -91,16 +132,8 @@ var formatTimeAgo = (date) => {
91
132
  if (!date) return "";
92
133
  const dateObj = typeof date === "string" ? dateFns.parseISO(date) : date;
93
134
  if (!dateFns.isValid(dateObj)) return "";
94
- const now = /* @__PURE__ */ new Date();
95
- const seconds = Math.floor((now.getTime() - dateObj.getTime()) / 1e3);
96
- if (seconds < 60) return "Just now";
97
- const minutes = Math.floor(seconds / 60);
98
- if (minutes < 60) return `${minutes}m ago`;
99
- const hours = Math.floor(minutes / 60);
100
- if (hours < 24) return `${hours}h ago`;
101
- const days = Math.floor(hours / 24);
102
- if (days < 7) return `${days}d ago`;
103
- return formatDate(dateObj);
135
+ const diffMinutes = Math.floor(((/* @__PURE__ */ new Date()).getTime() - dateObj.getTime()) / MS_PER_MINUTE);
136
+ return relativeTimePhrase(diffMinutes) ?? formatDate(dateObj);
104
137
  };
105
138
 
106
139
  // src/date/formatValidation.ts
@@ -225,42 +258,19 @@ var timespanToMilliseconds = (timespan) => {
225
258
  const seconds = parseInt(s || "0", 10);
226
259
  return (isNaN(hours) ? 0 : hours) * 60 * 60 * 1e3 + (isNaN(minutes) ? 0 : minutes) * 60 * 1e3 + (isNaN(seconds) ? 0 : seconds) * 1e3;
227
260
  };
228
- var millisecondsToHours = (ms) => ms / (1e3 * 60 * 60);
229
- var hoursToMilliseconds = (hours) => hours * (1e3 * 60 * 60);
230
- var formatMillisecondsAsTimeSpent = (ms) => {
231
- if (!ms || ms < 0) return "0h 0min";
232
- const totalMinutes = Math.floor(ms / (1e3 * 60));
233
- const hours = Math.floor(totalMinutes / 60);
234
- const minutes = totalMinutes % 60;
235
- let result = "";
236
- if (hours > 0) result += `${hours}h`;
237
- if (minutes > 0 || hours === 0) result += (hours > 0 ? " " : "") + `${minutes}min`;
238
- return result;
239
- };
240
- function formatDuration(duration) {
241
- if (!duration) return "-";
242
- const [hours, minutes, seconds] = duration.split(":").map(Number);
243
- let result = "";
244
- if (hours) result += `${hours}h`;
245
- if (minutes) result += (result ? " " : "") + `${minutes}m`;
246
- if (seconds) result += (result ? " " : "") + `${seconds}s`;
247
- return result || "0m";
248
- }
249
- var formatDurationNumber = (durationHours) => {
250
- if (durationHours == null || durationHours === 0) return "\u2014";
251
- if (durationHours > 8760) return "Unknown";
252
- if (durationHours < 0) return "Error";
253
- const hours = Math.floor(durationHours);
254
- const minutes = Math.floor(durationHours % 1 * 60);
255
- if (durationHours < 1) return `${minutes}m`;
256
- if (hours < 24) {
257
- if (minutes === 0) return `${hours}h`;
258
- return `${hours}h ${minutes}m`;
259
- }
260
- const days = Math.floor(hours / 24);
261
- const remainingHours = hours % 24;
262
- if (remainingHours === 0) return `${days}d`;
263
- return `${days}d ${remainingHours}h`;
261
+ var millisecondsToHours = (ms) => ms / MS_PER_HOUR;
262
+ var hoursToMilliseconds = (hours) => hours * MS_PER_HOUR;
263
+ var formatDuration = (ms) => {
264
+ if (ms == null || Number.isNaN(ms) || ms < 0) return "\u2014";
265
+ if (ms < MS_PER_MINUTE) return "0m";
266
+ const days = Math.floor(ms / MS_PER_DAY);
267
+ const hours = Math.floor(ms % MS_PER_DAY / MS_PER_HOUR);
268
+ const minutes = Math.floor(ms % MS_PER_HOUR / MS_PER_MINUTE);
269
+ const parts = [];
270
+ if (days) parts.push(`${days}d`);
271
+ if (hours) parts.push(`${hours}h`);
272
+ if (minutes) parts.push(`${minutes}m`);
273
+ return parts.join(" ");
264
274
  };
265
275
  function toHHmm(value, fallback) {
266
276
  if (!value || value.length < 5) return fallback;
@@ -271,19 +281,6 @@ function toHHmmss(value) {
271
281
  if (!trimmed) return null;
272
282
  return trimmed.length === 5 ? `${trimmed}:00` : trimmed;
273
283
  }
274
- var isoToDateInput = (isoString) => {
275
- if (!isoString) return "";
276
- try {
277
- return new Date(isoString).toISOString().split("T")[0];
278
- } catch {
279
- return "";
280
- }
281
- };
282
- var dateInputToIso = (dateString) => {
283
- if (!dateString) return "";
284
- const parsed = /* @__PURE__ */ new Date(dateString + "T00:00:00Z");
285
- return Number.isNaN(parsed.getTime()) ? "" : parsed.toISOString();
286
- };
287
284
  function dateOnlyToIsoUtc(value) {
288
285
  if (!value) return void 0;
289
286
  const parsed = value.includes("T") ? new Date(value) : /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
@@ -356,22 +353,31 @@ exports.DATE_FORMAT = DATE_FORMAT;
356
353
  exports.DAY_DATE_FORMAT = DAY_DATE_FORMAT;
357
354
  exports.DAY_MONTH_FORMAT = DAY_MONTH_FORMAT;
358
355
  exports.DAY_MONTH_YEAR_FORMAT = DAY_MONTH_YEAR_FORMAT;
356
+ exports.DISPLAY_DATETIME_SLASH_FORMAT = DISPLAY_DATETIME_SLASH_FORMAT;
359
357
  exports.DISPLAY_DATE_FORMAT = DISPLAY_DATE_FORMAT;
358
+ exports.DISPLAY_DATE_SLASH_FORMAT = DISPLAY_DATE_SLASH_FORMAT;
359
+ exports.DISPLAY_SHORT_DATE_FORMAT = DISPLAY_SHORT_DATE_FORMAT;
360
+ exports.EM_DASH = EM_DASH;
361
+ exports.MS_PER_DAY = MS_PER_DAY;
362
+ exports.MS_PER_HOUR = MS_PER_HOUR;
363
+ exports.MS_PER_MINUTE = MS_PER_MINUTE;
360
364
  exports.SHORT_DATETIME_FORMAT = SHORT_DATETIME_FORMAT;
361
365
  exports.TIME_FORMAT = TIME_FORMAT;
362
366
  exports.addDaysToIsoDate = addDaysToIsoDate;
367
+ exports.addYearsToIsoDate = addYearsToIsoDate;
363
368
  exports.computeDateRange = computeDateRange;
364
369
  exports.currentMonthRangeUtc = currentMonthRangeUtc;
365
- exports.dateInputToIso = dateInputToIso;
366
370
  exports.dateOnlyToIsoUtc = dateOnlyToIsoUtc;
371
+ exports.daysBetweenIso = daysBetweenIso;
367
372
  exports.ensureUtcIso = ensureUtcIso;
373
+ exports.formatCompactDateTime = formatCompactDateTime;
368
374
  exports.formatDate = formatDate;
369
375
  exports.formatDateSafe = formatDateSafe;
370
- exports.formatDateString = formatDateString;
371
376
  exports.formatDateWithOrdinal = formatDateWithOrdinal;
372
377
  exports.formatDuration = formatDuration;
373
- exports.formatDurationNumber = formatDurationNumber;
374
- exports.formatMillisecondsAsTimeSpent = formatMillisecondsAsTimeSpent;
378
+ exports.formatMonthDay = formatMonthDay;
379
+ exports.formatShortDate = formatShortDate;
380
+ exports.formatSlashDate = formatSlashDate;
375
381
  exports.formatTimeAgo = formatTimeAgo;
376
382
  exports.getCurrentOffsetMinutes = getCurrentOffsetMinutes;
377
383
  exports.getDateRange = getDateRange;
@@ -380,16 +386,18 @@ exports.getMonthStartIso = getMonthStartIso;
380
386
  exports.getTodayIsoDate = getTodayIsoDate;
381
387
  exports.hoursToMilliseconds = hoursToMilliseconds;
382
388
  exports.isDateOnlyString = isDateOnlyString;
383
- exports.isoToDateInput = isoToDateInput;
384
389
  exports.isoUtcToLocalDateTimeInput = isoUtcToLocalDateTimeInput;
385
390
  exports.localDateTimeInputToIsoUtc = localDateTimeInputToIsoUtc;
386
391
  exports.millisecondsToHours = millisecondsToHours;
387
392
  exports.nowLocalDateTimeInputValue = nowLocalDateTimeInputValue;
388
393
  exports.parseIsoDateLocal = parseIsoDateLocal;
394
+ exports.relativeTimePhrase = relativeTimePhrase;
395
+ exports.startOfMonthOffset = startOfMonthOffset;
389
396
  exports.timespanToMilliseconds = timespanToMilliseconds;
390
397
  exports.toDateOnlyString = toDateOnlyString;
391
398
  exports.toHHmm = toHHmm;
392
399
  exports.toHHmmss = toHHmmss;
400
+ exports.toIsoDate = toIsoDate;
393
401
  exports.validateDateFormat = validateDateFormat;
394
402
  exports.validateTimeFormat = validateTimeFormat;
395
403
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/date/constants.ts","../../src/date/iso.ts","../../src/date/format.ts","../../src/date/formatValidation.ts","../../src/date/duration.ts","../../src/date/wire.ts","../../src/date/range.ts"],"names":["format","isValid","parseISO","year","month","day","subMonths","subYears","subDays"],"mappings":"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","file":"index.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","import { format, isValid } from \"date-fns\";\n\nimport { DATE_FORMAT } from \"./constants\";\n\n/** Type guard: is `value` a bare `yyyy-MM-dd` date-only string? */\nexport const isDateOnlyString = (value: unknown): value is string => {\n return typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value);\n};\n\n/**\n * Formats a `Date`, defaulting to the standard application ISO date format\n * (`yyyy-MM-dd`). Pass `dateFormat` to override.\n */\nexport const formatDate = (date: Date, dateFormat: string = DATE_FORMAT): string => {\n return format(date, dateFormat);\n};\n\n/** Today's date as a `yyyy-MM-dd` ISO string (local calendar day). */\nexport const getTodayIsoDate = (): string => {\n return formatDate(new Date());\n};\n\n/** First day of the given date's month, as a `yyyy-MM-dd` ISO string. */\nexport const getMonthStartIso = (date: Date): string =>\n formatDate(new Date(date.getFullYear(), date.getMonth(), 1));\n\n/** Last day of the given date's month, as a `yyyy-MM-dd` ISO string. */\nexport const getMonthEndIso = (date: Date): string =>\n formatDate(new Date(date.getFullYear(), date.getMonth() + 1, 0));\n\n/**\n * Parses a date-only `yyyy-MM-dd` string as a LOCAL date. Returns `null` when\n * the value is not a date-only string or is not a valid calendar date. Avoids\n * the `new Date(\"yyyy-MM-dd\")` UTC parse, which shifts the day in non-UTC zones.\n */\nexport const parseIsoDateLocal = (value: string | null | undefined): Date | null => {\n if (!value || !isDateOnlyString(value)) return null;\n const [year, month, day] = value.split(\"-\").map(Number);\n const d = new Date(year, month - 1, day);\n return isValid(d) ? d : null;\n};\n\n/**\n * Adds `days` to an ISO `yyyy-MM-dd` date and returns the result as a `yyyy-MM-dd`\n * string. Parses and formats in local time (via `parseIsoDateLocal` / `formatDate`)\n * so the arithmetic can't drift across a day boundary in non-UTC timezones/DST.\n * Returns the input unchanged when it is not a valid date-only string.\n */\nexport const addDaysToIsoDate = (isoDate: string, days: number): string => {\n const base = parseIsoDateLocal(isoDate);\n if (!base) return isoDate;\n base.setDate(base.getDate() + days);\n return formatDate(base);\n};\n\n/**\n * Extracts a date-only string (`yyyy-MM-dd`) from a date string.\n * - Date-only strings (`2024-05-15`) are returned as-is.\n * - ISO datetime strings (`2024-05-15T00:00:00+05:30`) have the date portion\n * extracted directly from the string to preserve the original calendar date\n * without timezone conversion.\n * - Returns empty string for null, undefined, or unparseable values.\n */\nexport const toDateOnlyString = (value: string | null | undefined): string => {\n if (!value) return \"\";\n // Extract the yyyy-MM-dd portion before the \"T\" separator to avoid timezone\n // conversion that can shift the date by a day.\n const datepart = value.split(\"T\")[0];\n return isDateOnlyString(datepart) ? datepart : \"\";\n};\n","import { format, isValid, parseISO } from \"date-fns\";\n\nimport { DATE_FORMAT } from \"./constants\";\nimport { formatDate, isDateOnlyString } from \"./iso\";\n\n/**\n * Formats a date string, defaulting to the standard application format\n * (`yyyy-MM-dd`). Uses `parseISO` to safely handle ISO strings. Pass\n * `dateFormat` to override. Returns `\"-\"` for empty, missing, or unparseable\n * input rather than throwing.\n */\nexport const formatDateString = (date?: string | null, dateFormat: string = DATE_FORMAT): string => {\n if (!date) return \"-\";\n const parsed = parseISO(date);\n if (!isValid(parsed)) return \"-\";\n return format(parsed, dateFormat);\n};\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 now = new Date();\n const seconds = Math.floor((now.getTime() - dateObj.getTime()) / 1000);\n\n // Future dates (shouldn't normally happen, but safety first).\n if (seconds < 60) return \"Just now\";\n\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n\n const hours = Math.floor(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n\n const days = Math.floor(hours / 24);\n if (days < 7) return `${days}d ago`;\n\n return 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 * Pure duration / timespan helpers — no external dependencies.\n *\n * Cover the two duration shapes that flow across the EthisysCore wire: a\n * `\"HH:mm:ss\"` timespan string (.NET `TimeSpan`) and a millisecond count.\n */\n\n/** Converts a `\"HH:mm:ss\"` timespan string to milliseconds. */\nexport const timespanToMilliseconds = (timespan: string): number => {\n if (!timespan) return 0;\n\n const [h = \"0\", m = \"0\", s = \"0\"] = timespan.split(\":\");\n const hours = parseInt(h || \"0\", 10);\n const minutes = parseInt(m || \"0\", 10);\n const seconds = parseInt(s || \"0\", 10);\n\n return (\n (isNaN(hours) ? 0 : hours) * 60 * 60 * 1000 +\n (isNaN(minutes) ? 0 : minutes) * 60 * 1000 +\n (isNaN(seconds) ? 0 : seconds) * 1000\n );\n};\n\n/** Converts milliseconds to hours (as a float). */\nexport const millisecondsToHours = (ms: number): number => ms / (1000 * 60 * 60);\n\n/** Converts hours to milliseconds. */\nexport const hoursToMilliseconds = (hours: number): number => hours * (1000 * 60 * 60);\n\n/** Formats a milliseconds count as `4h 15min`. */\nexport const formatMillisecondsAsTimeSpent = (ms: number): string => {\n if (!ms || ms < 0) return \"0h 0min\";\n\n const totalMinutes = Math.floor(ms / (1000 * 60));\n const hours = Math.floor(totalMinutes / 60);\n const minutes = totalMinutes % 60;\n\n let result = \"\";\n if (hours > 0) result += `${hours}h`;\n if (minutes > 0 || hours === 0) result += (hours > 0 ? \" \" : \"\") + `${minutes}min`;\n return result;\n};\n\n/** Formats a `\"HH:mm:ss\"` duration string as human readable (e.g. `2h 30m 0s`). */\nexport function formatDuration(duration?: string): string {\n if (!duration) return \"-\";\n const [hours, minutes, seconds] = duration.split(\":\").map(Number);\n let result = \"\";\n if (hours) result += `${hours}h`;\n if (minutes) result += (result ? \" \" : \"\") + `${minutes}m`;\n if (seconds) result += (result ? \" \" : \"\") + `${seconds}s`;\n return result || \"0m\";\n}\n\n/**\n * Formats a fractional-hours number as a compact human-readable duration\n * (`45m`, `2h 30m`, `3d 4h`). Defensively caps unreasonable values.\n */\nexport const formatDurationNumber = (durationHours?: number | null): string => {\n if (durationHours == null || durationHours === 0) return \"—\";\n\n // Defensive: cap at a reasonable max (1 year = 8760 hours).\n if (durationHours > 8760) return \"Unknown\";\n // Negative duration is also suspicious.\n if (durationHours < 0) return \"Error\";\n\n const hours = Math.floor(durationHours);\n const minutes = Math.floor((durationHours % 1) * 60);\n\n if (durationHours < 1) return `${minutes}m`;\n\n if (hours < 24) {\n if (minutes === 0) return `${hours}h`;\n return `${hours}h ${minutes}m`;\n }\n\n const days = Math.floor(hours / 24);\n const remainingHours = hours % 24;\n if (remainingHours === 0) return `${days}d`;\n return `${days}d ${remainingHours}h`;\n};\n\n/**\n * Trims an API-supplied `\"HH:mm:ss\"` down to the `\"HH:mm\"` string used by\n * MUI `TimePicker`-backed forms. Returns the supplied fallback when the value\n * is missing or shorter than five characters.\n */\nexport function toHHmm(value: string | null | undefined, fallback: string): string {\n if (!value || value.length < 5) return fallback;\n return value.slice(0, 5);\n}\n\n/**\n * Serialises a non-empty `\"HH:mm\"` / `\"HH:mm:ss\"` value back to the canonical\n * `\"HH:mm:ss\"` API format. Returns `null` for empty input so callers can block\n * the save rather than silently persisting midnight — an empty TimePicker\n * (cleared via keyboard) is an unsaved edit, not a legitimate `00:00:00` value.\n */\nexport function toHHmmss(value: string): string | null {\n const trimmed = value.trim();\n if (!trimmed) return null;\n return trimmed.length === 5 ? `${trimmed}:00` : trimmed;\n}\n","import { format, isValid } from \"date-fns\";\n\n// ---------------------------------------------------------------------------\n// Wire-boundary helpers\n// ---------------------------------------------------------------------------\n//\n// HTML's `<input type=\"date\">` and `<input type=\"datetime-local\">` emit naive\n// local strings with NO timezone suffix. Sending those raw to a .NET backend\n// that deserialises into `DateTimeOffset` parses them using the BE process's\n// local offset (UTC on most container hosts), silently misinterpreting the\n// user's wall-clock as UTC and shifting the stored timestamp by the user's\n// offset (e.g. losing the first hour of the local day during BST). These\n// helpers bridge cleanly between the input shapes and full ISO-8601 UTC.\n//\n// All are pure and timezone-aware via the runtime `Date` object.\n\n/** Converts an ISO date string to date-input format (`yyyy-MM-dd`). */\nexport const isoToDateInput = (isoString: string | undefined): string => {\n if (!isoString) return \"\";\n try {\n return new Date(isoString).toISOString().split(\"T\")[0];\n } catch {\n return \"\";\n }\n};\n\n/** Converts date-input format (`yyyy-MM-dd`) to a UTC ISO string at midnight UTC. */\nexport const dateInputToIso = (dateString: string): string => {\n if (!dateString) return \"\";\n const parsed = new Date(dateString + \"T00:00:00Z\");\n return Number.isNaN(parsed.getTime()) ? \"\" : parsed.toISOString();\n};\n\n/**\n * Normalises a date-only value from `<input type=\"date\">` (`yyyy-MM-dd`) into a\n * canonical UTC ISO-8601 timestamp at midnight UTC, e.g.\n * `2026-07-14` → `2026-07-14T00:00:00.000Z`. Use at the API-payload boundary\n * for fields the BE types as `DateTimeOffset` — a bare date-only string risks\n * ambiguous/failed deserialisation. A value that is already a full ISO\n * timestamp is passed through unchanged. Empty/invalid → `undefined` so callers\n * omit the field (BE clears it).\n */\nexport function dateOnlyToIsoUtc(value: string | null | undefined): string | undefined {\n if (!value) return undefined;\n const parsed = value.includes(\"T\") ? new Date(value) : new Date(`${value}T00:00:00Z`);\n return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString();\n}\n\n/**\n * Returns the current local wall-clock as a `yyyy-MM-ddTHH:mm` string — the\n * format an `<input type=\"datetime-local\">` element expects. Minute precision;\n * seconds and timezone deliberately omitted.\n */\nexport function nowLocalDateTimeInputValue(): string {\n return format(new Date(), \"yyyy-MM-dd'T'HH:mm\");\n}\n\n/**\n * Converts a naive local-time string from `<input type=\"datetime-local\">`\n * (shape `yyyy-MM-ddTHH:mm` or `yyyy-MM-ddTHH:mm:ss`) into a full ISO-8601 UTC\n * string with `Z` suffix that .NET `DateTimeOffset` cannot ambiguously\n * interpret. Empty / undefined inputs pass through as `undefined`.\n */\nexport function localDateTimeInputToIsoUtc(value: string | null | undefined): string | undefined {\n if (!value) return undefined;\n const parsed = new Date(value);\n if (!isValid(parsed)) return undefined;\n return parsed.toISOString();\n}\n\n/**\n * Ensures an ISO-8601 string is treated as UTC by appending a `Z` suffix when\n * no explicit offset or UTC indicator is present. The backend may emit\n * timestamps like `2026-05-26T06:21:00` (no Z), which JS would otherwise parse\n * as local time.\n */\nexport function ensureUtcIso(value: string): string {\n return /[Zz]|[+-]\\d{2}:?\\d{2}$/.test(value) ? value : value + \"Z\";\n}\n\n/**\n * Converts an ISO-8601 string (with or without explicit offset) into the naive\n * local-time shape `yyyy-MM-ddTHH:mm` expected by `<input type=\"datetime-local\">`.\n * Returns the supplied fallback (default empty string) when the input is\n * missing or unparseable. When `value` carries no offset or `Z` suffix it is\n * treated as UTC before converting to local time, so the edit form displays the\n * correct time regardless of the user's timezone.\n */\nexport function isoUtcToLocalDateTimeInput(value: string | null | undefined, fallback = \"\"): string {\n if (!value) return fallback;\n const parsed = new Date(ensureUtcIso(value));\n if (!isValid(parsed)) return fallback;\n return format(parsed, \"yyyy-MM-dd'T'HH:mm\");\n}\n","import { format, subDays, subMonths, subYears } from \"date-fns\";\n\nimport { DATE_FORMAT } from \"./constants\";\n\nexport interface DateRange {\n startDate: string;\n endDate: string;\n}\n\n/**\n * Returns a `{ startDate, endDate }` date range for a named relative period,\n * formatted with `dateFormat` (defaults to the ISO date-only format). Unknown\n * periods fall back to the last week.\n * @param timePeriod - one of `week` | `month` | `quarter` | `year`\n * @param dateFormat - output format for both bounds (defaults to `DATE_FORMAT`)\n */\nexport const getDateRange = (timePeriod: string, dateFormat: string = DATE_FORMAT): DateRange => {\n const today = new Date();\n switch (timePeriod) {\n case \"month\":\n return { startDate: format(subMonths(today, 1), dateFormat), endDate: format(today, dateFormat) };\n case \"quarter\":\n return { startDate: format(subMonths(today, 3), dateFormat), endDate: format(today, dateFormat) };\n case \"year\":\n return { startDate: format(subYears(today, 1), dateFormat), endDate: format(today, dateFormat) };\n case \"week\":\n default:\n return { startDate: format(subDays(today, 7), dateFormat), endDate: format(today, dateFormat) };\n }\n};\n\n/** Computes an ISO timestamp range from a number of days back to now. */\nexport function computeDateRange(days: number): { from: string; to: string } {\n const to = new Date().toISOString();\n const from = new Date(Date.now() - days * 86_400_000).toISOString();\n return { from, to };\n}\n\n/**\n * Returns the current calendar month as a half-open UTC range `[fromUtc, beforeUtc)`,\n * suitable for \"this month\" count filters. `fromUtc` is the first instant of the\n * month; `beforeUtc` is the first instant of the next month (exclusive).\n */\nexport function currentMonthRangeUtc(today: Date = new Date()): { fromUtc: string; beforeUtc: string } {\n const year = today.getUTCFullYear();\n const month = today.getUTCMonth();\n return {\n fromUtc: new Date(Date.UTC(year, month, 1)).toISOString(),\n beforeUtc: new Date(Date.UTC(year, month + 1, 1)).toISOString(),\n };\n}\n\n/**\n * Computes the current UTC-offset minutes for a given IANA zone name using the\n * runtime's `Intl` implementation. Returns `null` when the zone is unknown.\n *\n * Used by timezone auto-populate fallbacks so a browser reporting an IANA zone\n * that isn't in the backend seed (e.g. `Europe/London` during BST) can still\n * match against a seeded zone sharing the same current offset.\n */\nexport function getCurrentOffsetMinutes(iana: string): number | null {\n try {\n const parts = new Intl.DateTimeFormat(\"en\", {\n timeZone: iana,\n timeZoneName: \"longOffset\",\n }).formatToParts(new Date());\n const label = parts.find((p) => p.type === \"timeZoneName\")?.value ?? \"\";\n if (label === \"GMT\" || label === \"UTC\") return 0;\n const m = label.match(/GMT([+-])(\\d{2}):(\\d{2})/);\n if (!m) return null;\n const sign = m[1] === \"+\" ? 1 : -1;\n return sign * (parseInt(m[2], 10) * 60 + parseInt(m[3], 10));\n } catch {\n return null;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/date/constants.ts","../../src/date/iso.ts","../../src/date/format.ts","../../src/date/formatValidation.ts","../../src/date/duration.ts","../../src/date/wire.ts","../../src/date/range.ts"],"names":["format","isValid","parseISO","year","month","day","subMonths","subYears","subDays"],"mappings":"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","file":"index.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 } from \"date-fns\";\n\nimport { DATE_FORMAT, MS_PER_DAY } from \"./constants\";\n\n/** Type guard: is `value` a bare `yyyy-MM-dd` date-only string? */\nexport const isDateOnlyString = (value: unknown): value is string => {\n return typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value);\n};\n\n/**\n * Formats a `Date`, defaulting to the standard application ISO date format\n * (`yyyy-MM-dd`). Pass `dateFormat` to override.\n *\n * Name note: this `formatDate` takes a `Date`. The org-aware `formatDate` on the\n * `@ethisyscore/core-utils/date/org-format` sub-path takes an ISO STRING and\n * returns org-configured display text — a different contract that never collides\n * because it lives on a different entry point. For the common Date→`yyyy-MM-dd`\n * case prefer {@link toIsoDate}, whose name sidesteps the overload.\n */\nexport const formatDate = (date: Date, dateFormat: string = DATE_FORMAT): string => {\n return format(date, dateFormat);\n};\n\n/** Today's date as a `yyyy-MM-dd` ISO string (local calendar day). */\nexport const getTodayIsoDate = (): string => {\n return formatDate(new Date());\n};\n\n/**\n * Formats a `Date` as a `yyyy-MM-dd` ISO date-only string in local time — the\n * inverse of {@link parseIsoDateLocal}. Equivalent to `formatDate(date)`, exposed\n * under the clearer name for the date-only round-trip.\n */\nexport const toIsoDate = (date: Date): string => formatDate(date);\n\n/**\n * Returns a new local `Date` at the first day of `date`'s month, offset by `months`\n * (may be negative), normalising the day-of-month to 1 — e.g. offsetting 15 Mar 2026\n * by -1 gives 1 Feb 2026, not 15 Feb. Returns an `Invalid Date` for a null/invalid\n * input so month-axis arithmetic propagates NaN rather than throwing. Used for\n * month-granular axis/range building (e.g. a Gantt chart).\n */\nexport const startOfMonthOffset = (date: Date | null | undefined, months: number): Date => {\n if (!date || !isValid(date)) {\n return new Date(NaN);\n }\n return new Date(date.getFullYear(), date.getMonth() + months, 1);\n};\n\n/**\n * Whole-day difference between two `yyyy-MM-dd` date-only strings (`toIso - fromIso`),\n * rounded to guard against DST half-day drift when the range crosses a clock-change\n * boundary. Parses both bounds as LOCAL dates (via {@link parseIsoDateLocal}); returns\n * `NaN` when either is not a valid date-only string.\n */\nexport const daysBetweenIso = (fromIso: string, toIso: string): number => {\n const from = parseIsoDateLocal(fromIso);\n const to = parseIsoDateLocal(toIso);\n if (!from || !to) return NaN;\n return Math.round((to.getTime() - from.getTime()) / MS_PER_DAY);\n};\n\n/** First day of the given date's month, as a `yyyy-MM-dd` ISO string. */\nexport const getMonthStartIso = (date: Date): string =>\n formatDate(new Date(date.getFullYear(), date.getMonth(), 1));\n\n/** Last day of the given date's month, as a `yyyy-MM-dd` ISO string. */\nexport const getMonthEndIso = (date: Date): string =>\n formatDate(new Date(date.getFullYear(), date.getMonth() + 1, 0));\n\n/**\n * Parses a date-only `yyyy-MM-dd` string as a LOCAL date. Returns `null` when\n * the value is not a date-only string or is not a valid calendar date. Avoids\n * the `new Date(\"yyyy-MM-dd\")` UTC parse, which shifts the day in non-UTC zones.\n */\nexport const parseIsoDateLocal = (value: string | null | undefined): Date | null => {\n if (!value || !isDateOnlyString(value)) return null;\n const [year, month, day] = value.split(\"-\").map(Number);\n const d = new Date(year, month - 1, day);\n return isValid(d) ? d : null;\n};\n\n/**\n * Adds `days` to an ISO `yyyy-MM-dd` date and returns the result as a `yyyy-MM-dd`\n * string. Parses and formats in local time (via `parseIsoDateLocal` / `formatDate`)\n * so the arithmetic can't drift across a day boundary in non-UTC timezones/DST.\n * Returns the input unchanged when it is not a valid date-only string.\n */\nexport const addDaysToIsoDate = (isoDate: string, days: number): string => {\n const base = parseIsoDateLocal(isoDate);\n if (!base) return isoDate;\n base.setDate(base.getDate() + days);\n return formatDate(base);\n};\n\n/**\n * Adds `years` to an ISO `yyyy-MM-dd` date and returns the result as a `yyyy-MM-dd`\n * string. Parses and formats in local time (like {@link addDaysToIsoDate}) so the\n * arithmetic can't drift across a day boundary in non-UTC timezones/DST. Used to seed\n * default expiry / end-of-term dates (insurance cover, licences, fixed-term contracts).\n * A 29 Feb base in a non-leap target year rolls to 1 Mar, matching `Date.setFullYear`.\n * Returns the input unchanged when it is not a valid date-only string.\n */\nexport const addYearsToIsoDate = (isoDate: string, years: number): string => {\n const base = parseIsoDateLocal(isoDate);\n if (!base) return isoDate;\n base.setFullYear(base.getFullYear() + years);\n return formatDate(base);\n};\n\n/**\n * Extracts a date-only string (`yyyy-MM-dd`) from a date string.\n * - Date-only strings (`2024-05-15`) are returned as-is.\n * - ISO datetime strings (`2024-05-15T00:00:00+05:30`) have the date portion\n * extracted directly from the string to preserve the original calendar date\n * without timezone conversion.\n * - Returns empty string for null, undefined, or unparseable values.\n */\nexport const toDateOnlyString = (value: string | null | undefined): string => {\n if (!value) return \"\";\n // Extract the yyyy-MM-dd portion before the \"T\" separator to avoid timezone\n // conversion that can shift the date by a day.\n const datepart = value.split(\"T\")[0];\n return isDateOnlyString(datepart) ? datepart : \"\";\n};\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 * Pure duration / timespan helpers — no external dependencies.\n *\n * Cover the two duration shapes that flow across the EthisysCore wire: a\n * `\"HH:mm:ss\"` timespan string (.NET `TimeSpan`) and a millisecond count.\n */\nimport { MS_PER_DAY, MS_PER_HOUR, MS_PER_MINUTE } from \"./constants\";\n\n/** Converts a `\"HH:mm:ss\"` timespan string to milliseconds. */\nexport const timespanToMilliseconds = (timespan: string): number => {\n if (!timespan) return 0;\n\n const [h = \"0\", m = \"0\", s = \"0\"] = timespan.split(\":\");\n const hours = parseInt(h || \"0\", 10);\n const minutes = parseInt(m || \"0\", 10);\n const seconds = parseInt(s || \"0\", 10);\n\n return (\n (isNaN(hours) ? 0 : hours) * 60 * 60 * 1000 +\n (isNaN(minutes) ? 0 : minutes) * 60 * 1000 +\n (isNaN(seconds) ? 0 : seconds) * 1000\n );\n};\n\n/** Converts milliseconds to hours (as a float). */\nexport const millisecondsToHours = (ms: number): number => ms / MS_PER_HOUR;\n\n/** Converts hours to milliseconds. */\nexport const hoursToMilliseconds = (hours: number): number => hours * MS_PER_HOUR;\n\n/**\n * The one human-readable duration formatter. Takes a MILLISECOND count and renders\n * the largest non-zero units as `Xd Xh Xm` (e.g. `4h 15m`, `2d 3h`, `45m`). Zero is\n * a real value (`0m`); a nullish/NaN/negative input is missing data and returns the\n * em-dash. Minute granularity — seconds are not shown.\n *\n * Feed non-millisecond inputs through the converters in this module:\n * `formatDuration(timespanToMilliseconds(\"04:15:00\"))` // \"HH:mm:ss\" TimeSpan\n * `formatDuration(hoursToMilliseconds(2.5))` // fractional hours\n */\nexport const formatDuration = (ms: number | null | undefined): string => {\n if (ms == null || Number.isNaN(ms) || ms < 0) return \"—\";\n if (ms < MS_PER_MINUTE) return \"0m\";\n\n const days = Math.floor(ms / MS_PER_DAY);\n const hours = Math.floor((ms % MS_PER_DAY) / MS_PER_HOUR);\n const minutes = Math.floor((ms % MS_PER_HOUR) / MS_PER_MINUTE);\n\n const parts: string[] = [];\n if (days) parts.push(`${days}d`);\n if (hours) parts.push(`${hours}h`);\n if (minutes) parts.push(`${minutes}m`);\n return parts.join(\" \");\n};\n\n/**\n * Trims an API-supplied `\"HH:mm:ss\"` down to the `\"HH:mm\"` string used by\n * MUI `TimePicker`-backed forms. Returns the supplied fallback when the value\n * is missing or shorter than five characters.\n */\nexport function toHHmm(value: string | null | undefined, fallback: string): string {\n if (!value || value.length < 5) return fallback;\n return value.slice(0, 5);\n}\n\n/**\n * Serialises a non-empty `\"HH:mm\"` / `\"HH:mm:ss\"` value back to the canonical\n * `\"HH:mm:ss\"` API format. Returns `null` for empty input so callers can block\n * the save rather than silently persisting midnight — an empty TimePicker\n * (cleared via keyboard) is an unsaved edit, not a legitimate `00:00:00` value.\n */\nexport function toHHmmss(value: string): string | null {\n const trimmed = value.trim();\n if (!trimmed) return null;\n return trimmed.length === 5 ? `${trimmed}:00` : trimmed;\n}\n","import { format, isValid } from \"date-fns\";\n\n// ---------------------------------------------------------------------------\n// Wire-boundary helpers\n// ---------------------------------------------------------------------------\n//\n// HTML's `<input type=\"date\">` and `<input type=\"datetime-local\">` emit naive\n// local strings with NO timezone suffix. Sending those raw to a .NET backend\n// that deserialises into `DateTimeOffset` parses them using the BE process's\n// local offset (UTC on most container hosts), silently misinterpreting the\n// user's wall-clock as UTC and shifting the stored timestamp by the user's\n// offset (e.g. losing the first hour of the local day during BST). These\n// helpers bridge cleanly between the input shapes and full ISO-8601 UTC.\n//\n// All are pure and timezone-aware via the runtime `Date` object.\n\n/**\n * Normalises a date-only value from `<input type=\"date\">` (`yyyy-MM-dd`) into a\n * canonical UTC ISO-8601 timestamp at midnight UTC, e.g.\n * `2026-07-14` → `2026-07-14T00:00:00.000Z`. Use at the API-payload boundary\n * for fields the BE types as `DateTimeOffset` — a bare date-only string risks\n * ambiguous/failed deserialisation. A value that is already a full ISO\n * timestamp is passed through unchanged. Empty/invalid → `undefined` so callers\n * omit the field (BE clears it).\n */\nexport function dateOnlyToIsoUtc(value: string | null | undefined): string | undefined {\n if (!value) return undefined;\n const parsed = value.includes(\"T\") ? new Date(value) : new Date(`${value}T00:00:00Z`);\n return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString();\n}\n\n/**\n * Returns the current local wall-clock as a `yyyy-MM-ddTHH:mm` string — the\n * format an `<input type=\"datetime-local\">` element expects. Minute precision;\n * seconds and timezone deliberately omitted.\n */\nexport function nowLocalDateTimeInputValue(): string {\n return format(new Date(), \"yyyy-MM-dd'T'HH:mm\");\n}\n\n/**\n * Converts a naive local-time string from `<input type=\"datetime-local\">`\n * (shape `yyyy-MM-ddTHH:mm` or `yyyy-MM-ddTHH:mm:ss`) into a full ISO-8601 UTC\n * string with `Z` suffix that .NET `DateTimeOffset` cannot ambiguously\n * interpret. Empty / undefined inputs pass through as `undefined`.\n */\nexport function localDateTimeInputToIsoUtc(value: string | null | undefined): string | undefined {\n if (!value) return undefined;\n const parsed = new Date(value);\n if (!isValid(parsed)) return undefined;\n return parsed.toISOString();\n}\n\n/**\n * Ensures an ISO-8601 string is treated as UTC by appending a `Z` suffix when\n * no explicit offset or UTC indicator is present. The backend may emit\n * timestamps like `2026-05-26T06:21:00` (no Z), which JS would otherwise parse\n * as local time.\n */\nexport function ensureUtcIso(value: string): string {\n return /[Zz]|[+-]\\d{2}:?\\d{2}$/.test(value) ? value : value + \"Z\";\n}\n\n/**\n * Converts an ISO-8601 string (with or without explicit offset) into the naive\n * local-time shape `yyyy-MM-ddTHH:mm` expected by `<input type=\"datetime-local\">`.\n * Returns the supplied fallback (default empty string) when the input is\n * missing or unparseable. When `value` carries no offset or `Z` suffix it is\n * treated as UTC before converting to local time, so the edit form displays the\n * correct time regardless of the user's timezone.\n */\nexport function isoUtcToLocalDateTimeInput(value: string | null | undefined, fallback = \"\"): string {\n if (!value) return fallback;\n const parsed = new Date(ensureUtcIso(value));\n if (!isValid(parsed)) return fallback;\n return format(parsed, \"yyyy-MM-dd'T'HH:mm\");\n}\n","import { format, subDays, subMonths, subYears } from \"date-fns\";\n\nimport { DATE_FORMAT } from \"./constants\";\n\nexport interface DateRange {\n startDate: string;\n endDate: string;\n}\n\n/**\n * Returns a `{ startDate, endDate }` date range for a named relative period,\n * formatted with `dateFormat` (defaults to the ISO date-only format). Unknown\n * periods fall back to the last week.\n * @param timePeriod - one of `week` | `month` | `quarter` | `year`\n * @param dateFormat - output format for both bounds (defaults to `DATE_FORMAT`)\n */\nexport const getDateRange = (timePeriod: string, dateFormat: string = DATE_FORMAT): DateRange => {\n const today = new Date();\n switch (timePeriod) {\n case \"month\":\n return { startDate: format(subMonths(today, 1), dateFormat), endDate: format(today, dateFormat) };\n case \"quarter\":\n return { startDate: format(subMonths(today, 3), dateFormat), endDate: format(today, dateFormat) };\n case \"year\":\n return { startDate: format(subYears(today, 1), dateFormat), endDate: format(today, dateFormat) };\n case \"week\":\n default:\n return { startDate: format(subDays(today, 7), dateFormat), endDate: format(today, dateFormat) };\n }\n};\n\n/** Computes an ISO timestamp range from a number of days back to now. */\nexport function computeDateRange(days: number): { from: string; to: string } {\n const to = new Date().toISOString();\n const from = new Date(Date.now() - days * 86_400_000).toISOString();\n return { from, to };\n}\n\n/**\n * Returns the current calendar month as a half-open UTC range `[fromUtc, beforeUtc)`,\n * suitable for \"this month\" count filters. `fromUtc` is the first instant of the\n * month; `beforeUtc` is the first instant of the next month (exclusive).\n */\nexport function currentMonthRangeUtc(today: Date = new Date()): { fromUtc: string; beforeUtc: string } {\n const year = today.getUTCFullYear();\n const month = today.getUTCMonth();\n return {\n fromUtc: new Date(Date.UTC(year, month, 1)).toISOString(),\n beforeUtc: new Date(Date.UTC(year, month + 1, 1)).toISOString(),\n };\n}\n\n/**\n * Computes the current UTC-offset minutes for a given IANA zone name using the\n * runtime's `Intl` implementation. Returns `null` when the zone is unknown.\n *\n * Used by timezone auto-populate fallbacks so a browser reporting an IANA zone\n * that isn't in the backend seed (e.g. `Europe/London` during BST) can still\n * match against a seeded zone sharing the same current offset.\n */\nexport function getCurrentOffsetMinutes(iana: string): number | null {\n try {\n const parts = new Intl.DateTimeFormat(\"en\", {\n timeZone: iana,\n timeZoneName: \"longOffset\",\n }).formatToParts(new Date());\n const label = parts.find((p) => p.type === \"timeZoneName\")?.value ?? \"\";\n if (label === \"GMT\" || label === \"UTC\") return 0;\n const m = label.match(/GMT([+-])(\\d{2}):(\\d{2})/);\n if (!m) return null;\n const sign = m[1] === \"+\" ? 1 : -1;\n return sign * (parseInt(m[2], 10) * 60 + parseInt(m[3], 10));\n } catch {\n return null;\n }\n}\n"]}
@@ -21,16 +21,55 @@ declare const DAY_MONTH_YEAR_FORMAT = "MMM d, yyyy";
21
21
  declare const TIME_FORMAT = "h:mm a";
22
22
  /** Short datetime, e.g. `Mar 5, 2:30 PM`. */
23
23
  declare const SHORT_DATETIME_FORMAT = "MMM d, h:mm a";
24
+ /** Milliseconds in a day — whole-day duration / diff math (`daysBetweenIso`, axis geometry). */
25
+ declare const MS_PER_DAY = 86400000;
26
+ /** Milliseconds in an hour — duration math without inline `1000 * 60 * 60`. */
27
+ declare const MS_PER_HOUR = 3600000;
28
+ /** Milliseconds in a minute — duration / relative-time math without inline `1000 * 60`. */
29
+ declare const MS_PER_MINUTE = 60000;
30
+ /** Fixed numeric UK date, e.g. `05/03/2026`. Deliberately NOT org-driven — form-field / compact contexts. */
31
+ declare const DISPLAY_DATE_SLASH_FORMAT = "dd/MM/yyyy";
32
+ /** Fixed numeric UK datetime, e.g. `05/03/2026 14:30`. Deliberately NOT org-driven. */
33
+ declare const DISPLAY_DATETIME_SLASH_FORMAT = "dd/MM/yyyy HH:mm";
34
+ /** Weekday + day + abbreviated month, e.g. `Wed 5 Mar`. */
35
+ declare const DISPLAY_SHORT_DATE_FORMAT = "EEE d MMM";
24
36
 
25
37
  /** Type guard: is `value` a bare `yyyy-MM-dd` date-only string? */
26
38
  declare const isDateOnlyString: (value: unknown) => value is string;
27
39
  /**
28
40
  * Formats a `Date`, defaulting to the standard application ISO date format
29
41
  * (`yyyy-MM-dd`). Pass `dateFormat` to override.
42
+ *
43
+ * Name note: this `formatDate` takes a `Date`. The org-aware `formatDate` on the
44
+ * `@ethisyscore/core-utils/date/org-format` sub-path takes an ISO STRING and
45
+ * returns org-configured display text — a different contract that never collides
46
+ * because it lives on a different entry point. For the common Date→`yyyy-MM-dd`
47
+ * case prefer {@link toIsoDate}, whose name sidesteps the overload.
30
48
  */
31
49
  declare const formatDate: (date: Date, dateFormat?: string) => string;
32
50
  /** Today's date as a `yyyy-MM-dd` ISO string (local calendar day). */
33
51
  declare const getTodayIsoDate: () => string;
52
+ /**
53
+ * Formats a `Date` as a `yyyy-MM-dd` ISO date-only string in local time — the
54
+ * inverse of {@link parseIsoDateLocal}. Equivalent to `formatDate(date)`, exposed
55
+ * under the clearer name for the date-only round-trip.
56
+ */
57
+ declare const toIsoDate: (date: Date) => string;
58
+ /**
59
+ * Returns a new local `Date` at the first day of `date`'s month, offset by `months`
60
+ * (may be negative), normalising the day-of-month to 1 — e.g. offsetting 15 Mar 2026
61
+ * by -1 gives 1 Feb 2026, not 15 Feb. Returns an `Invalid Date` for a null/invalid
62
+ * input so month-axis arithmetic propagates NaN rather than throwing. Used for
63
+ * month-granular axis/range building (e.g. a Gantt chart).
64
+ */
65
+ declare const startOfMonthOffset: (date: Date | null | undefined, months: number) => Date;
66
+ /**
67
+ * Whole-day difference between two `yyyy-MM-dd` date-only strings (`toIso - fromIso`),
68
+ * rounded to guard against DST half-day drift when the range crosses a clock-change
69
+ * boundary. Parses both bounds as LOCAL dates (via {@link parseIsoDateLocal}); returns
70
+ * `NaN` when either is not a valid date-only string.
71
+ */
72
+ declare const daysBetweenIso: (fromIso: string, toIso: string) => number;
34
73
  /** First day of the given date's month, as a `yyyy-MM-dd` ISO string. */
35
74
  declare const getMonthStartIso: (date: Date) => string;
36
75
  /** Last day of the given date's month, as a `yyyy-MM-dd` ISO string. */
@@ -48,6 +87,15 @@ declare const parseIsoDateLocal: (value: string | null | undefined) => Date | nu
48
87
  * Returns the input unchanged when it is not a valid date-only string.
49
88
  */
50
89
  declare const addDaysToIsoDate: (isoDate: string, days: number) => string;
90
+ /**
91
+ * Adds `years` to an ISO `yyyy-MM-dd` date and returns the result as a `yyyy-MM-dd`
92
+ * string. Parses and formats in local time (like {@link addDaysToIsoDate}) so the
93
+ * arithmetic can't drift across a day boundary in non-UTC timezones/DST. Used to seed
94
+ * default expiry / end-of-term dates (insurance cover, licences, fixed-term contracts).
95
+ * A 29 Feb base in a non-leap target year rolls to 1 Mar, matching `Date.setFullYear`.
96
+ * Returns the input unchanged when it is not a valid date-only string.
97
+ */
98
+ declare const addYearsToIsoDate: (isoDate: string, years: number) => string;
51
99
  /**
52
100
  * Extracts a date-only string (`yyyy-MM-dd`) from a date string.
53
101
  * - Date-only strings (`2024-05-15`) are returned as-is.
@@ -59,12 +107,48 @@ declare const addDaysToIsoDate: (isoDate: string, days: number) => string;
59
107
  declare const toDateOnlyString: (value: string | null | undefined) => string;
60
108
 
61
109
  /**
62
- * Formats a date string, defaulting to the standard application format
63
- * (`yyyy-MM-dd`). Uses `parseISO` to safely handle ISO strings. Pass
64
- * `dateFormat` to override. Returns `"-"` for empty, missing, or unparseable
65
- * input rather than throwing.
110
+ * FALLBACK CONVENTION: a display formatter given falsy/unparseable input returns
111
+ * the em-dash placeholder {@link EM_DASH} so callers can render unconditionally.
112
+ * (Wire/parse helpers in `iso.ts` / `wire.ts` return `""`/`undefined` instead so
113
+ * the value is omitted rather than shown; `formatDuration` likewise uses the
114
+ * em-dash for missing/invalid input, but `0m` for a real zero.) The one exception
115
+ * is {@link formatDateWithOrdinal}, whose "Not specified" / "Invalid date" are
116
+ * deliberate, richer domain messages.
117
+ */
118
+ declare const EM_DASH = "\u2014";
119
+ /**
120
+ * Shared relative-time ladder: maps a whole-minute age to a short phrase
121
+ * ("Just now", "5m ago", "3h ago", "2d ago"), or `null` for anything a week or
122
+ * older so the caller can substitute an absolute date. Backs both
123
+ * {@link formatTimeAgo} here and `formatRelativeTime` in the org-format module.
66
124
  */
67
- declare const formatDateString: (date?: string | null, dateFormat?: string) => string;
125
+ declare const relativeTimePhrase: (diffMinutes: number) => string | null;
126
+ /**
127
+ * Formats a date-only `yyyy-MM-dd` string in the user's LOCAL timezone as the fixed
128
+ * numeric UK date `dd/MM/yyyy`. Parses via {@link parseIsoDateLocal} to avoid the
129
+ * `new Date("yyyy-MM-dd")` UTC-midnight parse that renders as the previous day west
130
+ * of UTC. Returns the original string when it is not a valid date-only value.
131
+ * Deliberately fixed (not org-driven) — for form-field / compact contexts.
132
+ */
133
+ declare const formatSlashDate: (dateString: string) => string;
134
+ /**
135
+ * Formats an ISO datetime string as the fixed numeric UK datetime `dd/MM/yyyy HH:mm`
136
+ * for compact display contexts such as table columns. Returns em-dash for missing or
137
+ * invalid values. Deliberately fixed (not org-driven) — distinct from the org-aware
138
+ * `formatDateTime` in `@ethisyscore/core-utils/date/org-format`.
139
+ */
140
+ declare const formatCompactDateTime: (value?: string | null) => string;
141
+ /**
142
+ * Formats an ISO date/datetime string as a short weekday date `EEE d MMM` (e.g. `Wed 5 Mar`).
143
+ * Returns em-dash for missing or invalid values. Deliberately fixed (not org-driven).
144
+ */
145
+ declare const formatShortDate: (value?: string | null) => string;
146
+ /**
147
+ * Formats an ISO date/datetime string as an abbreviated month-day `MMM d` (e.g. `Mar 5`).
148
+ * Intended for chart axis ticks where vertical space is limited. Returns em-dash for
149
+ * missing or invalid values. Deliberately fixed (not org-driven).
150
+ */
151
+ declare const formatMonthDay: (value?: string | null) => string;
68
152
  /**
69
153
  * Safely formats a date string or `Date`. Handles null, undefined, and invalid
70
154
  * dates by returning a fallback string.
@@ -121,27 +205,23 @@ declare function validateDateFormat(value: string | null | undefined): FormatVal
121
205
  */
122
206
  declare function validateTimeFormat(value: string | null | undefined): FormatValidationResult;
123
207
 
124
- /**
125
- * Pure duration / timespan helpers — no external dependencies.
126
- *
127
- * Cover the two duration shapes that flow across the EthisysCore wire: a
128
- * `"HH:mm:ss"` timespan string (.NET `TimeSpan`) and a millisecond count.
129
- */
130
208
  /** Converts a `"HH:mm:ss"` timespan string to milliseconds. */
131
209
  declare const timespanToMilliseconds: (timespan: string) => number;
132
210
  /** Converts milliseconds to hours (as a float). */
133
211
  declare const millisecondsToHours: (ms: number) => number;
134
212
  /** Converts hours to milliseconds. */
135
213
  declare const hoursToMilliseconds: (hours: number) => number;
136
- /** Formats a milliseconds count as `4h 15min`. */
137
- declare const formatMillisecondsAsTimeSpent: (ms: number) => string;
138
- /** Formats a `"HH:mm:ss"` duration string as human readable (e.g. `2h 30m 0s`). */
139
- declare function formatDuration(duration?: string): string;
140
214
  /**
141
- * Formats a fractional-hours number as a compact human-readable duration
142
- * (`45m`, `2h 30m`, `3d 4h`). Defensively caps unreasonable values.
215
+ * The one human-readable duration formatter. Takes a MILLISECOND count and renders
216
+ * the largest non-zero units as `Xd Xh Xm` (e.g. `4h 15m`, `2d 3h`, `45m`). Zero is
217
+ * a real value (`0m`); a nullish/NaN/negative input is missing data and returns the
218
+ * em-dash. Minute granularity — seconds are not shown.
219
+ *
220
+ * Feed non-millisecond inputs through the converters in this module:
221
+ * `formatDuration(timespanToMilliseconds("04:15:00"))` // "HH:mm:ss" TimeSpan
222
+ * `formatDuration(hoursToMilliseconds(2.5))` // fractional hours
143
223
  */
144
- declare const formatDurationNumber: (durationHours?: number | null) => string;
224
+ declare const formatDuration: (ms: number | null | undefined) => string;
145
225
  /**
146
226
  * Trims an API-supplied `"HH:mm:ss"` down to the `"HH:mm"` string used by
147
227
  * MUI `TimePicker`-backed forms. Returns the supplied fallback when the value
@@ -156,10 +236,6 @@ declare function toHHmm(value: string | null | undefined, fallback: string): str
156
236
  */
157
237
  declare function toHHmmss(value: string): string | null;
158
238
 
159
- /** Converts an ISO date string to date-input format (`yyyy-MM-dd`). */
160
- declare const isoToDateInput: (isoString: string | undefined) => string;
161
- /** Converts date-input format (`yyyy-MM-dd`) to a UTC ISO string at midnight UTC. */
162
- declare const dateInputToIso: (dateString: string) => string;
163
239
  /**
164
240
  * Normalises a date-only value from `<input type="date">` (`yyyy-MM-dd`) into a
165
241
  * canonical UTC ISO-8601 timestamp at midnight UTC, e.g.
@@ -236,4 +312,4 @@ declare function currentMonthRangeUtc(today?: Date): {
236
312
  */
237
313
  declare function getCurrentOffsetMinutes(iana: string): number | null;
238
314
 
239
- export { AUDIT_DATE_FORMAT, DATE_FORMAT, DAY_DATE_FORMAT, DAY_MONTH_FORMAT, DAY_MONTH_YEAR_FORMAT, DISPLAY_DATE_FORMAT, type DateRange, type FormatValidationResult, SHORT_DATETIME_FORMAT, TIME_FORMAT, addDaysToIsoDate, computeDateRange, currentMonthRangeUtc, dateInputToIso, dateOnlyToIsoUtc, ensureUtcIso, formatDate, formatDateSafe, formatDateString, formatDateWithOrdinal, formatDuration, formatDurationNumber, formatMillisecondsAsTimeSpent, formatTimeAgo, getCurrentOffsetMinutes, getDateRange, getMonthEndIso, getMonthStartIso, getTodayIsoDate, hoursToMilliseconds, isDateOnlyString, isoToDateInput, isoUtcToLocalDateTimeInput, localDateTimeInputToIsoUtc, millisecondsToHours, nowLocalDateTimeInputValue, parseIsoDateLocal, timespanToMilliseconds, toDateOnlyString, toHHmm, toHHmmss, validateDateFormat, validateTimeFormat };
315
+ export { AUDIT_DATE_FORMAT, DATE_FORMAT, DAY_DATE_FORMAT, DAY_MONTH_FORMAT, DAY_MONTH_YEAR_FORMAT, DISPLAY_DATETIME_SLASH_FORMAT, DISPLAY_DATE_FORMAT, DISPLAY_DATE_SLASH_FORMAT, DISPLAY_SHORT_DATE_FORMAT, type DateRange, EM_DASH, type FormatValidationResult, MS_PER_DAY, MS_PER_HOUR, MS_PER_MINUTE, SHORT_DATETIME_FORMAT, TIME_FORMAT, addDaysToIsoDate, addYearsToIsoDate, computeDateRange, currentMonthRangeUtc, dateOnlyToIsoUtc, daysBetweenIso, ensureUtcIso, formatCompactDateTime, formatDate, formatDateSafe, formatDateWithOrdinal, formatDuration, formatMonthDay, formatShortDate, formatSlashDate, formatTimeAgo, getCurrentOffsetMinutes, getDateRange, getMonthEndIso, getMonthStartIso, getTodayIsoDate, hoursToMilliseconds, isDateOnlyString, isoUtcToLocalDateTimeInput, localDateTimeInputToIsoUtc, millisecondsToHours, nowLocalDateTimeInputValue, parseIsoDateLocal, relativeTimePhrase, startOfMonthOffset, timespanToMilliseconds, toDateOnlyString, toHHmm, toHHmmss, toIsoDate, validateDateFormat, validateTimeFormat };