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