xypriss 9.6.97 → 9.7.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 (171) hide show
  1. package/README.md +55 -22
  2. package/dist/cjs/src/index.js +0 -19
  3. package/dist/cjs/src/index.js.map +1 -1
  4. package/dist/cjs/src/middleware/built-in/BuiltInMiddleware.js +4 -4
  5. package/dist/cjs/src/middleware/built-in/BuiltInMiddleware.js.map +1 -1
  6. package/dist/cjs/src/middleware/built-in/security/HoneypotTarpit.js +510 -0
  7. package/dist/cjs/src/middleware/built-in/security/HoneypotTarpit.js.map +1 -0
  8. package/dist/cjs/src/middleware/built-in/security/SQLInjectionDetector.js +113 -34
  9. package/dist/cjs/src/middleware/built-in/security/SQLInjectionDetector.js.map +1 -1
  10. package/dist/cjs/src/middleware/built-in/security/UriNormalizer.js +413 -0
  11. package/dist/cjs/src/middleware/built-in/security/UriNormalizer.js.map +1 -0
  12. package/dist/cjs/src/middleware/built-in/security/XyPrissCors.js +98 -0
  13. package/dist/cjs/src/middleware/built-in/security/XyPrissCors.js.map +1 -0
  14. package/dist/cjs/src/middleware/built-in/security/XyPrissHPP.js +52 -0
  15. package/dist/cjs/src/middleware/built-in/security/XyPrissHPP.js.map +1 -0
  16. package/dist/cjs/src/middleware/safe-json-middleware.js +0 -82
  17. package/dist/cjs/src/middleware/safe-json-middleware.js.map +1 -1
  18. package/dist/cjs/src/middleware/security-middleware.js +7 -4
  19. package/dist/cjs/src/middleware/security-middleware.js.map +1 -1
  20. package/dist/cjs/src/plugins/builtin/xems/XemsBuiltinPlugin.js +33 -10
  21. package/dist/cjs/src/plugins/builtin/xems/XemsBuiltinPlugin.js.map +1 -1
  22. package/dist/cjs/src/plugins/builtin/xems/XemsPlugin.js +34 -3
  23. package/dist/cjs/src/plugins/builtin/xems/XemsPlugin.js.map +1 -1
  24. package/dist/cjs/src/plugins/const/OFFICIAL_PLUGINS.js +1 -1
  25. package/dist/cjs/src/plugins/const/OFFICIAL_PLUGINS.js.map +1 -1
  26. package/dist/cjs/src/plugins/const/XyprissTempDir.js +72 -9
  27. package/dist/cjs/src/plugins/const/XyprissTempDir.js.map +1 -1
  28. package/dist/cjs/src/plugins/core/manager/PluginLoader.js +8 -0
  29. package/dist/cjs/src/plugins/core/manager/PluginLoader.js.map +1 -1
  30. package/dist/cjs/src/plugins/core/manager/PluginSecurity.js +87 -0
  31. package/dist/cjs/src/plugins/core/manager/PluginSecurity.js.map +1 -1
  32. package/dist/cjs/src/server/FastServer/index.js +5 -0
  33. package/dist/cjs/src/server/FastServer/index.js.map +1 -1
  34. package/dist/cjs/src/server/ServerFactory.js +8 -13
  35. package/dist/cjs/src/server/ServerFactory.js.map +1 -1
  36. package/dist/cjs/src/server/components/fastapi/RedirectManager.js +8 -58
  37. package/dist/cjs/src/server/components/fastapi/RedirectManager.js.map +1 -1
  38. package/dist/cjs/src/server/components/fastapi/upload/FileUploadManager.js +1 -1
  39. package/dist/cjs/src/server/components/fastapi/upload/FileUploadManager.js.map +1 -1
  40. package/dist/cjs/src/server/components/multi-server/MultiServerManager.js +9 -1
  41. package/dist/cjs/src/server/components/multi-server/MultiServerManager.js.map +1 -1
  42. package/dist/cjs/src/server/core/HttpServer.js +22 -0
  43. package/dist/cjs/src/server/core/HttpServer.js.map +1 -1
  44. package/dist/cjs/src/server/core/XHSCBridge/EngineManager.js +33 -2
  45. package/dist/cjs/src/server/core/XHSCBridge/EngineManager.js.map +1 -1
  46. package/dist/cjs/src/server/core/XHSCBridge/cmd/buildSecurityArgs.js +1 -1
  47. package/dist/cjs/src/server/core/XHSCBridge/cmd/buildSecurityArgs.js.map +1 -1
  48. package/dist/cjs/src/server/core/XyServerCreator.js +1 -0
  49. package/dist/cjs/src/server/core/XyServerCreator.js.map +1 -1
  50. package/dist/cjs/src/shared/logger/quickLogger.js +162 -0
  51. package/dist/cjs/src/shared/logger/quickLogger.js.map +1 -0
  52. package/dist/cjs/src/xhsc/PathApi.js +44 -3
  53. package/dist/cjs/src/xhsc/PathApi.js.map +1 -1
  54. package/dist/cjs/src/xhsc/System.js +2 -0
  55. package/dist/cjs/src/xhsc/System.js.map +1 -1
  56. package/dist/cjs/src/xhsc/UtilsApi.js +52 -0
  57. package/dist/cjs/src/xhsc/UtilsApi.js.map +1 -0
  58. package/dist/cjs/src/xhsc/XyPrissRunner.js +30 -2
  59. package/dist/cjs/src/xhsc/XyPrissRunner.js.map +1 -1
  60. package/dist/cjs/src/xhsc/fs/FSCore.js +148 -0
  61. package/dist/cjs/src/xhsc/fs/FSCore.js.map +1 -1
  62. package/dist/cjs/src/xhsc/fs/FileHandle.js +112 -0
  63. package/dist/cjs/src/xhsc/fs/FileHandle.js.map +1 -0
  64. package/dist/cjs/src/xhsc/ipc/XHSCDirectIPC.js +134 -0
  65. package/dist/cjs/src/xhsc/ipc/XHSCDirectIPC.js.map +1 -0
  66. package/dist/cjs/src/xhsc/utils/ArrayUtils.js +67 -0
  67. package/dist/cjs/src/xhsc/utils/ArrayUtils.js.map +1 -0
  68. package/dist/cjs/src/xhsc/utils/AsyncUtils.js +582 -0
  69. package/dist/cjs/src/xhsc/utils/AsyncUtils.js.map +1 -0
  70. package/dist/cjs/src/xhsc/utils/DateUtils.js +959 -0
  71. package/dist/cjs/src/xhsc/utils/DateUtils.js.map +1 -0
  72. package/dist/cjs/src/xhsc/utils/FunctionUtils.js +29 -0
  73. package/dist/cjs/src/xhsc/utils/FunctionUtils.js.map +1 -0
  74. package/dist/cjs/src/xhsc/utils/IdUtils.js +22 -0
  75. package/dist/cjs/src/xhsc/utils/IdUtils.js.map +1 -0
  76. package/dist/cjs/src/xhsc/utils/NumberUtils.js +79 -0
  77. package/dist/cjs/src/xhsc/utils/NumberUtils.js.map +1 -0
  78. package/dist/cjs/src/xhsc/utils/ObjectUtils.js +141 -0
  79. package/dist/cjs/src/xhsc/utils/ObjectUtils.js.map +1 -0
  80. package/dist/cjs/src/xhsc/utils/StringUtils.js +145 -0
  81. package/dist/cjs/src/xhsc/utils/StringUtils.js.map +1 -0
  82. package/dist/cjs/src/xhsc/utils/ValidationUtils.js +49 -0
  83. package/dist/cjs/src/xhsc/utils/ValidationUtils.js.map +1 -0
  84. package/dist/cjs/src/xhsc.js +5 -3
  85. package/dist/cjs/src/xhsc.js.map +1 -1
  86. package/dist/esm/src/index.js +0 -2
  87. package/dist/esm/src/index.js.map +1 -1
  88. package/dist/esm/src/middleware/built-in/BuiltInMiddleware.js +4 -4
  89. package/dist/esm/src/middleware/built-in/BuiltInMiddleware.js.map +1 -1
  90. package/dist/esm/src/middleware/built-in/security/HoneypotTarpit.js +508 -0
  91. package/dist/esm/src/middleware/built-in/security/HoneypotTarpit.js.map +1 -0
  92. package/dist/esm/src/middleware/built-in/security/SQLInjectionDetector.js +113 -34
  93. package/dist/esm/src/middleware/built-in/security/SQLInjectionDetector.js.map +1 -1
  94. package/dist/esm/src/middleware/built-in/security/UriNormalizer.js +391 -0
  95. package/dist/esm/src/middleware/built-in/security/UriNormalizer.js.map +1 -0
  96. package/dist/esm/src/middleware/built-in/security/XyPrissCors.js +96 -0
  97. package/dist/esm/src/middleware/built-in/security/XyPrissCors.js.map +1 -0
  98. package/dist/esm/src/middleware/built-in/security/XyPrissHPP.js +50 -0
  99. package/dist/esm/src/middleware/built-in/security/XyPrissHPP.js.map +1 -0
  100. package/dist/esm/src/middleware/safe-json-middleware.js +1 -79
  101. package/dist/esm/src/middleware/safe-json-middleware.js.map +1 -1
  102. package/dist/esm/src/middleware/security-middleware.js +7 -4
  103. package/dist/esm/src/middleware/security-middleware.js.map +1 -1
  104. package/dist/esm/src/plugins/builtin/xems/XemsBuiltinPlugin.js +33 -10
  105. package/dist/esm/src/plugins/builtin/xems/XemsBuiltinPlugin.js.map +1 -1
  106. package/dist/esm/src/plugins/builtin/xems/XemsPlugin.js +34 -3
  107. package/dist/esm/src/plugins/builtin/xems/XemsPlugin.js.map +1 -1
  108. package/dist/esm/src/plugins/const/OFFICIAL_PLUGINS.js +1 -1
  109. package/dist/esm/src/plugins/const/OFFICIAL_PLUGINS.js.map +1 -1
  110. package/dist/esm/src/plugins/const/XyprissTempDir.js +70 -9
  111. package/dist/esm/src/plugins/const/XyprissTempDir.js.map +1 -1
  112. package/dist/esm/src/plugins/core/manager/PluginLoader.js +8 -0
  113. package/dist/esm/src/plugins/core/manager/PluginLoader.js.map +1 -1
  114. package/dist/esm/src/plugins/core/manager/PluginSecurity.js +87 -0
  115. package/dist/esm/src/plugins/core/manager/PluginSecurity.js.map +1 -1
  116. package/dist/esm/src/server/FastServer/index.js +5 -0
  117. package/dist/esm/src/server/FastServer/index.js.map +1 -1
  118. package/dist/esm/src/server/ServerFactory.js +8 -1
  119. package/dist/esm/src/server/ServerFactory.js.map +1 -1
  120. package/dist/esm/src/server/components/fastapi/RedirectManager.js +8 -58
  121. package/dist/esm/src/server/components/fastapi/RedirectManager.js.map +1 -1
  122. package/dist/esm/src/server/components/fastapi/upload/FileUploadManager.js +1 -1
  123. package/dist/esm/src/server/components/fastapi/upload/FileUploadManager.js.map +1 -1
  124. package/dist/esm/src/server/components/multi-server/MultiServerManager.js +9 -1
  125. package/dist/esm/src/server/components/multi-server/MultiServerManager.js.map +1 -1
  126. package/dist/esm/src/server/core/HttpServer.js +22 -0
  127. package/dist/esm/src/server/core/HttpServer.js.map +1 -1
  128. package/dist/esm/src/server/core/XHSCBridge/EngineManager.js +33 -2
  129. package/dist/esm/src/server/core/XHSCBridge/EngineManager.js.map +1 -1
  130. package/dist/esm/src/server/core/XHSCBridge/cmd/buildSecurityArgs.js +1 -1
  131. package/dist/esm/src/server/core/XHSCBridge/cmd/buildSecurityArgs.js.map +1 -1
  132. package/dist/esm/src/server/core/XyServerCreator.js +1 -0
  133. package/dist/esm/src/server/core/XyServerCreator.js.map +1 -1
  134. package/dist/esm/src/shared/logger/quickLogger.js +160 -0
  135. package/dist/esm/src/shared/logger/quickLogger.js.map +1 -0
  136. package/dist/esm/src/xhsc/PathApi.js +44 -3
  137. package/dist/esm/src/xhsc/PathApi.js.map +1 -1
  138. package/dist/esm/src/xhsc/System.js +2 -0
  139. package/dist/esm/src/xhsc/System.js.map +1 -1
  140. package/dist/esm/src/xhsc/UtilsApi.js +50 -0
  141. package/dist/esm/src/xhsc/UtilsApi.js.map +1 -0
  142. package/dist/esm/src/xhsc/XyPrissRunner.js +30 -2
  143. package/dist/esm/src/xhsc/XyPrissRunner.js.map +1 -1
  144. package/dist/esm/src/xhsc/fs/FSCore.js +148 -0
  145. package/dist/esm/src/xhsc/fs/FSCore.js.map +1 -1
  146. package/dist/esm/src/xhsc/fs/FileHandle.js +110 -0
  147. package/dist/esm/src/xhsc/fs/FileHandle.js.map +1 -0
  148. package/dist/esm/src/xhsc/ipc/XHSCDirectIPC.js +113 -0
  149. package/dist/esm/src/xhsc/ipc/XHSCDirectIPC.js.map +1 -0
  150. package/dist/esm/src/xhsc/utils/ArrayUtils.js +65 -0
  151. package/dist/esm/src/xhsc/utils/ArrayUtils.js.map +1 -0
  152. package/dist/esm/src/xhsc/utils/AsyncUtils.js +580 -0
  153. package/dist/esm/src/xhsc/utils/AsyncUtils.js.map +1 -0
  154. package/dist/esm/src/xhsc/utils/DateUtils.js +957 -0
  155. package/dist/esm/src/xhsc/utils/DateUtils.js.map +1 -0
  156. package/dist/esm/src/xhsc/utils/FunctionUtils.js +27 -0
  157. package/dist/esm/src/xhsc/utils/FunctionUtils.js.map +1 -0
  158. package/dist/esm/src/xhsc/utils/IdUtils.js +20 -0
  159. package/dist/esm/src/xhsc/utils/IdUtils.js.map +1 -0
  160. package/dist/esm/src/xhsc/utils/NumberUtils.js +77 -0
  161. package/dist/esm/src/xhsc/utils/NumberUtils.js.map +1 -0
  162. package/dist/esm/src/xhsc/utils/ObjectUtils.js +139 -0
  163. package/dist/esm/src/xhsc/utils/ObjectUtils.js.map +1 -0
  164. package/dist/esm/src/xhsc/utils/StringUtils.js +143 -0
  165. package/dist/esm/src/xhsc/utils/StringUtils.js.map +1 -0
  166. package/dist/esm/src/xhsc/utils/ValidationUtils.js +47 -0
  167. package/dist/esm/src/xhsc/utils/ValidationUtils.js.map +1 -0
  168. package/dist/esm/src/xhsc.js +6 -5
  169. package/dist/esm/src/xhsc.js.map +1 -1
  170. package/dist/index.d.ts +1872 -234
  171. package/package.json +119 -137
