@forthtilliath/ts-kit 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +100 -22
  2. package/dist/array/flattenDeep.d.ts +7 -5
  3. package/dist/array/flattenDeep.d.ts.map +1 -1
  4. package/dist/array/flattenDeep.js +8 -5
  5. package/dist/array/index.d.ts +1 -0
  6. package/dist/array/index.d.ts.map +1 -1
  7. package/dist/array/index.js +1 -0
  8. package/dist/array/shuffle.d.ts +14 -0
  9. package/dist/array/shuffle.d.ts.map +1 -0
  10. package/dist/array/shuffle.js +20 -0
  11. package/dist/async/abortReason.d.ts +9 -0
  12. package/dist/async/abortReason.d.ts.map +1 -0
  13. package/dist/async/abortReason.js +11 -0
  14. package/dist/async/memoize.d.ts +2 -18
  15. package/dist/async/memoize.d.ts.map +1 -1
  16. package/dist/async/memoize.js +4 -25
  17. package/dist/async/retry.d.ts +15 -3
  18. package/dist/async/retry.d.ts.map +1 -1
  19. package/dist/async/retry.js +15 -11
  20. package/dist/async/sleep.d.ts +2 -1
  21. package/dist/async/sleep.d.ts.map +1 -1
  22. package/dist/async/sleep.js +23 -2
  23. package/dist/async/withTimeout.d.ts +11 -4
  24. package/dist/async/withTimeout.d.ts.map +1 -1
  25. package/dist/async/withTimeout.js +31 -9
  26. package/dist/csv/escapeCsvField.d.ts +21 -0
  27. package/dist/csv/escapeCsvField.d.ts.map +1 -0
  28. package/dist/csv/escapeCsvField.js +28 -0
  29. package/dist/csv/formatCsvNumber.d.ts +14 -0
  30. package/dist/csv/formatCsvNumber.d.ts.map +1 -0
  31. package/dist/csv/formatCsvNumber.js +15 -0
  32. package/dist/csv/index.d.ts +2 -0
  33. package/dist/csv/index.d.ts.map +1 -1
  34. package/dist/csv/index.js +2 -0
  35. package/dist/date/dateInTimeZone.d.ts +22 -0
  36. package/dist/date/dateInTimeZone.d.ts.map +1 -0
  37. package/dist/date/dateInTimeZone.js +35 -0
  38. package/dist/date/daysUntil.d.ts +15 -0
  39. package/dist/date/daysUntil.d.ts.map +1 -0
  40. package/dist/date/daysUntil.js +22 -0
  41. package/dist/date/formatRecentDate.d.ts +22 -0
  42. package/dist/date/formatRecentDate.d.ts.map +1 -0
  43. package/dist/date/formatRecentDate.js +46 -0
  44. package/dist/date/index.d.ts +7 -0
  45. package/dist/date/index.d.ts.map +1 -1
  46. package/dist/date/index.js +7 -0
  47. package/dist/date/mondayFirstWeekday.d.ts +12 -0
  48. package/dist/date/mondayFirstWeekday.d.ts.map +1 -0
  49. package/dist/date/mondayFirstWeekday.js +15 -0
  50. package/dist/date/parseIsoDate.d.ts +12 -0
  51. package/dist/date/parseIsoDate.d.ts.map +1 -0
  52. package/dist/date/parseIsoDate.js +13 -0
  53. package/dist/date/parseSqliteDate.d.ts +13 -0
  54. package/dist/date/parseSqliteDate.d.ts.map +1 -0
  55. package/dist/date/parseSqliteDate.js +17 -0
  56. package/dist/date/todayInTimeZone.d.ts +14 -0
  57. package/dist/date/todayInTimeZone.d.ts.map +1 -0
  58. package/dist/date/todayInTimeZone.js +15 -0
  59. package/dist/files/downloadText.d.ts.map +1 -1
  60. package/dist/files/downloadText.js +9 -1
  61. package/dist/files/formatFileTimestamp.d.ts +22 -0
  62. package/dist/files/formatFileTimestamp.d.ts.map +1 -0
  63. package/dist/files/formatFileTimestamp.js +20 -0
  64. package/dist/files/index.d.ts +1 -0
  65. package/dist/files/index.d.ts.map +1 -1
  66. package/dist/files/index.js +1 -0
  67. package/dist/function/index.d.ts +2 -0
  68. package/dist/function/index.d.ts.map +1 -0
  69. package/dist/function/index.js +1 -0
  70. package/dist/function/memoize.d.ts +34 -0
  71. package/dist/function/memoize.d.ts.map +1 -0
  72. package/dist/function/memoize.js +50 -0
  73. package/dist/ics/escapeIcsText.d.ts +12 -0
  74. package/dist/ics/escapeIcsText.d.ts.map +1 -0
  75. package/dist/ics/escapeIcsText.js +17 -0
  76. package/dist/ics/foldIcsLine.d.ts +15 -0
  77. package/dist/ics/foldIcsLine.d.ts.map +1 -0
  78. package/dist/ics/foldIcsLine.js +40 -0
  79. package/dist/ics/icsTimestamp.d.ts +10 -0
  80. package/dist/ics/icsTimestamp.d.ts.map +1 -0
  81. package/dist/ics/icsTimestamp.js +11 -0
  82. package/dist/ics/index.d.ts +6 -0
  83. package/dist/ics/index.d.ts.map +1 -0
  84. package/dist/ics/index.js +5 -0
  85. package/dist/ics/toIcsDate.d.ts +11 -0
  86. package/dist/ics/toIcsDate.d.ts.map +1 -0
  87. package/dist/ics/toIcsDate.js +12 -0
  88. package/dist/ics/toIcsDateTime.d.ts +13 -0
  89. package/dist/ics/toIcsDateTime.d.ts.map +1 -0
  90. package/dist/ics/toIcsDateTime.js +16 -0
  91. package/dist/index.d.ts +2 -1
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +2 -1
  94. package/dist/maths/average.d.ts +2 -7
  95. package/dist/maths/average.d.ts.map +1 -1
  96. package/dist/maths/average.js +4 -10
  97. package/dist/maths/sum.d.ts +2 -7
  98. package/dist/maths/sum.d.ts.map +1 -1
  99. package/dist/maths/sum.js +4 -9
  100. package/dist/number/average.d.ts +13 -0
  101. package/dist/number/average.d.ts.map +1 -0
  102. package/dist/number/average.js +15 -0
  103. package/dist/number/formatBytes.d.ts.map +1 -1
  104. package/dist/number/formatBytes.js +6 -2
  105. package/dist/number/formatCsvNumber.d.ts +2 -13
  106. package/dist/number/formatCsvNumber.d.ts.map +1 -1
  107. package/dist/number/formatCsvNumber.js +4 -15
  108. package/dist/number/index.d.ts +2 -0
  109. package/dist/number/index.d.ts.map +1 -1
  110. package/dist/number/index.js +2 -0
  111. package/dist/number/sum.d.ts +10 -0
  112. package/dist/number/sum.d.ts.map +1 -0
  113. package/dist/number/sum.js +11 -0
  114. package/dist/object/deepEqual.d.ts +6 -0
  115. package/dist/object/deepEqual.d.ts.map +1 -1
  116. package/dist/object/deepEqual.js +45 -11
  117. package/dist/string/escapeCsvField.d.ts +2 -13
  118. package/dist/string/escapeCsvField.d.ts.map +1 -1
  119. package/dist/string/escapeCsvField.js +4 -17
  120. package/dist/string/escapeHtml.d.ts +6 -4
  121. package/dist/string/escapeHtml.d.ts.map +1 -1
  122. package/dist/string/escapeHtml.js +8 -5
  123. package/dist/string/pluralize.d.ts +30 -7
  124. package/dist/string/pluralize.d.ts.map +1 -1
  125. package/dist/string/pluralize.js +48 -15
  126. package/dist/version/compareVersions.d.ts +8 -1
  127. package/dist/version/compareVersions.d.ts.map +1 -1
  128. package/dist/version/compareVersions.js +63 -9
  129. package/package.json +7 -6
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Escapes a single CSV field per RFC 4180, quoting it only when needed: if it
3
+ * contains a double quote, the delimiter or a line break, it's wrapped in
4
+ * double quotes with any internal double quote doubled; otherwise it's
5
+ * returned as-is.
6
+ *
7
+ * Builds rows cell by cell. To serialize whole rows at once, see `toCsv`
8
+ * (which always quotes, and defaults to `,`).
9
+ *
10
+ * @param value - The raw field value.
11
+ * @param delimiter - The cell separator the field will be joined with.
12
+ * Defaults to `";"`, what French-locale Excel expects.
13
+ * @returns The value, quoted and escaped only if needed.
14
+ * @example
15
+ * escapeCsvField("Apple"); // => "Apple"
16
+ * escapeCsvField("a;b"); // => '"a;b"'
17
+ * escapeCsvField("a,b", ","); // => '"a,b"'
18
+ * escapeCsvField('He said "hi"'); // => '"He said ""hi"""'
19
+ */
20
+ export declare function escapeCsvField(value: string, delimiter?: string): string;
21
+ //# sourceMappingURL=escapeCsvField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escapeCsvField.d.ts","sourceRoot":"","sources":["../../src/csv/escapeCsvField.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,SAAM,GAAG,MAAM,CAQrE"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Escapes a single CSV field per RFC 4180, quoting it only when needed: if it
3
+ * contains a double quote, the delimiter or a line break, it's wrapped in
4
+ * double quotes with any internal double quote doubled; otherwise it's
5
+ * returned as-is.
6
+ *
7
+ * Builds rows cell by cell. To serialize whole rows at once, see `toCsv`
8
+ * (which always quotes, and defaults to `,`).
9
+ *
10
+ * @param value - The raw field value.
11
+ * @param delimiter - The cell separator the field will be joined with.
12
+ * Defaults to `";"`, what French-locale Excel expects.
13
+ * @returns The value, quoted and escaped only if needed.
14
+ * @example
15
+ * escapeCsvField("Apple"); // => "Apple"
16
+ * escapeCsvField("a;b"); // => '"a;b"'
17
+ * escapeCsvField("a,b", ","); // => '"a,b"'
18
+ * escapeCsvField('He said "hi"'); // => '"He said ""hi"""'
19
+ */
20
+ export function escapeCsvField(value, delimiter = ";") {
21
+ const needsQuoting = value.includes('"') ||
22
+ value.includes(delimiter) ||
23
+ value.includes("\n") ||
24
+ value.includes("\r");
25
+ if (!needsQuoting)
26
+ return value;
27
+ return `"${value.replace(/"/g, '""')}"`;
28
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Formats a number as a raw value (no unit suffix) for use in a spreadsheet
3
+ * (sum, average...), with a comma decimal separator as expected by
4
+ * French-locale spreadsheets instead of JS's dot.
5
+ *
6
+ * @param value - The number to format.
7
+ * @param decimals - Number of decimal places. Defaults to `1`.
8
+ * @returns The formatted number, comma as decimal separator.
9
+ * @example
10
+ * formatCsvNumber(1.5); // => "1,5"
11
+ * formatCsvNumber(12.345, 2); // => "12,35"
12
+ */
13
+ export declare function formatCsvNumber(value: number, decimals?: number): string;
14
+ //# sourceMappingURL=formatCsvNumber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatCsvNumber.d.ts","sourceRoot":"","sources":["../../src/csv/formatCsvNumber.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,MAAM,CAEnE"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Formats a number as a raw value (no unit suffix) for use in a spreadsheet
3
+ * (sum, average...), with a comma decimal separator as expected by
4
+ * French-locale spreadsheets instead of JS's dot.
5
+ *
6
+ * @param value - The number to format.
7
+ * @param decimals - Number of decimal places. Defaults to `1`.
8
+ * @returns The formatted number, comma as decimal separator.
9
+ * @example
10
+ * formatCsvNumber(1.5); // => "1,5"
11
+ * formatCsvNumber(12.345, 2); // => "12,35"
12
+ */
13
+ export function formatCsvNumber(value, decimals = 1) {
14
+ return value.toFixed(decimals).replace(".", ",");
15
+ }
@@ -1,4 +1,6 @@
1
1
  export * from "./downloadCsv.js";
2
+ export * from "./escapeCsvField.js";
3
+ export * from "./formatCsvNumber.js";
2
4
  export * from "./parseCsvLine.js";
3
5
  export * from "./toCsv.js";
4
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/csv/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/csv/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,YAAY,CAAC"}
package/dist/csv/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export * from "./downloadCsv.js";
2
+ export * from "./escapeCsvField.js";
3
+ export * from "./formatCsvNumber.js";
2
4
  export * from "./parseCsvLine.js";
3
5
  export * from "./toCsv.js";
@@ -0,0 +1,22 @@
1
+ export interface CalendarDate {
2
+ year: number;
3
+ /** 1–12. */
4
+ month: number;
5
+ day: number;
6
+ /** `YYYY-MM-DD`. */
7
+ iso: string;
8
+ }
9
+ /**
10
+ * Returns the calendar date of an instant in a given time zone, whatever the
11
+ * machine's own time zone (e.g. a server running in UTC).
12
+ *
13
+ * @param date - The instant (a `Date`, a timestamp in ms or a parsable string).
14
+ * @param timeZone - An IANA time zone, e.g. `"Europe/Paris"`.
15
+ * @returns `{ year, month, day, iso }`, `month` from 1 to 12.
16
+ * @throws {RangeError} If the time zone or the date is invalid.
17
+ * @example
18
+ * dateInTimeZone("2026-03-14T23:30:00Z", "Europe/Paris");
19
+ * // => { year: 2026, month: 3, day: 15, iso: "2026-03-15" }
20
+ */
21
+ export declare function dateInTimeZone(date: Date | number | string, timeZone: string): CalendarDate;
22
+ //# sourceMappingURL=dateInTimeZone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dateInTimeZone.d.ts","sourceRoot":"","sources":["../../src/date/dateInTimeZone.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,oBAAoB;IACpB,GAAG,EAAE,MAAM,CAAC;CACb;AAkBD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,EAC5B,QAAQ,EAAE,MAAM,GACf,YAAY,CASd"}
@@ -0,0 +1,35 @@
1
+ const formatters = new Map();
2
+ function formatter(timeZone) {
3
+ let result = formatters.get(timeZone);
4
+ if (!result) {
5
+ result = new Intl.DateTimeFormat("en-US", {
6
+ timeZone,
7
+ year: "numeric",
8
+ month: "numeric",
9
+ day: "numeric",
10
+ });
11
+ formatters.set(timeZone, result);
12
+ }
13
+ return result;
14
+ }
15
+ /**
16
+ * Returns the calendar date of an instant in a given time zone, whatever the
17
+ * machine's own time zone (e.g. a server running in UTC).
18
+ *
19
+ * @param date - The instant (a `Date`, a timestamp in ms or a parsable string).
20
+ * @param timeZone - An IANA time zone, e.g. `"Europe/Paris"`.
21
+ * @returns `{ year, month, day, iso }`, `month` from 1 to 12.
22
+ * @throws {RangeError} If the time zone or the date is invalid.
23
+ * @example
24
+ * dateInTimeZone("2026-03-14T23:30:00Z", "Europe/Paris");
25
+ * // => { year: 2026, month: 3, day: 15, iso: "2026-03-15" }
26
+ */
27
+ export function dateInTimeZone(date, timeZone) {
28
+ const parts = formatter(timeZone).formatToParts(new Date(date));
29
+ const get = (type) => Number(parts.find((p) => p.type === type)?.value);
30
+ const year = get("year");
31
+ const month = get("month");
32
+ const day = get("day");
33
+ const iso = `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
34
+ return { year, month, day, iso };
35
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Counts the calendar days between today and a date, both taken in a given
3
+ * time zone: the time of day doesn't matter, only the date on the calendar.
4
+ *
5
+ * @param date - The target instant (a `Date`, a timestamp in ms or a parsable string).
6
+ * @param timeZone - An IANA time zone, e.g. `"Europe/Paris"`.
7
+ * @param now - The reference instant (defaults to now).
8
+ * @returns `0` today, `1` tomorrow, `-1` yesterday…
9
+ * @throws {RangeError} If the time zone or the date is invalid.
10
+ * @example
11
+ * // It's 2026-03-14 23:00 in Paris
12
+ * daysUntil("2026-03-15T08:00:00+01:00", "Europe/Paris"); // => 1
13
+ */
14
+ export declare function daysUntil(date: Date | number | string, timeZone: string, now?: Date): number;
15
+ //# sourceMappingURL=daysUntil.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daysUntil.d.ts","sourceRoot":"","sources":["../../src/date/daysUntil.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,EAC5B,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,IAAiB,GACrB,MAAM,CAER"}
@@ -0,0 +1,22 @@
1
+ import { dateInTimeZone } from "./dateInTimeZone.js";
2
+ const DAY_MS = 24 * 60 * 60 * 1000;
3
+ function dayNumber(date, timeZone) {
4
+ const { year, month, day } = dateInTimeZone(date, timeZone);
5
+ return Date.UTC(year, month - 1, day) / DAY_MS;
6
+ }
7
+ /**
8
+ * Counts the calendar days between today and a date, both taken in a given
9
+ * time zone: the time of day doesn't matter, only the date on the calendar.
10
+ *
11
+ * @param date - The target instant (a `Date`, a timestamp in ms or a parsable string).
12
+ * @param timeZone - An IANA time zone, e.g. `"Europe/Paris"`.
13
+ * @param now - The reference instant (defaults to now).
14
+ * @returns `0` today, `1` tomorrow, `-1` yesterday…
15
+ * @throws {RangeError} If the time zone or the date is invalid.
16
+ * @example
17
+ * // It's 2026-03-14 23:00 in Paris
18
+ * daysUntil("2026-03-15T08:00:00+01:00", "Europe/Paris"); // => 1
19
+ */
20
+ export function daysUntil(date, timeZone, now = new Date()) {
21
+ return dayNumber(date, timeZone) - dayNumber(now, timeZone);
22
+ }
@@ -0,0 +1,22 @@
1
+ /** Languages {@link formatRecentDate} has wording for. */
2
+ export type RecentDateLocale = "fr" | "en";
3
+ export interface FormatRecentDateOptions {
4
+ /** Reference "now" (defaults to the current time). */
5
+ now?: Date;
6
+ /** Wording and `Intl` formats. @defaultValue "fr" */
7
+ locale?: RecentDateLocale;
8
+ }
9
+ /**
10
+ * Describes a past moment the way a "last saved…" line reads best: with the
11
+ * time when it's today or yesterday, as a plain day otherwise. A date in the
12
+ * future (clock change between devices) reads as today.
13
+ *
14
+ * @param date - The moment to describe.
15
+ * @param options - See {@link FormatRecentDateOptions}.
16
+ * @returns E.g. `"aujourd'hui à 21:42"`, `"hier à 08:05"`, `"le 11 octobre 2026"`.
17
+ * @example
18
+ * formatRecentDate(lastBackupAt); // => "hier à 21:42"
19
+ * formatRecentDate(lastBackupAt, { locale: "en" }); // => "yesterday at 9:42 PM"
20
+ */
21
+ export declare function formatRecentDate(date: Date, { now, locale }?: FormatRecentDateOptions): string;
22
+ //# sourceMappingURL=formatRecentDate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatRecentDate.d.ts","sourceRoot":"","sources":["../../src/date/formatRecentDate.ts"],"names":[],"mappings":"AAEA,0DAA0D;AAC1D,MAAM,MAAM,gBAAgB,GAAG,IAAI,GAAG,IAAI,CAAC;AAE3C,MAAM,WAAW,uBAAuB;IACtC,sDAAsD;IACtD,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,qDAAqD;IACrD,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B;AA2BD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,IAAI,EACV,EAAE,GAAgB,EAAE,MAAa,EAAE,GAAE,uBAA4B,GAChE,MAAM,CAmBR"}
@@ -0,0 +1,46 @@
1
+ import { startOfDay } from "./startOfDay.js";
2
+ const WORDING = {
3
+ fr: {
4
+ tag: "fr-FR",
5
+ today: (time) => `aujourd'hui à ${time}`,
6
+ yesterday: (time) => `hier à ${time}`,
7
+ older: (day) => `le ${day}`,
8
+ },
9
+ en: {
10
+ tag: "en-US",
11
+ today: (time) => `today at ${time}`,
12
+ yesterday: (time) => `yesterday at ${time}`,
13
+ older: (day) => `on ${day}`,
14
+ },
15
+ };
16
+ const DAY_MS = 24 * 60 * 60 * 1000;
17
+ /**
18
+ * Describes a past moment the way a "last saved…" line reads best: with the
19
+ * time when it's today or yesterday, as a plain day otherwise. A date in the
20
+ * future (clock change between devices) reads as today.
21
+ *
22
+ * @param date - The moment to describe.
23
+ * @param options - See {@link FormatRecentDateOptions}.
24
+ * @returns E.g. `"aujourd'hui à 21:42"`, `"hier à 08:05"`, `"le 11 octobre 2026"`.
25
+ * @example
26
+ * formatRecentDate(lastBackupAt); // => "hier à 21:42"
27
+ * formatRecentDate(lastBackupAt, { locale: "en" }); // => "yesterday at 9:42 PM"
28
+ */
29
+ export function formatRecentDate(date, { now = new Date(), locale = "fr" } = {}) {
30
+ const wording = WORDING[locale];
31
+ // Rounded: a day that has a DST change lasts 23 or 25 hours.
32
+ const dayDiff = Math.round((startOfDay(now).getTime() - startOfDay(date).getTime()) / DAY_MS);
33
+ if (dayDiff <= 1) {
34
+ const time = new Intl.DateTimeFormat(wording.tag, {
35
+ hour: "2-digit",
36
+ minute: "2-digit",
37
+ }).format(date);
38
+ return dayDiff === 1 ? wording.yesterday(time) : wording.today(time);
39
+ }
40
+ const day = new Intl.DateTimeFormat(wording.tag, {
41
+ day: "numeric",
42
+ month: "long",
43
+ year: "numeric",
44
+ }).format(date);
45
+ return wording.older(day);
46
+ }
@@ -1,5 +1,12 @@
1
+ export * from "./dateInTimeZone.js";
2
+ export * from "./daysUntil.js";
3
+ export * from "./formatRecentDate.js";
1
4
  export * from "./formatRelativeTime.js";
