@ix-xs/node-comfort 1.1.3 → 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 -24
- 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 +526 -0
- package/types/index.d.ts.map +1 -0
- package/types/src/Arr.d.ts +252 -0
- package/types/src/Arr.d.ts.map +1 -0
- package/types/src/Checker.d.ts +407 -0
- package/types/src/Checker.d.ts.map +1 -0
- package/types/src/FS.d.ts +303 -0
- package/types/src/FS.d.ts.map +1 -0
- 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 +68 -0
- package/types/src/Logger.d.ts.map +1 -0
- 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 +396 -0
- package/types/src/SQLite.d.ts.map +1 -0
- 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/types/src/Utils.d.ts +124 -0
- package/types/src/Utils.d.ts.map +1 -0
package/src/Id.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ID, token & hashing utilities.
|
|
5
|
+
*
|
|
6
|
+
* Cryptographically-secure identifiers (UUID v4, ULID, nanoid-style tokens),
|
|
7
|
+
* hashing helpers and constant-time comparison • all built on Node's native
|
|
8
|
+
* `node:crypto`, with zero external dependencies.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const NANOID_ALPHABET = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
12
|
+
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
13
|
+
const ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // Crockford base32
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Per-prefix counters for {@link module:Id.seq}.
|
|
17
|
+
* @private
|
|
18
|
+
* @type {Record<string, number>}
|
|
19
|
+
*/
|
|
20
|
+
const _seqCounters = {};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fills a buffer with cryptographically secure random bytes.
|
|
24
|
+
* @private
|
|
25
|
+
* @param {number} size
|
|
26
|
+
* @returns {Buffer}
|
|
27
|
+
*/
|
|
28
|
+
const _randomBytes = (size) => crypto.randomBytes(size);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Encodes a buffer as Base58 using the Bitcoin alphabet.
|
|
32
|
+
* @private
|
|
33
|
+
* @param {Buffer} buffer - Bytes to encode.
|
|
34
|
+
* @returns {string} The Base58 string.
|
|
35
|
+
*/
|
|
36
|
+
const _toBase58 = (buffer) => {
|
|
37
|
+
if (buffer.length === 0) return "";
|
|
38
|
+
const digits = [0];
|
|
39
|
+
for (const byte of buffer) {
|
|
40
|
+
let carry = byte;
|
|
41
|
+
for (let i = 0; i < digits.length; i++) {
|
|
42
|
+
carry += digits[i] << 8;
|
|
43
|
+
digits[i] = carry % 58;
|
|
44
|
+
carry = Math.floor(carry / 58);
|
|
45
|
+
}
|
|
46
|
+
while (carry > 0) {
|
|
47
|
+
digits.push(carry % 58);
|
|
48
|
+
carry = Math.floor(carry / 58);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let str = "";
|
|
52
|
+
for (let i = 0; buffer[i] === 0 && i < buffer.length - 1; i++) str += BASE58_ALPHABET[0];
|
|
53
|
+
for (let i = digits.length - 1; i >= 0; i--) str += BASE58_ALPHABET[digits[i]];
|
|
54
|
+
return str;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
/**
|
|
59
|
+
* Generates a random UUID v4.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* nodeComfort.id.uuid(); // "3f2504e0-4f89-41d3-9a0c-0305e82c3301"
|
|
63
|
+
*
|
|
64
|
+
* @returns {string} A UUID v4 string.
|
|
65
|
+
*/
|
|
66
|
+
uuid() {
|
|
67
|
+
return crypto.randomUUID();
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generates a URL-safe, collision-resistant ID (nanoid-compatible alphabet).
|
|
72
|
+
*
|
|
73
|
+
* Uses secure randomness with unbiased rejection sampling.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* nodeComfort.id.nano(); // "V1StGXR8_Z5jdHi6B-myT"
|
|
77
|
+
* nodeComfort.id.nano(10); // "IRFa-VaY2b"
|
|
78
|
+
*
|
|
79
|
+
* @param {number} [size=21] - Desired length.
|
|
80
|
+
* @param {string} [alphabet] - Custom alphabet.
|
|
81
|
+
* @returns {string} The generated ID.
|
|
82
|
+
*/
|
|
83
|
+
nano(size = 21, alphabet = NANOID_ALPHABET) {
|
|
84
|
+
const len = alphabet.length;
|
|
85
|
+
// Bitmask for unbiased sampling.
|
|
86
|
+
const mask = (2 << Math.floor(Math.log2(len - 1))) - 1;
|
|
87
|
+
const step = Math.ceil((1.6 * mask * size) / len);
|
|
88
|
+
let id = "";
|
|
89
|
+
while (id.length < size) {
|
|
90
|
+
const bytes = _randomBytes(step);
|
|
91
|
+
for (let i = 0; i < step && id.length < size; i++) {
|
|
92
|
+
const index = bytes[i] & mask;
|
|
93
|
+
if (index < len) id += alphabet[index];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return id;
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Generates a ULID • a lexicographically-sortable, timestamp-prefixed ID.
|
|
101
|
+
*
|
|
102
|
+
* ULIDs sort by creation time and are a great primary-key alternative to UUIDs.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* nodeComfort.id.ulid(); // "01ARZ3NDEKTSV4RRFFQ69G5FAV"
|
|
106
|
+
*
|
|
107
|
+
* @param {number} [time=Date.now()] - Timestamp in ms to encode.
|
|
108
|
+
* @returns {string} A 26-character ULID.
|
|
109
|
+
*/
|
|
110
|
+
ulid(time = Date.now()) {
|
|
111
|
+
let timeChars = "";
|
|
112
|
+
let t = time;
|
|
113
|
+
for (let i = 0; i < 10; i++) {
|
|
114
|
+
timeChars = ULID_ALPHABET[t % 32] + timeChars;
|
|
115
|
+
t = Math.floor(t / 32);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const bytes = _randomBytes(16);
|
|
119
|
+
let randChars = "";
|
|
120
|
+
for (let i = 0; i < 16; i++) {
|
|
121
|
+
randChars += ULID_ALPHABET[bytes[i] % 32];
|
|
122
|
+
}
|
|
123
|
+
return timeChars + randChars;
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generates a cryptographically-secure random token.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* nodeComfort.id.token(); // 32-byte hex string
|
|
131
|
+
* nodeComfort.id.token(16, "base64url"); // 16 bytes, URL-safe base64
|
|
132
|
+
*
|
|
133
|
+
* @param {number} [bytes=32] - Number of random bytes.
|
|
134
|
+
* @param {"hex"|"base64"|"base64url"|"base58"} [encoding="hex"] - Output encoding.
|
|
135
|
+
* @returns {string} The token.
|
|
136
|
+
*/
|
|
137
|
+
token(bytes = 32, encoding = "hex") {
|
|
138
|
+
const buffer = _randomBytes(bytes);
|
|
139
|
+
if (encoding === "base58") return _toBase58(buffer);
|
|
140
|
+
return buffer.toString(encoding);
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Generates a short, human-friendly random code (uppercase + digits by default).
|
|
145
|
+
*
|
|
146
|
+
* Ambiguous characters (`0/O`, `1/I`) are excluded, making it suitable for
|
|
147
|
+
* coupons, OTP-like codes and invite links.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* nodeComfort.id.code(); // "K7QF9X"
|
|
151
|
+
* nodeComfort.id.code(4, "1234"); // "3142"
|
|
152
|
+
*
|
|
153
|
+
* @param {number} [length=6] - Number of characters.
|
|
154
|
+
* @param {string} [alphabet="ABCDEFGHJKLMNPQRSTUVWXYZ23456789"] - Characters to pick from.
|
|
155
|
+
* @returns {string} The random code.
|
|
156
|
+
*/
|
|
157
|
+
code(length = 6, alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789") {
|
|
158
|
+
const bytes = _randomBytes(length);
|
|
159
|
+
let out = "";
|
|
160
|
+
for (let i = 0; i < length; i++) {
|
|
161
|
+
out += alphabet[bytes[i] % alphabet.length];
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Hashes a value with a chosen algorithm.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* nodeComfort.id.hash("hello"); // sha256 hex
|
|
171
|
+
* nodeComfort.id.hash("hello", { algorithm: "md5" });
|
|
172
|
+
*
|
|
173
|
+
* @param {string|Buffer} value - Data to hash.
|
|
174
|
+
* @param {{ algorithm?: string, encoding?: "hex"|"base64"|"base64url" }} [options] - Hash options.
|
|
175
|
+
* @returns {string} The digest.
|
|
176
|
+
*/
|
|
177
|
+
hash(value, options = {}) {
|
|
178
|
+
const algorithm = options.algorithm ?? "sha256";
|
|
179
|
+
const encoding = options.encoding ?? "hex";
|
|
180
|
+
return crypto.createHash(algorithm).update(value).digest(encoding);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Computes an HMAC signature for a value using a secret key.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* nodeComfort.id.hmac("payload", "secret"); // sha256 HMAC hex
|
|
188
|
+
*
|
|
189
|
+
* @param {string|Buffer} value - Data to sign.
|
|
190
|
+
* @param {string|Buffer} secret - Secret key.
|
|
191
|
+
* @param {{ algorithm?: string, encoding?: "hex"|"base64"|"base64url" }} [options] - HMAC options.
|
|
192
|
+
* @returns {string} The signature.
|
|
193
|
+
*/
|
|
194
|
+
hmac(value, secret, options = {}) {
|
|
195
|
+
const algorithm = options.algorithm ?? "sha256";
|
|
196
|
+
const encoding = options.encoding ?? "hex";
|
|
197
|
+
return crypto.createHmac(algorithm, secret).update(value).digest(encoding);
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Compares two strings/buffers in constant time to prevent timing attacks.
|
|
202
|
+
*
|
|
203
|
+
* Use this when comparing secrets, tokens, or signatures.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* if (nodeComfort.id.safeEqual(provided, expected)) { ... }
|
|
207
|
+
*
|
|
208
|
+
* @param {string|Buffer} a - First value.
|
|
209
|
+
* @param {string|Buffer} b - Second value.
|
|
210
|
+
* @returns {boolean} True if equal.
|
|
211
|
+
*/
|
|
212
|
+
safeEqual(a, b) {
|
|
213
|
+
const bufA = Buffer.isBuffer(a) ? a : Buffer.from(String(a));
|
|
214
|
+
const bufB = Buffer.isBuffer(b) ? b : Buffer.from(String(b));
|
|
215
|
+
if (bufA.length !== bufB.length) return false;
|
|
216
|
+
return crypto.timingSafeEqual(bufA, bufB);
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Returns a monotonically-increasing counter ID prefixed with an optional label.
|
|
221
|
+
*
|
|
222
|
+
* Useful for readable, unique keys within a single process run.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* nodeComfort.id.seq("user"); // "user-1"
|
|
226
|
+
* nodeComfort.id.seq("user"); // "user-2"
|
|
227
|
+
*
|
|
228
|
+
* @param {string} [prefix=""] - Optional prefix.
|
|
229
|
+
* @returns {string} The sequential ID.
|
|
230
|
+
*/
|
|
231
|
+
seq(prefix = "") {
|
|
232
|
+
_seqCounters[prefix] = (_seqCounters[prefix] ?? 0) + 1;
|
|
233
|
+
return prefix ? `${prefix}-${_seqCounters[prefix]}` : String(_seqCounters[prefix]);
|
|
234
|
+
},
|
|
235
|
+
};
|
package/src/Logger.js
CHANGED
|
@@ -220,6 +220,24 @@ const _indent = () => {
|
|
|
220
220
|
return " ".repeat(_groups.length);
|
|
221
221
|
};
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Coerces any value to a string for the level helpers (info/warn/...).
|
|
225
|
+
* @private
|
|
226
|
+
* @param {*} content - Value to stringify.
|
|
227
|
+
* @returns {string} The string representation.
|
|
228
|
+
*/
|
|
229
|
+
const _stringify = (content) => {
|
|
230
|
+
if (typeof content === "string") return content;
|
|
231
|
+
if (typeof content === "object" && content !== null) {
|
|
232
|
+
try {
|
|
233
|
+
return JSON.stringify(content, null, 2);
|
|
234
|
+
} catch {
|
|
235
|
+
return String(content);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return String(content);
|
|
239
|
+
};
|
|
240
|
+
|
|
223
241
|
module.exports = {
|
|
224
242
|
/**
|
|
225
243
|
* Enables or disables timestamp in logs
|
|
@@ -284,6 +302,59 @@ module.exports = {
|
|
|
284
302
|
return /** @type {Logger} */ (this);
|
|
285
303
|
},
|
|
286
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Logs an informational message, prefixed with a cyan `ℹ INFO` label.
|
|
307
|
+
* @param {*} content - Content to print.
|
|
308
|
+
* @returns {Logger} The same Logger instance (for chaining).
|
|
309
|
+
*/
|
|
310
|
+
info(content) {
|
|
311
|
+
return this.log(`<% cyan bold ℹ INFO %> ${_stringify(content)}`);
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Logs a success message, prefixed with a green `✔ OK` label.
|
|
316
|
+
* @param {*} content - Content to print.
|
|
317
|
+
* @returns {Logger} The same Logger instance (for chaining).
|
|
318
|
+
*/
|
|
319
|
+
success(content) {
|
|
320
|
+
return this.log(`<% greenBright bold ✔ OK %> ${_stringify(content)}`);
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Logs a warning, prefixed with a yellow `⚠ WARN` label (writes to stderr).
|
|
325
|
+
* @param {*} content - Content to print.
|
|
326
|
+
* @returns {Logger} The same Logger instance (for chaining).
|
|
327
|
+
*/
|
|
328
|
+
warn(content) {
|
|
329
|
+
return this.log(`<% yellowBright bold ⚠ WARN %> ${_stringify(content)}`);
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Logs an error, prefixed with a red `✖ ERROR` label.
|
|
334
|
+
*
|
|
335
|
+
* Accepts an `Error` and prints its stack when available.
|
|
336
|
+
* @param {*} content - Content or Error to print.
|
|
337
|
+
* @returns {Logger} The same Logger instance (for chaining).
|
|
338
|
+
*/
|
|
339
|
+
error(content) {
|
|
340
|
+
const text =
|
|
341
|
+
content instanceof Error ? content.stack ?? content.message : _stringify(content);
|
|
342
|
+
return this.log(`<% redBright bold ✖ ERROR %> ${text}`);
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Logs a debug message (only when `DEBUG`/`NODE_DEBUG` env is set), prefixed
|
|
347
|
+
* with a gray `● DEBUG` label.
|
|
348
|
+
* @param {*} content - Content to print.
|
|
349
|
+
* @returns {Logger} The same Logger instance (for chaining).
|
|
350
|
+
*/
|
|
351
|
+
debug(content) {
|
|
352
|
+
if (!process.env.DEBUG && !process.env.NODE_DEBUG) {
|
|
353
|
+
return /** @type {Logger} */ (this);
|
|
354
|
+
}
|
|
355
|
+
return this.log(`<% gray bold ● DEBUG %> <% gray ${_stringify(content)} %>`);
|
|
356
|
+
},
|
|
357
|
+
|
|
287
358
|
/**
|
|
288
359
|
* Start a log group (displays a label and adds indentation)
|
|
289
360
|
* @param {string} label - The group name to display
|
package/src/Num.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Number & math utilities.
|
|
3
|
+
*
|
|
4
|
+
* Clamping, rounding, ranges, human-friendly formatting (bytes, durations,
|
|
5
|
+
* ordinals, abbreviations) and safe parsing • the stuff `Math` leaves out.
|
|
6
|
+
*/
|
|
7
|
+
module.exports = {
|
|
8
|
+
/**
|
|
9
|
+
* Clamps a number between a minimum and maximum.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* nodeComfort.num.clamp(15, 0, 10); // 10
|
|
13
|
+
* nodeComfort.num.clamp(-3, 0, 10); // 0
|
|
14
|
+
*
|
|
15
|
+
* @param {number} value - Value to clamp.
|
|
16
|
+
* @param {number} min - Lower bound.
|
|
17
|
+
* @param {number} max - Upper bound.
|
|
18
|
+
* @returns {number} The clamped value.
|
|
19
|
+
*/
|
|
20
|
+
clamp(value, min, max) {
|
|
21
|
+
return Math.min(Math.max(value, min), max);
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Checks whether a number is within an inclusive `[min, max]` range.
|
|
26
|
+
*
|
|
27
|
+
* @param {number} value - Value to test.
|
|
28
|
+
* @param {number} min - Lower bound.
|
|
29
|
+
* @param {number} max - Upper bound.
|
|
30
|
+
* @returns {boolean} True if within range.
|
|
31
|
+
*/
|
|
32
|
+
inRange(value, min, max) {
|
|
33
|
+
return value >= min && value <= max;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Rounds a number to a given number of decimal places (avoids float artifacts).
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* nodeComfort.num.round(1.005, 2); // 1.01
|
|
41
|
+
*
|
|
42
|
+
* @param {number} value - Value to round.
|
|
43
|
+
* @param {number} [decimals=0] - Number of decimal places.
|
|
44
|
+
* @returns {number} The rounded number.
|
|
45
|
+
*/
|
|
46
|
+
round(value, decimals = 0) {
|
|
47
|
+
const factor = 10 ** decimals;
|
|
48
|
+
return Math.round((value + Number.EPSILON) * factor) / factor;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Rounds a number down to a given number of decimal places.
|
|
53
|
+
*
|
|
54
|
+
* @param {number} value - Value to round.
|
|
55
|
+
* @param {number} [decimals=0] - Number of decimal places.
|
|
56
|
+
* @returns {number} The floored number.
|
|
57
|
+
*/
|
|
58
|
+
floor(value, decimals = 0) {
|
|
59
|
+
const factor = 10 ** decimals;
|
|
60
|
+
return Math.floor(value * factor) / factor;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Rounds a number up to a given number of decimal places.
|
|
65
|
+
*
|
|
66
|
+
* @param {number} value - Value to round.
|
|
67
|
+
* @param {number} [decimals=0] - Number of decimal places.
|
|
68
|
+
* @returns {number} The ceiled number.
|
|
69
|
+
*/
|
|
70
|
+
ceil(value, decimals = 0) {
|
|
71
|
+
const factor = 10 ** decimals;
|
|
72
|
+
return Math.ceil(value * factor) / factor;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Linearly interpolates between `start` and `end` by ratio `t` (0..1).
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* nodeComfort.num.lerp(0, 100, 0.5); // 50
|
|
80
|
+
*
|
|
81
|
+
* @param {number} start - Start value.
|
|
82
|
+
* @param {number} end - End value.
|
|
83
|
+
* @param {number} t - Interpolation factor (usually 0..1).
|
|
84
|
+
* @returns {number} The interpolated value.
|
|
85
|
+
*/
|
|
86
|
+
lerp(start, end, t) {
|
|
87
|
+
return start + (end - start) * t;
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Re-maps a number from one range to another.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* nodeComfort.num.mapRange(5, 0, 10, 0, 100); // 50
|
|
95
|
+
*
|
|
96
|
+
* @param {number} value - Value to map.
|
|
97
|
+
* @param {number} inMin - Input range minimum.
|
|
98
|
+
* @param {number} inMax - Input range maximum.
|
|
99
|
+
* @param {number} outMin - Output range minimum.
|
|
100
|
+
* @param {number} outMax - Output range maximum.
|
|
101
|
+
* @returns {number} The mapped value.
|
|
102
|
+
*/
|
|
103
|
+
mapRange(value, inMin, inMax, outMin, outMax) {
|
|
104
|
+
if (inMax === inMin) return outMin;
|
|
105
|
+
return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns a random number (float) in `[min, max)`.
|
|
110
|
+
*
|
|
111
|
+
* Not cryptographically secure. For secure randomness use `nodeComfort.id`.
|
|
112
|
+
*
|
|
113
|
+
* @param {number} [min=0] - Lower bound (inclusive).
|
|
114
|
+
* @param {number} [max=1] - Upper bound (exclusive).
|
|
115
|
+
* @returns {number} A random float.
|
|
116
|
+
*/
|
|
117
|
+
random(min = 0, max = 1) {
|
|
118
|
+
return Math.random() * (max - min) + min;
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Returns a random integer in `[min, max]` (both inclusive).
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* nodeComfort.num.randomInt(1, 6); // dice roll
|
|
126
|
+
*
|
|
127
|
+
* @param {number} min - Lower bound (inclusive).
|
|
128
|
+
* @param {number} max - Upper bound (inclusive).
|
|
129
|
+
* @returns {number} A random integer.
|
|
130
|
+
*/
|
|
131
|
+
randomInt(min, max) {
|
|
132
|
+
min = Math.ceil(min);
|
|
133
|
+
max = Math.floor(max);
|
|
134
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Sums all numbers in an array.
|
|
139
|
+
*
|
|
140
|
+
* @param {number[]} values - Array of numbers.
|
|
141
|
+
* @returns {number} The sum (0 for an empty array).
|
|
142
|
+
*/
|
|
143
|
+
sum(values) {
|
|
144
|
+
return values.reduce((acc, n) => acc + n, 0);
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Returns the arithmetic mean (average) of an array of numbers.
|
|
149
|
+
*
|
|
150
|
+
* @param {number[]} values - Array of numbers.
|
|
151
|
+
* @returns {number} The average (0 for an empty array).
|
|
152
|
+
*/
|
|
153
|
+
average(values) {
|
|
154
|
+
return values.length ? this.sum(values) / values.length : 0;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Returns the median value of an array of numbers.
|
|
159
|
+
*
|
|
160
|
+
* @param {number[]} values - Array of numbers.
|
|
161
|
+
* @returns {number} The median (0 for an empty array).
|
|
162
|
+
*/
|
|
163
|
+
median(values) {
|
|
164
|
+
if (!values.length) return 0;
|
|
165
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
166
|
+
const mid = Math.floor(sorted.length / 2);
|
|
167
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Returns the smallest number in an array.
|
|
172
|
+
*
|
|
173
|
+
* @param {number[]} values - Array of numbers.
|
|
174
|
+
* @returns {number|undefined} The minimum, or undefined if empty.
|
|
175
|
+
*/
|
|
176
|
+
min(values) {
|
|
177
|
+
return values.length ? Math.min(...values) : undefined;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Returns the largest number in an array.
|
|
182
|
+
*
|
|
183
|
+
* @param {number[]} values - Array of numbers.
|
|
184
|
+
* @returns {number|undefined} The maximum, or undefined if empty.
|
|
185
|
+
*/
|
|
186
|
+
max(values) {
|
|
187
|
+
return values.length ? Math.max(...values) : undefined;
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Computes what percentage `value` is of `total`.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* nodeComfort.num.percent(25, 200); // 12.5
|
|
195
|
+
*
|
|
196
|
+
* @param {number} value - Partial value.
|
|
197
|
+
* @param {number} total - Total value.
|
|
198
|
+
* @param {number} [decimals=2] - Decimal places to keep.
|
|
199
|
+
* @returns {number} The percentage.
|
|
200
|
+
*/
|
|
201
|
+
percent(value, total, decimals = 2) {
|
|
202
|
+
if (!total) return 0;
|
|
203
|
+
return this.round((value / total) * 100, decimals);
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Formats a byte count into a human-readable string (KB, MB, GB, ...).
|
|
208
|
+
*
|
|
209
|
+
* By default uses binary sizing (base 1024) with familiar `KB`/`MB` labels.
|
|
210
|
+
* Set `binary: false` for SI sizing (base 1000), or `iec: true` for
|
|
211
|
+
* strict `KiB`/`MiB` labels.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* nodeComfort.num.formatBytes(1536); // "1.5 KB"
|
|
215
|
+
* nodeComfort.num.formatBytes(1073741824); // "1 GB"
|
|
216
|
+
* nodeComfort.num.formatBytes(1000, { binary: false }); // "1 KB"
|
|
217
|
+
* nodeComfort.num.formatBytes(1024, { iec: true }); // "1 KiB"
|
|
218
|
+
*
|
|
219
|
+
* @param {number} bytes - Number of bytes.
|
|
220
|
+
* @param {{ decimals?: number, binary?: boolean, iec?: boolean }} [options] - Formatting options.
|
|
221
|
+
* @returns {string} The human-readable size.
|
|
222
|
+
*/
|
|
223
|
+
formatBytes(bytes, options = {}) {
|
|
224
|
+
if (!Number.isFinite(bytes) || bytes === 0) return "0 B";
|
|
225
|
+
const decimals = options.decimals ?? 1;
|
|
226
|
+
const base = options.binary === false ? 1000 : 1024;
|
|
227
|
+
const units = options.iec
|
|
228
|
+
? ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
|
|
229
|
+
: ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
|
230
|
+
const sign = bytes < 0 ? "-" : "";
|
|
231
|
+
bytes = Math.abs(bytes);
|
|
232
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(base)), units.length - 1);
|
|
233
|
+
return `${sign}${this.round(bytes / base ** i, decimals)} ${units[i]}`;
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Formats a large number with a compact suffix (K, M, B, T).
|
|
238
|
+
*
|
|
239
|
+
* @example
|
|
240
|
+
* nodeComfort.num.abbreviate(1500); // "1.5K"
|
|
241
|
+
* nodeComfort.num.abbreviate(2_400_000); // "2.4M"
|
|
242
|
+
*
|
|
243
|
+
* @param {number} value - Number to abbreviate.
|
|
244
|
+
* @param {number} [decimals=1] - Decimal places.
|
|
245
|
+
* @returns {string} The abbreviated number.
|
|
246
|
+
*/
|
|
247
|
+
abbreviate(value, decimals = 1) {
|
|
248
|
+
const sign = value < 0 ? "-" : "";
|
|
249
|
+
value = Math.abs(value);
|
|
250
|
+
const units = [
|
|
251
|
+
{ v: 1e12, s: "T" },
|
|
252
|
+
{ v: 1e9, s: "B" },
|
|
253
|
+
{ v: 1e6, s: "M" },
|
|
254
|
+
{ v: 1e3, s: "K" },
|
|
255
|
+
];
|
|
256
|
+
for (const { v, s } of units) {
|
|
257
|
+
if (value >= v) return `${sign}${this.round(value / v, decimals)}${s}`;
|
|
258
|
+
}
|
|
259
|
+
return `${sign}${value}`;
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Inserts thousands separators into a number.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* nodeComfort.num.thousands(1234567); // "1,234,567"
|
|
267
|
+
* nodeComfort.num.thousands(1234.5, " "); // "1 234.5"
|
|
268
|
+
*
|
|
269
|
+
* @param {number} value - Number to format.
|
|
270
|
+
* @param {string} [separator=","] - Thousands separator.
|
|
271
|
+
* @returns {string} The formatted number.
|
|
272
|
+
*/
|
|
273
|
+
thousands(value, separator = ",") {
|
|
274
|
+
const [int, dec] = String(value).split(".");
|
|
275
|
+
const withSep = int.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
|
|
276
|
+
return dec ? `${withSep}.${dec}` : withSep;
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Returns the English ordinal suffix for a number ("st", "nd", "rd", "th").
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* nodeComfort.num.ordinal(1); // "1st"
|
|
284
|
+
* nodeComfort.num.ordinal(22); // "22nd"
|
|
285
|
+
*
|
|
286
|
+
* @param {number} value - The number.
|
|
287
|
+
* @returns {string} The number with its ordinal suffix.
|
|
288
|
+
*/
|
|
289
|
+
ordinal(value) {
|
|
290
|
+
const abs = Math.abs(value) % 100;
|
|
291
|
+
const suffix =
|
|
292
|
+
abs >= 11 && abs <= 13
|
|
293
|
+
? "th"
|
|
294
|
+
: ["th", "st", "nd", "rd"][Math.abs(value) % 10] || "th";
|
|
295
|
+
return `${value}${suffix}`;
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Parses a value into a number, returning a fallback when it isn't numeric.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* nodeComfort.num.parse("42px"); // 42
|
|
303
|
+
* nodeComfort.num.parse("nope", 0); // 0
|
|
304
|
+
*
|
|
305
|
+
* @param {*} value - Value to parse.
|
|
306
|
+
* @param {number} [fallback=NaN] - Value returned when parsing fails.
|
|
307
|
+
* @returns {number} The parsed number or the fallback.
|
|
308
|
+
*/
|
|
309
|
+
parse(value, fallback = NaN) {
|
|
310
|
+
if (typeof value === "number") return Number.isNaN(value) ? fallback : value;
|
|
311
|
+
const parsed = parseFloat(String(value).replace(/[^0-9.eE+-]/g, ""));
|
|
312
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Checks whether a number is even.
|
|
317
|
+
* @param {number} value - Number to check.
|
|
318
|
+
* @returns {boolean} True if even.
|
|
319
|
+
*/
|
|
320
|
+
isEven(value) {
|
|
321
|
+
return value % 2 === 0;
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Checks whether a number is odd.
|
|
326
|
+
* @param {number} value - Number to check.
|
|
327
|
+
* @returns {boolean} True if odd.
|
|
328
|
+
*/
|
|
329
|
+
isOdd(value) {
|
|
330
|
+
return Math.abs(value % 2) === 1;
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Returns an array of numbers from `start` to `end`.
|
|
335
|
+
*
|
|
336
|
+
* `end` is exclusive. Supports a custom step and descending ranges.
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* nodeComfort.num.range(4); // [0, 1, 2, 3]
|
|
340
|
+
* nodeComfort.num.range(1, 5); // [1, 2, 3, 4]
|
|
341
|
+
* nodeComfort.num.range(0, 10, 2); // [0, 2, 4, 6, 8]
|
|
342
|
+
* nodeComfort.num.range(5, 0); // [5, 4, 3, 2, 1]
|
|
343
|
+
*
|
|
344
|
+
* @param {number} start - Start (or end if it's the only argument).
|
|
345
|
+
* @param {number} [end] - End (exclusive).
|
|
346
|
+
* @param {number} [step] - Step size (defaults to 1 or -1 based on direction).
|
|
347
|
+
* @returns {number[]} The generated range.
|
|
348
|
+
*/
|
|
349
|
+
range(start, end, step) {
|
|
350
|
+
if (end === undefined) {
|
|
351
|
+
end = start;
|
|
352
|
+
start = 0;
|
|
353
|
+
}
|
|
354
|
+
const descending = end < start;
|
|
355
|
+
step = step ?? (descending ? -1 : 1);
|
|
356
|
+
if (step === 0) return [];
|
|
357
|
+
|
|
358
|
+
const result = [];
|
|
359
|
+
if (step > 0) {
|
|
360
|
+
for (let i = start; i < end; i += step) result.push(i);
|
|
361
|
+
} else {
|
|
362
|
+
for (let i = start; i > end; i += step) result.push(i);
|
|
363
|
+
}
|
|
364
|
+
return result;
|
|
365
|
+
},
|
|
366
|
+
};
|