xypriss 9.13.13 → 9.13.15

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.
@@ -1,75 +1,1178 @@
1
+ import { Random } from 'xypriss-security';
2
+
3
+ /***************************************************************************
4
+ * XyPrissJS - Fast And Secure
5
+ *
6
+ * @author Nehonix
7
+ * @license Nehonix OSL (NOSL)
8
+ *
9
+ * Copyright (c) 2025 Nehonix. All rights reserved.
10
+ *
11
+ * This License governs the use, modification, and distribution of software
12
+ * provided by NEHONIX under its open source projects.
13
+ * NEHONIX is committed to fostering collaborative innovation while strictly
14
+ * protecting its intellectual property rights.
15
+ * Violation of any term of this License will result in immediate termination of all granted rights
16
+ * and may subject the violator to legal action.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
19
+ * INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
20
+ * AND NON-INFRINGEMENT.
21
+ * IN NO EVENT SHALL NEHONIX BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
22
+ * OR CONSEQUENTIAL DAMAGES ARISING FROM THE USE OR INABILITY TO USE THE SOFTWARE,
23
+ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
24
+ *
25
+ ***************************************************************************** */
26
+ /**
27
+ * @file NumberUtils.ts
28
+ * @description XyPriss Number & Math Utilities — A comprehensive suite of
29
+ * everyday numeric calculations, file & storage conversions, percentage analytics,
30
+ * pagination algorithms, precision roundings, and localized formatters.
31
+ *
32
+ * @remarks
33
+ * All methods are pure and deterministic unless utilizing cryptographic randomness
34
+ * provided by `xypriss-security`.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const num = new NumberUtils();
39
+ *
40
+ * // File & Storage Calculations
41
+ * num.formatBytes(15_728_640); // "15 MB"
42
+ * num.parseBytes("25.5 MB"); // 26738688
43
+ * num.toMB(104857600); // 100
44
+ *
45
+ * // Daily Percentages & Ratios
46
+ * num.percentage(45, 200); // 22.5 (%)
47
+ * num.percentageOf(20, 150); // 30
48
+ * num.percentageChange(100, 125); // 25 (+25%)
49
+ *
50
+ * // Pagination Helper
51
+ * num.paginate(154, 20, 2); // { currentPage: 2, totalPages: 8, offset: 20, limit: 20, ... }
52
+ * ```
53
+ */
1
54
  /**
2
55
  * **NumberUtils — XyPriss Number & Math Utilities**
56
+ *
57
+ * Provides ready-to-use helpers for daily mathematical operations, data storage units,
58
+ * rates, roundings, statistical metrics, bounds, and formatted output.
3
59
  */
