@ix-xs/node-comfort 1.2.0 → 2.0.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/CHANGELOG.md +190 -0
- package/README.md +136 -549
- package/index.js +80 -35
- package/package.json +160 -33
- package/src/Arr.js +887 -419
- package/src/Async.js +585 -0
- package/src/Cache.js +329 -0
- package/src/Checker.js +971 -539
- package/src/Cli.js +1034 -0
- package/src/Color.js +415 -0
- package/src/Crypto.js +707 -0
- package/src/Emitter.js +289 -0
- package/src/Env.js +555 -0
- package/src/FS.js +1483 -894
- package/src/Func.js +670 -353
- package/src/Http.js +538 -0
- package/src/Id.js +384 -201
- package/src/Logger.js +739 -378
- package/src/Num.js +844 -361
- package/src/Obj.js +855 -372
- package/src/SQLite.js +1308 -1005
- package/src/Schema.js +1586 -0
- package/src/Str.js +983 -436
- package/src/Sys.js +511 -0
- package/src/Time.js +1260 -422
- package/src/Utils.js +321 -325
- package/src/errors.js +292 -0
- package/types/index.d.ts +693 -526
- package/types/src/Arr.d.ts +455 -145
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Async.d.ts +321 -0
- package/types/src/Async.d.ts.map +1 -0
- package/types/src/Cache.d.ts +195 -0
- package/types/src/Cache.d.ts.map +1 -0
- package/types/src/Checker.d.ts +501 -235
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +582 -0
- package/types/src/Cli.d.ts.map +1 -0
- package/types/src/Color.d.ts +294 -0
- package/types/src/Color.d.ts.map +1 -0
- package/types/src/Crypto.d.ts +534 -0
- package/types/src/Crypto.d.ts.map +1 -0
- package/types/src/Emitter.d.ts +170 -0
- package/types/src/Emitter.d.ts.map +1 -0
- package/types/src/Env.d.ts +382 -0
- package/types/src/Env.d.ts.map +1 -0
- package/types/src/FS.d.ts +686 -238
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Func.d.ts +380 -135
- package/types/src/Func.d.ts.map +1 -1
- package/types/src/Http.d.ts +374 -0
- package/types/src/Http.d.ts.map +1 -0
- package/types/src/Id.d.ts +149 -68
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +363 -46
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts +473 -137
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +390 -104
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +646 -309
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +940 -0
- package/types/src/Schema.d.ts.map +1 -0
- package/types/src/Str.d.ts +569 -166
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +269 -0
- package/types/src/Sys.d.ts.map +1 -0
- package/types/src/Time.d.ts +663 -204
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +154 -102
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +292 -0
- package/types/src/errors.d.ts.map +1 -0
- package/types/index.d.ts.map +0 -1
package/src/Num.js
CHANGED
|
@@ -1,366 +1,849 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* Numbers: clamping, rounding that gets decimals right, statistics, and
|
|
5
|
+
* formatting for humans (sizes, money, ordinals, compact numbers) in any
|
|
6
|
+
* language. Formatting is English by default: change it with `setLocale()`
|
|
7
|
+
* or per call with `{ locale }`.
|
|
3
8
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
9
|
+
* @example
|
|
10
|
+
* num.round(1.005, 2); // 1.01
|
|
11
|
+
* num.formatBytes(1536); // "1.5 KB"
|
|
12
|
+
* num.currency(1234.5, "EUR", { locale: "fr" }); // "1 234,50 €"
|
|
13
|
+
* num.percentile(latencies, 95);
|
|
6
14
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
15
|
+
|
|
16
|
+
/** The default language, changed with `setLocale()`. */
|
|
17
|
+
const _config = { locale: "en" };
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Options for `formatBytes()`.
|
|
21
|
+
* @typedef {object} FormatBytesOptions
|
|
22
|
+
* @property {number} [decimals] Maximum decimals. Defaults to `1`.
|
|
23
|
+
* @property {boolean} [binary] 1 KB is 1024 bytes when `true`, 1000 when `false`. Defaults to `true`.
|
|
24
|
+
* @property {boolean} [iec] Use `KiB`, `MiB`, `GiB` labels.
|
|
25
|
+
* @property {string} [locale] Formats the number for a language, like `"fr"` for `"1,5 KB"`.
|
|
26
|
+
* @property {string[]} [units] Your own labels from bytes up, like `["o", "Ko", "Mo", "Go", "To"]`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Options for `parseBytes()`.
|
|
31
|
+
* @typedef {object} ParseBytesOptions
|
|
32
|
+
* @property {boolean} [binary] 1 KB is 1024 bytes when `true`, 1000 when `false`. `KiB` units are always 1024. Defaults to `true`.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options for `format()`.
|
|
37
|
+
* @typedef {object} FormatNumberOptions
|
|
38
|
+
* @property {string} [locale] Language, like `"fr"` or `"en-US"`. Defaults to the one set with `setLocale()`, or `"en"`.
|
|
39
|
+
* @property {number} [decimals] Exact number of decimals.
|
|
40
|
+
* @property {number} [minDecimals] Minimum decimals.
|
|
41
|
+
* @property {number} [maxDecimals] Maximum decimals. Defaults to `3`.
|
|
42
|
+
* @property {boolean} [compact] Short notation like `1.2K` or `3.4M`.
|
|
43
|
+
* @property {"percent"|"decimal"} [style] `"percent"` multiplies by 100 and adds `%`.
|
|
44
|
+
* @property {string} [unit] Any `Intl` unit: `"kilometer"`, `"celsius"`, `"megabyte"`...
|
|
45
|
+
* @property {"short"|"long"|"narrow"} [unitDisplay] How the unit is written. Defaults to `"short"`.
|
|
46
|
+
* @property {"auto"|"always"|"never"|"exceptZero"} [signDisplay] When to show the sign. `"exceptZero"` gives `+5`.
|
|
47
|
+
* @property {boolean} [grouping] Thousands separators. Defaults to `true`.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Options for `currency()`.
|
|
52
|
+
* @typedef {object} CurrencyOptions
|
|
53
|
+
* @property {string} [locale] Language. Defaults to the one set with `setLocale()`, or `"en"`.
|
|
54
|
+
* @property {number} [decimals] Defaults to the currency's usual decimals (2 for EUR, 0 for JPY).
|
|
55
|
+
* @property {"symbol"|"narrowSymbol"|"code"|"name"} [display] `€`, `€`, `EUR` or `euros`. Defaults to `"symbol"`.
|
|
56
|
+
* @property {boolean} [accounting] Show negative amounts in parentheses, like `($5.00)`.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Options for `ordinal()`.
|
|
61
|
+
* @typedef {object} OrdinalOptions
|
|
62
|
+
* @property {string} [locale] Built in: `en`, `fr`, `es`, `it`, `pt`, `de`, `nl`. Defaults to the one set with `setLocale()`, or `"en"`.
|
|
63
|
+
* @property {Partial<Record<Intl.LDMLPluralRule, string>>} [suffixes] Your own suffixes, for other languages.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Options for `variance()` and `stdDev()`.
|
|
68
|
+
* @typedef {object} VarianceOptions
|
|
69
|
+
* @property {boolean} [sample] Sample statistic (divides by n - 1) instead of population.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Multiplies by 10^exp without floating-point error.
|
|
74
|
+
* @param {number} value
|
|
75
|
+
* @param {number} exp
|
|
76
|
+
* @returns {number}
|
|
77
|
+
*/
|
|
78
|
+
const _shift = (value, exp) => {
|
|
79
|
+
const [mantissa, e = "0"] = String(value).split("e");
|
|
80
|
+
return Number(`${mantissa}e${Number(e) + exp}`);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {number} value
|
|
85
|
+
* @param {number} decimals
|
|
86
|
+
* @param {(n: number) => number} fn
|
|
87
|
+
* @returns {number}
|
|
88
|
+
*/
|
|
89
|
+
const _roundWith = (value, decimals, fn) => {
|
|
90
|
+
if (!Number.isFinite(value)) return value;
|
|
91
|
+
return _shift(fn(_shift(value, decimals)), -decimals);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const _SIZE_UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
|
95
|
+
const _IEC_UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
|
|
96
|
+
|
|
97
|
+
/** @type {Record<string, Partial<Record<Intl.LDMLPluralRule, string>>>} */
|
|
98
|
+
const _ORDINAL_SUFFIXES = {
|
|
99
|
+
en: { one: "st", two: "nd", few: "rd", other: "th" },
|
|
100
|
+
fr: { one: "er", other: "e" },
|
|
101
|
+
es: { other: "º" },
|
|
102
|
+
it: { other: "º" },
|
|
103
|
+
pt: { other: "º" },
|
|
104
|
+
de: { other: "." },
|
|
105
|
+
nl: { other: "e" },
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Keeps a number between `min` and `max`.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* num.clamp(150, 0, 100); // 100
|
|
113
|
+
* num.clamp(-5, 0, 100); // 0
|
|
114
|
+
*
|
|
115
|
+
* @param {number} value
|
|
116
|
+
* @param {number} min
|
|
117
|
+
* @param {number} max
|
|
118
|
+
* @returns {number}
|
|
119
|
+
*/
|
|
120
|
+
function clamp(value, min, max) {
|
|
121
|
+
return Math.min(Math.max(value, min), max);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Is the number between `min` and `max`, both included?
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* num.inRange(10, 0, 10); // true
|
|
129
|
+
*
|
|
130
|
+
* @param {number} value
|
|
131
|
+
* @param {number} min
|
|
132
|
+
* @param {number} max
|
|
133
|
+
* @returns {boolean}
|
|
134
|
+
*/
|
|
135
|
+
function inRange(value, min, max) {
|
|
136
|
+
return value >= min && value <= max;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Wraps a number around a range, like an angle or a carousel index.
|
|
141
|
+
* `max` is excluded.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* num.wrap(370, 0, 360); // 10
|
|
145
|
+
* num.wrap(-1, 0, 5); // 4
|
|
146
|
+
*
|
|
147
|
+
* @param {number} value
|
|
148
|
+
* @param {number} min
|
|
149
|
+
* @param {number} max
|
|
150
|
+
* @returns {number}
|
|
151
|
+
*/
|
|
152
|
+
function wrap(value, min, max) {
|
|
153
|
+
const span = max - min;
|
|
154
|
+
if (span <= 0) return min;
|
|
155
|
+
return ((((value - min) % span) + span) % span) + min;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Rounds to `decimals` places without floating-point surprises:
|
|
160
|
+
* `Math.round(1.005 * 100) / 100` gives `1`, this gives `1.01`. Halves round
|
|
161
|
+
* away from zero, like in a spreadsheet.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* num.round(1.005, 2); // 1.01
|
|
165
|
+
* num.round(1234, -2); // 1200
|
|
166
|
+
* num.round(-2.5); // -3
|
|
167
|
+
*
|
|
168
|
+
* @param {number} value
|
|
169
|
+
* @param {number} [decimals=0] Negative values round to tens, hundreds...
|
|
170
|
+
* @returns {number}
|
|
171
|
+
*/
|
|
172
|
+
function round(value, decimals = 0) {
|
|
173
|
+
return _roundWith(value, decimals, (n) => Math.sign(n) * Math.round(Math.abs(n)) || 0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Rounds down to `decimals` places.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* num.floor(1.789, 2); // 1.78
|
|
181
|
+
*
|
|
182
|
+
* @param {number} value
|
|
183
|
+
* @param {number} [decimals=0]
|
|
184
|
+
* @returns {number}
|
|
185
|
+
*/
|
|
186
|
+
function floor(value, decimals = 0) {
|
|
187
|
+
return _roundWith(value, decimals, Math.floor);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Rounds up to `decimals` places.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* num.ceil(1.231, 2); // 1.24
|
|
195
|
+
*
|
|
196
|
+
* @param {number} value
|
|
197
|
+
* @param {number} [decimals=0]
|
|
198
|
+
* @returns {number}
|
|
199
|
+
*/
|
|
200
|
+
function ceil(value, decimals = 0) {
|
|
201
|
+
return _roundWith(value, decimals, Math.ceil);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Rounds to the nearest multiple of `step`.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* num.snap(17, 5); // 15
|
|
209
|
+
* num.snap(0.37, 0.25); // 0.25
|
|
210
|
+
*
|
|
211
|
+
* @param {number} value
|
|
212
|
+
* @param {number} step
|
|
213
|
+
* @returns {number}
|
|
214
|
+
*/
|
|
215
|
+
function snap(value, step) {
|
|
216
|
+
if (!step) return value;
|
|
217
|
+
const decimals = (String(step).split(".")[1] ?? "").length;
|
|
218
|
+
return round(Math.round(value / step) * step, decimals);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Are two numbers equal within a small tolerance? The right way to compare
|
|
223
|
+
* float results, since `0.1 + 0.2 === 0.3` is `false`.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* num.approxEqual(0.1 + 0.2, 0.3); // true
|
|
227
|
+
*
|
|
228
|
+
* @param {number} a
|
|
229
|
+
* @param {number} b
|
|
230
|
+
* @param {number} [epsilon=Number.EPSILON * 16] Allowed difference, relative to the size of the numbers.
|
|
231
|
+
* @returns {boolean}
|
|
232
|
+
*/
|
|
233
|
+
function approxEqual(a, b, epsilon = Number.EPSILON * 16) {
|
|
234
|
+
if (a === b) return true;
|
|
235
|
+
const diff = Math.abs(a - b);
|
|
236
|
+
return diff <= epsilon * Math.max(1, Math.abs(a), Math.abs(b));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The value at ratio `t` between `start` and `end`.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* num.lerp(0, 100, 0.25); // 25
|
|
244
|
+
*
|
|
245
|
+
* @param {number} start
|
|
246
|
+
* @param {number} end
|
|
247
|
+
* @param {number} t Usually between 0 and 1.
|
|
248
|
+
* @returns {number}
|
|
249
|
+
*/
|
|
250
|
+
function lerp(start, end, t) {
|
|
251
|
+
return start + (end - start) * t;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Moves a number from one range to another, keeping its relative position.
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* num.mapRange(5, 0, 10, 0, 100); // 50
|
|
259
|
+
* num.mapRange(75, 0, 100, 1, 0); // 0.25
|
|
260
|
+
*
|
|
261
|
+
* @param {number} value
|
|
262
|
+
* @param {number} inMin
|
|
263
|
+
* @param {number} inMax
|
|
264
|
+
* @param {number} outMin
|
|
265
|
+
* @param {number} outMax
|
|
266
|
+
* @returns {number}
|
|
267
|
+
*/
|
|
268
|
+
function mapRange(value, inMin, inMax, outMin, outMax) {
|
|
269
|
+
if (inMax === inMin) return outMin;
|
|
270
|
+
return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* A random float from `min` (included) to `max` (excluded). Not for
|
|
275
|
+
* security: it uses `Math.random`.
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* num.random(10, 20); // 13.84...
|
|
279
|
+
*
|
|
280
|
+
* @param {number} [min=0]
|
|
281
|
+
* @param {number} [max=1]
|
|
282
|
+
* @returns {number}
|
|
283
|
+
*/
|
|
284
|
+
function random(min = 0, max = 1) {
|
|
285
|
+
return Math.random() * (max - min) + min;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* A random integer from `min` to `max`, both included. Not for security.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* num.randomInt(1, 6); // a dice roll
|
|
293
|
+
*
|
|
294
|
+
* @param {number} min
|
|
295
|
+
* @param {number} max
|
|
296
|
+
* @returns {number}
|
|
297
|
+
*/
|
|
298
|
+
function randomInt(min, max) {
|
|
299
|
+
const lo = Math.ceil(Math.min(min, max));
|
|
300
|
+
const hi = Math.floor(Math.max(min, max));
|
|
301
|
+
return Math.floor(Math.random() * (hi - lo + 1)) + lo;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Adds up numbers.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* num.sum([1, 2, 3]); // 6
|
|
309
|
+
*
|
|
310
|
+
* @param {Iterable<number>} values
|
|
311
|
+
* @returns {number}
|
|
312
|
+
*/
|
|
313
|
+
function sum(values) {
|
|
314
|
+
let total = 0;
|
|
315
|
+
for (const n of values) total += n;
|
|
316
|
+
return total;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The mean, or `0` for an empty list.
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* num.average([2, 4, 9]); // 5
|
|
324
|
+
*
|
|
325
|
+
* @param {Iterable<number>} values
|
|
326
|
+
* @returns {number}
|
|
327
|
+
*/
|
|
328
|
+
function average(values) {
|
|
329
|
+
const list = [...values];
|
|
330
|
+
return list.length ? sum(list) / list.length : 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* The middle value once sorted, or `0` for an empty list.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* num.median([3, 1, 2]); // 2
|
|
338
|
+
* num.median([1, 2, 3, 4]); // 2.5
|
|
339
|
+
*
|
|
340
|
+
* @param {Iterable<number>} values
|
|
341
|
+
* @returns {number}
|
|
342
|
+
*/
|
|
343
|
+
function median(values) {
|
|
344
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
345
|
+
if (!sorted.length) return 0;
|
|
346
|
+
const mid = Math.floor(sorted.length / 2);
|
|
347
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The most frequent values.
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* num.mode([1, 1, 2, 2, 3]); // [1, 2]
|
|
355
|
+
*
|
|
356
|
+
* @param {Iterable<number>} values
|
|
357
|
+
* @returns {number[]}
|
|
358
|
+
*/
|
|
359
|
+
function mode(values) {
|
|
360
|
+
/** @type {Map<number, number>} */
|
|
361
|
+
const counts = new Map();
|
|
362
|
+
for (const n of values) counts.set(n, (counts.get(n) ?? 0) + 1);
|
|
363
|
+
const best = Math.max(0, ...counts.values());
|
|
364
|
+
return [...counts].filter(([, c]) => c === best && best > 0).map(([n]) => n);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The variance: the average squared distance from the mean.
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* num.variance([2, 4, 4, 4, 5, 5, 7, 9]); // 4
|
|
372
|
+
*
|
|
373
|
+
* @param {Iterable<number>} values
|
|
374
|
+
* @param {VarianceOptions} [options]
|
|
375
|
+
* @returns {number}
|
|
376
|
+
*/
|
|
377
|
+
function variance(values, options = {}) {
|
|
378
|
+
const list = [...values];
|
|
379
|
+
const divisor = options.sample ? list.length - 1 : list.length;
|
|
380
|
+
if (divisor <= 0) return 0;
|
|
381
|
+
const mean = sum(list) / list.length;
|
|
382
|
+
return list.reduce((acc, n) => acc + (n - mean) ** 2, 0) / divisor;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* The standard deviation.
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* num.stdDev([2, 4, 4, 4, 5, 5, 7, 9]); // 2
|
|
390
|
+
*
|
|
391
|
+
* @param {Iterable<number>} values
|
|
392
|
+
* @param {VarianceOptions} [options]
|
|
393
|
+
* @returns {number}
|
|
394
|
+
*/
|
|
395
|
+
function stdDev(values, options = {}) {
|
|
396
|
+
return Math.sqrt(variance(values, options));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* The value under which `p` percent of the values fall, interpolated like
|
|
401
|
+
* Excel's `PERCENTILE.INC`.
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* num.percentile([1, 2, 3, 4, 5], 90); // 4.6
|
|
405
|
+
* num.percentile(responseTimes, 95); // p95 latency
|
|
406
|
+
*
|
|
407
|
+
* @param {Iterable<number>} values
|
|
408
|
+
* @param {number} p From 0 to 100.
|
|
409
|
+
* @returns {number} `NaN` for an empty list.
|
|
410
|
+
*/
|
|
411
|
+
function percentile(values, p) {
|
|
412
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
413
|
+
if (!sorted.length) return NaN;
|
|
414
|
+
const rank = (clamp(p, 0, 100) / 100) * (sorted.length - 1);
|
|
415
|
+
const lower = Math.floor(rank);
|
|
416
|
+
const upper = Math.ceil(rank);
|
|
417
|
+
return round(sorted[lower] + (sorted[upper] - sorted[lower]) * (rank - lower), 12);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* The smallest number. Unlike `Math.min(...array)`, huge arrays are fine.
|
|
422
|
+
*
|
|
423
|
+
* @param {Iterable<number>} values
|
|
424
|
+
* @returns {number | undefined} `undefined` for an empty list.
|
|
425
|
+
*/
|
|
426
|
+
function min(values) {
|
|
427
|
+
let best;
|
|
428
|
+
for (const n of values) if (best === undefined || n < best) best = n;
|
|
429
|
+
return best;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* The largest number. Unlike `Math.max(...array)`, huge arrays are fine.
|
|
434
|
+
*
|
|
435
|
+
* @param {Iterable<number>} values
|
|
436
|
+
* @returns {number | undefined} `undefined` for an empty list.
|
|
437
|
+
*/
|
|
438
|
+
function max(values) {
|
|
439
|
+
let best;
|
|
440
|
+
for (const n of values) if (best === undefined || n > best) best = n;
|
|
441
|
+
return best;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* What percentage `value` is of `total`. Returns `0` when `total` is `0`.
|
|
446
|
+
*
|
|
447
|
+
* @example
|
|
448
|
+
* num.percent(25, 200); // 12.5
|
|
449
|
+
* num.percent(1, 3, 1); // 33.3
|
|
450
|
+
*
|
|
451
|
+
* @param {number} value
|
|
452
|
+
* @param {number} total
|
|
453
|
+
* @param {number} [decimals=2]
|
|
454
|
+
* @returns {number}
|
|
455
|
+
*/
|
|
456
|
+
function percent(value, total, decimals = 2) {
|
|
457
|
+
if (!total) return 0;
|
|
458
|
+
return round((value / total) * 100, decimals);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Greatest common divisor.
|
|
463
|
+
*
|
|
464
|
+
* @example
|
|
465
|
+
* num.gcd(12, 18, 27); // 3
|
|
466
|
+
*
|
|
467
|
+
* @param {...number} values
|
|
468
|
+
* @returns {number}
|
|
469
|
+
*/
|
|
470
|
+
function gcd(...values) {
|
|
471
|
+
return values.reduce((a, b) => {
|
|
472
|
+
let x = Math.abs(a);
|
|
473
|
+
let y = Math.abs(b);
|
|
474
|
+
while (y) [x, y] = [y, x % y];
|
|
475
|
+
return x;
|
|
476
|
+
}, 0);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Least common multiple.
|
|
481
|
+
*
|
|
482
|
+
* @example
|
|
483
|
+
* num.lcm(2, 3, 4); // 12
|
|
484
|
+
*
|
|
485
|
+
* @param {...number} values
|
|
486
|
+
* @returns {number}
|
|
487
|
+
*/
|
|
488
|
+
function lcm(...values) {
|
|
489
|
+
if (!values.length) return 0;
|
|
490
|
+
return values.reduce((a, b) => (a === 0 || b === 0 ? 0 : Math.abs(a * b) / gcd(a, b)));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Is it a prime number?
|
|
495
|
+
*
|
|
496
|
+
* @example
|
|
497
|
+
* num.isPrime(7); // true
|
|
498
|
+
* num.isPrime(1); // false
|
|
499
|
+
*
|
|
500
|
+
* @param {number} value
|
|
501
|
+
* @returns {boolean}
|
|
502
|
+
*/
|
|
503
|
+
function isPrime(value) {
|
|
504
|
+
if (!Number.isInteger(value) || value < 2) return false;
|
|
505
|
+
if (value < 4) return true;
|
|
506
|
+
if (value % 2 === 0 || value % 3 === 0) return false;
|
|
507
|
+
for (let i = 5; i * i <= value; i += 6) {
|
|
508
|
+
if (value % i === 0 || value % (i + 2) === 0) return false;
|
|
509
|
+
}
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* `n!`. Pass `true` to get an exact bigint, since numbers lose precision
|
|
515
|
+
* past 18!.
|
|
516
|
+
*
|
|
517
|
+
* @example
|
|
518
|
+
* num.factorial(5); // 120
|
|
519
|
+
* num.factorial(25, true); // 15511210043330985984000000n
|
|
520
|
+
*
|
|
521
|
+
* @param {number} n
|
|
522
|
+
* @param {boolean} [asBigInt=false]
|
|
523
|
+
* @returns {number | bigint}
|
|
524
|
+
* @throws {RangeError} If `n` is negative or not an integer.
|
|
525
|
+
*/
|
|
526
|
+
function factorial(n, asBigInt = false) {
|
|
527
|
+
if (!Number.isInteger(n) || n < 0) throw new RangeError("factorial() expects a non-negative integer");
|
|
528
|
+
if (asBigInt) {
|
|
529
|
+
let result = 1n;
|
|
530
|
+
for (let i = 2n; i <= BigInt(n); i++) result *= i;
|
|
364
531
|
return result;
|
|
365
|
-
}
|
|
532
|
+
}
|
|
533
|
+
let result = 1;
|
|
534
|
+
for (let i = 2; i <= n; i++) result *= i;
|
|
535
|
+
return result;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Is it even?
|
|
540
|
+
*
|
|
541
|
+
* @param {number} value
|
|
542
|
+
* @returns {boolean}
|
|
543
|
+
*/
|
|
544
|
+
function isEven(value) {
|
|
545
|
+
return value % 2 === 0;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Is it odd? Negative numbers work too.
|
|
550
|
+
*
|
|
551
|
+
* @param {number} value
|
|
552
|
+
* @returns {boolean}
|
|
553
|
+
*/
|
|
554
|
+
function isOdd(value) {
|
|
555
|
+
return Math.abs(value % 2) === 1;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Numbers from `start` up to `end` (excluded). With one argument, counts
|
|
560
|
+
* from 0. Goes down when `end` is smaller.
|
|
561
|
+
*
|
|
562
|
+
* @example
|
|
563
|
+
* num.range(4); // [0, 1, 2, 3]
|
|
564
|
+
* num.range(0, 10, 2); // [0, 2, 4, 6, 8]
|
|
565
|
+
* num.range(5, 0); // [5, 4, 3, 2, 1]
|
|
566
|
+
*
|
|
567
|
+
* @param {number} start
|
|
568
|
+
* @param {number} [end]
|
|
569
|
+
* @param {number} [step] Defaults to `1`, or `-1` when counting down.
|
|
570
|
+
* @returns {number[]}
|
|
571
|
+
*/
|
|
572
|
+
function range(start, end, step) {
|
|
573
|
+
let from = start;
|
|
574
|
+
let to = end;
|
|
575
|
+
if (to === undefined) {
|
|
576
|
+
to = from;
|
|
577
|
+
from = 0;
|
|
578
|
+
}
|
|
579
|
+
const inc = step ?? (to < from ? -1 : 1);
|
|
580
|
+
if (inc === 0 || (inc > 0 && from > to) || (inc < 0 && from < to)) return [];
|
|
581
|
+
const decimals = Math.max((String(inc).split(".")[1] ?? "").length, (String(from).split(".")[1] ?? "").length);
|
|
582
|
+
const out = [];
|
|
583
|
+
const count = Math.ceil((to - from) / inc);
|
|
584
|
+
for (let i = 0; i < count; i++) out.push(decimals ? round(from + i * inc, decimals) : from + i * inc);
|
|
585
|
+
return out;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Reads a number from anything, ignoring trailing text like units.
|
|
590
|
+
*
|
|
591
|
+
* @example
|
|
592
|
+
* num.parse("42px"); // 42
|
|
593
|
+
* num.parse(" -3.5 kg"); // -3.5
|
|
594
|
+
* num.parse("nope", 0); // 0
|
|
595
|
+
*
|
|
596
|
+
* @param {unknown} value
|
|
597
|
+
* @param {number} [fallback=NaN] Returned when there's no number to read.
|
|
598
|
+
* @returns {number}
|
|
599
|
+
*/
|
|
600
|
+
function parse(value, fallback = NaN) {
|
|
601
|
+
if (typeof value === "number") return Number.isNaN(value) ? fallback : value;
|
|
602
|
+
if (typeof value === "bigint") return Number(value);
|
|
603
|
+
const match = /[-+]?(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?/i.exec(String(value));
|
|
604
|
+
const parsed = match ? Number(match[0]) : NaN;
|
|
605
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Turns a size like `"10 MB"` into bytes. The opposite of `formatBytes()`.
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* num.parseBytes("1.5 KB"); // 1536
|
|
613
|
+
* num.parseBytes("2 GiB"); // 2147483648
|
|
614
|
+
* num.parseBytes("1 KB", { binary: false }); // 1000
|
|
615
|
+
*
|
|
616
|
+
* @param {string | number} value A size like `"512kb"` or `"1.5G"`, or a number of bytes.
|
|
617
|
+
* @param {ParseBytesOptions} [options]
|
|
618
|
+
* @returns {number} Bytes, or `NaN` if it can't be read.
|
|
619
|
+
*/
|
|
620
|
+
function parseBytes(value, options = {}) {
|
|
621
|
+
if (typeof value === "number") return value;
|
|
622
|
+
const match = /^\s*([-+]?(?:\d+\.?\d*|\.\d+))\s*([kmgtpezy]?)(i?)(b?)(?:ytes?)?\s*$/i.exec(String(value));
|
|
623
|
+
if (!match) return NaN;
|
|
624
|
+
const [, amount, prefix, iec] = match;
|
|
625
|
+
const base = iec || options.binary !== false ? 1024 : 1000;
|
|
626
|
+
const power = prefix ? "kmgtpezy".indexOf(prefix.toLowerCase()) + 1 : 0;
|
|
627
|
+
return Math.floor(Number(amount) * base ** power);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Formats a byte count as a readable size.
|
|
632
|
+
*
|
|
633
|
+
* @example
|
|
634
|
+
* num.formatBytes(1536); // "1.5 KB"
|
|
635
|
+
* num.formatBytes(1000, { binary: false }); // "1 KB"
|
|
636
|
+
* num.formatBytes(1536, { locale: "fr", units: ["o", "Ko", "Mo", "Go"] }); // "1,5 Ko"
|
|
637
|
+
*
|
|
638
|
+
* @param {number} bytes
|
|
639
|
+
* @param {FormatBytesOptions} [options]
|
|
640
|
+
* @returns {string}
|
|
641
|
+
*/
|
|
642
|
+
function formatBytes(bytes, options = {}) {
|
|
643
|
+
const units = options.units ?? (options.iec ? _IEC_UNITS : _SIZE_UNITS);
|
|
644
|
+
if (!Number.isFinite(bytes) || bytes === 0) return `0 ${units[0]}`;
|
|
645
|
+
const decimals = options.decimals ?? 1;
|
|
646
|
+
const base = options.binary === false ? 1000 : 1024;
|
|
647
|
+
const sign = bytes < 0 ? "-" : "";
|
|
648
|
+
const abs = Math.abs(bytes);
|
|
649
|
+
const i = Math.min(Math.floor(Math.log(abs) / Math.log(base)), units.length - 1);
|
|
650
|
+
const amount = round(abs / base ** i, decimals);
|
|
651
|
+
const locale = options.locale ?? (_config.locale === "en" ? undefined : _config.locale);
|
|
652
|
+
const text = locale ? new Intl.NumberFormat(locale, { maximumFractionDigits: decimals }).format(amount) : String(amount);
|
|
653
|
+
return `${sign}${text} ${units[i]}`;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Formats a number for display, with `Intl.NumberFormat` doing the work:
|
|
658
|
+
* separators, decimals, compact notation, percentages, units.
|
|
659
|
+
*
|
|
660
|
+
* @example
|
|
661
|
+
* num.format(1234567.891); // "1,234,567.891"
|
|
662
|
+
* num.format(1234567.891, { locale: "fr" }); // "1 234 567,891"
|
|
663
|
+
* num.format(0.256, { style: "percent" }); // "26%"
|
|
664
|
+
* num.format(1500, { compact: true }); // "1.5K"
|
|
665
|
+
* num.format(3.5, { unit: "kilometer" }); // "3.5 km"
|
|
666
|
+
*
|
|
667
|
+
* @param {number | bigint} value
|
|
668
|
+
* @param {FormatNumberOptions} [options]
|
|
669
|
+
* @returns {string}
|
|
670
|
+
*/
|
|
671
|
+
function format(value, options = {}) {
|
|
672
|
+
/** @type {Intl.NumberFormatOptions} */
|
|
673
|
+
const intl = {};
|
|
674
|
+
if (options.decimals !== undefined) {
|
|
675
|
+
intl.minimumFractionDigits = options.decimals;
|
|
676
|
+
intl.maximumFractionDigits = options.decimals;
|
|
677
|
+
}
|
|
678
|
+
if (options.minDecimals !== undefined) intl.minimumFractionDigits = options.minDecimals;
|
|
679
|
+
if (options.maxDecimals !== undefined) intl.maximumFractionDigits = options.maxDecimals;
|
|
680
|
+
if (options.compact) {
|
|
681
|
+
intl.notation = "compact";
|
|
682
|
+
intl.compactDisplay = "short";
|
|
683
|
+
}
|
|
684
|
+
if (options.style === "percent") intl.style = "percent";
|
|
685
|
+
if (options.unit) {
|
|
686
|
+
intl.style = "unit";
|
|
687
|
+
intl.unit = options.unit;
|
|
688
|
+
intl.unitDisplay = options.unitDisplay ?? "short";
|
|
689
|
+
}
|
|
690
|
+
if (options.signDisplay) intl.signDisplay = options.signDisplay;
|
|
691
|
+
if (options.grouping === false) intl.useGrouping = false;
|
|
692
|
+
return new Intl.NumberFormat(options.locale ?? _config.locale, intl).format(value);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Formats an amount of money.
|
|
697
|
+
*
|
|
698
|
+
* @example
|
|
699
|
+
* num.currency(1234.5, "USD"); // "$1,234.50"
|
|
700
|
+
* num.currency(1234.5, "EUR", { locale: "fr" }); // "1 234,50 €"
|
|
701
|
+
* num.currency(-5, "USD", { accounting: true }); // "($5.00)"
|
|
702
|
+
*
|
|
703
|
+
* @param {number | bigint} value
|
|
704
|
+
* @param {string} code A currency code like `"EUR"`, `"USD"` or `"JPY"`.
|
|
705
|
+
* @param {CurrencyOptions} [options]
|
|
706
|
+
* @returns {string}
|
|
707
|
+
*/
|
|
708
|
+
function currency(value, code, options = {}) {
|
|
709
|
+
/** @type {Intl.NumberFormatOptions} */
|
|
710
|
+
const intl = { style: "currency", currency: code, currencyDisplay: options.display ?? "symbol" };
|
|
711
|
+
if (options.decimals !== undefined) {
|
|
712
|
+
intl.minimumFractionDigits = options.decimals;
|
|
713
|
+
intl.maximumFractionDigits = options.decimals;
|
|
714
|
+
}
|
|
715
|
+
if (options.accounting) intl.currencySign = "accounting";
|
|
716
|
+
return new Intl.NumberFormat(options.locale ?? _config.locale, intl).format(value);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Shortens big numbers with `K`, `M`, `B` or `T`. For other languages, use
|
|
721
|
+
* `format(value, { compact: true, locale })`.
|
|
722
|
+
*
|
|
723
|
+
* @example
|
|
724
|
+
* num.abbreviate(1500); // "1.5K"
|
|
725
|
+
* num.abbreviate(2_400_000); // "2.4M"
|
|
726
|
+
*
|
|
727
|
+
* @param {number} value
|
|
728
|
+
* @param {number} [decimals=1]
|
|
729
|
+
* @returns {string}
|
|
730
|
+
*/
|
|
731
|
+
function abbreviate(value, decimals = 1) {
|
|
732
|
+
const sign = value < 0 ? "-" : "";
|
|
733
|
+
const abs = Math.abs(value);
|
|
734
|
+
for (const [limit, suffix] of /** @type {Array<[number, string]>} */ ([[1e12, "T"], [1e9, "B"], [1e6, "M"], [1e3, "K"]])) {
|
|
735
|
+
if (abs >= limit) return `${sign}${round(abs / limit, decimals)}${suffix}`;
|
|
736
|
+
}
|
|
737
|
+
return `${sign}${round(abs, decimals)}`;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Adds thousands separators, leaving decimals alone.
|
|
742
|
+
*
|
|
743
|
+
* @example
|
|
744
|
+
* num.thousands(1234567.89, " "); // "1 234 567.89"
|
|
745
|
+
*
|
|
746
|
+
* @param {number | string} value
|
|
747
|
+
* @param {string} [separator=","]
|
|
748
|
+
* @returns {string}
|
|
749
|
+
*/
|
|
750
|
+
function thousands(value, separator = ",") {
|
|
751
|
+
const [int, dec] = String(value).split(".");
|
|
752
|
+
const grouped = int.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
|
|
753
|
+
return dec === undefined ? grouped : `${grouped}.${dec}`;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Adds the ordinal suffix: 1st, 2nd, 3rd... in several languages.
|
|
758
|
+
*
|
|
759
|
+
* @example
|
|
760
|
+
* num.ordinal(22); // "22nd"
|
|
761
|
+
* num.ordinal(1, { locale: "fr" }); // "1er"
|
|
762
|
+
* num.ordinal(3, { locale: "de" }); // "3."
|
|
763
|
+
*
|
|
764
|
+
* @param {number} value
|
|
765
|
+
* @param {OrdinalOptions} [options]
|
|
766
|
+
* @returns {string}
|
|
767
|
+
*/
|
|
768
|
+
function ordinal(value, options = {}) {
|
|
769
|
+
const locale = options.locale ?? _config.locale;
|
|
770
|
+
const language = locale.split(/[-_]/)[0].toLowerCase();
|
|
771
|
+
const suffixes = options.suffixes ?? _ORDINAL_SUFFIXES[language] ?? _ORDINAL_SUFFIXES.en;
|
|
772
|
+
let category;
|
|
773
|
+
try {
|
|
774
|
+
category = new Intl.PluralRules(locale, { type: "ordinal" }).select(value);
|
|
775
|
+
} catch {
|
|
776
|
+
category = new Intl.PluralRules("en", { type: "ordinal" }).select(value);
|
|
777
|
+
}
|
|
778
|
+
return `${value}${suffixes[category] ?? suffixes.other ?? ""}`;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Sets the default language of `format`, `currency`, `percent`,
|
|
783
|
+
* `abbreviate`, `ordinal` and `formatBytes`.
|
|
784
|
+
*
|
|
785
|
+
* @example
|
|
786
|
+
* num.setLocale("fr");
|
|
787
|
+
* num.currency(1234.5, "EUR"); // "1 234,50 €"
|
|
788
|
+
*
|
|
789
|
+
* @param {string} locale Like `"fr"`, `"en-GB"` or `"pt-BR"`.
|
|
790
|
+
* @returns {typeof import("./Num")} The module, so calls chain.
|
|
791
|
+
*/
|
|
792
|
+
function setLocale(locale) {
|
|
793
|
+
_config.locale = locale;
|
|
794
|
+
return module.exports;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* The current default language.
|
|
799
|
+
*
|
|
800
|
+
* @example
|
|
801
|
+
* num.getLocale(); // "en"
|
|
802
|
+
*
|
|
803
|
+
* @returns {string}
|
|
804
|
+
*/
|
|
805
|
+
function getLocale() {
|
|
806
|
+
return _config.locale;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
module.exports = {
|
|
810
|
+
setLocale,
|
|
811
|
+
getLocale,
|
|
812
|
+
clamp,
|
|
813
|
+
inRange,
|
|
814
|
+
wrap,
|
|
815
|
+
round,
|
|
816
|
+
floor,
|
|
817
|
+
ceil,
|
|
818
|
+
snap,
|
|
819
|
+
approxEqual,
|
|
820
|
+
lerp,
|
|
821
|
+
mapRange,
|
|
822
|
+
random,
|
|
823
|
+
randomInt,
|
|
824
|
+
sum,
|
|
825
|
+
average,
|
|
826
|
+
median,
|
|
827
|
+
mode,
|
|
828
|
+
variance,
|
|
829
|
+
stdDev,
|
|
830
|
+
percentile,
|
|
831
|
+
min,
|
|
832
|
+
max,
|
|
833
|
+
percent,
|
|
834
|
+
gcd,
|
|
835
|
+
lcm,
|
|
836
|
+
isPrime,
|
|
837
|
+
factorial,
|
|
838
|
+
isEven,
|
|
839
|
+
isOdd,
|
|
840
|
+
range,
|
|
841
|
+
parse,
|
|
842
|
+
parseBytes,
|
|
843
|
+
formatBytes,
|
|
844
|
+
format,
|
|
845
|
+
currency,
|
|
846
|
+
abbreviate,
|
|
847
|
+
thousands,
|
|
848
|
+
ordinal,
|
|
366
849
|
};
|