@ix-xs/node-comfort 1.1.31 → 1.2.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/LICENSE +21 -0
- package/README.md +623 -748
- package/index.js +39 -13
- package/package.json +74 -21
- package/src/Arr.js +433 -0
- package/src/Checker.js +547 -188
- package/src/FS.js +127 -1
- package/src/Func.js +375 -0
- package/src/Id.js +235 -0
- package/src/Logger.js +71 -0
- package/src/Num.js +366 -0
- package/src/Obj.js +421 -0
- package/src/SQLite.js +20 -9
- package/src/Str.js +452 -0
- package/src/Time.js +485 -0
- package/types/index.d.ts +406 -0
- package/types/index.d.ts.map +1 -1
- package/types/src/Arr.d.ts +252 -0
- package/types/src/Arr.d.ts.map +1 -0
- package/types/src/Checker.d.ts +292 -30
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/FS.d.ts +60 -0
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +210 -0
- package/types/src/Func.d.ts.map +1 -0
- package/types/src/Id.d.ts +119 -0
- package/types/src/Id.d.ts.map +1 -0
- package/types/src/Logger.d.ts +33 -0
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +242 -0
- package/types/src/Num.d.ts.map +1 -0
- package/types/src/Obj.d.ts +189 -0
- package/types/src/Obj.d.ts.map +1 -0
- package/types/src/SQLite.d.ts +6 -1
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Str.d.ts +293 -0
- package/types/src/Str.d.ts.map +1 -0
- package/types/src/Time.d.ts +205 -0
- package/types/src/Time.d.ts.map +1 -0
- package/tsconfig.json +0 -18
package/src/Time.js
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date & time utilities.
|
|
3
|
+
*
|
|
4
|
+
* Parsing durations, human-friendly relative time ("3 hours ago"), duration
|
|
5
|
+
* formatting, add/diff arithmetic and a tiny date formatter • with no
|
|
6
|
+
* dependency on heavyweight date libraries.
|
|
7
|
+
*
|
|
8
|
+
* ### Internationalization & time zones
|
|
9
|
+
*
|
|
10
|
+
* `relative`, `formatDuration` and `format` are locale-aware and use the
|
|
11
|
+
* platform's built-in `Intl` (full ICU ships with Node), so **every language
|
|
12
|
+
* is supported out of the box** • no locale files, no dependencies.
|
|
13
|
+
*
|
|
14
|
+
* The default locale is `"en"` (for deterministic output). Change it globally
|
|
15
|
+
* with {@link module:Time.setLocale}, or per call via an `options.locale`. A
|
|
16
|
+
* time zone can be set globally with {@link module:Time.setTimezone} or passed
|
|
17
|
+
* per call via `options.timeZone` (IANA name, e.g. `"Europe/Paris"`).
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* nodeComfort.time.setLocale("fr").setTimezone("Europe/Paris");
|
|
21
|
+
* nodeComfort.time.relative(Date.now() - 3600e3); // "il y a 1 heure"
|
|
22
|
+
* nodeComfort.time.format(Date.now(), "dddd D MMMM YYYY"); // "lundi 15 janvier 2024"
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The time module itself • used as the return type of the chainable setters so
|
|
27
|
+
* `setLocale().setTimezone()...` stays fully typed without inlining the type.
|
|
28
|
+
* @typedef {typeof import("./Time")} TimeModule
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const MS = {
|
|
32
|
+
ms: 1,
|
|
33
|
+
s: 1000,
|
|
34
|
+
m: 60_000,
|
|
35
|
+
h: 3_600_000,
|
|
36
|
+
d: 86_400_000,
|
|
37
|
+
w: 604_800_000,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const UNIT_ALIASES = {
|
|
41
|
+
ms: "ms", milli: "ms", millisecond: "ms", milliseconds: "ms",
|
|
42
|
+
s: "s", sec: "s", secs: "s", second: "s", seconds: "s",
|
|
43
|
+
m: "m", min: "m", mins: "m", minute: "m", minutes: "m",
|
|
44
|
+
h: "h", hr: "h", hrs: "h", hour: "h", hours: "h",
|
|
45
|
+
d: "d", day: "d", days: "d",
|
|
46
|
+
w: "w", week: "w", weeks: "w",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Global defaults for locale-aware formatting. `locale` defaults to `"en"` so
|
|
51
|
+
* output is deterministic regardless of the host machine; `timeZone`
|
|
52
|
+
* `undefined` means the system local zone.
|
|
53
|
+
* @private
|
|
54
|
+
* @type {{ locale: string, timeZone: string|undefined }}
|
|
55
|
+
*/
|
|
56
|
+
const _config = { locale: "en", timeZone: undefined };
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Coerces a value into a Date.
|
|
60
|
+
* @private
|
|
61
|
+
* @param {Date|number|string} [value]
|
|
62
|
+
* @returns {Date}
|
|
63
|
+
*/
|
|
64
|
+
const _toDate = (value) => (value instanceof Date ? new Date(value.getTime()) : value === undefined ? new Date() : new Date(value));
|
|
65
|
+
|
|
66
|
+
const _pad = (n, len = 2) => String(Math.abs(n)).padStart(len, "0");
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extracts numeric calendar parts of a date, optionally in a target time zone.
|
|
70
|
+
* When no zone is given, the host's local time is used (fast path).
|
|
71
|
+
* @private
|
|
72
|
+
* @param {Date} d - Date to read.
|
|
73
|
+
* @param {string} [timeZone] - IANA time-zone name.
|
|
74
|
+
* @returns {{ year: number, month: number, day: number, hour: number, minute: number, second: number, ms: number }}
|
|
75
|
+
*/
|
|
76
|
+
const _numericParts = (d, timeZone) => {
|
|
77
|
+
if (!timeZone) {
|
|
78
|
+
return {
|
|
79
|
+
year: d.getFullYear(),
|
|
80
|
+
month: d.getMonth() + 1,
|
|
81
|
+
day: d.getDate(),
|
|
82
|
+
hour: d.getHours(),
|
|
83
|
+
minute: d.getMinutes(),
|
|
84
|
+
second: d.getSeconds(),
|
|
85
|
+
ms: d.getMilliseconds(),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const dtf = new Intl.DateTimeFormat("en-US", {
|
|
89
|
+
timeZone,
|
|
90
|
+
year: "numeric",
|
|
91
|
+
month: "2-digit",
|
|
92
|
+
day: "2-digit",
|
|
93
|
+
hour: "2-digit",
|
|
94
|
+
minute: "2-digit",
|
|
95
|
+
second: "2-digit",
|
|
96
|
+
hourCycle: "h23",
|
|
97
|
+
});
|
|
98
|
+
const parts = {};
|
|
99
|
+
for (const p of dtf.formatToParts(d)) parts[p.type] = p.value;
|
|
100
|
+
return {
|
|
101
|
+
year: +parts.year,
|
|
102
|
+
month: +parts.month,
|
|
103
|
+
day: +parts.day,
|
|
104
|
+
hour: +parts.hour,
|
|
105
|
+
minute: +parts.minute,
|
|
106
|
+
second: +parts.second,
|
|
107
|
+
ms: d.getMilliseconds(),
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Formats a single localized component (month/weekday name) via `Intl`.
|
|
113
|
+
* @private
|
|
114
|
+
* @param {Date} d - Date to format.
|
|
115
|
+
* @param {Intl.DateTimeFormatOptions} opt - Component options.
|
|
116
|
+
* @param {string} locale - BCP-47 locale.
|
|
117
|
+
* @param {string} [timeZone] - IANA time-zone name.
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
const _named = (d, opt, locale, timeZone) =>
|
|
121
|
+
new Intl.DateTimeFormat(locale, { ...opt, timeZone }).format(d);
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
/**
|
|
125
|
+
* Sets the global default locale used by `relative`, `formatDuration` and
|
|
126
|
+
* `format` (named tokens). Chainable.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* nodeComfort.time.setLocale("fr");
|
|
130
|
+
*
|
|
131
|
+
* @param {string} locale - A BCP-47 locale tag (e.g. `"fr"`, `"en-GB"`, `"ja"`).
|
|
132
|
+
* @returns {TimeModule} The time module (for chaining).
|
|
133
|
+
*/
|
|
134
|
+
setLocale(locale) {
|
|
135
|
+
_config.locale = locale;
|
|
136
|
+
return this;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Sets the global default time zone used by `format` and `isSameDay`.
|
|
141
|
+
* Chainable. Pass `undefined` to fall back to the system local zone.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* nodeComfort.time.setTimezone("Europe/Paris");
|
|
145
|
+
*
|
|
146
|
+
* @param {string|undefined} timeZone - An IANA time-zone name (e.g. `"America/New_York"`).
|
|
147
|
+
* @returns {TimeModule} The time module (for chaining).
|
|
148
|
+
*/
|
|
149
|
+
setTimezone(timeZone) {
|
|
150
|
+
_config.timeZone = timeZone;
|
|
151
|
+
return this;
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Returns the current global `{ locale, timeZone }` defaults.
|
|
156
|
+
*
|
|
157
|
+
* @returns {{ locale: string, timeZone: string|undefined }} A copy of the config.
|
|
158
|
+
*/
|
|
159
|
+
getConfig() {
|
|
160
|
+
return { ..._config };
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parses a human duration string into milliseconds.
|
|
165
|
+
*
|
|
166
|
+
* Accepts things like `"1h"`, `"30m"`, `"1h30m"`, `"2 days"`, `"500ms"`.
|
|
167
|
+
* A plain number is treated as milliseconds.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* nodeComfort.time.parseDuration("1h30m"); // 5400000
|
|
171
|
+
* nodeComfort.time.parseDuration("2 days"); // 172800000
|
|
172
|
+
*
|
|
173
|
+
* @param {string|number} input - Duration string or millisecond count.
|
|
174
|
+
* @returns {number|null} Milliseconds, or null if it can't be parsed.
|
|
175
|
+
*/
|
|
176
|
+
parseDuration(input) {
|
|
177
|
+
if (typeof input === "number") return input;
|
|
178
|
+
if (typeof input !== "string") return null;
|
|
179
|
+
|
|
180
|
+
const regex = /(-?\d*\.?\d+)\s*([a-z]+)/gi;
|
|
181
|
+
let total = 0;
|
|
182
|
+
let matched = false;
|
|
183
|
+
let match;
|
|
184
|
+
while ((match = regex.exec(input)) !== null) {
|
|
185
|
+
const value = parseFloat(match[1]);
|
|
186
|
+
const unit = UNIT_ALIASES[match[2].toLowerCase()];
|
|
187
|
+
if (!unit) return null;
|
|
188
|
+
total += value * MS[unit];
|
|
189
|
+
matched = true;
|
|
190
|
+
}
|
|
191
|
+
return matched ? total : null;
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Formats a millisecond duration into a human string.
|
|
196
|
+
*
|
|
197
|
+
* The compact form (default) is language-neutral (`"1h 30m"`). The `long`
|
|
198
|
+
* form is fully localized through `Intl` • pass `options.locale` or set a
|
|
199
|
+
* global locale with {@link module:Time.setLocale}.
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* nodeComfort.time.formatDuration(5400000); // "1h 30m"
|
|
203
|
+
* nodeComfort.time.formatDuration(90000, { long: true }); // "1 minute 30 seconds"
|
|
204
|
+
* nodeComfort.time.formatDuration(90000, { long: true, locale: "fr" }); // "1 minute 30 secondes"
|
|
205
|
+
*
|
|
206
|
+
* @param {number} ms - Duration in milliseconds.
|
|
207
|
+
* @param {{ long?: boolean, units?: number, locale?: string }} [options] - Formatting options (`units` caps how many parts show).
|
|
208
|
+
* @returns {string} The formatted duration.
|
|
209
|
+
*/
|
|
210
|
+
formatDuration(ms, options = {}) {
|
|
211
|
+
const long = options.long ?? false;
|
|
212
|
+
const maxUnits = options.units ?? Infinity;
|
|
213
|
+
const locale = options.locale ?? _config.locale;
|
|
214
|
+
if (!Number.isFinite(ms)) return String(ms);
|
|
215
|
+
const sign = ms < 0 ? "-" : "";
|
|
216
|
+
ms = Math.abs(Math.trunc(ms));
|
|
217
|
+
|
|
218
|
+
const parts = [
|
|
219
|
+
{ v: Math.floor(ms / MS.d), short: "d", unit: "day" },
|
|
220
|
+
{ v: Math.floor((ms % MS.d) / MS.h), short: "h", unit: "hour" },
|
|
221
|
+
{ v: Math.floor((ms % MS.h) / MS.m), short: "m", unit: "minute" },
|
|
222
|
+
{ v: Math.floor((ms % MS.m) / MS.s), short: "s", unit: "second" },
|
|
223
|
+
{ v: ms % MS.s, short: "ms", unit: "millisecond" },
|
|
224
|
+
].filter((p) => p.v > 0);
|
|
225
|
+
|
|
226
|
+
if (!parts.length) {
|
|
227
|
+
return long
|
|
228
|
+
? new Intl.NumberFormat(locale, { style: "unit", unit: "second", unitDisplay: "long" }).format(0)
|
|
229
|
+
: "0s";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return sign + parts
|
|
233
|
+
.slice(0, maxUnits)
|
|
234
|
+
.map((p) =>
|
|
235
|
+
long
|
|
236
|
+
? new Intl.NumberFormat(locale, { style: "unit", unit: p.unit, unitDisplay: "long" }).format(p.v)
|
|
237
|
+
: `${p.v}${p.short}`,
|
|
238
|
+
)
|
|
239
|
+
.join(" ");
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Returns a human-friendly, localized relative time string.
|
|
244
|
+
*
|
|
245
|
+
* Built on `Intl.RelativeTimeFormat`, so it speaks every language. Set
|
|
246
|
+
* `options.locale` (or a global locale) to translate; `options.numeric`
|
|
247
|
+
* controls wording (`"always"` → "1 day ago", `"auto"` → "yesterday").
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* nodeComfort.time.relative(Date.now() - 3600e3); // "1 hour ago"
|
|
251
|
+
* nodeComfort.time.relative(Date.now() - 3600e3, undefined, { locale: "fr" }); // "il y a 1 heure"
|
|
252
|
+
* nodeComfort.time.relative(Date.now() - 86400e3, undefined, { numeric: "auto" }); // "yesterday"
|
|
253
|
+
*
|
|
254
|
+
* @param {Date|number|string} date - Target date.
|
|
255
|
+
* @param {Date|number|string} [from] - Reference date (defaults to now).
|
|
256
|
+
* @param {{ locale?: string, numeric?: "always"|"auto" }} [options] - Formatting options.
|
|
257
|
+
* @returns {string} The relative time string.
|
|
258
|
+
*/
|
|
259
|
+
relative(date, from, options = {}) {
|
|
260
|
+
const locale = options.locale ?? _config.locale;
|
|
261
|
+
const numeric = options.numeric ?? "always";
|
|
262
|
+
const target = _toDate(date).getTime();
|
|
263
|
+
const base = _toDate(from).getTime();
|
|
264
|
+
const diff = target - base;
|
|
265
|
+
const abs = Math.abs(diff);
|
|
266
|
+
|
|
267
|
+
if (abs < MS.s) {
|
|
268
|
+
return new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(0, "second");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const units = [
|
|
272
|
+
{ limit: MS.m, div: MS.s, name: "second" },
|
|
273
|
+
{ limit: MS.h, div: MS.m, name: "minute" },
|
|
274
|
+
{ limit: MS.d, div: MS.h, name: "hour" },
|
|
275
|
+
{ limit: MS.w, div: MS.d, name: "day" },
|
|
276
|
+
{ limit: MS.d * 30, div: MS.w, name: "week" },
|
|
277
|
+
{ limit: MS.d * 365, div: MS.d * 30, name: "month" },
|
|
278
|
+
{ limit: Infinity, div: MS.d * 365, name: "year" },
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric });
|
|
282
|
+
for (const unit of units) {
|
|
283
|
+
if (abs < unit.limit) {
|
|
284
|
+
const value = Math.floor(abs / unit.div);
|
|
285
|
+
return rtf.format(diff < 0 ? -value : value, /** @type {Intl.RelativeTimeFormatUnit} */ (unit.name));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return rtf.format(0, "second");
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Formats a date using a small token syntax, with optional locale & time zone.
|
|
293
|
+
*
|
|
294
|
+
* **Numeric tokens:** `YYYY` `YY` `MM` `M` `DD` `D` `HH` `H` (24h) `hh` `h`
|
|
295
|
+
* (12h) `mm` `m` `ss` `s` `SSS`.
|
|
296
|
+
* **Localized tokens:** `MMMM` (month name) `MMM` (short month) `dddd` (weekday)
|
|
297
|
+
* `ddd` (short weekday) `A` (AM/PM) `a` (am/pm).
|
|
298
|
+
*
|
|
299
|
+
* Wrap literal text in square brackets to keep it verbatim: `"[Today is] dddd"`.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* nodeComfort.time.format(new Date(), "YYYY-MM-DD HH:mm:ss");
|
|
303
|
+
* nodeComfort.time.format(Date.now(), "dddd D MMMM YYYY", { locale: "fr" });
|
|
304
|
+
* nodeComfort.time.format(Date.now(), "HH:mm", { timeZone: "Asia/Tokyo" });
|
|
305
|
+
*
|
|
306
|
+
* @param {Date|number|string} date - Date to format.
|
|
307
|
+
* @param {string} [pattern="YYYY-MM-DD HH:mm:ss"] - Format pattern.
|
|
308
|
+
* @param {{ locale?: string, timeZone?: string }} [options] - Locale / time-zone overrides.
|
|
309
|
+
* @returns {string} The formatted date.
|
|
310
|
+
*/
|
|
311
|
+
format(date, pattern = "YYYY-MM-DD HH:mm:ss", options = {}) {
|
|
312
|
+
const d = _toDate(date);
|
|
313
|
+
if (Number.isNaN(d.getTime())) return "Invalid Date";
|
|
314
|
+
|
|
315
|
+
const locale = options.locale ?? _config.locale;
|
|
316
|
+
const timeZone = options.timeZone ?? _config.timeZone;
|
|
317
|
+
const p = _numericParts(d, timeZone);
|
|
318
|
+
|
|
319
|
+
const replacer = (token, escaped) => {
|
|
320
|
+
if (escaped !== undefined) return escaped;
|
|
321
|
+
switch (token) {
|
|
322
|
+
case "YYYY": return String(p.year);
|
|
323
|
+
case "YY": return String(p.year).slice(-2);
|
|
324
|
+
case "MMMM": return _named(d, { month: "long" }, locale, timeZone);
|
|
325
|
+
case "MMM": return _named(d, { month: "short" }, locale, timeZone);
|
|
326
|
+
case "MM": return _pad(p.month);
|
|
327
|
+
case "M": return String(p.month);
|
|
328
|
+
case "DD": return _pad(p.day);
|
|
329
|
+
case "D": return String(p.day);
|
|
330
|
+
case "dddd": return _named(d, { weekday: "long" }, locale, timeZone);
|
|
331
|
+
case "ddd": return _named(d, { weekday: "short" }, locale, timeZone);
|
|
332
|
+
case "HH": return _pad(p.hour);
|
|
333
|
+
case "H": return String(p.hour);
|
|
334
|
+
case "hh": return _pad(p.hour % 12 === 0 ? 12 : p.hour % 12);
|
|
335
|
+
case "h": return String(p.hour % 12 === 0 ? 12 : p.hour % 12);
|
|
336
|
+
case "mm": return _pad(p.minute);
|
|
337
|
+
case "m": return String(p.minute);
|
|
338
|
+
case "ss": return _pad(p.second);
|
|
339
|
+
case "s": return String(p.second);
|
|
340
|
+
case "SSS": return _pad(p.ms, 3);
|
|
341
|
+
case "A": return p.hour < 12 ? "AM" : "PM";
|
|
342
|
+
case "a": return p.hour < 12 ? "am" : "pm";
|
|
343
|
+
default: return token;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
return pattern.replace(
|
|
348
|
+
/\[([^\]]*)\]|YYYY|MMMM|dddd|SSS|MMM|ddd|YY|MM|DD|HH|hh|mm|ss|M|D|H|h|m|s|A|a/g,
|
|
349
|
+
replacer,
|
|
350
|
+
);
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Adds a duration to a date and returns a new Date.
|
|
355
|
+
*
|
|
356
|
+
* The duration may be a millisecond number or a string like `"1h30m"`.
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* nodeComfort.time.add(new Date(), "2h");
|
|
360
|
+
* nodeComfort.time.add(new Date(), -1, "days");
|
|
361
|
+
*
|
|
362
|
+
* @param {Date|number|string} date - Base date.
|
|
363
|
+
* @param {number|string} amount - Amount (ms/string) or a number paired with `unit`.
|
|
364
|
+
* @param {"ms"|"s"|"m"|"h"|"d"|"w"} [unit] - Unit when `amount` is a plain number.
|
|
365
|
+
* @returns {Date} The resulting date.
|
|
366
|
+
*/
|
|
367
|
+
add(date, amount, unit) {
|
|
368
|
+
const d = _toDate(date);
|
|
369
|
+
let ms;
|
|
370
|
+
if (typeof amount === "number") {
|
|
371
|
+
ms = unit ? amount * (MS[UNIT_ALIASES[unit] ?? unit] ?? 1) : amount;
|
|
372
|
+
} else {
|
|
373
|
+
ms = this.parseDuration(amount) ?? 0;
|
|
374
|
+
}
|
|
375
|
+
return new Date(d.getTime() + ms);
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Subtracts a duration from a date and returns a new Date.
|
|
380
|
+
*
|
|
381
|
+
* @param {Date|number|string} date - Base date.
|
|
382
|
+
* @param {number|string} amount - Amount (ms/string) or a number paired with `unit`.
|
|
383
|
+
* @param {"ms"|"s"|"m"|"h"|"d"|"w"} [unit] - Unit when `amount` is a plain number.
|
|
384
|
+
* @returns {Date} The resulting date.
|
|
385
|
+
*/
|
|
386
|
+
subtract(date, amount, unit) {
|
|
387
|
+
if (typeof amount === "number") return this.add(date, -amount, unit);
|
|
388
|
+
const ms = this.parseDuration(amount) ?? 0;
|
|
389
|
+
return this.add(date, -ms);
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Returns the difference between two dates in the requested unit.
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* nodeComfort.time.diff("2024-01-02", "2024-01-01", "d"); // 1
|
|
397
|
+
*
|
|
398
|
+
* @param {Date|number|string} a - First date.
|
|
399
|
+
* @param {Date|number|string} b - Second date.
|
|
400
|
+
* @param {"ms"|"s"|"m"|"h"|"d"|"w"} [unit="ms"] - Result unit.
|
|
401
|
+
* @returns {number} The signed difference (`a - b`) in the given unit.
|
|
402
|
+
*/
|
|
403
|
+
diff(a, b, unit = "ms") {
|
|
404
|
+
const ms = _toDate(a).getTime() - _toDate(b).getTime();
|
|
405
|
+
return ms / (MS[UNIT_ALIASES[unit] ?? unit] ?? 1);
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Checks whether two dates fall on the same calendar day.
|
|
410
|
+
*
|
|
411
|
+
* Uses local time by default; pass `options.timeZone` (or set a global zone)
|
|
412
|
+
* to compare calendar days in a specific zone.
|
|
413
|
+
*
|
|
414
|
+
* @param {Date|number|string} a - First date.
|
|
415
|
+
* @param {Date|number|string} b - Second date.
|
|
416
|
+
* @param {{ timeZone?: string }} [options] - Time-zone override.
|
|
417
|
+
* @returns {boolean} True if both are the same day.
|
|
418
|
+
*/
|
|
419
|
+
isSameDay(a, b, options = {}) {
|
|
420
|
+
const timeZone = options.timeZone ?? _config.timeZone;
|
|
421
|
+
const pa = _numericParts(_toDate(a), timeZone);
|
|
422
|
+
const pb = _numericParts(_toDate(b), timeZone);
|
|
423
|
+
return pa.year === pb.year && pa.month === pb.month && pa.day === pb.day;
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Returns a new Date at the start of the given unit (day/hour/etc).
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* nodeComfort.time.startOf(new Date(), "day"); // midnight today
|
|
431
|
+
*
|
|
432
|
+
* @param {Date|number|string} date - Base date.
|
|
433
|
+
* @param {"year"|"month"|"day"|"hour"|"minute"|"second"} unit - Unit to reset to.
|
|
434
|
+
* @returns {Date} The start-of-unit date.
|
|
435
|
+
*/
|
|
436
|
+
startOf(date, unit) {
|
|
437
|
+
const d = _toDate(date);
|
|
438
|
+
switch (unit) {
|
|
439
|
+
case "year": d.setMonth(0);
|
|
440
|
+
// falls through
|
|
441
|
+
case "month": d.setDate(1);
|
|
442
|
+
// falls through
|
|
443
|
+
case "day": d.setHours(0);
|
|
444
|
+
// falls through
|
|
445
|
+
case "hour": d.setMinutes(0);
|
|
446
|
+
// falls through
|
|
447
|
+
case "minute": d.setSeconds(0);
|
|
448
|
+
// falls through
|
|
449
|
+
case "second": d.setMilliseconds(0);
|
|
450
|
+
}
|
|
451
|
+
return d;
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Returns the current Unix timestamp in seconds.
|
|
456
|
+
*
|
|
457
|
+
* @returns {number} Seconds since the Unix epoch.
|
|
458
|
+
*/
|
|
459
|
+
unix() {
|
|
460
|
+
return Math.floor(Date.now() / 1000);
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Returns a high-resolution stopwatch you can `.stop()` to get elapsed ms.
|
|
465
|
+
*
|
|
466
|
+
* @example
|
|
467
|
+
* const timer = nodeComfort.time.stopwatch();
|
|
468
|
+
* doWork();
|
|
469
|
+
* console.log(timer.stop()); // e.g. 12.34 (ms, sub-ms precise)
|
|
470
|
+
* console.log(timer.stop(true)); // "12.34ms"
|
|
471
|
+
*
|
|
472
|
+
* @returns {{ elapsed: () => number, stop: (format?: boolean) => number|string }} A stopwatch handle.
|
|
473
|
+
*/
|
|
474
|
+
stopwatch() {
|
|
475
|
+
const start = process.hrtime.bigint();
|
|
476
|
+
const elapsed = () => Number(process.hrtime.bigint() - start) / 1e6;
|
|
477
|
+
return {
|
|
478
|
+
elapsed,
|
|
479
|
+
stop: (format = false) => {
|
|
480
|
+
const ms = elapsed();
|
|
481
|
+
return format ? `${Math.round(ms * 100) / 100}ms` : ms;
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
},
|
|
485
|
+
};
|