4
60
  class NumberUtils {
61
+ // ─────────────────────────────────────────────
62
+ // Internal Helpers
63
+ // ─────────────────────────────────────────────
64
+ /**
65
+ * Flattens a variadic list of numbers or number arrays into a flat numeric array.
66
+ *
67
+ * @param args - Numbers or arrays of numbers.
68
+ * @returns A flat array of valid numbers.
69
+ * @internal
70
+ */
71
+ flattenNumbers(args) {
72
+ const result = [];
73
+ for (const item of args) {
74
+ if (Array.isArray(item)) {
75
+ for (const num of item) {
76
+ if (typeof num === "number" && !isNaN(num)) {
77
+ result.push(num);
78
+ }
79
+ }
80
+ }
81
+ else if (typeof item === "number" && !isNaN(item)) {
82
+ result.push(item);
83
+ }
84
+ }
85
+ return result;
86
+ }
87
+ // ─────────────────────────────────────────────
88
+ // File Sizes & Storage Data Calculations
89
+ // ─────────────────────────────────────────────
90
+ /**
91
+ * **Format Bytes**
92
+ *
93
+ * Converts a raw number of bytes into a human-readable size string (e.g., `KB`, `MB`, `GB` or `KiB`, `MiB`, `GiB`).
94
+ * Supports negative values and configurable decimal precision.
95
+ *
96
+ * @param bytes - The number of bytes to format.
97
+ * @param optionsOrDecimals - Number of decimal places (default: `2`) or a {@link FormatBytesOptions} configuration object.
98
+ * @returns A human-readable file size string.
99
+ *
100
+ * @example
101
+ * ```ts
102
+ * num.formatBytes(0); // "0 Bytes"
103
+ * num.formatBytes(1024); // "1 KB"
104
+ * num.formatBytes(15728640, 1); // "15 MB"
105
+ * num.formatBytes(1073741824, { binary: true }); // "1 GiB"
106
+ * ```
107
+ */
108
+ formatBytes(bytes, optionsOrDecimals = 2) {
109
+ const options = typeof optionsOrDecimals === "number"
110
+ ? { decimals: optionsOrDecimals }
111
+ : optionsOrDecimals || {};
112
+ const decimals = options.decimals !== undefined && options.decimals >= 0
113
+ ? options.decimals
114
+ : 2;
115
+ const binary = options.binary ?? false;
116
+ const spacer = options.spacer ?? " ";
117
+ if (bytes === 0) {
118
+ return `0${spacer}Bytes`;
119
+ }
120
+ const isNegative = bytes < 0;
121
+ const absBytes = Math.abs(bytes);
122
+ const k = 1024;
123
+ const standardSizes = [
124
+ "Bytes",
125
+ "KB",
126
+ "MB",
127
+ "GB",
128
+ "TB",
129
+ "PB",
130
+ "EB",
131
+ "ZB",
132
+ "YB",
133
+ ];
134
+ const binarySizes = [
135
+ "Bytes",
136
+ "KiB",
137
+ "MiB",
138
+ "GiB",
139
+ "TiB",
140
+ "PiB",
141
+ "EiB",
142
+ "ZiB",
143
+ "YiB",
144
+ ];
145
+ const sizes = binary ? binarySizes : standardSizes;
146
+ const i = Math.min(Math.floor(Math.log(absBytes) / Math.log(k)), sizes.length - 1);
147
+ if (i === 0) {
148
+ return `${isNegative ? "-" : ""}${absBytes}${spacer}${sizes[0]}`;
149
+ }
150
+ const val = absBytes / Math.pow(k, i);
151
+ const formatted = this.round(val, decimals);
152
+ return `${isNegative ? "-" : ""}${formatted}${spacer}${sizes[i]}`;
153
+ }
154
+ /**
155
+ * **Parse Bytes**
156
+ *
157
+ * Parses a human-readable storage string (e.g., `"10MB"`, `"1.5 GB"`, `"500KB"`, `"64KiB"`, `"1024 B"`)
158
+ * into the equivalent raw number of bytes. Very helpful for upload limits, payload sizes, and cache config.
159
+ *
160
+ * @param input - The size string to parse or raw byte number.
161
+ * @returns The parsed number of bytes, or `NaN` if parsing fails.
162
+ *
163
+ * @example
164
+ * ```ts
165
+ * num.parseBytes("500 KB"); // 512000
166
+ * num.parseBytes("10MB"); // 10485760
167
+ * num.parseBytes("1.5 GB"); // 1610612736
168
+ * num.parseBytes("256 B"); // 256
169
+ * ```
170
+ */
171
+ parseBytes(input) {
172
+ if (typeof input === "number") {
173
+ return isNaN(input) ? 0 : input;
174
+ }
175
+ if (!input || typeof input !== "string") {
176
+ return 0;
177
+ }
178
+ const trimmed = input.trim();
179
+ const match = trimmed.match(/^([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*([a-zA-Z]*)$/);
180
+ if (!match) {
181
+ return NaN;
182
+ }
183
+ const value = parseFloat(match[1]);
184
+ const unit = match[2].toUpperCase();
185
+ if (!unit || unit === "B" || unit === "BYTES" || unit === "BYTE") {
186
+ return Math.round(value);
187
+ }
188
+ const multipliers = {
189
+ K: 1024,
190
+ KB: 1024,
191
+ KIB: 1024,
192
+ M: 1024 ** 2,
193
+ MB: 1024 ** 2,
194
+ MIB: 1024 ** 2,
195
+ G: 1024 ** 3,
196
+ GB: 1024 ** 3,
197
+ GIB: 1024 ** 3,
198
+ T: 1024 ** 4,
199
+ TB: 1024 ** 4,
200
+ TIB: 1024 ** 4,
201
+ P: 1024 ** 5,
202
+ PB: 1024 ** 5,
203
+ PIB: 1024 ** 5,
204
+ };
205
+ const mult = multipliers[unit];
206
+ return mult !== undefined ? Math.round(value * mult) : NaN;
207
+ }
208
+ /**
209
+ * **Convert Bytes to Kilobytes (KB)**
210
+ *
211
+ * @param bytes - Number of raw bytes.
212
+ * @param decimals - Decimal places (default: `2`).
213
+ * @returns The value in Kilobytes.
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * num.toKB(2048); // 2
218
+ * ```
219
+ */
220
+ toKB(bytes, decimals = 2) {
221
+ return this.round(bytes / 1024, decimals);
222
+ }
223
+ /**
224
+ * **Convert Bytes to Megabytes (MB)**
225
+ *
226
+ * @param bytes - Number of raw bytes.
227
+ * @param decimals - Decimal places (default: `2`).
228
+ * @returns The value in Megabytes.
229
+ *
230
+ * @example
231
+ * ```ts
232
+ * num.toMB(10485760); // 10
233
+ * ```
234
+ */
235
+ toMB(bytes, decimals = 2) {
236
+ return this.round(bytes / 1024 ** 2, decimals);
237
+ }
238
+ /**
239
+ * **Convert Bytes to Gigabytes (GB)**
240
+ *
241
+ * @param bytes - Number of raw bytes.
242
+ * @param decimals - Decimal places (default: `2`).
243
+ * @returns The value in Gigabytes.
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * num.toGB(1073741824); // 1
248
+ * ```
249
+ */
250
+ toGB(bytes, decimals = 2) {
251
+ return this.round(bytes / 1024 ** 3, decimals);
252
+ }
253
+ /**
254
+ * **Convert Bytes to Terabytes (TB)**
255
+ *
256
+ * @param bytes - Number of raw bytes.
257
+ * @param decimals - Decimal places (default: `2`).
258
+ * @returns The value in Terabytes.
259
+ *
260
+ * @example
261
+ * ```ts
262
+ * num.toTB(1099511627776); // 1
263
+ * ```
264
+ */
265
+ toTB(bytes, decimals = 2) {
266
+ return this.round(bytes / 1024 ** 4, decimals);
267
+ }
268
+ /**
269
+ * **Convert Bytes to Target Unit**
270
+ *
271
+ * Converts a byte count to any specified target unit (`KB`, `MB`, `GB`, `TB`, etc.).
272
+ *
273
+ * @param bytes - The number of bytes.
274
+ * @param targetUnit - Destination unit (e.g., `"MB"`, `"GB"`).
275
+ * @param decimals - Decimal places (default: `2`).
276
+ * @returns The converted numeric value.
277
+ *
278
+ * @example
279
+ * ```ts
280
+ * num.convertBytes(52428800, "MB"); // 50
281
+ * ```
282
+ */
283
+ convertBytes(bytes, targetUnit, decimals = 2) {
284
+ const unitMap = {
285
+ B: 1,
286
+ KB: 1024,
287
+ KIB: 1024,
288
+ MB: 1024 ** 2,
289
+ MIB: 1024 ** 2,
290
+ GB: 1024 ** 3,
291
+ GIB: 1024 ** 3,
292
+ TB: 1024 ** 4,
293
+ TIB: 1024 ** 4,
294
+ PB: 1024 ** 5,
295
+ PIB: 1024 ** 5,
296
+ EB: 1024 ** 6,
297
+ EIB: 1024 ** 6,
298
+ ZB: 1024 ** 7,
299
+ ZIB: 1024 ** 7,
300
+ YB: 1024 ** 8,
301
+ YIB: 1024 ** 8,
302
+ };
303
+ const divisor = unitMap[targetUnit.toUpperCase()] ?? 1;
304
+ return this.round(bytes / divisor, decimals);
305
+ }
306
+ /**
307
+ * **Transfer Rate / Speed**
308
+ *
309
+ * Calculates the transfer throughput given transferred bytes and elapsed duration in milliseconds.
310
+ *
311
+ * @param bytes - Transferred bytes.
312
+ * @param durationMs - Duration in milliseconds.
313
+ * @param decimals - Decimal places (default: `2`).
314
+ * @returns A formatted speed string (e.g., `"12.5 MB/s"`).
315
+ *
316
+ * @example
317
+ * ```ts
318
+ * num.transferRate(10485760, 2000); // "5 MB/s"
319
+ * ```
320
+ */
321
+ transferRate(bytes, durationMs, decimals = 2) {
322
+ if (durationMs <= 0 || bytes <= 0) {
323
+ return "0 Bytes/s";
324
+ }
325
+ const bytesPerSec = (bytes / durationMs) * 1000;
326
+ return `${this.formatBytes(bytesPerSec, decimals)}/s`;
327
+ }
328
+ /**
329
+ * **Transfer Estimated Time of Arrival (ETA)**
330
+ *
331
+ * Calculates the remaining time in seconds to complete a file or data transfer.
332
+ *
333
+ * @param remainingBytes - Bytes left to transfer.
334
+ * @param bytesPerSec - Transfer throughput in bytes per second.
335
+ * @returns Remaining time in seconds (rounded up), or `0` if invalid.
336
+ *
337
+ * @example
338
+ * ```ts
339
+ * num.transferETA(52428800, 10485760); // 5 (seconds)
340
+ * ```
341
+ */
342
+ transferETA(remainingBytes, bytesPerSec) {
343
+ if (bytesPerSec <= 0 || remainingBytes <= 0) {
344
+ return 0;
345
+ }
346
+ return Math.ceil(remainingBytes / bytesPerSec);
347
+ }
348
+ // ─────────────────────────────────────────────
349
+ // Percentages, Ratios & Progress
350
+ // ─────────────────────────────────────────────
351
+ /**
352
+ * **Calculate Percentage (Partial of Total)**
353
+ *
354
+ * Returns what percentage `partial` represents relative to `total`: `(partial / total) * 100`.
355
+ * Safely returns `0` if `total === 0` to prevent division by zero.
356
+ *
357
+ * @param partial - The partial amount.
358
+ * @param total - The reference total.
359
+ * @param decimals - Decimal places (default: `2`).
360
+ * @returns The percentage value (e.g. `25` for 25%).
361
+ *
362
+ * @example
363
+ * ```ts
364
+ * num.percentage(25, 200); // 12.5
365
+ * num.percentage(50, 100); // 50
366
+ * num.percentage(0, 0); // 0
367
+ * ```
368
+ */
369
+ percentage(partial, total, decimals = 2) {
370
+ if (total === 0) {
371
+ return 0;
372
+ }
373
+ return this.round((partial / total) * 100, decimals);
374
+ }
375
+ /**
376
+ * **Calculate Value From Percentage (X% of Total)**
377
+ *
378
+ * Returns the actual value corresponding to `percent` of `total`: `(percent / 100) * total`.
379
+ *
380
+ * @param percent - The percentage rate (e.g., `20` for 20%).
381
+ * @param total - The reference total.
382
+ * @param decimals - Decimal places (default: `2`).
383
+ * @returns The calculated portion.
384
+ *
385
+ * @example
386
+ * ```ts
387
+ * num.percentageOf(20, 150); // 30 (20% of 150 is 30)
388
+ * num.percentageOf(5.5, 200); // 11
389
+ * ```
390
+ */
391
+ percentageOf(percent, total, decimals = 2) {
392
+ return this.round((percent / 100) * total, decimals);
393
+ }
394
+ /**
395
+ * **Percentage Change / Growth Rate**
396
+ *
397
+ * Computes the relative percentage variation from `oldValue` to `newValue`.
398
+ * Positive indicates an increase, negative indicates a decrease.
399
+ *
400
+ * @param oldValue - The baseline value.
401
+ * @param newValue - The current value.
402
+ * @param decimals - Decimal precision (default: `2`).
403
+ * @returns Percentage variation.
404
+ *
405
+ * @example
406
+ * ```ts
407
+ * num.percentageChange(100, 150); // 50 (+50%)
408
+ * num.percentageChange(200, 150); // -25 (-25%)
409
+ * ```
410
+ */
411
+ percentageChange(oldValue, newValue, decimals = 2) {
412
+ if (oldValue === 0) {
413
+ return newValue === 0 ? 0 : 100;
414
+ }
415
+ const change = ((newValue - oldValue) / Math.abs(oldValue)) * 100;
416
+ return this.round(change, decimals);
417
+ }
418
+ /**
419
+ * **Progress Ratio**
420
+ *
421
+ * Calculates a normalized progress value clamped strictly between `0.0` and `1.0`.
422
+ * Ideal for progress bars and completion meters.
423
+ *
424
+ * @param current - The current progress value.
425
+ * @param total - The target total value.
426
+ * @returns A float between `0.0` and `1.0`.
427
+ *
428
+ * @example
429
+ * ```ts
430
+ * num.progress(50, 100); // 0.5
431
+ * num.progress(120, 100); // 1.0 (clamped)
432
+ * num.progress(-10, 100); // 0.0 (clamped)
433
+ * ```
434
+ */
435
+ progress(current, total) {
436
+ if (total <= 0) {
437
+ return 0;
438
+ }
439
+ return this.clamp(current / total, 0, 1);
440
+ }
441
+ // ─────────────────────────────────────────────
442
+ // Rounding & Decimal Precision
443
+ // ─────────────────────────────────────────────
444
+ /**
445
+ * **Accurate Decimal Rounding**
446
+ *
447
+ * Accurately rounds a number to a specified number of decimal places.
448
+ * Uses exponential notation internally to avoid common JavaScript floating-point rounding bugs (e.g. `1.005 * 100`).
449
+ *
450
+ * @param value - The number to round.
451
+ * @param decimals - Decimal places (default: `0`).
452
+ * @returns The rounded number.
453
+ *
454
+ * @example
455
+ * ```ts
456
+ * num.round(1.005, 2); // 1.01 (avoids standard JS Math.round bug)
457
+ * num.round(12.3456, 2); // 12.35
458
+ * num.round(12.3456, 0); // 12
459
+ * ```
460
+ */
461
+ round(value, decimals = 0) {
462
+ if (isNaN(value)) {
463
+ return NaN;
464
+ }
465
+ if (decimals <= 0) {
466
+ return Math.round(value);
467
+ }
468
+ return Number(Math.round(Number(value + "e" + decimals)) + "e-" + decimals);
469
+ }
470
+ /**
471
+ * **Accurate Decimal Ceil**
472
+ *
473
+ * Rounds up to the nearest specified decimal precision.
474
+ *
475
+ * @param value - The number to round up.
476
+ * @param decimals - Decimal places (default: `0`).
477
+ * @returns Ceiled value.
478
+ *
479
+ * @example
480
+ * ```ts
481
+ * num.ceil(1.234, 2); // 1.24
482
+ * num.ceil(1.2, 0); // 2
483
+ * ```
484
+ */
485
+ ceil(value, decimals = 0) {
486
+ if (isNaN(value)) {
487
+ return NaN;
488
+ }
489
+ if (decimals <= 0) {
490
+ return Math.ceil(value);
491
+ }
492
+ return Number(Math.ceil(Number(value + "e" + decimals)) + "e-" + decimals);
493
+ }
494
+ /**
495
+ * **Accurate Decimal Floor**
496
+ *
497
+ * Rounds down to the nearest specified decimal precision.
498
+ *
499
+ * @param value - The number to round down.
500
+ * @param decimals - Decimal places (default: `0`).
501
+ * @returns Floored value.
502
+ *
503
+ * @example
504
+ * ```ts
505
+ * num.floor(1.239, 2); // 1.23
506
+ * num.floor(1.9, 0); // 1
507
+ * ```
508
+ */
509
+ floor(value, decimals = 0) {
510
+ if (isNaN(value)) {
511
+ return NaN;
512
+ }
513
+ if (decimals <= 0) {
514
+ return Math.floor(value);
515
+ }
516
+ return Number(Math.floor(Number(value + "e" + decimals)) + "e-" + decimals);
517
+ }
518
+ /**
519
+ * **Round to Step / Multiple**
520
+ *
521
+ * Rounds a value to the nearest multiple of a given step (e.g., nearest `5`, `0.25`, or `100`).
522
+ *
523
+ * @param value - The value to round.
524
+ * @param step - The increment step size.
525
+ * @returns Value rounded to the nearest step.
526
+ *
527
+ * @example
528
+ * ```ts
529
+ * num.roundToStep(23, 5); // 25
530
+ * num.roundToStep(1.234, 0.05); // 1.25
531
+ * num.roundToStep(1430, 500); // 1500
532
+ * ```
533
+ */
534
+ roundToStep(value, step) {
535
+ if (step <= 0) {
536
+ return value;
537
+ }
538
+ const inv = 1 / step;
539
+ return Math.round(value * inv) / inv;
540
+ }
541
+ /**
542
+ * **Round to Significant Digits**
543
+ *
544
+ * Formats a number to a specified number of significant digits.
545
+ *
546
+ * @param value - The number to format.
547
+ * @param digits - Number of significant digits.
548
+ * @returns The number rounded to significant precision.
549
+ *
550
+ * @example
551
+ * ```ts
552
+ * num.toPrecision(12345, 3); // 12300
553
+ * num.toPrecision(0.001234, 2); // 0.0012
554
+ * ```
555
+ */
556
+ toPrecision(value, digits) {
557
+ if (isNaN(value) || digits <= 0) {
558
+ return value;
559
+ }
560
+ return parseFloat(value.toPrecision(digits));
561
+ }
562
+ // ─────────────────────────────────────────────
563
+ // Bounds, Clamping & Mapping
564
+ // ─────────────────────────────────────────────
5
565
  /**
6
566
  * **Clamp a Value**
7
567
  *
8
- * Restricts a numeric value to a defined range [min, max].
568
+ * Restricts a numeric value within a defined range `[min, max]`.
569
+ * If `min > max`, the bounds are automatically swapped for safety.
9
570
  *
10
571
  * @param value - The value to clamp.
11
572
  * @param min - The lower bound.
12
573
  * @param max - The upper bound.
13
574
  * @returns The clamped value.
575
+ *
576
+ * @example
577
+ * ```ts
578
+ * num.clamp(15, 0, 10); // 10
579
+ * num.clamp(-5, 0, 10); // 0
580
+ * num.clamp(5, 0, 10); // 5
581
+ * ```
14
582
  */
15
583
  clamp(value, min, max) {
16
- return Math.min(Math.max(value, min), max);
584
+ const lower = Math.min(min, max);
585
+ const upper = Math.max(min, max);
586
+ return Math.min(Math.max(value, lower), upper);
587
+ }
588
+ /**
589
+ * **Check if Value is in Range**
590
+ *
591
+ * Determines whether a value falls within `[min, max]`.
592
+ *
593
+ * @param value - The value to test.
594
+ * @param min - Range start.
595
+ * @param max - Range end.
596
+ * @param inclusive - Whether the bounds are inclusive (default: `true`).
597
+ * @returns `true` if value is within bounds.
598
+ *
599
+ * @example
600
+ * ```ts
601
+ * num.inRange(5, 1, 10); // true
602
+ * num.inRange(10, 1, 10, true); // true
603
+ * num.inRange(10, 1, 10, false);// false
604
+ * ```
605
+ */
606
+ inRange(value, min, max, inclusive = true) {
607
+ const lower = Math.min(min, max);
608
+ const upper = Math.max(min, max);
609
+ return inclusive
610
+ ? value >= lower && value <= upper
611
+ : value > lower && value < upper;
612
+ }
613
+ /**
614
+ * **Normalize a Value (0 to 1)**
615
+ *
616
+ * Maps a value from its original range `[min, max]` into a normalized unit float `[0.0, 1.0]`.
617
+ *
618
+ * @param value - The input value.
619
+ * @param min - The minimum of the original range.
620
+ * @param max - The maximum of the original range.
621
+ * @returns Normalized float.
622
+ *
623
+ * @example
624
+ * ```ts
625
+ * num.normalize(50, 0, 100); // 0.5
626
+ * num.normalize(15, 10, 20); // 0.5
627
+ * ```
628
+ */
629
+ normalize(value, min, max) {
630
+ if (min === max) {
631
+ return 0;
632
+ }
633
+ return (value - min) / (max - min);
17
634
  }
18
635
  /**
19
636
  * **Linear Interpolation (LERP)**
20
637
  *
21
- * Returns the linear interpolation between `start` and `end` for a factor `t`.
638
+ * Returns the linear interpolation between `start` and `end` for a factor `t`:
639
+ * Formula: `start * (1 - t) + end * t`.
22
640
  *
23
641
  * @param start - The start value.
24
642
  * @param end - The end value.
25
- * @param t - The interpolation factor (0.0 to 1.0).
643
+ * @param t - The interpolation factor (typically `0.0` to `1.0`).
26
644
  * @returns The interpolated value.
645
+ *
646
+ * @example
647
+ * ```ts
648
+ * num.lerp(0, 100, 0.5); // 50
649
+ * num.lerp(20, 80, 0.25); // 35
650
+ * ```
27
651
  */
28
652
  lerp(start, end, t) {
29
653
  return start * (1 - t) + end * t;
30
654
  }
655
+ /**
656
+ * **Map Range (Re-scale)**
657
+ *
658
+ * Maps an input number from an incoming range `[inMin, inMax]` to an outgoing range `[outMin, outMax]`.
659
+ *
660
+ * @param value - The number to map.
661
+ * @param inMin - Lower bound of input range.
662
+ * @param inMax - Upper bound of input range.
663
+ * @param outMin - Lower bound of output range.
664
+ * @param outMax - Upper bound of output range.
665
+ * @returns The re-scaled value.
666
+ *
667
+ * @example
668
+ * ```ts
669
+ * // Map a 0-10 rating to a 0-100 percentage
670
+ * num.mapRange(7.5, 0, 10, 0, 100); // 75
671
+ *
672
+ * // Map temperature sensor (0-1023) to degrees C (0-100)
673
+ * num.mapRange(511.5, 0, 1023, 0, 100); // 50
674
+ * ```
675
+ */
676
+ mapRange(value, inMin, inMax, outMin, outMax) {
677
+ if (inMin === inMax) {
678
+ return outMin;
679
+ }
680
+ return (((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin);
681
+ }
682
+ // ─────────────────────────────────────────────
683
+ // Aggregates & Statistics
684
+ // ─────────────────────────────────────────────
685
+ /**
686
+ * **Sum of Numbers**
687
+ *
688
+ * Computes the sum of all provided numbers or numeric arrays.
689
+ *
690
+ * @param numbers - Variadic numbers or number arrays.
691
+ * @returns Total sum.
692
+ *
693
+ * @example
694
+ * ```ts
695
+ * num.sum(1, 2, 3, 4); // 10
696
+ * num.sum([10, 20, 30]); // 60
697
+ * ```
698
+ */
699
+ sum(...numbers) {
700
+ const flat = this.flattenNumbers(numbers);
701
+ return flat.reduce((acc, curr) => acc + curr, 0);
702
+ }
703
+ /**
704
+ * **Average / Arithmetic Mean**
705
+ *
706
+ * Computes the arithmetic mean of all provided numbers or numeric arrays.
707
+ *
708
+ * @param numbers - Variadic numbers or number arrays.
709
+ * @returns The average, or `0` if empty.
710
+ *
711
+ * @example
712
+ * ```ts
713
+ * num.average(10, 20, 30); // 20
714
+ * num.average([5, 15]); // 10
715
+ * ```
716
+ */
717
+ average(...numbers) {
718
+ const flat = this.flattenNumbers(numbers);
719
+ if (flat.length === 0) {
720
+ return 0;
721
+ }
722
+ return this.sum(flat) / flat.length;
723
+ }
724
+ /**
725
+ * **Median**
726
+ *
727
+ * Returns the statistical median (middle value) of a set of numbers.
728
+ *
729
+ * @param numbers - Variadic numbers or number arrays.
730
+ * @returns The median value, or `0` if empty.
731
+ *
732
+ * @example
733
+ * ```ts
734
+ * num.median(1, 3, 5); // 3
735
+ * num.median([1, 2, 3, 4]); // 2.5
736
+ * ```
737
+ */
738
+ median(...numbers) {
739
+ const flat = this.flattenNumbers(numbers);
740
+ if (flat.length === 0) {
741
+ return 0;
742
+ }
743
+ const sorted = [...flat].sort((a, b) => a - b);
744
+ const mid = Math.floor(sorted.length / 2);
745
+ return sorted.length % 2 !== 0
746
+ ? sorted[mid]
747
+ : (sorted[mid - 1] + sorted[mid]) / 2;
748
+ }
749
+ /**
750
+ * **Minimum Value**
751
+ *
752
+ * Returns the minimum value from numbers or number arrays.
753
+ *
754
+ * @param numbers - Variadic numbers or number arrays.
755
+ * @returns The smallest number, or `Infinity` if no numbers provided.
756
+ *
757
+ * @example
758
+ * ```ts
759
+ * num.min(10, 5, 20); // 5
760
+ * num.min([12, 4, 89]); // 4
761
+ * ```
762
+ */
763
+ min(...numbers) {
764
+ const flat = this.flattenNumbers(numbers);
765
+ if (flat.length === 0) {
766
+ return Infinity;
767
+ }
768
+ return Math.min(...flat);
769
+ }
770
+ /**
771
+ * **Maximum Value**
772
+ *
773
+ * Returns the maximum value from numbers or number arrays.
774
+ *
775
+ * @param numbers - Variadic numbers or number arrays.
776
+ * @returns The largest number, or `-Infinity` if no numbers provided.
777
+ *
778
+ * @example
779
+ * ```ts
780
+ * num.max(10, 5, 20); // 20
781
+ * num.max([12, 4, 89]); // 89
782
+ * ```
783
+ */
784
+ max(...numbers) {
785
+ const flat = this.flattenNumbers(numbers);
786
+ if (flat.length === 0) {
787
+ return -Infinity;
788
+ }
789
+ return Math.max(...flat);
790
+ }
791
+ // ─────────────────────────────────────────────
792
+ // Pagination & Integer Division
793
+ // ─────────────────────────────────────────────
794
+ /**
795
+ * **Calculate Pagination**
796
+ *
797
+ * Computes all essential pagination parameters (`totalPages`, `offset`, `limit`, `startIndex`, `endIndex`, `hasNext`, `hasPrev`)
798
+ * for database queries and user interfaces.
799
+ *
800
+ * @param totalItems - Total number of items across the entire dataset.
801
+ * @param pageSize - Items per page (minimum 1).
802
+ * @param currentPage - Requested 1-based page number (default: `1`).
803
+ * @returns A structured {@link PaginationResult} object.
804
+ *
805
+ * @example
806
+ * ```ts
807
+ * const pageInfo = num.paginate(154, 20, 2);
808
+ * // {
809
+ * // currentPage: 2,
810
+ * // totalPages: 8,
811
+ * // pageSize: 20,
812
+ * // totalItems: 154,
813
+ * // offset: 20,
814
+ * // limit: 20,
815
+ * // startIndex: 20,
816
+ * // endIndex: 39,
817
+ * // hasNext: true,
818
+ * // hasPrev: true
819
+ * // }
820
+ * ```
821
+ */
822
+ paginate(totalItems, pageSize, currentPage = 1) {
823
+ const safeTotal = Math.max(0, totalItems);
824
+ const safePageSize = Math.max(1, pageSize);
825
+ const totalPages = Math.max(1, Math.ceil(safeTotal / safePageSize));
826
+ const safeCurrentPage = this.clamp(currentPage, 1, totalPages);
827
+ const offset = (safeCurrentPage - 1) * safePageSize;
828
+ const limit = safePageSize;
829
+ const startIndex = safeTotal === 0 ? 0 : offset;
830
+ const endIndex = safeTotal === 0
831
+ ? 0
832
+ : Math.min(offset + safePageSize - 1, safeTotal - 1);
833
+ return {
834
+ currentPage: safeCurrentPage,
835
+ totalPages,
836
+ pageSize: safePageSize,
837
+ totalItems: safeTotal,
838
+ offset,
839
+ limit,
840
+ startIndex,
841
+ endIndex,
842
+ hasNext: safeCurrentPage < totalPages,
843
+ hasPrev: safeCurrentPage > 1,
844
+ };
845
+ }
846
+ /**
847
+ * **Euclidean Division (DivMod)**
848
+ *
849
+ * Simultaneously calculates the integer quotient and remainder of two numbers: `[quotient, remainder]`.
850
+ * Extremely convenient for converting time units (seconds to minutes/seconds), grid positioning, and pagination.
851
+ *
852
+ * @param numerator - The dividend.
853
+ * @param denominator - The divisor.
854
+ * @returns A tuple `[quotient, remainder]`.
855
+ *
856
+ * @example
857
+ * ```ts
858
+ * const [minutes, seconds] = num.divmod(135, 60); // [2, 15] (2 min 15 sec)
859
+ * const [hours, remainingMin] = num.divmod(150, 60); // [2, 30] (2 h 30 min)
860
+ * ```
861
+ */
862
+ divmod(numerator, denominator) {
863
+ if (denominator === 0) {
864
+ return [NaN, NaN];
865
+ }
866
+ const quotient = Math.trunc(numerator / denominator);
867
+ const remainder = numerator % denominator;
868
+ return [quotient, remainder];
869
+ }
870
+ // ─────────────────────────────────────────────
871
+ // Secure Randomness
872
+ // ─────────────────────────────────────────────
31
873
  /**
32
874
  * **Get a Random Integer**
33
875
  *
34
- * Returns a random integer between min (inclusive) and max (inclusive).
876
+ * Generates a cryptographically secure random integer.
877
+ * Powered by `xypriss-security` Random core.
878
+ *
879
+ * - If one argument is provided: `[0, max)`
880
+ * - If two arguments are provided: `[min, max)`
35
881
  *
36
- * @param min - Minimum value.
37
- * @param max - Maximum value.
38
- * @returns A random integer.
882
+ * @param minOrMax - Minimum value (inclusive) or maximum if second argument is omitted.
883
+ * @param max - Maximum value (exclusive).
884
+ * @returns A secure random integer.
885
+ *
886
+ * @example
887
+ * ```ts
888
+ * num.randomInt(10); // Secure integer in [0, 10)
889
+ * num.randomInt(10, 50); // Secure integer in [10, 50)
890
+ * ```
39
891
  */
40
- randomInt(min, max) {
41
- return Math.floor(Math.random() * (max - min + 1)) + min;
892
+ randomInt(...I) {
893
+ return Random.Int(...I);
42
894
  }
895
+ /**
896
+ * **Get a Random Float**
897
+ *
898
+ * Generates a cryptographically secure random floating-point number between `min` and `max`.
899
+ *
900
+ * @param min - Lower bound (default: `0`).
901
+ * @param max - Upper bound (default: `1`).
902
+ * @param decimals - Decimal places (default: `4`).
903
+ * @returns A secure random float.
904
+ *
905
+ * @example
906
+ * ```ts
907
+ * num.randomFloat(); // e.g. 0.4821
908
+ * num.randomFloat(1.5, 9.5, 2); // e.g. 6.34
909
+ * ```
910
+ */
911
+ randomFloat(min = 0, max = 1, decimals = 4) {
912
+ let low = min;
913
+ let high = max;
914
+ if (low > high) {
915
+ [low, high] = [high, low];
916
+ }
917
+ const bytes = Random.Bytes(4);
918
+ const view = new DataView(bytes.buffer, bytes.byteOffset, 4);
919
+ const uint32 = view.getUint32(0, true);
920
+ const norm = uint32 / 0xffffffff;
921
+ const val = low + norm * (high - low);
922
+ return this.round(val, decimals);
923
+ }
924
+ /**
925
+ * **Random Item Selection**
926
+ *
927
+ * Randomly picks an element from an array using cryptographically secure random numbers.
928
+ *
929
+ * @param items - The candidate array.
930
+ * @returns A selected item from the array.
931
+ *
932
+ * @example
933
+ * ```ts
934
+ * num.randomChoice(["apple", "banana", "cherry"]); // e.g. "banana"
935
+ * ```
936
+ */
937
+ randomChoice(items) {
938
+ return Random.pick(items);
939
+ }
940
+ // ─────────────────────────────────────────────
941
+ // Formatting & Localization
942
+ // ─────────────────────────────────────────────
43
943
  /**
44
944
  * **Format Number**
45
945
  *
46
- * Locale-aware number formatting.
946
+ * Locale-aware number formatting powered by `Intl.NumberFormat`.
47
947
  *
48
948
  * @param value - The number to format.
49
- * @param locale - Optional locale (default: `en-US`).
50
- * @param options - Intl.NumberFormatOptions.
949
+ * @param locale - Optional BCP 47 locale (default: `en-US`).
950
+ * @param options - `Intl.NumberFormatOptions`.
51
951
  * @returns A localized string.
952
+ *
953
+ * @example
954
+ * ```ts
955
+ * num.formatNumber(1250000); // "1,250,000"
956
+ * num.formatNumber(1250000, "fr-FR"); // "1 250 000"
957
+ * ```
52
958
  */
53
959
  formatNumber(value, locale = "en-US", options) {
54
960
  return new Intl.NumberFormat(locale, options).format(value);
55
961
  }
56
962
  /**
57
- * **Format Bytes**
963
+ * **Compact Number Formatting**
58
964
  *
59
- * Converts raw bytes into a human-readable size string (KB, MB, GB, etc.).
965
+ * Formats large numbers compactly for dashboards, views, and metrics (e.g., `1.5K`, `2.3M`, `4.8B`).
60
966
  *
61
- * @param bytes - The number of bytes.
62
- * @param decimals - Decimal precision.
63
- * @returns A human-readable size string.
967
+ * @param value - The number to format.
968
+ * @param locale - Locale tag (default: `en-US`).
969
+ * @param decimals - Decimal digits (default: `1`).
970
+ * @returns A compact string.
971
+ *
972
+ * @example
973
+ * ```ts
974
+ * num.formatCompact(1500); // "1.5K"
975
+ * num.formatCompact(2500000); // "2.5M"
976
+ * num.formatCompact(1200000000); // "1.2B"
977
+ * ```
64
978
  */
65
- formatBytes(bytes, decimals = 2) {
66
- if (bytes === 0)
67
- return "0 Bytes";
68
- const k = 1024;
69
- const dm = decimals < 0 ? 0 : decimals;
70
- const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
71
- const i = Math.floor(Math.log(bytes) / Math.log(k));
72
- return (parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]);
979
+ formatCompact(value, locale = "en-US", decimals = 1) {
980
+ return new Intl.NumberFormat(locale, {
981
+ notation: "compact",
982
+ compactDisplay: "short",
983
+ maximumFractionDigits: decimals,
984
+ }).format(value);
985
+ }
986
+ /**
987
+ * **Format Currency**
988
+ *
989
+ * Formats a numeric value into a localized currency string.
990
+ *
991
+ * @param value - The amount to format.
992
+ * @param currency - ISO 4217 currency code (default: `"USD"`).
993
+ * @param locale - Locale tag (default: `"en-US"`).
994
+ * @param options - Additional `Intl.NumberFormatOptions`.
995
+ * @returns Formatted currency string.
996
+ *
997
+ * @example
998
+ * ```ts
999
+ * num.formatCurrency(49.99, "USD"); // "$49.99"
1000
+ * num.formatCurrency(49.99, "EUR", "fr-FR"); // "49,99 €"
1001
+ * ```
1002
+ */
1003
+ formatCurrency(value, currency = "USD", locale = "en-US", options) {
1004
+ return new Intl.NumberFormat(locale, {
1005
+ style: "currency",
1006
+ currency,
1007
+ ...options,
1008
+ }).format(value);
1009
+ }
1010
+ /**
1011
+ * **Format Percentage String**
1012
+ *
1013
+ * Formats a numeric value into a localized percentage string with the `%` symbol.
1014
+ *
1015
+ * @param value - The percentage value (e.g. `15.5` or `0.155` if `isRatio: true`).
1016
+ * @param decimals - Decimal places (default: `1`).
1017
+ * @param locale - Locale tag (default: `"en-US"`).
1018
+ * @param isRatio - If `true`, multiplies value by 100 first (default: `false`).
1019
+ * @returns Formatted percentage string.
1020
+ *
1021
+ * @example
1022
+ * ```ts
1023
+ * num.formatPercent(15.5); // "15.5%"
1024
+ * num.formatPercent(0.155, 1, "en-US", true); // "15.5%"
1025
+ * ```
1026
+ */
1027
+ formatPercent(value, decimals = 1, locale = "en-US", isRatio = false) {
1028
+ const percentValue = isRatio ? value * 100 : value;
1029
+ const formatted = this.formatNumber(this.round(percentValue, decimals), locale, {
1030
+ minimumFractionDigits: decimals,
1031
+ maximumFractionDigits: decimals,
1032
+ });
1033
+ return `${formatted}%`;
1034
+ }
1035
+ /**
1036
+ * **Ordinal Number Suffix**
1037
+ *
1038
+ * Appends the appropriate ordinal suffix to a number (e.g. `1st`, `2nd`, `3rd`, `4th` in English, or `1er`, `2e` in French).
1039
+ *
1040
+ * @param num - The integer number.
1041
+ * @param locale - Target locale (default: `"en-US"`).
1042
+ * @returns The number with its ordinal suffix.
1043
+ *
1044
+ * @example
1045
+ * ```ts
1046
+ * num.ordinal(1); // "1st"
1047
+ * num.ordinal(2); // "2nd"
1048
+ * num.ordinal(23); // "23rd"
1049
+ * num.ordinal(1, "fr-FR"); // "1er"
1050
+ * num.ordinal(2, "fr-FR"); // "2e"
1051
+ * ```
1052
+ */
1053
+ ordinal(num, locale = "en-US") {
1054
+ const intVal = Math.trunc(num);
1055
+ if (locale.startsWith("fr")) {
1056
+ return intVal === 1 ? "1er" : `${intVal}e`;
1057
+ }
1058
+ const abs = Math.abs(intVal);
1059
+ const mod100 = abs % 100;
1060
+ if (mod100 >= 11 && mod100 <= 13) {
1061
+ return `${intVal}th`;
1062
+ }
1063
+ switch (abs % 10) {
1064
+ case 1:
1065
+ return `${intVal}st`;
1066
+ case 2:
1067
+ return `${intVal}nd`;
1068
+ case 3:
1069
+ return `${intVal}rd`;
1070
+ default:
1071
+ return `${intVal}th`;
1072
+ }
1073
+ }
1074
+ // ─────────────────────────────────────────────
1075
+ // Predicates & Checks
1076
+ // ─────────────────────────────────────────────
1077
+ /**
1078
+ * **Check Even**
1079
+ *
1080
+ * Returns `true` if the number is an even integer.
1081
+ *
1082
+ * @param value - The number to test.
1083
+ * @returns `true` if even.
1084
+ *
1085
+ * @example
1086
+ * ```ts
1087
+ * num.isEven(4); // true
1088
+ * num.isEven(3); // false
1089
+ * ```
1090
+ */
1091
+ isEven(value) {
1092
+ return value % 2 === 0;
1093
+ }
1094
+ /**
1095
+ * **Check Odd**
1096
+ *
1097
+ * Returns `true` if the number is an odd integer.
1098
+ *
1099
+ * @param value - The number to test.
1100
+ * @returns `true` if odd.
1101
+ *
1102
+ * @example
1103
+ * ```ts
1104
+ * num.isOdd(3); // true
1105
+ * num.isOdd(4); // false
1106
+ * ```
1107
+ */
1108
+ isOdd(value) {
1109
+ return Math.abs(value % 2) === 1;
1110
+ }
1111
+ /**
1112
+ * **Check Integer**
1113
+ *
1114
+ * Returns `true` if the value is a safe, finite integer.
1115
+ *
1116
+ * @param value - The number to test.
1117
+ * @returns `true` if an integer.
1118
+ *
1119
+ * @example
1120
+ * ```ts
1121
+ * num.isInteger(42); // true
1122
+ * num.isInteger(42.5); // false
1123
+ * ```
1124
+ */
1125
+ isInteger(value) {
1126
+ return Number.isInteger(value);
1127
+ }
1128
+ /**
1129
+ * **Check Numeric**
1130
+ *
1131
+ * Checks if a value is a valid numeric value or a numeric string representing a finite number.
1132
+ *
1133
+ * @param value - Any value to test.
1134
+ * @returns Type guard confirming if the value is numeric.
1135
+ *
1136
+ * @example
1137
+ * ```ts
1138
+ * num.isNumeric(42); // true
1139
+ * num.isNumeric("42.5"); // true
1140
+ * num.isNumeric("abc"); // false
1141
+ * num.isNumeric(null); // false
1142
+ * ```
1143
+ */
1144
+ isNumeric(value) {
1145
+ if (typeof value === "number") {
1146
+ return !isNaN(value) && isFinite(value);
1147
+ }
1148
+ if (typeof value !== "string") {
1149
+ return false;
1150
+ }
1151
+ return (value.trim() !== "" &&
1152
+ !isNaN(Number(value)) &&
1153
+ isFinite(Number(value)));
1154
+ }
1155
+ /**
1156
+ * **Sign of a Number**
1157
+ *
1158
+ * Returns `-1` for negative numbers, `1` for positive numbers, and `0` for zero.
1159
+ *
1160
+ * @param value - The number.
1161
+ * @returns `-1 | 0 | 1`.
1162
+ *
1163
+ * @example
1164
+ * ```ts
1165
+ * num.sign(-42); // -1
1166
+ * num.sign(42); // 1
1167
+ * num.sign(0); // 0
1168
+ * ```
1169
+ */
1170
+ sign(value) {
1171
+ if (value > 0)
1172
+ return 1;
1173
+ if (value < 0)
1174
+ return -1;
1175
+ return 0;
73
1176
  }
74
1177
  }
75
1178