2
5
  export * from "./getPeriodStartMs.js";
3
6
  export * from "./isSameDay.js";
7
+ export * from "./mondayFirstWeekday.js";
8
+ export * from "./parseIsoDate.js";
9
+ export * from "./parseSqliteDate.js";
4
10
  export * from "./startOfDay.js";
11
+ export * from "./todayInTimeZone.js";
5
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/date/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/date/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC;AACxC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC"}
@@ -1,4 +1,11 @@
1
+ export * from "./dateInTimeZone.js";
2
+ export * from "./daysUntil.js";
3
+ export * from "./formatRecentDate.js";
1
4
  export * from "./formatRelativeTime.js";
2
5
  export * from "./getPeriodStartMs.js";
3
6
  export * from "./isSameDay.js";
7
+ export * from "./mondayFirstWeekday.js";
8
+ export * from "./parseIsoDate.js";
9
+ export * from "./parseSqliteDate.js";
4
10
  export * from "./startOfDay.js";
11
+ export * from "./todayInTimeZone.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Day of the week with Monday as the first day: `0` for Monday … `6` for
3
+ * Sunday (instead of JavaScript's Sunday = `0`). Handy to offset the first
4
+ * day of a month in a Monday-first calendar grid.
5
+ *
6
+ * @param date - A `Date`, or a `YYYY-MM-DD` calendar date read as local midnight.
7
+ * @returns The weekday index, Monday = `0`.
8
+ * @example
9
+ * mondayFirstWeekday("2026-10-01"); // => 3 (a Thursday)
10
+ */
11
+ export declare function mondayFirstWeekday(date: Date | string): number;
12
+ //# sourceMappingURL=mondayFirstWeekday.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mondayFirstWeekday.d.ts","sourceRoot":"","sources":["../../src/date/mondayFirstWeekday.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAG9D"}
@@ -0,0 +1,15 @@
1
+ import { parseIsoDate } from "./parseIsoDate.js";
2
+ /**
3
+ * Day of the week with Monday as the first day: `0` for Monday … `6` for
4
+ * Sunday (instead of JavaScript's Sunday = `0`). Handy to offset the first
5
+ * day of a month in a Monday-first calendar grid.
6
+ *
7
+ * @param date - A `Date`, or a `YYYY-MM-DD` calendar date read as local midnight.
8
+ * @returns The weekday index, Monday = `0`.
9
+ * @example
10
+ * mondayFirstWeekday("2026-10-01"); // => 3 (a Thursday)
11
+ */
12
+ export function mondayFirstWeekday(date) {
13
+ const day = (typeof date === "string" ? parseIsoDate(date) : date).getDay();
14
+ return (day + 6) % 7;
15
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Parses a `YYYY-MM-DD` calendar date as **local** midnight — unlike
3
+ * `new Date("2026-03-14")`, which the spec reads as UTC midnight and which
4
+ * therefore lands on the previous day west of Greenwich.
5
+ *
6
+ * @param iso - A calendar date, `YYYY-MM-DD`.
7
+ * @returns A `Date` at local midnight on that day.
8
+ * @example
9
+ * parseIsoDate("2026-03-14").getDate(); // => 14, whatever the time zone
10
+ */
11
+ export declare function parseIsoDate(iso: string): Date;
12
+ //# sourceMappingURL=parseIsoDate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseIsoDate.d.ts","sourceRoot":"","sources":["../../src/date/parseIsoDate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAE9C"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Parses a `YYYY-MM-DD` calendar date as **local** midnight — unlike
3
+ * `new Date("2026-03-14")`, which the spec reads as UTC midnight and which
4
+ * therefore lands on the previous day west of Greenwich.
5
+ *
6
+ * @param iso - A calendar date, `YYYY-MM-DD`.
7
+ * @returns A `Date` at local midnight on that day.
8
+ * @example
9
+ * parseIsoDate("2026-03-14").getDate(); // => 14, whatever the time zone
10
+ */
11
+ export function parseIsoDate(iso) {
12
+ return new Date(`${iso}T00:00:00`);
13
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Parses a date read from SQLite. A `current_timestamp` value
3
+ * (`"2026-10-09 19:46:00"`, UTC without a zone marker) is read as **UTC** —
4
+ * `new Date()` would take it for local time and shift it by the time zone
5
+ * offset. Full ISO strings (with `Z` or an offset) are read as they are.
6
+ *
7
+ * @param value - A SQLite datetime or a full ISO string.
8
+ * @returns The corresponding `Date`.
9
+ * @example
10
+ * parseSqliteDate("2026-10-09 19:46:00").toISOString(); // => "2026-10-09T19:46:00.000Z"
11
+ */
12
+ export declare function parseSqliteDate(value: string): Date;
13
+ //# sourceMappingURL=parseSqliteDate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseSqliteDate.d.ts","sourceRoot":"","sources":["../../src/date/parseSqliteDate.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAInD"}
@@ -0,0 +1,17 @@
1
+ // SQLite's `current_timestamp` stores UTC as "YYYY-MM-DD HH:MM:SS", with no
2
+ // zone marker: `new Date()` would read it as local time.
3
+ const SQLITE_DATETIME = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/;
4
+ /**
5
+ * Parses a date read from SQLite. A `current_timestamp` value
6
+ * (`"2026-10-09 19:46:00"`, UTC without a zone marker) is read as **UTC** —
7
+ * `new Date()` would take it for local time and shift it by the time zone
8
+ * offset. Full ISO strings (with `Z` or an offset) are read as they are.
9
+ *
10
+ * @param value - A SQLite datetime or a full ISO string.
11
+ * @returns The corresponding `Date`.
12
+ * @example
13
+ * parseSqliteDate("2026-10-09 19:46:00").toISOString(); // => "2026-10-09T19:46:00.000Z"
14
+ */
15
+ export function parseSqliteDate(value) {
16
+ return new Date(SQLITE_DATETIME.test(value) ? `${value.replace(" ", "T")}Z` : value);
17
+ }
@@ -0,0 +1,14 @@
1
+ import type { CalendarDate } from "./dateInTimeZone.js";
2
+ /**
3
+ * Returns today's calendar date in a given time zone — on a server running in
4
+ * UTC, `new Date()` is still "yesterday" in Paris between midnight and 1–2 am.
5
+ *
6
+ * @param timeZone - An IANA time zone, e.g. `"Europe/Paris"`.
7
+ * @param now - The reference instant (defaults to now).
8
+ * @returns `{ year, month, day, iso }`, `month` from 1 to 12.
9
+ * @throws {RangeError} If the time zone is invalid.
10
+ * @example
11
+ * todayInTimeZone("Europe/Paris").iso; // => "2026-09-30"
12
+ */
13
+ export declare function todayInTimeZone(timeZone: string, now?: Date): CalendarDate;
14
+ //# sourceMappingURL=todayInTimeZone.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"todayInTimeZone.d.ts","sourceRoot":"","sources":["../../src/date/todayInTimeZone.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAGxD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,IAAiB,GACrB,YAAY,CAEd"}
@@ -0,0 +1,15 @@
1
+ import { dateInTimeZone } from "./dateInTimeZone.js";
2
+ /**
3
+ * Returns today's calendar date in a given time zone — on a server running in
4
+ * UTC, `new Date()` is still "yesterday" in Paris between midnight and 1–2 am.
5
+ *
6
+ * @param timeZone - An IANA time zone, e.g. `"Europe/Paris"`.
7
+ * @param now - The reference instant (defaults to now).
8
+ * @returns `{ year, month, day, iso }`, `month` from 1 to 12.
9
+ * @throws {RangeError} If the time zone is invalid.
10
+ * @example
11
+ * todayInTimeZone("Europe/Paris").iso; // => "2026-09-30"
12
+ */
13
+ export function todayInTimeZone(timeZone, now = new Date()) {
14
+ return dateInTimeZone(now, timeZone);
15
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"downloadText.d.ts","sourceRoot":"","sources":["../../src/files/downloadText.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAcjE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,SAAe,GACtB,IAAI,CAcN"}
1
+ {"version":3,"file":"downloadText.d.ts","sourceRoot":"","sources":["../../src/files/downloadText.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAcjE;AASD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,QAAQ,SAAe,GACtB,IAAI,CAgBN"}
@@ -12,6 +12,12 @@ export function downloadText(filename, text) {
12
12
  element.click();
13
13
  document.body.removeChild(element);
14
14
  }
15
+ /**
16
+ * How long the object URL stays valid after the click. Revoking it right
17
+ * away can cancel the download in Firefox and Safari, which start it
18
+ * asynchronously; FileSaver.js uses the same 40 s delay.
19
+ */
20
+ const REVOKE_DELAY_MS = 40_000;
15
21
  /**
16
22
  * Downloads a text blob as a file.
17
23
  * @param filename The filename to save to.
@@ -26,6 +32,8 @@ export function downloadTextBlob(filename, text, mimeType = "text/plain") {
26
32
  a.style.setProperty("display", "none");
27
33
  document.body.appendChild(a);
28
34
  a.click();
29
- URL.revokeObjectURL(url);
30
35
  document.body.removeChild(a);
36
+ setTimeout(() => {
37
+ URL.revokeObjectURL(url);
38
+ }, REVOKE_DELAY_MS);
31
39
  }
@@ -0,0 +1,22 @@
1
+ export interface FormatFileTimestampOptions {
2
+ /** Append the seconds (`HHMMSS` instead of `HHMM`). Default: `false`. */
3
+ seconds?: boolean;
4
+ /** Put between the date and the time. Default: `"-"`. */
5
+ separator?: string;
6
+ }
7
+ /**
8
+ * Compact, file-name-safe timestamp in **local** time, `YYYYMMDD-HHMM` by
9
+ * default: unlike `toISOString()` (UTC, with `:` that Windows rejects), an
10
+ * export made at 00:30 carries the day it was made on, and names built with it
11
+ * sort chronologically in a file explorer.
12
+ *
13
+ * @param date - The instant to format. Default: now.
14
+ * @param options - `seconds`, `separator` between date and time.
15
+ * @returns The formatted timestamp.
16
+ * @example
17
+ * formatFileTimestamp(new Date(2026, 9, 8, 9, 2)); // => "20261008-0902"
18
+ * formatFileTimestamp(new Date(2026, 9, 8, 9, 2, 5), { seconds: true, separator: "" });
19
+ * // => "20261008090205"
20
+ */
21
+ export declare function formatFileTimestamp(date?: Date, { seconds, separator }?: FormatFileTimestampOptions): string;
22
+ //# sourceMappingURL=formatFileTimestamp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatFileTimestamp.d.ts","sourceRoot":"","sources":["../../src/files/formatFileTimestamp.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,0BAA0B;IACzC,yEAAyE;IACzE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,GAAE,IAAiB,EACvB,EAAE,OAAe,EAAE,SAAe,EAAE,GAAE,0BAA+B,GACpE,MAAM,CAIR"}
@@ -0,0 +1,20 @@
1
+ const pad2 = (value) => String(value).padStart(2, "0");
2
+ /**
3
+ * Compact, file-name-safe timestamp in **local** time, `YYYYMMDD-HHMM` by
4
+ * default: unlike `toISOString()` (UTC, with `:` that Windows rejects), an
5
+ * export made at 00:30 carries the day it was made on, and names built with it
6
+ * sort chronologically in a file explorer.
7
+ *
8
+ * @param date - The instant to format. Default: now.
9
+ * @param options - `seconds`, `separator` between date and time.
10
+ * @returns The formatted timestamp.
11
+ * @example
12
+ * formatFileTimestamp(new Date(2026, 9, 8, 9, 2)); // => "20261008-0902"
13
+ * formatFileTimestamp(new Date(2026, 9, 8, 9, 2, 5), { seconds: true, separator: "" });
14
+ * // => "20261008090205"
15
+ */
16
+ export function formatFileTimestamp(date = new Date(), { seconds = false, separator = "-" } = {}) {
17
+ const day = `${date.getFullYear()}${pad2(date.getMonth() + 1)}${pad2(date.getDate())}`;
18
+ const time = `${pad2(date.getHours())}${pad2(date.getMinutes())}${seconds ? pad2(date.getSeconds()) : ""}`;
19
+ return `${day}${separator}${time}`;
20
+ }
@@ -1,3 +1,4 @@
1
1
  export * from "./downloadText.js";
2
+ export * from "./formatFileTimestamp.js";
2
3
  export * from "./sanitizeFileName.js";
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/files/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/files/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC"}
@@ -1,2 +1,3 @@
1
1
  export * from "./downloadText.js";
2
+ export * from "./formatFileTimestamp.js";
2
3
  export * from "./sanitizeFileName.js";
@@ -0,0 +1,2 @@
1
+ export * from "./memoize.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/function/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
@@ -0,0 +1 @@
1
+ export * from "./memoize.js";
@@ -0,0 +1,34 @@
1
+ export interface MemoizedFunction<Args extends unknown[], R> {
2
+ (...args: Args): R;
3
+ /** The underlying cache, exposed for inspection or manual clearing. */
4
+ cache: Map<string, R>;
5
+ }
6
+ export interface MemoizeOptions<Args extends unknown[]> {
7
+ /** Computes the cache key for a set of arguments (defaults to `JSON.stringify`). */
8
+ getKey?: (...args: Args) => string;
9
+ /**
10
+ * Max number of cached results; beyond it, the least recently used one is
11
+ * evicted. Unbounded by default — set it for a long-lived function called
12
+ * with many distinct arguments.
13
+ */
14
+ maxSize?: number;
15
+ }
16
+ /**
17
+ * Caches the result of `fn` per argument tuple (JSON-stringified by
18
+ * default).
19
+ *
20
+ * A returned promise that rejects is evicted from the cache, so a failed
21
+ * async call is retried on the next invocation instead of failing forever.
22
+ *
23
+ * @param fn - The function to memoize.
24
+ * @param getKeyOrOptions - A key function, or `{ getKey, maxSize }`.
25
+ * @returns A memoized version of `fn`, with a `cache` map exposed.
26
+ * @example
27
+ * const square = memoize((n: number) => expensiveSquare(n));
28
+ * square(4); // computed
29
+ * square(4); // cached
30
+ *
31
+ * const getUser = memoize(fetchUser, { maxSize: 100 });
32
+ */
33
+ export declare function memoize<Args extends unknown[], R>(fn: (...args: Args) => R, getKeyOrOptions?: ((...args: Args) => string) | MemoizeOptions<Args>): MemoizedFunction<Args, R>;
34
+ //# sourceMappingURL=memoize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memoize.d.ts","sourceRoot":"","sources":["../../src/function/memoize.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB,CAAC,IAAI,SAAS,OAAO,EAAE,EAAE,CAAC;IACzD,CAAC,GAAG,IAAI,EAAE,IAAI,GAAG,CAAC,CAAC;IACnB,uEAAuE;IACvE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,cAAc,CAAC,IAAI,SAAS,OAAO,EAAE;IACpD,oFAAoF;IACpF,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,OAAO,CAAC,IAAI,SAAS,OAAO,EAAE,EAAE,CAAC,EAC/C,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,CAAC,EACxB,eAAe,GAAE,CAAC,CAAC,GAAG,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC,GAAG,cAAc,CAAC,IAAI,CAAM,GACvE,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC,CAqC3B"}