@@ -0,0 +1,959 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @file DateUtils.ts
5
+ * @description XyPriss Date & Time Utilities — A comprehensive, zero-dependency
6
+ * date/time utility class built on the native `Date` object and the `Intl` API.
7
+ *
8
+ * @remarks
9
+ * All methods are pure and side-effect-free unless otherwise noted.
10
+ * Timestamps can be provided in either **seconds** (Unix) or **milliseconds**
11
+ * (JavaScript). The class auto-detects the unit using the heuristic:
12
+ * values below `1e11` are treated as seconds; values at or above `1e11` are
13
+ * treated as milliseconds. This heuristic is valid until **November 2286**.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const du = new DateUtils();
18
+ *
19
+ * du.format(Date.now()); // "Apr 15, 2026"
20
+ * du.timeAgo(Date.now() - 90_000); // "2 minutes ago"
21
+ * du.formatDuration(3_661_000); // "1h 1m 1s"
22
+ * du.startOf("month"); // 2026-04-01T00:00:00.000Z
23
+ * ```
24
+ */
25
+ class DateUtils {
26
+ // ─────────────────────────────────────────────
27
+ // Internal helpers
28
+ // ─────────────────────────────────────────────
29
+ /**
30
+ * Converts any supported input value to a `Date` object.
31
+ *
32
+ * Supported inputs:
33
+ * - `Date` — returned as-is (not cloned).
34
+ * - `number` — auto-detected as Unix seconds (`< 1e11`) or milliseconds.
35
+ * - `string` — parsed via `new Date(string)` (ISO 8601 recommended).
36
+ *
37
+ * @param date - The value to convert.
38
+ * @returns The corresponding `Date` object.
39
+ * @throws {RangeError} If the resulting `Date` is invalid.
40
+ *
41
+ * @internal
42
+ */
43
+ toDate(date) {
44
+ let d;
45
+ if (date instanceof Date) {
46
+ d = date;
47
+ }
48
+ else if (typeof date === "number") {
49
+ // Heuristic: Unix timestamps (seconds) are currently ~1.7e9.
50
+ // JavaScript timestamps (ms) are ~1.7e12.
51
+ // Values below 1e11 are assumed to be in seconds.
52
+ const ms = date < 100000000000 ? date * 1000 : date;
53
+ d = new Date(ms);
54
+ }
55
+ else {
56
+ d = new Date(date);
57
+ }
58
+ if (isNaN(d.getTime())) {
59
+ throw new RangeError(`DateUtils: invalid date value — "${date}"`);
60
+ }
61
+ return d;
62
+ }
63
+ /**
64
+ * Converts any supported input value to a millisecond timestamp.
65
+ *
66
+ * @param date - The value to convert.
67
+ * @returns Milliseconds since the Unix epoch.
68
+ *
69
+ * @internal
70
+ */
71
+ toMs(date) {
72
+ return this.toDate(date).getTime();
73
+ }
74
+ // ─────────────────────────────────────────────
75
+ // Current time
76
+ // ─────────────────────────────────────────────
77
+ /**
78
+ * Returns the current Unix timestamp in **seconds**.
79
+ *
80
+ * @returns The current time as a Unix timestamp (integer seconds).
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * du.now(); // e.g. 1776287197
85
+ * ```
86
+ */
87
+ now() {
88
+ return Math.floor(Date.now() / 1000);
89
+ }
90
+ /**
91
+ * Returns the current time as a JavaScript timestamp in **milliseconds**.
92
+ *
93
+ * Equivalent to `Date.now()`.
94
+ *
95
+ * @returns The current time in milliseconds since the Unix epoch.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * du.nowMs(); // e.g. 1776287197000
100
+ * ```
101
+ */
102
+ nowMs() {
103
+ return Date.now();
104
+ }
105
+ /**
106
+ * Returns the current time as a `Date` object.
107
+ *
108
+ * @returns A new `Date` representing the current instant.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * du.today(); // Date { ... }
113
+ * ```
114
+ */
115
+ today() {
116
+ return new Date();
117
+ }
118
+ // ─────────────────────────────────────────────
119
+ // Formatting
120
+ // ─────────────────────────────────────────────
121
+ /**
122
+ * Serializes a date value into a localized string using `Intl.DateTimeFormat`.
123
+ *
124
+ * Automatically handles Unix timestamps (seconds) and JavaScript timestamps
125
+ * (milliseconds) via the `< 1e11` heuristic.
126
+ *
127
+ * @param date - The input to format: `Date`, number (timestamp), or ISO string.
128
+ * @param locale - BCP 47 locale tag (default: `"en-US"`).
129
+ * @param options - `Intl.DateTimeFormatOptions` to customize the output.
130
+ * @returns A localized date string.
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * du.format(1776287197);
135
+ * // → "Apr 15, 2026"
136
+ *
137
+ * du.format(new Date(), "fr-FR", { dateStyle: "full" });
138
+ * // → "mercredi 15 avril 2026"
139
+ *
140
+ * du.format("2026-04-15T12:00:00Z", "en-GB", { timeStyle: "short" });
141
+ * // → "12:00"
142
+ * ```
143
+ */
144
+ format(date, locale = "en-US", options = { dateStyle: "long" }) {
145
+ return new Intl.DateTimeFormat(locale, options).format(this.toDate(date));
146
+ }
147
+ /**
148
+ * Formats a date as an ISO 8601 string (`YYYY-MM-DDTHH:mm:ss.sssZ`).
149
+ *
150
+ * This is the format recommended for data interchange and storage.
151
+ *
152
+ * @param date - The date to format (default: current time).
153
+ * @returns An ISO 8601 UTC string.
154
+ *
155
+ * @example
156
+ * ```ts
157
+ * du.toISO();
158
+ * // → "2026-04-15T10:30:00.000Z"
159
+ *
160
+ * du.toISO(1776287197);
161
+ * // → "2026-04-15T..."
162
+ * ```
163
+ */
164
+ toISO(date = new Date()) {
165
+ return this.toDate(date).toISOString();
166
+ }
167
+ /**
168
+ * Formats a date as a plain date string: `YYYY-MM-DD`.
169
+ *
170
+ * The date is rendered in **local time** unless a UTC flag is set.
171
+ *
172
+ * @param date - The date to format (default: current time).
173
+ * @param utc - If `true`, use UTC date components instead of local time (default: `false`).
174
+ * @returns A date-only string in `YYYY-MM-DD` format.
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * du.toDateString();
179
+ * // → "2026-04-15"
180
+ *
181
+ * du.toDateString(new Date("2026-12-31T23:59:00Z"), true);
182
+ * // → "2026-12-31"
183
+ * ```
184
+ */
185
+ toDateString(date = new Date(), utc = false) {
186
+ const d = this.toDate(date);
187
+ const y = utc ? d.getUTCFullYear() : d.getFullYear();
188
+ const m = String(utc ? d.getUTCMonth() + 1 : d.getMonth() + 1).padStart(2, "0");
189
+ const day = String(utc ? d.getUTCDate() : d.getDate()).padStart(2, "0");
190
+ return `${y}-${m}-${day}`;
191
+ }
192
+ /**
193
+ * Formats a date as a time string: `HH:mm:ss`.
194
+ *
195
+ * The time is rendered in **local time** unless the UTC flag is set.
196
+ *
197
+ * @param date - The date to format (default: current time).
198
+ * @param utc - If `true`, use UTC time components (default: `false`).
199
+ * @returns A time-only string in `HH:mm:ss` format.
200
+ *
201
+ * @example
202
+ * ```ts
203
+ * du.toTimeString(new Date("2026-04-15T08:05:03Z"), true);
204
+ * // → "08:05:03"
205
+ * ```
206
+ */
207
+ toTimeString(date = new Date(), utc = false) {
208
+ const d = this.toDate(date);
209
+ const h = String(utc ? d.getUTCHours() : d.getHours()).padStart(2, "0");
210
+ const min = String(utc ? d.getUTCMinutes() : d.getMinutes()).padStart(2, "0");
211
+ const sec = String(utc ? d.getUTCSeconds() : d.getSeconds()).padStart(2, "0");
212
+ return `${h}:${min}:${sec}`;
213
+ }
214
+ /**
215
+ * Converts a duration into a human-readable string (e.g., `"1d 2h 30m 5s"`).
216
+ *
217
+ * Components are omitted when their value is zero, except for seconds which
218
+ * always appear when the total duration is less than one minute.
219
+ *
220
+ * @param value - The duration to format.
221
+ * @param unit - The unit of `value`: `"ms"` (milliseconds, default) or `"s"` (seconds).
222
+ * @returns A compact, space-separated duration string.
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * du.formatDuration(3_661_000); // → "1h 1m 1s"
227
+ * du.formatDuration(90, "s"); // → "1m 30s"
228
+ * du.formatDuration(0); // → "0s"
229
+ * du.formatDuration(86_400_000); // → "1d"
230
+ * ```
231
+ */
232
+ formatDuration(value, unit = "ms") {
233
+ const ms = unit === "s" ? value * 1000 : value;
234
+ const seconds = Math.floor((ms / 1000) % 60);
235
+ const minutes = Math.floor((ms / (1000 * 60)) % 60);
236
+ const hours = Math.floor((ms / (1000 * 60 * 60)) % 24);
237
+ const days = Math.floor(ms / (1000 * 60 * 60 * 24));
238
+ const parts = [];
239
+ if (days > 0)
240
+ parts.push(`${days}d`);
241
+ if (hours > 0)
242
+ parts.push(`${hours}h`);
243
+ if (minutes > 0)
244
+ parts.push(`${minutes}m`);
245
+ if (seconds > 0 || parts.length === 0)
246
+ parts.push(`${seconds}s`);
247
+ return parts.join(" ");
248
+ }
249
+ /**
250
+ * Returns a relative time string (e.g., `"5 minutes ago"`, `"in 2 hours"`).
251
+ *
252
+ * Uses `Intl.RelativeTimeFormat` for locale-aware output. Automatically
253
+ * selects the most appropriate unit (second → minute → hour → day → month → year).
254
+ *
255
+ * @param date - The reference date, in the past or future.
256
+ * @param locale - BCP 47 locale tag (default: `"en-US"`).
257
+ * @returns A localized relative time string.
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * du.timeAgo(Date.now() - 90_000); // → "2 minutes ago"
262
+ * du.timeAgo(Date.now() + 3_600_000); // → "in 1 hour"
263
+ * du.timeAgo(Date.now() - 90_000, "fr"); // → "il y a 2 minutes"
264
+ * ```
265
+ */
266
+ timeAgo(date, locale = "en-US") {
267
+ const ms = this.toMs(date);
268
+ const deltaSeconds = Math.floor((Date.now() - ms) / 1000);
269
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
270
+ if (Math.abs(deltaSeconds) < 60)
271
+ return rtf.format(-deltaSeconds, "second");
272
+ const minutes = Math.floor(deltaSeconds / 60);
273
+ if (Math.abs(minutes) < 60)
274
+ return rtf.format(-minutes, "minute");
275
+ const hours = Math.floor(minutes / 60);
276
+ if (Math.abs(hours) < 24)
277
+ return rtf.format(-hours, "hour");
278
+ const days = Math.floor(hours / 24);
279
+ if (Math.abs(days) < 30)
280
+ return rtf.format(-days, "day");
281
+ const months = Math.floor(days / 30);
282
+ if (Math.abs(months) < 12)
283
+ return rtf.format(-months, "month");
284
+ const years = Math.floor(months / 12);
285
+ return rtf.format(-years, "year");
286
+ }
287
+ // ─────────────────────────────────────────────
288
+ // Arithmetic
289
+ // ─────────────────────────────────────────────
290
+ /**
291
+ * Adds a given amount of time to a date and returns a new `Date`.
292
+ *
293
+ * Supported units: `"ms"`, `"s"` (seconds), `"m"` (minutes), `"h"` (hours),
294
+ * `"d"` (days), `"w"` (weeks), `"mo"` (months), `"y"` (years).
295
+ *
296
+ * Month and year arithmetic is handled using `setMonth` / `setFullYear`,
297
+ * which correctly accounts for varying month lengths (e.g., adding 1 month
298
+ * to January 31 yields February 28/29).
299
+ *
300
+ * @param date - The starting date.
301
+ * @param value - The amount to add (can be negative to subtract).
302
+ * @param unit - The time unit.
303
+ * @returns A new `Date` offset by the specified amount.
304
+ *
305
+ * @example
306
+ * ```ts
307
+ * du.add(new Date("2026-01-31"), 1, "mo");
308
+ * // → Date("2026-02-28") — accounts for shorter February
309
+ *
310
+ * du.add(new Date("2026-04-15"), -7, "d");
311
+ * // → Date("2026-04-08")
312
+ *
313
+ * du.add(Date.now(), 2, "h");
314
+ * // → Date 2 hours from now
315
+ * ```
316
+ */
317
+ add(date, value, unit) {
318
+ const d = new Date(this.toMs(date));
319
+ switch (unit) {
320
+ case "ms":
321
+ d.setMilliseconds(d.getMilliseconds() + value);
322
+ break;
323
+ case "s":
324
+ d.setSeconds(d.getSeconds() + value);
325
+ break;
326
+ case "m":
327
+ d.setMinutes(d.getMinutes() + value);
328
+ break;
329
+ case "h":
330
+ d.setHours(d.getHours() + value);
331
+ break;
332
+ case "d":
333
+ d.setDate(d.getDate() + value);
334
+ break;
335
+ case "w":
336
+ d.setDate(d.getDate() + value * 7);
337
+ break;
338
+ case "mo": {
339
+ const day = d.getDate();
340
+ d.setMonth(d.getMonth() + value);
341
+ if (d.getDate() !== day) {
342
+ d.setDate(0);
343
+ }
344
+ break;
345
+ }
346
+ case "y": {
347
+ d.getDate();
348
+ const month = d.getMonth();
349
+ d.setFullYear(d.getFullYear() + value);
350
+ if (d.getMonth() !== month) {
351
+ d.setDate(0);
352
+ }
353
+ break;
354
+ }
355
+ }
356
+ return d;
357
+ }
358
+ /**
359
+ * Subtracts a given amount of time from a date and returns a new `Date`.
360
+ *
361
+ * This is a convenience wrapper around {@link add} with a negated `value`.
362
+ *
363
+ * @param date - The starting date.
364
+ * @param value - The amount to subtract (must be positive).
365
+ * @param unit - The time unit (same options as {@link add}).
366
+ * @returns A new `Date` shifted back by the specified amount.
367
+ *
368
+ * @example
369
+ * ```ts
370
+ * du.subtract(new Date("2026-03-01"), 1, "mo");
371
+ * // → Date("2026-02-01")
372
+ * ```
373
+ */
374
+ subtract(date, value, unit) {
375
+ return this.add(date, -value, unit);
376
+ }
377
+ // ─────────────────────────────────────────────
378
+ // Difference & comparison
379
+ // ─────────────────────────────────────────────
380
+ /**
381
+ * Returns the difference between two dates in the specified unit.
382
+ *
383
+ * The result is the **signed** difference `(dateA - dateB)` truncated
384
+ * toward zero. A positive value means `dateA` is later than `dateB`.
385
+ *
386
+ * Supported units: `"ms"`, `"s"`, `"m"`, `"h"`, `"d"`, `"w"`.
387
+ * Month and year differences are intentionally excluded because their
388
+ * variable length makes a lossless inverse impossible; use {@link add}
389
+ * and calendar-aware logic for those cases.
390
+ *
391
+ * @param dateA - The first date.
392
+ * @param dateB - The second date.
393
+ * @param unit - The unit for the result (default: `"ms"`).
394
+ * @returns The signed integer difference in the requested unit.
395
+ *
396
+ * @example
397
+ * ```ts
398
+ * du.diff("2026-04-20", "2026-04-15", "d"); // → 5
399
+ * du.diff("2026-04-10", "2026-04-15", "d"); // → -5
400
+ * du.diff(Date.now(), Date.now() - 3600_000, "h"); // → 1
401
+ * ```
402
+ */
403
+ diff(dateA, dateB, unit = "ms") {
404
+ const delta = this.toMs(dateA) - this.toMs(dateB);
405
+ const divisors = {
406
+ ms: 1,
407
+ s: 1000,
408
+ m: 60000,
409
+ h: 3600000,
410
+ d: 86400000,
411
+ w: 604800000,
412
+ };
413
+ return Math.trunc(delta / divisors[unit]);
414
+ }
415
+ /**
416
+ * Returns `true` if `dateA` is strictly before `dateB`.
417
+ *
418
+ * @param dateA - The first date.
419
+ * @param dateB - The second date.
420
+ * @returns `true` if `dateA < dateB`.
421
+ *
422
+ * @example
423
+ * ```ts
424
+ * du.isBefore("2026-01-01", "2026-06-01"); // → true
425
+ * ```
426
+ */
427
+ isBefore(dateA, dateB) {
428
+ return this.toMs(dateA) < this.toMs(dateB);
429
+ }
430
+ /**
431
+ * Returns `true` if `dateA` is strictly after `dateB`.
432
+ *
433
+ * @param dateA - The first date.
434
+ * @param dateB - The second date.
435
+ * @returns `true` if `dateA > dateB`.
436
+ *
437
+ * @example
438
+ * ```ts
439
+ * du.isAfter("2026-12-31", "2026-06-01"); // → true
440
+ * ```
441
+ */
442
+ isAfter(dateA, dateB) {
443
+ return this.toMs(dateA) > this.toMs(dateB);
444
+ }
445
+ /**
446
+ * Returns `true` if two dates represent the same instant in time.
447
+ *
448
+ * Comparison is performed at millisecond precision.
449
+ *
450
+ * @param dateA - The first date.
451
+ * @param dateB - The second date.
452
+ * @returns `true` if both dates resolve to the same millisecond.
453
+ *
454
+ * @example
455
+ * ```ts
456
+ * du.isSame(new Date("2026-04-15"), 1776268800000); // → true (if same instant)
457
+ * ```
458
+ */
459
+ isSame(dateA, dateB) {
460
+ return this.toMs(dateA) === this.toMs(dateB);
461
+ }
462
+ /**
463
+ * Returns `true` if `date` falls within the range `[start, end]` (inclusive).
464
+ *
465
+ * @param date - The date to test.
466
+ * @param start - The start of the range.
467
+ * @param end - The end of the range.
468
+ * @returns `true` if `start ≤ date ≤ end`.
469
+ *
470
+ * @example
471
+ * ```ts
472
+ * du.isBetween("2026-04-15", "2026-01-01", "2026-12-31"); // → true
473
+ * du.isBetween("2025-12-31", "2026-01-01", "2026-12-31"); // → false
474
+ * ```
475
+ */
476
+ isBetween(date, start, end) {
477
+ const ms = this.toMs(date);
478
+ return ms >= this.toMs(start) && ms <= this.toMs(end);
479
+ }
480
+ // ─────────────────────────────────────────────
481
+ // Boundary helpers
482
+ // ─────────────────────────────────────────────
483
+ /**
484
+ * Returns a new `Date` set to the **start** of the specified unit
485
+ * (i.e., all smaller components zeroed out).
486
+ *
487
+ * Supported units: `"day"`, `"week"` (Monday = start), `"month"`, `"year"`.
488
+ *
489
+ * @param unit - The boundary unit.
490
+ * @param date - The reference date (default: current time).
491
+ * @returns A new `Date` at the beginning of the unit.
492
+ *
493
+ * @example
494
+ * ```ts
495
+ * du.startOf("day");
496
+ * // → 2026-04-15T00:00:00.000 (local)
497
+ *
498
+ * du.startOf("month", new Date("2026-04-15"));
499
+ * // → 2026-04-01T00:00:00.000
500
+ *
501
+ * du.startOf("year", "2026-06-15");
502
+ * // → 2026-01-01T00:00:00.000
503
+ * ```
504
+ */
505
+ startOf(unit, date = new Date()) {
506
+ const d = new Date(this.toMs(date));
507
+ switch (unit) {
508
+ case "day":
509
+ d.setHours(0, 0, 0, 0);
510
+ break;
511
+ case "week": {
512
+ const day = d.getDay(); // 0 = Sunday
513
+ const diff = day === 0 ? -6 : 1 - day; // shift to Monday
514
+ d.setDate(d.getDate() + diff);
515
+ d.setHours(0, 0, 0, 0);
516
+ break;
517
+ }
518
+ case "month":
519
+ d.setDate(1);
520
+ d.setHours(0, 0, 0, 0);
521
+ break;
522
+ case "year":
523
+ d.setMonth(0, 1);
524
+ d.setHours(0, 0, 0, 0);
525
+ break;
526
+ }
527
+ return d;
528
+ }
529
+ /**
530
+ * Returns a new `Date` set to the **end** of the specified unit
531
+ * (i.e., the last millisecond of that unit).
532
+ *
533
+ * Supported units: `"day"`, `"week"` (Sunday = end), `"month"`, `"year"`.
534
+ *
535
+ * @param unit - The boundary unit.
536
+ * @param date - The reference date (default: current time).
537
+ * @returns A new `Date` at the very last millisecond of the unit.
538
+ *
539
+ * @example
540
+ * ```ts
541
+ * du.endOf("month", new Date("2026-02-01"));
542
+ * // → 2026-02-28T23:59:59.999
543
+ *
544
+ * du.endOf("year", "2026-01-01");
545
+ * // → 2026-12-31T23:59:59.999
546
+ * ```
547
+ */
548
+ endOf(unit, date = new Date()) {
549
+ const d = new Date(this.toMs(date));
550
+ switch (unit) {
551
+ case "day":
552
+ d.setHours(23, 59, 59, 999);
553
+ break;
554
+ case "week": {
555
+ const day = d.getDay();
556
+ const diff = day === 0 ? 0 : 7 - day; // shift to next Sunday
557
+ d.setDate(d.getDate() + diff);
558
+ d.setHours(23, 59, 59, 999);
559
+ break;
560
+ }
561
+ case "month":
562
+ d.setMonth(d.getMonth() + 1, 0); // day 0 of next month = last day of current
563
+ d.setHours(23, 59, 59, 999);
564
+ break;
565
+ case "year":
566
+ d.setMonth(11, 31);
567
+ d.setHours(23, 59, 59, 999);
568
+ break;
569
+ }
570
+ return d;
571
+ }
572
+ // ─────────────────────────────────────────────
573
+ // Calendar queries
574
+ // ─────────────────────────────────────────────
575
+ /**
576
+ * Returns `true` if the given year is a leap year.
577
+ *
578
+ * A year is a leap year if it is divisible by 4, except for century years,
579
+ * which must also be divisible by 400.
580
+ *
581
+ * @param year - The four-digit year to test (default: current year).
582
+ * @returns `true` if the year is a leap year.
583
+ *
584
+ * @example
585
+ * ```ts
586
+ * du.isLeapYear(2024); // → true
587
+ * du.isLeapYear(2100); // → false
588
+ * du.isLeapYear(2000); // → true
589
+ * ```
590
+ */
591
+ isLeapYear(year = new Date().getFullYear()) {
592
+ return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
593
+ }
594
+ /**
595
+ * Returns the number of days in a given month of a given year.
596
+ *
597
+ * Correctly accounts for leap years when querying February.
598
+ *
599
+ * @param month - The 1-based month index (1 = January, 12 = December).
600
+ * @param year - The four-digit year (default: current year).
601
+ * @returns The number of days in the specified month (28–31).
602
+ *
603
+ * @example
604
+ * ```ts
605
+ * du.daysInMonth(2, 2024); // → 29 (leap year)
606
+ * du.daysInMonth(2, 2023); // → 28
607
+ * du.daysInMonth(1, 2026); // → 31
608
+ * ```
609
+ */
610
+ daysInMonth(month, year = new Date().getFullYear()) {
611
+ // Day 0 of the next month equals the last day of the given month.
612
+ return new Date(year, month, 0).getDate();
613
+ }
614
+ /**
615
+ * Returns the ISO 8601 week number (1–53) for a given date.
616
+ *
617
+ * ISO 8601 defines Week 1 as the week containing the year's first Thursday.
618
+ * Weeks run Monday–Sunday. A date in early January may belong to the
619
+ * final week of the previous year (e.g., January 1, 2016 → Week 53 of 2015).
620
+ *
621
+ * @param date - The reference date (default: current time).
622
+ * @returns The ISO week number.
623
+ *
624
+ * @example
625
+ * ```ts
626
+ * du.weekNumber(new Date("2026-01-01")); // → 1
627
+ * du.weekNumber(new Date("2016-01-03")); // → 53 (belongs to 2015)
628
+ * ```
629
+ */
630
+ weekNumber(date = new Date()) {
631
+ const d = new Date(this.toMs(date));
632
+ // Set to nearest Thursday: current date + 4 - current day number (Mon=1 .. Sun=7)
633
+ d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay() || 7));
634
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
635
+ return Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
636
+ }
637
+ /**
638
+ * Returns the day of the year (1–366) for a given date.
639
+ *
640
+ * @param date - The reference date (default: current time).
641
+ * @returns An integer from 1 to 366.
642
+ *
643
+ * @example
644
+ * ```ts
645
+ * du.dayOfYear(new Date("2026-02-01")); // → 32
646
+ * du.dayOfYear(new Date("2026-12-31")); // → 365
647
+ * ```
648
+ */
649
+ dayOfYear(date = new Date()) {
650
+ const d = this.toDate(date);
651
+ const start = new Date(d.getFullYear(), 0, 0);
652
+ const diff = d.getTime() - start.getTime();
653
+ return Math.floor(diff / 86400000);
654
+ }
655
+ /**
656
+ * Returns the ISO 8601 quarter (1–4) in which the given date falls.
657
+ *
658
+ * @param date - The reference date (default: current time).
659
+ * @returns A quarter number: `1`, `2`, `3`, or `4`.
660
+ *
661
+ * @example
662
+ * ```ts
663
+ * du.quarter(new Date("2026-04-15")); // → 2 (Apr–Jun)
664
+ * du.quarter(new Date("2026-11-01")); // → 4 (Oct–Dec)
665
+ * ```
666
+ */
667
+ quarter(date = new Date()) {
668
+ return (Math.floor(this.toDate(date).getMonth() / 3) + 1);
669
+ }
670
+ /**
671
+ * Returns `true` if the given date falls on a weekend (Saturday or Sunday).
672
+ *
673
+ * The check uses **local time**.
674
+ *
675
+ * @param date - The date to check (default: current time).
676
+ * @returns `true` if the date is a Saturday or Sunday.
677
+ *
678
+ * @example
679
+ * ```ts
680
+ * du.isWeekend(new Date("2026-04-18")); // → true (Saturday)
681
+ * du.isWeekend(new Date("2026-04-15")); // → false (Wednesday)
682
+ * ```
683
+ */
684
+ isWeekend(date = new Date()) {
685
+ const day = this.toDate(date).getDay();
686
+ return day === 0 || day === 6;
687
+ }
688
+ /**
689
+ * Returns `true` if the given date falls on a weekday (Monday–Friday).
690
+ *
691
+ * @param date - The date to check (default: current time).
692
+ * @returns `true` if the date is Monday through Friday.
693
+ *
694
+ * @example
695
+ * ```ts
696
+ * du.isWeekday(new Date("2026-04-15")); // → true
697
+ * du.isWeekday(new Date("2026-04-19")); // → false (Sunday)
698
+ * ```
699
+ */
700
+ isWeekday(date = new Date()) {
701
+ return !this.isWeekend(date);
702
+ }
703
+ /**
704
+ * Returns `true` if two dates fall on the same calendar day (in local time).
705
+ *
706
+ * Only the year, month, and date components are compared; time is ignored.
707
+ *
708
+ * @param dateA - The first date.
709
+ * @param dateB - The second date.
710
+ * @returns `true` if both dates share the same year, month, and day.
711
+ *
712
+ * @example
713
+ * ```ts
714
+ * du.isSameDay("2026-04-15T08:00:00", "2026-04-15T22:00:00"); // → true
715
+ * du.isSameDay("2026-04-15", "2026-04-16"); // → false
716
+ * ```
717
+ */
718
+ isSameDay(dateA, dateB) {
719
+ const a = this.toDate(dateA);
720
+ const b = this.toDate(dateB);
721
+ return (a.getFullYear() === b.getFullYear() &&
722
+ a.getMonth() === b.getMonth() &&
723
+ a.getDate() === b.getDate());
724
+ }
725
+ /**
726
+ * Returns `true` if the given date is today (in local time).
727
+ *
728
+ * @param date - The date to test.
729
+ * @returns `true` if `date` falls on the current calendar day.
730
+ *
731
+ * @example
732
+ * ```ts
733
+ * du.isToday(new Date()); // → true
734
+ * du.isToday("2020-01-01"); // → false
735
+ * ```
736
+ */
737
+ isToday(date) {
738
+ return this.isSameDay(date, new Date());
739
+ }
740
+ /**
741
+ * Returns `true` if the given date is in the past (before the current instant).
742
+ *
743
+ * @param date - The date to check.
744
+ * @returns `true` if `date` is strictly before `Date.now()`.
745
+ *
746
+ * @example
747
+ * ```ts
748
+ * du.isPast("2020-01-01"); // → true
749
+ * du.isPast(Date.now() + 1000); // → false
750
+ * ```
751
+ */
752
+ isPast(date) {
753
+ return this.toMs(date) < Date.now();
754
+ }
755
+ /**
756
+ * Returns `true` if the given date is in the future (after the current instant).
757
+ *
758
+ * @param date - The date to check.
759
+ * @returns `true` if `date` is strictly after `Date.now()`.
760
+ *
761
+ * @example
762
+ * ```ts
763
+ * du.isFuture(Date.now() + 5000); // → true
764
+ * du.isFuture("2020-01-01"); // → false
765
+ * ```
766
+ */
767
+ isFuture(date) {
768
+ return this.toMs(date) > Date.now();
769
+ }
770
+ // ─────────────────────────────────────────────
771
+ // Clamp & range utilities
772
+ // ─────────────────────────────────────────────
773
+ /**
774
+ * Clamps a date to a `[min, max]` range.
775
+ *
776
+ * If `date` is before `min`, `min` is returned.
777
+ * If `date` is after `max`, `max` is returned.
778
+ * Otherwise, `date` is returned unchanged (as a new `Date`).
779
+ *
780
+ * @param date - The date to clamp.
781
+ * @param min - The lower bound.
782
+ * @param max - The upper bound.
783
+ * @returns A new `Date` clamped within `[min, max]`.
784
+ *
785
+ * @example
786
+ * ```ts
787
+ * du.clamp("2025-01-01", "2026-01-01", "2026-12-31");
788
+ * // → Date("2026-01-01") — clamped to min
789
+ *
790
+ * du.clamp("2026-06-15", "2026-01-01", "2026-12-31");
791
+ * // → Date("2026-06-15") — within range, unchanged
792
+ * ```
793
+ */
794
+ clamp(date, min, max) {
795
+ const ms = Math.min(Math.max(this.toMs(date), this.toMs(min)), this.toMs(max));
796
+ return new Date(ms);
797
+ }
798
+ /**
799
+ * Generates an array of `Date` objects representing each day in the range
800
+ * `[start, end]`, inclusive.
801
+ *
802
+ * The range is capped at **3 650 days** (~10 years) to prevent accidental
803
+ * allocation of enormous arrays.
804
+ *
805
+ * @param start - The first day of the range.
806
+ * @param end - The last day of the range.
807
+ * @returns An ordered array of `Date` objects, one per day.
808
+ * @throws {RangeError} If the range exceeds 3 650 days.
809
+ *
810
+ * @example
811
+ * ```ts
812
+ * du.dateRange("2026-04-13", "2026-04-15");
813
+ * // → [Date("2026-04-13"), Date("2026-04-14"), Date("2026-04-15")]
814
+ * ```
815
+ */
816
+ dateRange(start, end) {
817
+ const startMs = this.toMs(this.startOf("day", start));
818
+ const endMs = this.toMs(this.startOf("day", end));
819
+ const days = Math.round((endMs - startMs) / 86400000);
820
+ if (days < 0)
821
+ return [];
822
+ if (days > 3650) {
823
+ throw new RangeError(`DateUtils.dateRange: range of ${days} days exceeds the 3,650-day limit.`);
824
+ }
825
+ return Array.from({ length: days + 1 }, (_, i) => new Date(startMs + i * 86400000));
826
+ }
827
+ // ─────────────────────────────────────────────
828
+ // Validation & parsing
829
+ // ─────────────────────────────────────────────
830
+ /**
831
+ * Returns `true` if the given value can be successfully interpreted
832
+ * as a valid, finite date.
833
+ *
834
+ * Accepts `Date`, `number`, and `string` inputs. A `Date` whose
835
+ * `getTime()` returns `NaN` is considered invalid.
836
+ *
837
+ * @param value - The value to validate.
838
+ * @returns `true` if the value represents a valid date.
839
+ *
840
+ * @example
841
+ * ```ts
842
+ * du.isValid(new Date()); // → true
843
+ * du.isValid("2026-04-15"); // → true
844
+ * du.isValid("not-a-date"); // → false
845
+ * du.isValid(new Date("invalid")); // → false
846
+ * du.isValid(NaN); // → false
847
+ * ```
848
+ */
849
+ isValid(value) {
850
+ try {
851
+ if (typeof value !== "string" &&
852
+ typeof value !== "number" &&
853
+ !(value instanceof Date)) {
854
+ return false;
855
+ }
856
+ const d = this.toDate(value);
857
+ return !isNaN(d.getTime());
858
+ }
859
+ catch {
860
+ return false;
861
+ }
862
+ }
863
+ /**
864
+ * Parses a date string using an ordered list of format patterns and returns
865
+ * the first successfully parsed `Date`, or `null` if none match.
866
+ *
867
+ * Supported format tokens:
868
+ * - `YYYY` — 4-digit year
869
+ * - `MM` — 2-digit month (01–12)
870
+ * - `DD` — 2-digit day (01–31)
871
+ * - `HH` — 2-digit hours (00–23)
872
+ * - `mm` — 2-digit minutes (00–59)
873
+ * - `ss` — 2-digit seconds (00–59)
874
+ *
875
+ * @param value - The date string to parse.
876
+ * @param formats - An ordered list of format strings to attempt.
877
+ * @returns The first successfully parsed `Date`, or `null`.
878
+ *
879
+ * @example
880
+ * ```ts
881
+ * du.parse("15/04/2026", ["DD/MM/YYYY"]);
882
+ * // → Date("2026-04-15")
883
+ *
884
+ * du.parse("2026-04-15 08:30:00", ["YYYY-MM-DD HH:mm:ss"]);
885
+ * // → Date("2026-04-15T08:30:00")
886
+ *
887
+ * du.parse("bad-input", ["YYYY-MM-DD"]);
888
+ * // → null
889
+ * ```
890
+ */
891
+ parse(value, formats) {
892
+ for (const fmt of formats) {
893
+ let regexStr = fmt
894
+ .replace("YYYY", "(?<YYYY>\\d{4})")
895
+ .replace("MM", "(?<MM>\\d{2})")
896
+ .replace("DD", "(?<DD>\\d{2})")
897
+ .replace("HH", "(?<HH>\\d{2})")
898
+ .replace("mm", "(?<mm>\\d{2})")
899
+ .replace("ss", "(?<ss>\\d{2})");
900
+ const match = new RegExp(`^${regexStr}$`).exec(value);
901
+ if (!match?.groups)
902
+ continue;
903
+ const g = match.groups;
904
+ const year = parseInt(g.YYYY ?? "1970", 10);
905
+ const month = parseInt(g.MM ?? "1", 10) - 1;
906
+ const day = parseInt(g.DD ?? "1", 10);
907
+ const hour = parseInt(g.HH ?? "0", 10);
908
+ const minute = parseInt(g.mm ?? "0", 10);
909
+ const second = parseInt(g.ss ?? "0", 10);
910
+ const d = new Date(year, month, day, hour, minute, second);
911
+ if (!isNaN(d.getTime()))
912
+ return d;
913
+ }
914
+ return null;
915
+ }
916
+ // ─────────────────────────────────────────────
917
+ // Timezone utilities
918
+ // ─────────────────────────────────────────────
919
+ /**
920
+ * Returns the UTC offset of the local environment in minutes.
921
+ *
922
+ * Positive values indicate zones **behind** UTC (e.g., UTC-5 → `300`),
923
+ * mirroring the behaviour of `Date.prototype.getTimezoneOffset()`.
924
+ *
925
+ * @returns The UTC offset in minutes.
926
+ *
927
+ * @example
928
+ * ```ts
929
+ * du.timezoneOffset(); // → -120 for UTC+2, 300 for UTC-5
930
+ * ```
931
+ */
932
+ timezoneOffset() {
933
+ return new Date().getTimezoneOffset();
934
+ }
935
+ /**
936
+ * Formats a date in a specific IANA timezone using `Intl.DateTimeFormat`.
937
+ *
938
+ * Requires the runtime to support the `timeZone` option (all modern
939
+ * environments do).
940
+ *
941
+ * @param date - The date to format.
942
+ * @param timeZone - A valid IANA timezone identifier (e.g., `"America/New_York"`).
943
+ * @param locale - BCP 47 locale tag (default: `"en-US"`).
944
+ * @param options - Additional `Intl.DateTimeFormatOptions`.
945
+ * @returns A localized date string in the given timezone.
946
+ *
947
+ * @example
948
+ * ```ts
949
+ * du.formatInTimezone(Date.now(), "Asia/Tokyo", "ja-JP", { dateStyle: "full", timeStyle: "short" });
950
+ * // → "2026年4月15日水曜日 19:00"
951
+ * ```
952
+ */
953
+ formatInTimezone(date, timeZone, locale = "en-US", options) {
954
+ return new Intl.DateTimeFormat(locale, { ...options, timeZone }).format(this.toDate(date));
955
+ }
956
+ }
957
+
958
+ exports.DateUtils = DateUtils;
959
+ //# sourceMappingURL=DateUtils.js.map