@ethisyscore/core-utils 1.94.0 → 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.
- package/dist/chunk-2F5FENXB.js +84 -0
- package/dist/chunk-2F5FENXB.js.map +1 -0
- package/dist/chunk-3LIIUXBE.js +14 -0
- package/dist/chunk-3LIIUXBE.js.map +1 -0
- package/dist/chunk-G2XGZH7P.cjs +16 -0
- package/dist/chunk-G2XGZH7P.cjs.map +1 -0
- package/dist/chunk-GAZLFZEL.js +105 -0
- package/dist/chunk-GAZLFZEL.js.map +1 -0
- package/dist/chunk-LT7I4JVK.cjs +121 -0
- package/dist/chunk-LT7I4JVK.cjs.map +1 -0
- package/dist/chunk-OEDAX6WN.js +121 -0
- package/dist/chunk-OEDAX6WN.js.map +1 -0
- package/dist/chunk-P5FSVRZO.cjs +127 -0
- package/dist/chunk-P5FSVRZO.cjs.map +1 -0
- package/dist/chunk-RLPXYKSM.cjs +23 -0
- package/dist/chunk-RLPXYKSM.cjs.map +1 -0
- package/dist/chunk-TN6DXBVD.js +252 -0
- package/dist/chunk-TN6DXBVD.js.map +1 -0
- package/dist/chunk-YNBKRMK3.cjs +290 -0
- package/dist/chunk-YNBKRMK3.cjs.map +1 -0
- package/dist/chunk-ZQCGDT7D.js +19 -0
- package/dist/chunk-ZQCGDT7D.js.map +1 -0
- package/dist/chunk-ZR7TABGT.cjs +94 -0
- package/dist/chunk-ZR7TABGT.cjs.map +1 -0
- package/dist/date/index.cjs +210 -397
- package/dist/date/index.cjs.map +1 -1
- package/dist/date/index.js +2 -349
- package/dist/date/index.js.map +1 -1
- package/dist/date/org-format.cjs +13 -151
- package/dist/date/org-format.cjs.map +1 -1
- package/dist/date/org-format.js +3 -141
- package/dist/date/org-format.js.map +1 -1
- package/dist/index.cjs +286 -600
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -539
- package/dist/index.js.map +1 -1
- package/dist/list/index.cjs +7 -11
- package/dist/list/index.cjs.map +1 -1
- package/dist/list/index.js +1 -12
- package/dist/list/index.js.map +1 -1
- package/dist/money/index.cjs +39 -51
- package/dist/money/index.cjs.map +1 -1
- package/dist/money/index.d.cts +129 -8
- package/dist/money/index.d.ts +129 -8
- package/dist/money/index.js +1 -49
- package/dist/money/index.js.map +1 -1
- package/dist/number/index.cjs +15 -18
- package/dist/number/index.cjs.map +1 -1
- package/dist/number/index.js +1 -17
- package/dist/number/index.js.map +1 -1
- package/dist/patch/index.cjs +22 -121
- package/dist/patch/index.cjs.map +1 -1
- package/dist/patch/index.js +1 -119
- package/dist/patch/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var dateFns = require('date-fns');
|
|
4
|
+
|
|
5
|
+
// src/date/constants.ts
|
|
6
|
+
var DATE_FORMAT = "yyyy-MM-dd";
|
|
7
|
+
var AUDIT_DATE_FORMAT = "d MMMM yyyy, HH:mm";
|
|
8
|
+
var DISPLAY_DATE_FORMAT = "d MMM yyyy";
|
|
9
|
+
var DAY_DATE_FORMAT = "EEEE, d MMMM";
|
|
10
|
+
var DAY_MONTH_FORMAT = "MMM d";
|
|
11
|
+
var DAY_MONTH_YEAR_FORMAT = "MMM d, yyyy";
|
|
12
|
+
var TIME_FORMAT = "h:mm a";
|
|
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";
|
|
20
|
+
var isDateOnlyString = (value) => {
|
|
21
|
+
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value);
|
|
22
|
+
};
|
|
23
|
+
var formatDate = (date, dateFormat = DATE_FORMAT) => {
|
|
24
|
+
return dateFns.format(date, dateFormat);
|
|
25
|
+
};
|
|
26
|
+
var getTodayIsoDate = () => {
|
|
27
|
+
return formatDate(/* @__PURE__ */ new Date());
|
|
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
|
+
};
|
|
42
|
+
var getMonthStartIso = (date) => formatDate(new Date(date.getFullYear(), date.getMonth(), 1));
|
|
43
|
+
var getMonthEndIso = (date) => formatDate(new Date(date.getFullYear(), date.getMonth() + 1, 0));
|
|
44
|
+
var parseIsoDateLocal = (value) => {
|
|
45
|
+
if (!value || !isDateOnlyString(value)) return null;
|
|
46
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
47
|
+
const d = new Date(year, month - 1, day);
|
|
48
|
+
return dateFns.isValid(d) ? d : null;
|
|
49
|
+
};
|
|
50
|
+
var addDaysToIsoDate = (isoDate, days) => {
|
|
51
|
+
const base = parseIsoDateLocal(isoDate);
|
|
52
|
+
if (!base) return isoDate;
|
|
53
|
+
base.setDate(base.getDate() + days);
|
|
54
|
+
return formatDate(base);
|
|
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
|
+
};
|
|
62
|
+
var toDateOnlyString = (value) => {
|
|
63
|
+
if (!value) return "";
|
|
64
|
+
const datepart = value.split("T")[0];
|
|
65
|
+
return isDateOnlyString(datepart) ? datepart : "";
|
|
66
|
+
};
|
|
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;
|
|
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);
|
|
89
|
+
var formatDateSafe = (date, formatStr = DATE_FORMAT, fallback = "\u2014") => {
|
|
90
|
+
if (!date) return fallback;
|
|
91
|
+
try {
|
|
92
|
+
const dateObj = typeof date === "string" ? dateFns.parseISO(date) : date;
|
|
93
|
+
if (!dateFns.isValid(dateObj)) return fallback;
|
|
94
|
+
return dateFns.format(dateObj, formatStr);
|
|
95
|
+
} catch {
|
|
96
|
+
return fallback;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var getOrdinalSuffix = (day) => {
|
|
100
|
+
if (day > 3 && day < 21) return "th";
|
|
101
|
+
switch (day % 10) {
|
|
102
|
+
case 1:
|
|
103
|
+
return "st";
|
|
104
|
+
case 2:
|
|
105
|
+
return "nd";
|
|
106
|
+
case 3:
|
|
107
|
+
return "rd";
|
|
108
|
+
default:
|
|
109
|
+
return "th";
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
var formatDateWithOrdinal = (date) => {
|
|
113
|
+
if (!date) return "Not specified";
|
|
114
|
+
let dateObj;
|
|
115
|
+
if (typeof date === "string") {
|
|
116
|
+
if (isDateOnlyString(date)) {
|
|
117
|
+
const [year2, month2, day2] = date.split("-").map(Number);
|
|
118
|
+
dateObj = new Date(year2, month2 - 1, day2);
|
|
119
|
+
} else {
|
|
120
|
+
dateObj = dateFns.parseISO(date);
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
dateObj = date;
|
|
124
|
+
}
|
|
125
|
+
if (!dateFns.isValid(dateObj)) return "Invalid date";
|
|
126
|
+
const day = dateObj.getDate();
|
|
127
|
+
const month = dateFns.format(dateObj, "MMM");
|
|
128
|
+
const year = dateObj.getFullYear();
|
|
129
|
+
return `${day}${getOrdinalSuffix(day)} ${month} ${year}`;
|
|
130
|
+
};
|
|
131
|
+
var formatTimeAgo = (date) => {
|
|
132
|
+
if (!date) return "";
|
|
133
|
+
const dateObj = typeof date === "string" ? dateFns.parseISO(date) : date;
|
|
134
|
+
if (!dateFns.isValid(dateObj)) return "";
|
|
135
|
+
const diffMinutes = Math.floor(((/* @__PURE__ */ new Date()).getTime() - dateObj.getTime()) / MS_PER_MINUTE);
|
|
136
|
+
return relativeTimePhrase(diffMinutes) ?? formatDate(dateObj);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// src/date/formatValidation.ts
|
|
140
|
+
var SEPARATOR_CHARS = " ,./-:()";
|
|
141
|
+
var DAY_TOKENS = /* @__PURE__ */ new Set(["d", "dd", "do"]);
|
|
142
|
+
var WEEKDAY_TOKENS = /* @__PURE__ */ new Set(["EEE", "EEEE"]);
|
|
143
|
+
var MONTH_TOKENS = /* @__PURE__ */ new Set(["M", "MM", "MMM", "MMMM"]);
|
|
144
|
+
var YEAR_TOKENS = /* @__PURE__ */ new Set(["yy", "yyyy"]);
|
|
145
|
+
var HOUR12_TOKENS = /* @__PURE__ */ new Set(["h", "hh"]);
|
|
146
|
+
var HOUR24_TOKENS = /* @__PURE__ */ new Set(["H", "HH"]);
|
|
147
|
+
var MINUTE_TOKENS = /* @__PURE__ */ new Set(["m", "mm"]);
|
|
148
|
+
var SECOND_TOKENS = /* @__PURE__ */ new Set(["s", "ss"]);
|
|
149
|
+
var MERIDIEM_TOKEN = "a";
|
|
150
|
+
var DATE_HINTS = {
|
|
151
|
+
D: "d",
|
|
152
|
+
DD: "dd",
|
|
153
|
+
Y: "yyyy",
|
|
154
|
+
YY: "yy",
|
|
155
|
+
YYYY: "yyyy",
|
|
156
|
+
E: "EEE",
|
|
157
|
+
EE: "EEE",
|
|
158
|
+
m: "M",
|
|
159
|
+
mm: "MM"
|
|
160
|
+
};
|
|
161
|
+
var TIME_HINTS = {
|
|
162
|
+
t: "a",
|
|
163
|
+
tt: "a",
|
|
164
|
+
A: "a",
|
|
165
|
+
M: "m",
|
|
166
|
+
MM: "mm"
|
|
167
|
+
};
|
|
168
|
+
var invalid = (error) => ({ valid: false, error });
|
|
169
|
+
var isLetter = (c) => /[a-zA-Z]/.test(c);
|
|
170
|
+
function tokenize(value, isKnownToken, hints) {
|
|
171
|
+
const tokens = [];
|
|
172
|
+
let i = 0;
|
|
173
|
+
while (i < value.length) {
|
|
174
|
+
const c = value[i];
|
|
175
|
+
if (c === "'") {
|
|
176
|
+
i++;
|
|
177
|
+
let closed = false;
|
|
178
|
+
while (i < value.length) {
|
|
179
|
+
if (value[i] === "'") {
|
|
180
|
+
if (i + 1 < value.length && value[i + 1] === "'") {
|
|
181
|
+
i += 2;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
closed = true;
|
|
185
|
+
i++;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
i++;
|
|
189
|
+
}
|
|
190
|
+
if (!closed) return { result: invalid("Unterminated quoted literal."), tokens };
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (isLetter(c)) {
|
|
194
|
+
const start = i;
|
|
195
|
+
while (i < value.length && value[i] === c) i++;
|
|
196
|
+
let run = value.slice(start, i);
|
|
197
|
+
if (run === "d" && value[i] === "o") {
|
|
198
|
+
run = "do";
|
|
199
|
+
i++;
|
|
200
|
+
}
|
|
201
|
+
if (!isKnownToken(run)) {
|
|
202
|
+
const hint = hints[run];
|
|
203
|
+
return {
|
|
204
|
+
result: invalid(hint ? `Unknown token '${run}' \u2014 use '${hint}' instead.` : `Unknown token '${run}'.`),
|
|
205
|
+
tokens
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
tokens.push(run);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (SEPARATOR_CHARS.includes(c)) {
|
|
212
|
+
i++;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
return { result: invalid(`Unsupported character '${c}'.`), tokens };
|
|
216
|
+
}
|
|
217
|
+
return { result: { valid: true }, tokens };
|
|
218
|
+
}
|
|
219
|
+
function validateDateFormat(value) {
|
|
220
|
+
if (!value) return { valid: true };
|
|
221
|
+
const { result, tokens } = tokenize(
|
|
222
|
+
value,
|
|
223
|
+
(t) => DAY_TOKENS.has(t) || WEEKDAY_TOKENS.has(t) || MONTH_TOKENS.has(t) || YEAR_TOKENS.has(t),
|
|
224
|
+
DATE_HINTS
|
|
225
|
+
);
|
|
226
|
+
if (!result.valid) return result;
|
|
227
|
+
if (!tokens.some((t) => DAY_TOKENS.has(t))) return invalid("Date format must include a day token (d, dd or do).");
|
|
228
|
+
if (!tokens.some((t) => MONTH_TOKENS.has(t))) {
|
|
229
|
+
return invalid("Date format must include a month token (M, MM, MMM or MMMM).");
|
|
230
|
+
}
|
|
231
|
+
if (!tokens.some((t) => YEAR_TOKENS.has(t))) return invalid("Date format must include a year token (yy or yyyy).");
|
|
232
|
+
return { valid: true };
|
|
233
|
+
}
|
|
234
|
+
function validateTimeFormat(value) {
|
|
235
|
+
if (!value) return { valid: true };
|
|
236
|
+
const { result, tokens } = tokenize(
|
|
237
|
+
value,
|
|
238
|
+
(t) => HOUR12_TOKENS.has(t) || HOUR24_TOKENS.has(t) || MINUTE_TOKENS.has(t) || SECOND_TOKENS.has(t) || t === MERIDIEM_TOKEN,
|
|
239
|
+
TIME_HINTS
|
|
240
|
+
);
|
|
241
|
+
if (!result.valid) return result;
|
|
242
|
+
const hasHour12 = tokens.some((t) => HOUR12_TOKENS.has(t));
|
|
243
|
+
const hasHour24 = tokens.some((t) => HOUR24_TOKENS.has(t));
|
|
244
|
+
if (!hasHour12 && !hasHour24) return invalid("Time format must include an hour token (h, hh, H or HH).");
|
|
245
|
+
if (!tokens.some((t) => MINUTE_TOKENS.has(t))) return invalid("Time format must include a minute token (m or mm).");
|
|
246
|
+
const hasMeridiem = tokens.includes(MERIDIEM_TOKEN);
|
|
247
|
+
if (hasHour12 && !hasMeridiem) return invalid("12-hour time format requires the meridiem token 'a'.");
|
|
248
|
+
if (hasMeridiem && !hasHour12) return invalid("Meridiem token 'a' requires a 12-hour hour token (h or hh).");
|
|
249
|
+
return { valid: true };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
exports.AUDIT_DATE_FORMAT = AUDIT_DATE_FORMAT;
|
|
253
|
+
exports.DATE_FORMAT = DATE_FORMAT;
|
|
254
|
+
exports.DAY_DATE_FORMAT = DAY_DATE_FORMAT;
|
|
255
|
+
exports.DAY_MONTH_FORMAT = DAY_MONTH_FORMAT;
|
|
256
|
+
exports.DAY_MONTH_YEAR_FORMAT = DAY_MONTH_YEAR_FORMAT;
|
|
257
|
+
exports.DISPLAY_DATETIME_SLASH_FORMAT = DISPLAY_DATETIME_SLASH_FORMAT;
|
|
258
|
+
exports.DISPLAY_DATE_FORMAT = DISPLAY_DATE_FORMAT;
|
|
259
|
+
exports.DISPLAY_DATE_SLASH_FORMAT = DISPLAY_DATE_SLASH_FORMAT;
|
|
260
|
+
exports.DISPLAY_SHORT_DATE_FORMAT = DISPLAY_SHORT_DATE_FORMAT;
|
|
261
|
+
exports.EM_DASH = EM_DASH;
|
|
262
|
+
exports.MS_PER_DAY = MS_PER_DAY;
|
|
263
|
+
exports.MS_PER_HOUR = MS_PER_HOUR;
|
|
264
|
+
exports.MS_PER_MINUTE = MS_PER_MINUTE;
|
|
265
|
+
exports.SHORT_DATETIME_FORMAT = SHORT_DATETIME_FORMAT;
|
|
266
|
+
exports.TIME_FORMAT = TIME_FORMAT;
|
|
267
|
+
exports.addDaysToIsoDate = addDaysToIsoDate;
|
|
268
|
+
exports.addYearsToIsoDate = addYearsToIsoDate;
|
|
269
|
+
exports.daysBetweenIso = daysBetweenIso;
|
|
270
|
+
exports.formatCompactDateTime = formatCompactDateTime;
|
|
271
|
+
exports.formatDate = formatDate;
|
|
272
|
+
exports.formatDateSafe = formatDateSafe;
|
|
273
|
+
exports.formatDateWithOrdinal = formatDateWithOrdinal;
|
|
274
|
+
exports.formatMonthDay = formatMonthDay;
|
|
275
|
+
exports.formatShortDate = formatShortDate;
|
|
276
|
+
exports.formatSlashDate = formatSlashDate;
|
|
277
|
+
exports.formatTimeAgo = formatTimeAgo;
|
|
278
|
+
exports.getMonthEndIso = getMonthEndIso;
|
|
279
|
+
exports.getMonthStartIso = getMonthStartIso;
|
|
280
|
+
exports.getTodayIsoDate = getTodayIsoDate;
|
|
281
|
+
exports.isDateOnlyString = isDateOnlyString;
|
|
282
|
+
exports.parseIsoDateLocal = parseIsoDateLocal;
|
|
283
|
+
exports.relativeTimePhrase = relativeTimePhrase;
|
|
284
|
+
exports.startOfMonthOffset = startOfMonthOffset;
|
|
285
|
+
exports.toDateOnlyString = toDateOnlyString;
|
|
286
|
+
exports.toIsoDate = toIsoDate;
|
|
287
|
+
exports.validateDateFormat = validateDateFormat;
|
|
288
|
+
exports.validateTimeFormat = validateTimeFormat;
|
|
289
|
+
//# sourceMappingURL=chunk-YNBKRMK3.cjs.map
|
|
290
|
+
//# sourceMappingURL=chunk-YNBKRMK3.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/date/constants.ts","../src/date/iso.ts","../src/date/format.ts","../src/date/formatValidation.ts"],"names":["format","isValid","parseISO","year","month","day"],"mappings":"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","file":"chunk-YNBKRMK3.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"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/number/format.ts
|
|
2
|
+
var NUMBER_FALLBACK_LOCALE = "en-GB";
|
|
3
|
+
function formatNumber(value, options) {
|
|
4
|
+
if (value == null) {
|
|
5
|
+
return "\u2014";
|
|
6
|
+
}
|
|
7
|
+
const dp = options?.decimalPlaces ?? 2;
|
|
8
|
+
return new Intl.NumberFormat(options?.locale ?? NUMBER_FALLBACK_LOCALE, {
|
|
9
|
+
minimumFractionDigits: dp,
|
|
10
|
+
maximumFractionDigits: dp
|
|
11
|
+
}).format(value);
|
|
12
|
+
}
|
|
13
|
+
function formatPercent(ratio, options) {
|
|
14
|
+
return `${(ratio * 100).toFixed(options?.decimalPlaces ?? 2)}%`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { NUMBER_FALLBACK_LOCALE, formatNumber, formatPercent };
|
|
18
|
+
//# sourceMappingURL=chunk-ZQCGDT7D.js.map
|
|
19
|
+
//# sourceMappingURL=chunk-ZQCGDT7D.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/number/format.ts"],"names":[],"mappings":";AAgBO,IAAM,sBAAA,GAAyB;AAS/B,SAAS,YAAA,CACd,OACA,OAAA,EACQ;AACR,EAAA,IAAI,SAAS,IAAA,EAAM;AACjB,IAAA,OAAO,QAAA;AAAA,EACT;AAEA,EAAA,MAAM,EAAA,GAAK,SAAS,aAAA,IAAiB,CAAA;AACrC,EAAA,OAAO,IAAI,IAAA,CAAK,YAAA,CAAa,OAAA,EAAS,UAAU,sBAAA,EAAwB;AAAA,IACtE,qBAAA,EAAuB,EAAA;AAAA,IACvB,qBAAA,EAAuB;AAAA,GACxB,CAAA,CAAE,MAAA,CAAO,KAAK,CAAA;AACjB;AAWO,SAAS,aAAA,CAAc,OAAe,OAAA,EAA8C;AACzF,EAAA,OAAO,IAAI,KAAA,GAAQ,GAAA,EAAK,QAAQ,OAAA,EAAS,aAAA,IAAiB,CAAC,CAAC,CAAA,CAAA,CAAA;AAC9D","file":"chunk-ZQCGDT7D.js","sourcesContent":["/**\n * Framework-agnostic number DISPLAY formatting.\n *\n * Pure helpers for rendering plain numeric values and ratios as strings, with a\n * deterministic `en-GB` fallback locale so output is machine-stable regardless\n * of the host OS locale - the same principle as the money and date seams.\n *\n * It is pure - no React, no host access - so it lives in core-utils behind the\n * `@ethisyscore/core-utils/number` sub-path.\n */\n\n/**\n * Deterministic fallback locale. Matches the money/date seams so numbers render\n * consistently when no explicit locale is supplied, and so output does not drift\n * with the OS locale of whatever host renders it.\n */\nexport const NUMBER_FALLBACK_LOCALE = \"en-GB\";\n\n/**\n * Formats a numeric value with grouping and a fixed number of decimal places via\n * `Intl.NumberFormat`. Returns an em-dash (`\"—\"`) for null/undefined so an\n * absent value renders as a readable placeholder rather than `\"NaN\"` or `\"null\"`.\n *\n * Defaults to two decimal places in {@link NUMBER_FALLBACK_LOCALE}.\n */\nexport function formatNumber(\n value: number | null | undefined,\n options?: { decimalPlaces?: number; locale?: string },\n): string {\n if (value == null) {\n return \"—\";\n }\n\n const dp = options?.decimalPlaces ?? 2;\n return new Intl.NumberFormat(options?.locale ?? NUMBER_FALLBACK_LOCALE, {\n minimumFractionDigits: dp,\n maximumFractionDigits: dp,\n }).format(value);\n}\n\n/**\n * Formats a RATIO as a percentage string with a fixed number of decimal places -\n * e.g. `0.1234` -> `\"12.34%\"`. The input is a ratio, not an already-scaled\n * percentage.\n *\n * Uses `toFixed` (no grouping) rather than `Intl` percent style so output is a\n * plain fixed-decimal percentage, matching what callers expect on compact labels.\n * Defaults to two decimal places.\n */\nexport function formatPercent(ratio: number, options?: { decimalPlaces?: number }): string {\n return `${(ratio * 100).toFixed(options?.decimalPlaces ?? 2)}%`;\n}\n"]}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/money/orgCurrency.ts
|
|
4
|
+
var MONEY_FALLBACK_CURRENCY = "GBP";
|
|
5
|
+
var orgReportingCurrencyRef = null;
|
|
6
|
+
function setOrgReportingCurrency(currency) {
|
|
7
|
+
orgReportingCurrencyRef = currency;
|
|
8
|
+
}
|
|
9
|
+
function getOrgReportingCurrency() {
|
|
10
|
+
return orgReportingCurrencyRef;
|
|
11
|
+
}
|
|
12
|
+
function resolveOrgReportingCurrencyCode() {
|
|
13
|
+
return orgReportingCurrencyRef?.code ?? MONEY_FALLBACK_CURRENCY;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/money/format.ts
|
|
17
|
+
var MONEY_FALLBACK_LOCALE = "en-GB";
|
|
18
|
+
function formatMoney(amount, currency) {
|
|
19
|
+
const options = typeof currency === "string" ? { currencyCode: currency } : currency ?? {};
|
|
20
|
+
const { currencyCode, decimalPlaces, locale = MONEY_FALLBACK_LOCALE, symbol } = options;
|
|
21
|
+
const orgCurrency = currencyCode === void 0 ? getOrgReportingCurrency() : null;
|
|
22
|
+
const effectiveCode = currencyCode ?? orgCurrency?.code ?? MONEY_FALLBACK_CURRENCY;
|
|
23
|
+
const effectiveDecimalPlaces = decimalPlaces !== void 0 ? decimalPlaces : orgCurrency?.decimalPlaces ?? null;
|
|
24
|
+
const normalizedCode = effectiveCode.toUpperCase();
|
|
25
|
+
try {
|
|
26
|
+
const formatter = new Intl.NumberFormat(locale, {
|
|
27
|
+
style: "currency",
|
|
28
|
+
currency: normalizedCode,
|
|
29
|
+
// Narrow symbol so a clear currency context renders "$100", not "US$100".
|
|
30
|
+
currencyDisplay: "narrowSymbol",
|
|
31
|
+
...effectiveDecimalPlaces != null ? { minimumFractionDigits: effectiveDecimalPlaces, maximumFractionDigits: effectiveDecimalPlaces } : {}
|
|
32
|
+
});
|
|
33
|
+
return formatter.format(amount);
|
|
34
|
+
} catch {
|
|
35
|
+
const fractionDigits = effectiveDecimalPlaces ?? 2;
|
|
36
|
+
const prefix = symbol ?? normalizedCode;
|
|
37
|
+
const sign = amount < 0 ? "-" : "";
|
|
38
|
+
return `${sign}${prefix}${Math.abs(amount).toFixed(fractionDigits)}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function formatCompactMoney(amount, options) {
|
|
42
|
+
const { currencyCode, symbol, locale = MONEY_FALLBACK_LOCALE } = options;
|
|
43
|
+
const sym = getCurrencySymbol(currencyCode, locale, symbol);
|
|
44
|
+
const sign = amount < 0 ? "-" : "";
|
|
45
|
+
const abs = Math.abs(amount);
|
|
46
|
+
if (abs >= 1e6) {
|
|
47
|
+
return `${sign}${sym}${(abs / 1e6).toFixed(1)}m`;
|
|
48
|
+
}
|
|
49
|
+
if (abs >= 1e3) {
|
|
50
|
+
return `${sign}${sym}${(abs / 1e3).toFixed(0)}k`;
|
|
51
|
+
}
|
|
52
|
+
return `${sign}${sym}${abs.toFixed(0)}`;
|
|
53
|
+
}
|
|
54
|
+
function getCurrencySymbol(currencyCode, locale = MONEY_FALLBACK_LOCALE, symbol) {
|
|
55
|
+
const normalizedCode = currencyCode.toUpperCase();
|
|
56
|
+
try {
|
|
57
|
+
const parts = new Intl.NumberFormat(locale, {
|
|
58
|
+
style: "currency",
|
|
59
|
+
currency: normalizedCode,
|
|
60
|
+
currencyDisplay: "narrowSymbol"
|
|
61
|
+
}).formatToParts(0);
|
|
62
|
+
return parts.find((part) => part.type === "currency")?.value ?? symbol ?? normalizedCode;
|
|
63
|
+
} catch {
|
|
64
|
+
return symbol ?? normalizedCode;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/money/parse.ts
|
|
69
|
+
var UNICODE_MINUS = /−/g;
|
|
70
|
+
var NON_NUMERIC_CHARS = /[^0-9.-]/g;
|
|
71
|
+
function parseMoney(value) {
|
|
72
|
+
if (value === null || value === void 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const trimmed = value.trim();
|
|
76
|
+
if (trimmed === "" || trimmed === "N/A") {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const cleaned = trimmed.replace(UNICODE_MINUS, "-").replace(NON_NUMERIC_CHARS, "");
|
|
80
|
+
const parsed = Number.parseFloat(cleaned);
|
|
81
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
exports.MONEY_FALLBACK_CURRENCY = MONEY_FALLBACK_CURRENCY;
|
|
85
|
+
exports.MONEY_FALLBACK_LOCALE = MONEY_FALLBACK_LOCALE;
|
|
86
|
+
exports.formatCompactMoney = formatCompactMoney;
|
|
87
|
+
exports.formatMoney = formatMoney;
|
|
88
|
+
exports.getCurrencySymbol = getCurrencySymbol;
|
|
89
|
+
exports.getOrgReportingCurrency = getOrgReportingCurrency;
|
|
90
|
+
exports.parseMoney = parseMoney;
|
|
91
|
+
exports.resolveOrgReportingCurrencyCode = resolveOrgReportingCurrencyCode;
|
|
92
|
+
exports.setOrgReportingCurrency = setOrgReportingCurrency;
|
|
93
|
+
//# sourceMappingURL=chunk-ZR7TABGT.cjs.map
|
|
94
|
+
//# sourceMappingURL=chunk-ZR7TABGT.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/money/orgCurrency.ts","../src/money/format.ts","../src/money/parse.ts"],"names":[],"mappings":";;;AAgCO,IAAM,uBAAA,GAA0B;AAkBvC,IAAI,uBAAA,GAAuD,IAAA;AAOpD,SAAS,wBAAwB,QAAA,EAA6C;AACnF,EAAA,uBAAA,GAA0B,QAAA;AAC5B;AAGO,SAAS,uBAAA,GAAuD;AACrE,EAAA,OAAO,uBAAA;AACT;AAOO,SAAS,+BAAA,GAA0C;AACxD,EAAA,OAAO,yBAAyB,IAAA,IAAQ,uBAAA;AAC1C;;;AC1CO,IAAM,qBAAA,GAAwB;AA2C9B,SAAS,WAAA,CAAY,QAAgB,QAAA,EAAgD;AAC1F,EAAA,MAAM,OAAA,GAA8B,OAAO,QAAA,KAAa,QAAA,GAAW,EAAE,YAAA,EAAc,QAAA,EAAS,GAAK,QAAA,IAAY,EAAC;AAC9G,EAAA,MAAM,EAAE,YAAA,EAAc,aAAA,EAAe,MAAA,GAAS,qBAAA,EAAuB,QAAO,GAAI,OAAA;AAIhF,EAAA,MAAM,WAAA,GAAc,YAAA,KAAiB,MAAA,GAAY,uBAAA,EAAwB,GAAI,IAAA;AAC7E,EAAA,MAAM,aAAA,GAAgB,YAAA,IAAgB,WAAA,EAAa,IAAA,IAAQ,uBAAA;AAK3D,EAAA,MAAM,sBAAA,GACJ,aAAA,KAAkB,MAAA,GAAY,aAAA,GAAiB,aAAa,aAAA,IAAiB,IAAA;AAI/E,EAAA,MAAM,cAAA,GAAiB,cAAc,WAAA,EAAY;AAEjD,EAAA,IAAI;AACF,IAAA,MAAM,SAAA,GAAY,IAAI,IAAA,CAAK,YAAA,CAAa,MAAA,EAAQ;AAAA,MAC9C,KAAA,EAAO,UAAA;AAAA,MACP,QAAA,EAAU,cAAA;AAAA;AAAA,MAEV,eAAA,EAAiB,cAAA;AAAA,MACjB,GAAI,0BAA0B,IAAA,GAC1B,EAAE,uBAAuB,sBAAA,EAAwB,qBAAA,EAAuB,sBAAA,EAAuB,GAC/F;AAAC,KACN,CAAA;AACD,IAAA,OAAO,SAAA,CAAU,OAAO,MAAM,CAAA;AAAA,EAChC,CAAA,CAAA,MAAQ;AACN,IAAA,MAAM,iBAAiB,sBAAA,IAA0B,CAAA;AACjD,IAAA,MAAM,SAAS,MAAA,IAAU,cAAA;AAEzB,IAAA,MAAM,IAAA,GAAO,MAAA,GAAS,CAAA,GAAI,GAAA,GAAM,EAAA;AAChC,IAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,MAAM,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA,CAAE,OAAA,CAAQ,cAAc,CAAC,CAAA,CAAA;AAAA,EACpE;AACF;AA8BO,SAAS,kBAAA,CAAmB,QAAgB,OAAA,EAA4C;AAC7F,EAAA,MAAM,EAAE,YAAA,EAAc,MAAA,EAAQ,MAAA,GAAS,uBAAsB,GAAI,OAAA;AAEjE,EAAA,MAAM,GAAA,GAAM,iBAAA,CAAkB,YAAA,EAAc,MAAA,EAAQ,MAAM,CAAA;AAC1D,EAAA,MAAM,IAAA,GAAO,MAAA,GAAS,CAAA,GAAI,GAAA,GAAM,EAAA;AAChC,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,MAAM,CAAA;AAE3B,EAAA,IAAI,OAAO,GAAA,EAAW;AACpB,IAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,GAAG,IAAI,GAAA,GAAM,GAAA,EAAW,OAAA,CAAQ,CAAC,CAAC,CAAA,CAAA,CAAA;AAAA,EACrD;AACA,EAAA,IAAI,OAAO,GAAA,EAAO;AAChB,IAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,GAAG,IAAI,GAAA,GAAM,GAAA,EAAO,OAAA,CAAQ,CAAC,CAAC,CAAA,CAAA,CAAA;AAAA,EACjD;AACA,EAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,GAAG,GAAG,GAAA,CAAI,OAAA,CAAQ,CAAC,CAAC,CAAA,CAAA;AACvC;AAOO,SAAS,iBAAA,CACd,YAAA,EACA,MAAA,GAAiB,qBAAA,EACjB,MAAA,EACQ;AAER,EAAA,MAAM,cAAA,GAAiB,aAAa,WAAA,EAAY;AAEhD,EAAA,IAAI;AACF,IAAA,MAAM,KAAA,GAAQ,IAAI,IAAA,CAAK,YAAA,CAAa,MAAA,EAAQ;AAAA,MAC1C,KAAA,EAAO,UAAA;AAAA,MACP,QAAA,EAAU,cAAA;AAAA,MACV,eAAA,EAAiB;AAAA,KAClB,CAAA,CAAE,aAAA,CAAc,CAAC,CAAA;AAElB,IAAA,OAAO,KAAA,CAAM,KAAK,CAAC,IAAA,KAAS,KAAK,IAAA,KAAS,UAAU,CAAA,EAAG,KAAA,IAAS,MAAA,IAAU,cAAA;AAAA,EAC5E,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,MAAA,IAAU,cAAA;AAAA,EACnB;AACF;;;AC3JA,IAAM,aAAA,GAAgB,IAAA;AAStB,IAAM,iBAAA,GAAoB,WAAA;AAuBnB,SAAS,WAAW,KAAA,EAAiD;AAC1E,EAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,KAAA,KAAU,MAAA,EAAW;AACzC,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAC3B,EAAA,IAAI,OAAA,KAAY,EAAA,IAAM,OAAA,KAAY,KAAA,EAAO;AACvC,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,MAAM,OAAA,GAAU,QAAQ,OAAA,CAAQ,aAAA,EAAe,GAAG,CAAA,CAAE,OAAA,CAAQ,mBAAmB,EAAE,CAAA;AACjF,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,UAAA,CAAW,OAAO,CAAA;AAExC,EAAA,OAAO,MAAA,CAAO,KAAA,CAAM,MAAM,CAAA,GAAI,IAAA,GAAO,MAAA;AACvC","file":"chunk-ZR7TABGT.cjs","sourcesContent":["/**\n * Organisation-driven reporting-currency resolution.\n *\n * Plugin (and host) surfaces render reporting-currency amounts in whatever the\n * organisation has configured as its reporting currency, without threading a\n * currency code through every call site. This module holds the module-level\n * reporting-currency ref, its setter, and the readers with a deterministic\n * fallback constant. It is pure — no React, no host access — so it lives in\n * core-utils behind the `@ethisyscore/core-utils/money` sub-path, mirroring the\n * date org-format seam (`@ethisyscore/core-utils/date/org-format`).\n *\n * The ref is stamped by the surface sync hook — the SDK\n * `useReportingCurrencySync` reads the host\n * `settings:get-organisation-reporting-currency` tool and calls\n * `setOrgReportingCurrency`. When no reporting currency is stamped (a\n * standalone/mock run, an old host, or a failed fetch) resolution falls back to\n * the deterministic UK default `GBP`, NOT the OS locale, so output is\n * machine-stable regardless of the host. This is the exact fail-open contract of\n * the date seam's UK `dd/MM/yyyy` fallback.\n *\n * Deciding WHICH currency an amount is in (its own currency vs the org's\n * reporting currency) and CONVERTING between currencies stay host-aware concerns\n * of the plugin-ui `useCurrency` hook. This seam only answers \"what is the org's\n * reporting currency\" for the pure {@link formatMoney} default path.\n */\n\n/**\n * Deterministic fallback reporting currency when no org currency is stamped.\n * Matches the date seam's UK fallback (`ORG_DATE_FALLBACK_FORMAT`) so money and\n * dates degrade consistently, and so output does not drift with the OS locale of\n * whatever host renders it.\n */\nexport const MONEY_FALLBACK_CURRENCY = \"GBP\";\n\n/** The organisation's reporting currency as stamped by the surface sync hook. */\nexport interface OrgReportingCurrency {\n /** ISO 4217 code (e.g. `\"GBP\"`, `\"EUR\"`) drives `Intl` currency formatting. */\n code: string;\n /**\n * Host decimal-places override for the currency, or null to use the currency's\n * ISO default (GBP -> 2, JPY -> 0). Applied by {@link formatMoney} only when the\n * caller supplies no explicit decimalPlaces.\n */\n decimalPlaces: number | null;\n}\n\n// ---------------------------------------------------------------------------\n// Module-level reporting-currency ref\n// ---------------------------------------------------------------------------\n\nlet orgReportingCurrencyRef: OrgReportingCurrency | null = null;\n\n/**\n * Stamps the module-level reporting-currency ref. Called by the surface sync hook\n * each time the organisation's reporting currency resolves. Pass `null` to clear\n * it and fall back to the deterministic {@link MONEY_FALLBACK_CURRENCY}.\n */\nexport function setOrgReportingCurrency(currency: OrgReportingCurrency | null): void {\n orgReportingCurrencyRef = currency;\n}\n\n/** The org's stamped reporting currency, or null to fall back to the UK default. */\nexport function getOrgReportingCurrency(): OrgReportingCurrency | null {\n return orgReportingCurrencyRef;\n}\n\n/**\n * The org's effective reporting-currency code: the stamped code when present, else\n * the deterministic {@link MONEY_FALLBACK_CURRENCY} (`GBP`). Never null, so callers\n * always have a well-formed code to format with.\n */\nexport function resolveOrgReportingCurrencyCode(): string {\n return orgReportingCurrencyRef?.code ?? MONEY_FALLBACK_CURRENCY;\n}\n","/**\n * Framework-agnostic money DISPLAY formatting.\n *\n * The platform stores amounts as a plain number plus a currency (an ISO 4217 code,\n * optionally a symbol and a decimal-places override from the host `Currency` row).\n * This module turns that into a localized string via `Intl.NumberFormat`, with a\n * deterministic fallback so output is machine-stable regardless of the host OS\n * locale - the same principle as the date org-format seam.\n *\n * It is pure - no React, no host access - so it lives in core-utils behind the\n * `@ethisyscore/core-utils/money` sub-path. Resolving WHICH currency to render in\n * (the amount's own currency, or the org's reporting currency) and CONVERTING\n * between currencies are host-aware concerns handled by the plugin-ui `useCurrency`\n * hook, which formats through this module.\n *\n * When no currency is given, `formatMoney` falls back to the organisation's\n * reporting currency via the {@link ./orgCurrency} seam (the same module-ref +\n * fail-open-to-`GBP` mechanism as the date org-format seam), so a plugin can render\n * reporting-currency amounts without threading a code or hardcoding one.\n */\n\nimport {\n getOrgReportingCurrency,\n MONEY_FALLBACK_CURRENCY,\n} from \"./orgCurrency\";\n\n/**\n * Deterministic fallback locale. Matches the date seam's UK fallback so money and\n * dates render consistently when no explicit locale is supplied, and so output does\n * not drift with the OS locale of whatever host renders it.\n */\nexport const MONEY_FALLBACK_LOCALE = \"en-GB\";\n\n/** Options for {@link formatMoney}. */\nexport interface FormatMoneyOptions {\n /**\n * ISO 4217 code (e.g. `\"GBP\"`, `\"EUR\"`) - drives `Intl` currency formatting.\n * Optional: when omitted, `formatMoney` resolves the organisation's reporting\n * currency (see {@link ./orgCurrency}), falling back to `GBP` when none is stamped.\n */\n currencyCode?: string;\n /**\n * Fraction digits to show. When null/undefined, `Intl`'s per-currency default is\n * used (GBP -> 2, JPY -> 0). Pass a number to force a currency's decimal-places\n * override from the host `Currency` row.\n */\n decimalPlaces?: number | null;\n /** BCP-47 locale; defaults to {@link MONEY_FALLBACK_LOCALE} for stable output. */\n locale?: string;\n /**\n * Symbol to use only in the fallback path when `Intl` cannot format the currency\n * code (e.g. a non-ISO custom code). Ignored on the happy path, where `Intl`\n * supplies the symbol.\n */\n symbol?: string | null;\n}\n\n/**\n * Formats a monetary amount. The currency is resolved in this order:\n *\n * - `formatMoney(amount, \"EUR\")` — a positional ISO code, with the currency's\n * natural decimals.\n * - `formatMoney(amount, { currencyCode, decimalPlaces, locale, symbol })` — the\n * options form; any of `currencyCode`/`decimalPlaces` may be omitted.\n * - `formatMoney(amount)` (or an options object without `currencyCode`) — the\n * organisation's reporting currency stamped via the {@link ./orgCurrency} seam,\n * including that currency's decimal-places override when the caller gave none.\n * When no org currency is stamped it falls back to {@link MONEY_FALLBACK_CURRENCY}\n * (`GBP`), the same deterministic fail-open as the date org-format seam.\n *\n * On an unknown/invalid currency code (which makes `Intl.NumberFormat` throw) it\n * falls back to a symbol/code prefix plus the fixed-decimal amount, so a bad code\n * degrades to a readable string rather than throwing on a render path.\n */\nexport function formatMoney(amount: number, currency?: string | FormatMoneyOptions): string {\n const options: FormatMoneyOptions = typeof currency === \"string\" ? { currencyCode: currency } : (currency ?? {});\n const { currencyCode, decimalPlaces, locale = MONEY_FALLBACK_LOCALE, symbol } = options;\n\n // No explicit code (undefined arg, or an options object without currencyCode)\n // resolves the org's reporting currency, falling back to GBP when none is stamped.\n const orgCurrency = currencyCode === undefined ? getOrgReportingCurrency() : null;\n const effectiveCode = currencyCode ?? orgCurrency?.code ?? MONEY_FALLBACK_CURRENCY;\n // The org currency's decimal-places override applies ONLY when the caller omitted\n // decimalPlaces entirely (undefined). An explicit `null` means \"use Intl's\n // per-currency default\" and must NOT pick up the org override, so distinguish\n // undefined from null rather than coalescing both with `??`.\n const effectiveDecimalPlaces =\n decimalPlaces !== undefined ? decimalPlaces : (orgCurrency?.decimalPlaces ?? null);\n\n // Intl.NumberFormat requires a well-formed (uppercase) ISO 4217 code; a lowercase\n // code (e.g. \"usd\") throws and forces the fallback, so normalise up front.\n const normalizedCode = effectiveCode.toUpperCase();\n\n try {\n const formatter = new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: normalizedCode,\n // Narrow symbol so a clear currency context renders \"$100\", not \"US$100\".\n currencyDisplay: \"narrowSymbol\",\n ...(effectiveDecimalPlaces != null\n ? { minimumFractionDigits: effectiveDecimalPlaces, maximumFractionDigits: effectiveDecimalPlaces }\n : {}),\n });\n return formatter.format(amount);\n } catch {\n const fractionDigits = effectiveDecimalPlaces ?? 2;\n const prefix = symbol ?? normalizedCode;\n // Keep the minus sign before the symbol/code prefix (\"-£10.00\", not \"£-10.00\").\n const sign = amount < 0 ? \"-\" : \"\";\n return `${sign}${prefix}${Math.abs(amount).toFixed(fractionDigits)}`;\n }\n}\n\n/** Options for {@link formatCompactMoney}. */\nexport interface FormatCompactMoneyOptions {\n /** ISO 4217 code (e.g. `\"GBP\"`, `\"EUR\"`) - resolves the leading symbol. */\n currencyCode: string;\n /**\n * Fallback symbol used ONLY when `Intl` cannot resolve `currencyCode` (a non-ISO\n * custom code). For a valid ISO code `Intl`'s own symbol always wins - this option\n * does not override it. Mirrors {@link FormatMoneyOptions.symbol} and the\n * {@link getCurrencySymbol} fallback contract.\n */\n symbol?: string | null;\n /** BCP-47 locale used only to resolve the symbol; defaults to {@link MONEY_FALLBACK_LOCALE}. */\n locale?: string;\n}\n\n/**\n * Formats an amount as an abbreviated currency string for compact display, such\n * as chart axis ticks where space is tight - e.g. `\"£1.3m\"`, `\"£500k\"`, `\"£99\"`.\n *\n * Thresholds on the absolute value: >= 1,000,000 renders in millions with a\n * lowercase `m` and one decimal place; >= 1,000 renders in thousands with a\n * lowercase `k` and no decimals; otherwise the whole amount with no decimals.\n * A leading minus is kept before the symbol (e.g. `\"-£1.3m\"`).\n *\n * Unlike {@link formatMoney} this is NOT org-locale driven beyond resolving the\n * leading symbol via {@link getCurrencySymbol}; the number itself is formatted\n * with fixed abbreviations so axis labels stay short and machine-stable.\n */\nexport function formatCompactMoney(amount: number, options: FormatCompactMoneyOptions): string {\n const { currencyCode, symbol, locale = MONEY_FALLBACK_LOCALE } = options;\n\n const sym = getCurrencySymbol(currencyCode, locale, symbol);\n const sign = amount < 0 ? \"-\" : \"\";\n const abs = Math.abs(amount);\n\n if (abs >= 1_000_000) {\n return `${sign}${sym}${(abs / 1_000_000).toFixed(1)}m`;\n }\n if (abs >= 1_000) {\n return `${sign}${sym}${(abs / 1_000).toFixed(0)}k`;\n }\n return `${sign}${sym}${abs.toFixed(0)}`;\n}\n\n/**\n * Resolves the currency symbol for a code in a locale (e.g. `\"GBP\"` -> `\"£\"`),\n * for use in input adornments and labels. Falls back to the supplied `symbol`, then\n * the code itself, when `Intl` cannot resolve it.\n */\nexport function getCurrencySymbol(\n currencyCode: string,\n locale: string = MONEY_FALLBACK_LOCALE,\n symbol?: string | null,\n): string {\n // Intl needs an uppercase ISO code; a lowercase one throws (see formatMoney).\n const normalizedCode = currencyCode.toUpperCase();\n\n try {\n const parts = new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: normalizedCode,\n currencyDisplay: \"narrowSymbol\",\n }).formatToParts(0);\n\n return parts.find((part) => part.type === \"currency\")?.value ?? symbol ?? normalizedCode;\n } catch {\n return symbol ?? normalizedCode;\n }\n}\n","/**\n * Framework-agnostic money PARSING — the inverse of {@link formatMoney} for form\n * inputs and editable amount fields.\n *\n * A user (or a round-tripped display value) types an amount in the en-GB /\n * dot-decimal display shape — a currency symbol or ISO code prefix, comma grouping\n * separators, stray whitespace, and a dot decimal point (\"£1,234.56\", \"GBP 1,234.56\").\n * This turns that back into a plain number, or null when there is no meaningful value\n * to parse.\n *\n * It is deliberately lenient about the symbol, currency-code prefix and grouping so\n * it tolerates the display forms {@link formatMoney} produces in the en-GB fallback\n * locale, but it is NOT a locale-universal inverse of {@link formatMoney}. Only the\n * dot-decimal shape is understood: a comma is always treated as a grouping separator\n * and dropped, so comma-decimal locales (de-DE `1.234,56`) are OUT OF SCOPE and would\n * mis-parse. The platform stores and edits amounts in the dot-decimal numeric form.\n *\n * Ported from the per-plugin `parseCurrency` helper so consuming plugins drop the\n * local copy.\n */\n\n/**\n * The Unicode minus sign (U+2212) that `Intl.NumberFormat` emits for negatives.\n * It is not the ASCII hyphen-minus, so `parseFloat` would not treat it as a sign;\n * normalise it to `-` before stripping.\n */\nconst UNICODE_MINUS = /−/g;\n\n/**\n * After normalising the minus, everything that is NOT a digit, dot or minus is\n * stripped: currency symbols (£ $ € ¥ ₹ and any other), alpha ISO-code prefixes\n * (the \"GBP \" prefix, and the invalid-code fallback prefix like \"ZZZZ\"), comma\n * grouping separators, and all whitespace (including the non-breaking / narrow\n * no-break spaces `Intl` inserts).\n */\nconst NON_NUMERIC_CHARS = /[^0-9.-]/g;\n\n/**\n * Parses a formatted currency string back to a number. Returns null for\n * null/undefined, an empty/whitespace-only string, or the literal `\"N/A\"`\n * placeholder; normalises the Unicode minus, strips every non-numeric character\n * (currency symbols, alpha currency-code prefixes, grouping commas and whitespace),\n * then `parseFloat`s the remainder, returning null when the result is not a number.\n *\n * Parses only the en-GB / dot-decimal display shape (comma thousands, dot decimal);\n * see the module docs — it is NOT a locale-universal inverse of {@link formatMoney}.\n * A leading minus is preserved, so `\"-£10.00\"` parses to `-10`. Garbage that contains\n * no leading number (`\"abc\"`) yields null.\n *\n * @example\n * parseMoney(\"£1,234.56\") // 1234.56\n * parseMoney(\"GBP 1,234.56\") // 1234.56\n * parseMoney(\"−£10.00\") // -10 (Unicode minus U+2212)\n * parseMoney(\"ZZZZ10.00\") // 10 (invalid-code fallback prefix)\n * parseMoney(\"N/A\") // null\n * parseMoney(\"\") // null\n * parseMoney(null) // null\n */\nexport function parseMoney(value: string | null | undefined): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n const trimmed = value.trim();\n if (trimmed === \"\" || trimmed === \"N/A\") {\n return null;\n }\n\n const cleaned = trimmed.replace(UNICODE_MINUS, \"-\").replace(NON_NUMERIC_CHARS, \"\");\n const parsed = Number.parseFloat(cleaned);\n\n return Number.isNaN(parsed) ? null : parsed;\n}\n"]}
|