@vbarbarosh/node-helpers 3.72.5 → 3.74.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/README.md CHANGED
@@ -20,7 +20,7 @@ npm install @vbarbarosh/node-helpers
20
20
  A subset of `@vbarbarosh/node-helpers` is available for browser:
21
21
 
22
22
  ```html
23
- <script src="https://unpkg.com/@vbarbarosh/node-helpers@3.72.5/dist/browser.js"></script>
23
+ <script src="https://unpkg.com/@vbarbarosh/node-helpers@3.74.0/dist/browser.js"></script>
24
24
  ```
25
25
 
26
26
  ⚠️ All browser functions are exposed globally.
package/dist/browser.js CHANGED
@@ -80,6 +80,8 @@ function serializer(replacer, cycleReplacer) {
80
80
 
81
81
  var map = {
82
82
  "./array_chunk.js": "./src/array_chunk.js",
83
+ "./array_chunk_balanced.js": "./src/array_chunk_balanced.js",
84
+ "./array_fps.js": "./src/array_fps.js",
83
85
  "./array_gcd.js": "./src/array_gcd.js",
84
86
  "./array_group.js": "./src/array_group.js",
85
87
  "./array_group_map.js": "./src/array_group_map.js",
@@ -201,6 +203,88 @@ function array_chunk(array = [], limit = 1)
201
203
  module.exports = array_chunk;
202
204
 
203
205
 
206
+ /***/ }),
207
+
208
+ /***/ "./src/array_chunk_balanced.js":
209
+ /*!*************************************!*\
210
+ !*** ./src/array_chunk_balanced.js ***!
211
+ \*************************************/
212
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
213
+
214
+ const clamp = __webpack_require__(/*! ./clamp */ "./src/clamp.js");
215
+
216
+ /**
217
+ * Split an array into up to `max_chunks` balanced chunks, while ensuring that
218
+ * each chunk contains at least `min_items_per_chunk` items (when possible).
219
+ *
220
+ * The function distributes items as evenly as possible:
221
+ * - Chunk sizes differ by at most 1 item.
222
+ * - The number of chunks never exceeds `max_chunks`.
223
+ *
224
+ * Originally designed for distributing workload across threads
225
+ * (e.g. `array_chunk_balanced(jobs, max_threads, min_jobs_per_thread)`).
226
+ *
227
+ * Edge cases:
228
+ * - If the input array is empty → returns [].
229
+ * - If there are too few items to satisfy `min_items_per_chunk`, even for a single
230
+ * chunk, all items are placed into one chunk.
231
+ *
232
+ * @param {Array} array
233
+ * @param {number} max_chunks
234
+ * @param {number} min_items_per_chunk
235
+ * @returns {Array<Array>}
236
+ */
237
+ function array_chunk_balanced(array = [], max_chunks = 1, min_items_per_chunk = 1)
238
+ {
239
+ const total_chunks = clamp(1, max_chunks, Math.floor(array.length / min_items_per_chunk));
240
+ const items_per_chunk = Math.floor(array.length / total_chunks);
241
+ let extra = array.length % total_chunks;
242
+
243
+ const out = [];
244
+ for (let i = 0; i < array.length; ) {
245
+ const size = items_per_chunk + (extra ? 1 : 0);
246
+ out.push(array.slice(i, i + size));
247
+ i += size;
248
+ if (extra) {
249
+ extra--;
250
+ }
251
+ }
252
+ return out;
253
+ }
254
+
255
+ module.exports = array_chunk_balanced;
256
+
257
+
258
+ /***/ }),
259
+
260
+ /***/ "./src/array_fps.js":
261
+ /*!**************************!*\
262
+ !*** ./src/array_fps.js ***!
263
+ \**************************/
264
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
265
+
266
+ const round = __webpack_require__(/*! ./round */ "./src/round.js");
267
+
268
+ const EPS = 0.00001;
269
+
270
+ function array_fps(duration_sec, fps)
271
+ {
272
+ const total_frames = Math.floor(duration_sec * fps);
273
+
274
+ if (total_frames <= 1) {
275
+ return [0];
276
+ }
277
+
278
+ const out = [];
279
+ for (let i = 0; i < total_frames; ++i) {
280
+ out.push(round((i / (total_frames - 1)) * (duration_sec - EPS), EPS));
281
+ }
282
+ return out;
283
+ }
284
+
285
+ module.exports = array_fps;
286
+
287
+
204
288
  /***/ }),
205
289
 
206
290
  /***/ "./src/array_gcd.js":
@@ -644,6 +728,22 @@ function array_unique_last(array, fn = identity)
644
728
  module.exports = array_unique_last;
645
729
 
646
730
 
731
+ /***/ }),
732
+
733
+ /***/ "./src/clamp.js":
734
+ /*!**********************!*\
735
+ !*** ./src/clamp.js ***!
736
+ \**********************/
737
+ /***/ ((module) => {
738
+
739
+ function clamp(min, max, value)
740
+ {
741
+ return Math.max(min, Math.min(max, value));
742
+ }
743
+
744
+ module.exports = clamp;
745
+
746
+
647
747
  /***/ }),
648
748
 
649
749
  /***/ "./src/date_add_hours.js":
@@ -2124,6 +2224,117 @@ function random_int(min, max)
2124
2224
  module.exports = random_int;
2125
2225
 
2126
2226
 
2227
+ /***/ }),
2228
+
2229
+ /***/ "./src/round.js":
2230
+ /*!**********************!*\
2231
+ !*** ./src/round.js ***!
2232
+ \**********************/
2233
+ /***/ ((module) => {
2234
+
2235
+ function round(value, precision = 1)
2236
+ {
2237
+ if (precision === 0) {
2238
+ return value;
2239
+ }
2240
+
2241
+ const factor = 1 / precision;
2242
+ const sign = Math.sign(value);
2243
+
2244
+ // Count decimal places for the cleanup (toFixed)
2245
+ const exponent = get_decimal_places(precision);
2246
+
2247
+ // Small epsilon helps fix most floating point edge cases involving ties
2248
+ const epsilon = 1e-12*sign;
2249
+
2250
+ // Scale → Nudge → Round → Unscale
2251
+ const final = (Math.round(value*factor + epsilon) / factor);
2252
+
2253
+ // Cleanup trailing garbage such as: 34.038000000000004 → 34.038
2254
+ return Number(final.toFixed(exponent));
2255
+ }
2256
+
2257
+ function get_decimal_places(precision)
2258
+ {
2259
+ if (Math.floor(precision) === precision) {
2260
+ return 0;
2261
+ }
2262
+
2263
+ // Scientific notation support: e.g., 1e-20
2264
+ const e = precision.toExponential().split('e');
2265
+ if (e.length > 1) {
2266
+ return Math.abs(parseInt(e[1], 10));
2267
+ }
2268
+
2269
+ // Normal decimal notation (e.g., 0.001)
2270
+ return precision.toString().split('.')[1]?.length || 0;
2271
+ }
2272
+
2273
+ // // round(34.037531, 0.001) -> 34.038000000000004
2274
+ // function round(value, precision = 1)
2275
+ // {
2276
+ // return Math.round(value / precision) * precision;
2277
+ // }
2278
+
2279
+ // // round(1.005, 0.01) -> 1
2280
+ // function round(value, precision = 1)
2281
+ // {
2282
+ // const p = 1 / precision;
2283
+ // return Math.round(value * p) / p;
2284
+ // }
2285
+
2286
+ // function round(value, precision = 1)
2287
+ // {
2288
+ // if (precision === 0) {
2289
+ // return value;
2290
+ // }
2291
+ // const factor = 1 / precision;
2292
+ // const epsilon = 1e-12 * Math.sign(value);
2293
+ // return Number((Math.round((value * factor) + epsilon) / factor).toPrecision(15));
2294
+ // }
2295
+
2296
+ // function round(value, precision = 1)
2297
+ // {
2298
+ // if (precision === 0) {
2299
+ // return value;
2300
+ // }
2301
+ //
2302
+ // const factor = 1 / precision;
2303
+ //
2304
+ // // The exponent is critical for defining the precision of the cleanup
2305
+ // const exponent = Math.max(0, String(precision).length - 2);
2306
+ //
2307
+ // // 1. Calculate the scaled value.
2308
+ // let scaled = value * factor;
2309
+ //
2310
+ // // 2. Strategic Nudge: Add a tiny, signed epsilon (1e-12) to correct floating-point
2311
+ // // representation errors and force symmetric rounding (as Math.abs failed).
2312
+ // const epsilon = 1e-12 * Math.sign(value);
2313
+ //
2314
+ // // 3. Round the nudged value.
2315
+ // const roundedScaled = Math.round(scaled + epsilon);
2316
+ //
2317
+ // // 4. Unscale the value.
2318
+ // const result = roundedScaled / factor;
2319
+ //
2320
+ // // 5. CRITICAL FINAL STEP: Use .toFixed() with the required decimal places for cleanup.
2321
+ // // This stabilizes the result against the strict equality checks, including
2322
+ // // the safe integer boundary errors caused by the final division.
2323
+ // return Number(result.toFixed(exponent));
2324
+ // }
2325
+
2326
+ // 💎 The most precise solution, but - extra dependency.
2327
+ //
2328
+ // const Decimal = require('decimal.js');
2329
+ //
2330
+ // function round(value, precision = 1)
2331
+ // {
2332
+ // return new Decimal(value).div(precision).round().times(precision).toNumber();
2333
+ // }
2334
+
2335
+ module.exports = round;
2336
+
2337
+
2127
2338
  /***/ }),
2128
2339
 
2129
2340
  /***/ "./src/urlmod.js":
@@ -2273,10 +2484,10 @@ var __webpack_exports__ = {};
2273
2484
  \******************************/
2274
2485
  const ns = new URL(document.currentScript.src).searchParams.get('var') ?? 'h';
2275
2486
  if (typeof window[ns] !== 'undefined') {
2276
- console.log(`❌ @vbarbarosh/node-helpers@${"3.72.5"} was not injected — window.${ns} is already in use`);
2487
+ console.log(`❌ @vbarbarosh/node-helpers@${"3.74.0"} was not injected — window.${ns} is already in use`);
2277
2488
  }
2278
2489
  else {
2279
- console.log(`🎉 @vbarbarosh/node-helpers@${"3.72.5"} successfully exposed as window.${ns}`);
2490
+ console.log(`🎉 @vbarbarosh/node-helpers@${"3.74.0"} successfully exposed as window.${ns}`);
2280
2491
  window[ns] = {};
2281
2492
  // https://github.com/webpack/webpack/issues/625
2282
2493
  // https://webpack.js.org/guides/dependency-management/#require-context
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "Vladimir Barbarosh <vladimir.barbarosh@gmail.com>",
3
3
  "license": "MIT",
4
4
  "name": "@vbarbarosh/node-helpers",
5
- "version": "3.72.5",
5
+ "version": "3.74.0",
6
6
  "description": "A set of helpers for JavaScript/Node.js",
7
7
  "unpkg": "dist/browser.js",
8
8
  "jsdelivr": "dist/browser.js",
@@ -0,0 +1,42 @@
1
+ const clamp = require('./clamp');
2
+
3
+ /**
4
+ * Split an array into up to `max_chunks` balanced chunks, while ensuring that
5
+ * each chunk contains at least `min_items_per_chunk` items (when possible).
6
+ *
7
+ * The function distributes items as evenly as possible:
8
+ * - Chunk sizes differ by at most 1 item.
9
+ * - The number of chunks never exceeds `max_chunks`.
10
+ *
11
+ * Originally designed for distributing workload across threads
12
+ * (e.g. `array_chunk_balanced(jobs, max_threads, min_jobs_per_thread)`).
13
+ *
14
+ * Edge cases:
15
+ * - If the input array is empty → returns [].
16
+ * - If there are too few items to satisfy `min_items_per_chunk`, even for a single
17
+ * chunk, all items are placed into one chunk.
18
+ *
19
+ * @param {Array} array
20
+ * @param {number} max_chunks
21
+ * @param {number} min_items_per_chunk
22
+ * @returns {Array<Array>}
23
+ */
24
+ function array_chunk_balanced(array = [], max_chunks = 1, min_items_per_chunk = 1)
25
+ {
26
+ const total_chunks = clamp(1, max_chunks, Math.floor(array.length / min_items_per_chunk));
27
+ const items_per_chunk = Math.floor(array.length / total_chunks);
28
+ let extra = array.length % total_chunks;
29
+
30
+ const out = [];
31
+ for (let i = 0; i < array.length; ) {
32
+ const size = items_per_chunk + (extra ? 1 : 0);
33
+ out.push(array.slice(i, i + size));
34
+ i += size;
35
+ if (extra) {
36
+ extra--;
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+
42
+ module.exports = array_chunk_balanced;
@@ -0,0 +1,37 @@
1
+ const array_chunk_balanced = require('./array_chunk_balanced');
2
+ const assert = require('assert');
3
+
4
+ describe('array_chunk_balanced', function () {
5
+ it('should handle basic input', function () {
6
+ assert.deepStrictEqual(array_chunk_balanced([1]), [[1]]);
7
+ assert.deepStrictEqual(array_chunk_balanced([1,2]), [[1,2]]);
8
+ assert.deepStrictEqual(array_chunk_balanced([1,2,3]), [[1,2,3]]);
9
+ assert.deepStrictEqual(array_chunk_balanced([1,2,3,4,5], 3, 2), [[1,2,3],[4,5]]);
10
+ assert.deepStrictEqual(array_chunk_balanced([1,2,3,4,5,6], 3, 2), [[1,2],[3,4],[5,6]]);
11
+ assert.deepStrictEqual(array_chunk_balanced([1,2,3,4,5,6,7], 3, 2), [[1,2,3],[4,5],[6,7]]);
12
+ assert.deepStrictEqual(array_chunk_balanced([1,2,3,4,5], 3, 3), [[1,2,3,4,5]]);
13
+ assert.deepStrictEqual(array_chunk_balanced([1,2,3,4,5,6], 3, 3), [[1,2,3],[4,5,6]]);
14
+ assert.deepStrictEqual(array_chunk_balanced([1,2,3,4,5,6,7], 3, 3), [[1,2,3,4],[5,6,7]]);
15
+ assert.deepStrictEqual(array_chunk_balanced([1,2,3,4,5,6,7,8], 3, 3), [[1,2,3,4],[5,6,7,8]]);
16
+ });
17
+ it('edge: If an array is empty → returns []', function () {
18
+ assert.deepStrictEqual(array_chunk_balanced([]), []);
19
+ });
20
+ it('edge: If there are too few items to satisfy `min_items_per_chunk`, even for 1 chunk, everything goes into a single chunk', function () {
21
+ assert.deepStrictEqual(array_chunk_balanced([1,2,3], 5, 5), [[1,2,3]]);
22
+ });
23
+ it('should never produce more chunks than max_threads', function () {
24
+ // 12 jobs, max 5 threads, min 2 jobs per thread
25
+ // Correct result MUST have exactly 5 chunks
26
+ assert.deepStrictEqual(
27
+ array_chunk_balanced([1,2,3,4,5,6,7,8,9,10,11,12], 5, 2),
28
+ [
29
+ [1,2,3],
30
+ [4,5,6],
31
+ [7,8],
32
+ [9,10],
33
+ [11,12]
34
+ ]
35
+ );
36
+ });
37
+ });
@@ -0,0 +1,20 @@
1
+ const round = require('./round');
2
+
3
+ const EPS = 0.00001;
4
+
5
+ function array_fps(duration_sec, fps)
6
+ {
7
+ const total_frames = Math.floor(duration_sec * fps);
8
+
9
+ if (total_frames <= 1) {
10
+ return [0];
11
+ }
12
+
13
+ const out = [];
14
+ for (let i = 0; i < total_frames; ++i) {
15
+ out.push(round((i / (total_frames - 1)) * (duration_sec - EPS), EPS));
16
+ }
17
+ return out;
18
+ }
19
+
20
+ module.exports = array_fps;
@@ -0,0 +1,72 @@
1
+ const array_fps = require('./array_fps');
2
+ const assert = require('assert');
3
+
4
+ describe('array_fps', function () {
5
+ it('should handle basic input', function () {
6
+ assert.deepStrictEqual(array_fps(1, 1), [0]);
7
+ assert.deepStrictEqual(array_fps(1, 2), [0, 0.99999]);
8
+ assert.deepStrictEqual(array_fps(1, 3), [0, 0.49999, 0.99999]);
9
+ assert.deepStrictEqual(array_fps(1, 4), [0, 0.33333, 0.66666, 0.99999]);
10
+ assert.deepStrictEqual(array_fps(5, 20), [
11
+ 0, 0.0505, 0.10101, 0.15151, 0.20202, 0.25252, 0.30303, 0.35353, 0.40404, 0.45454,
12
+ 0.50505, 0.55555, 0.60606, 0.65656, 0.70707, 0.75757, 0.80808, 0.85858, 0.90909, 0.95959,
13
+ 1.0101, 1.0606, 1.11111, 1.16161, 1.21212, 1.26262, 1.31313, 1.36363, 1.41414, 1.46464,
14
+ 1.51515, 1.56565, 1.61616, 1.66666, 1.71717, 1.76767, 1.81818, 1.86868, 1.91919, 1.96969,
15
+ 2.0202, 2.0707, 2.12121, 2.17171, 2.22222, 2.27272, 2.32323, 2.37373, 2.42424, 2.47474,
16
+ 2.52525, 2.57575, 2.62626, 2.67676, 2.72727, 2.77777, 2.82828, 2.87878, 2.92929, 2.97979,
17
+ 3.0303, 3.0808, 3.13131, 3.18181, 3.23232, 3.28282, 3.33333, 3.38383, 3.43434, 3.48484,
18
+ 3.53535, 3.58585, 3.63636, 3.68686, 3.73737, 3.78787, 3.83838, 3.88888, 3.93939, 3.98989,
19
+ 4.0404, 4.0909, 4.14141, 4.19191, 4.24242, 4.29292, 4.34343, 4.39393, 4.44444, 4.49494,
20
+ 4.54545, 4.59595, 4.64646, 4.69696, 4.74747, 4.79797, 4.84848, 4.89898, 4.94949, 4.99999,
21
+ ]);
22
+ assert.deepStrictEqual(array_fps(5, 30), [
23
+ 0, 0.03356, 0.06711, 0.10067, 0.13423, 0.16778, 0.20134, 0.2349, 0.26846, 0.30201,
24
+ 0.33557, 0.36913, 0.40268, 0.43624, 0.4698, 0.50335, 0.53691, 0.57047, 0.60403, 0.63758,
25
+ 0.67114, 0.7047, 0.73825, 0.77181, 0.80537, 0.83892, 0.87248, 0.90604, 0.9396, 0.97315,
26
+ 1.00671, 1.04027, 1.07382, 1.10738, 1.14094, 1.17449, 1.20805, 1.24161, 1.27517, 1.30872,
27
+ 1.34228, 1.37584, 1.40939, 1.44295, 1.47651, 1.51006, 1.54362, 1.57718, 1.61074, 1.64429,
28
+ 1.67785, 1.71141, 1.74496, 1.77852, 1.81208, 1.84563, 1.87919, 1.91275, 1.9463, 1.97986,
29
+ 2.01342, 2.04698, 2.08053, 2.11409, 2.14765, 2.1812, 2.21476, 2.24832, 2.28187, 2.31543,
30
+ 2.34899, 2.38255, 2.4161, 2.44966, 2.48322, 2.51677, 2.55033, 2.58389, 2.61744, 2.651,
31
+ 2.68456, 2.71812, 2.75167, 2.78523, 2.81879, 2.85234, 2.8859, 2.91946, 2.95301, 2.98657,
32
+ 3.02013, 3.05369, 3.08724, 3.1208, 3.15436, 3.18791, 3.22147, 3.25503, 3.28858, 3.32214,
33
+ 3.3557, 3.38925, 3.42281, 3.45637, 3.48993, 3.52348, 3.55704, 3.5906, 3.62415, 3.65771,
34
+ 3.69127, 3.72482, 3.75838, 3.79194, 3.8255, 3.85905, 3.89261, 3.92617, 3.95972, 3.99328,
35
+ 4.02684, 4.06039, 4.09395, 4.12751, 4.16107, 4.19462, 4.22818, 4.26174, 4.29529, 4.32885,
36
+ 4.36241, 4.39596, 4.42952, 4.46308, 4.49664, 4.53019, 4.56375, 4.59731, 4.63086, 4.66442,
37
+ 4.69798, 4.73153, 4.76509, 4.79865, 4.83221, 4.86576, 4.89932, 4.93288, 4.96643, 4.99999,
38
+ ]);
39
+ assert.deepStrictEqual(array_fps(5, 60), [
40
+ 0, 0.01672, 0.03344, 0.05017, 0.06689, 0.08361, 0.10033, 0.11706, 0.13378, 0.1505,
41
+ 0.16722, 0.18395, 0.20067, 0.21739, 0.23411, 0.25084, 0.26756, 0.28428, 0.301, 0.31773,
42
+ 0.33445, 0.35117, 0.36789, 0.38461, 0.40134, 0.41806, 0.43478, 0.4515, 0.46823, 0.48495,
43
+ 0.50167, 0.51839, 0.53512, 0.55184, 0.56856, 0.58528, 0.60201, 0.61873, 0.63545, 0.65217,
44
+ 0.66889, 0.68562, 0.70234, 0.71906, 0.73578, 0.75251, 0.76923, 0.78595, 0.80267, 0.8194,
45
+ 0.83612, 0.85284, 0.86956, 0.88629, 0.90301, 0.91973, 0.93645, 0.95318, 0.9699, 0.98662,
46
+ 1.00334, 1.02006, 1.03679, 1.05351, 1.07023, 1.08695, 1.10368, 1.1204, 1.13712, 1.15384,
47
+ 1.17057, 1.18729, 1.20401, 1.22073, 1.23746, 1.25418, 1.2709, 1.28762, 1.30435, 1.32107,
48
+ 1.33779, 1.35451, 1.37123, 1.38796, 1.40468, 1.4214, 1.43812, 1.45485, 1.47157, 1.48829,
49
+ 1.50501, 1.52174, 1.53846, 1.55518, 1.5719, 1.58863, 1.60535, 1.62207, 1.63879, 1.65552,
50
+ 1.67224, 1.68896, 1.70568, 1.7224, 1.73913, 1.75585, 1.77257, 1.78929, 1.80602, 1.82274,
51
+ 1.83946, 1.85618, 1.87291, 1.88963, 1.90635, 1.92307, 1.9398, 1.95652, 1.97324, 1.98996,
52
+ 2.00668, 2.02341, 2.04013, 2.05685, 2.07357, 2.0903, 2.10702, 2.12374, 2.14046, 2.15719,
53
+ 2.17391, 2.19063, 2.20735, 2.22408, 2.2408, 2.25752, 2.27424, 2.29097, 2.30769, 2.32441,
54
+ 2.34113, 2.35785, 2.37458, 2.3913, 2.40802, 2.42474, 2.44147, 2.45819, 2.47491, 2.49163,
55
+ 2.50836, 2.52508, 2.5418, 2.55852, 2.57525, 2.59197, 2.60869, 2.62541, 2.64214, 2.65886,
56
+ 2.67558, 2.6923, 2.70902, 2.72575, 2.74247, 2.75919, 2.77591, 2.79264, 2.80936, 2.82608,
57
+ 2.8428, 2.85953, 2.87625, 2.89297, 2.90969, 2.92642, 2.94314, 2.95986, 2.97658, 2.99331,
58
+ 3.01003, 3.02675, 3.04347, 3.06019, 3.07692, 3.09364, 3.11036, 3.12708, 3.14381, 3.16053,
59
+ 3.17725, 3.19397, 3.2107, 3.22742, 3.24414, 3.26086, 3.27759, 3.29431, 3.31103, 3.32775,
60
+ 3.34447, 3.3612, 3.37792, 3.39464, 3.41136, 3.42809, 3.44481, 3.46153, 3.47825, 3.49498,
61
+ 3.5117, 3.52842, 3.54514, 3.56187, 3.57859, 3.59531, 3.61203, 3.62876, 3.64548, 3.6622,
62
+ 3.67892, 3.69564, 3.71237, 3.72909, 3.74581, 3.76253, 3.77926, 3.79598, 3.8127, 3.82942,
63
+ 3.84615, 3.86287, 3.87959, 3.89631, 3.91304, 3.92976, 3.94648, 3.9632, 3.97993, 3.99665,
64
+ 4.01337, 4.03009, 4.04681, 4.06354, 4.08026, 4.09698, 4.1137, 4.13043, 4.14715, 4.16387,
65
+ 4.18059, 4.19732, 4.21404, 4.23076, 4.24748, 4.26421, 4.28093, 4.29765, 4.31437, 4.3311,
66
+ 4.34782, 4.36454, 4.38126, 4.39798, 4.41471, 4.43143, 4.44815, 4.46487, 4.4816, 4.49832,
67
+ 4.51504, 4.53176, 4.54849, 4.56521, 4.58193, 4.59865, 4.61538, 4.6321, 4.64882, 4.66554,
68
+ 4.68226, 4.69899, 4.71571, 4.73243, 4.74915, 4.76588, 4.7826, 4.79932, 4.81604, 4.83277,
69
+ 4.84949, 4.86621, 4.88293, 4.89966, 4.91638, 4.9331, 4.94982, 4.96655, 4.98327, 4.99999,
70
+ ]);
71
+ });
72
+ });
package/src/fs_lsr.js CHANGED
@@ -3,6 +3,9 @@ const fs_fi = require('./fs_fi');
3
3
  const fs_path_join = require('./fs_path_join');
4
4
  const fs_readdir = require('./fs_readdir');
5
5
 
6
+ /**
7
+ * @deprecated Deprecated in favor or fs_walk
8
+ */
6
9
  async function fs_lsr(pathname = '.')
7
10
  {
8
11
  const fi = await fs_fi(pathname);
package/src/fs_walk.js ADDED
@@ -0,0 +1,40 @@
1
+ const fs_lstat = require('./fs_lstat');
2
+ const fs_path_join = require('./fs_path_join');
3
+ const fs_readdir = require('./fs_readdir');
4
+ const ignore = require('./ignore');
5
+
6
+ async function fs_walk({path = '.', user_friendly_status = ignore, error_handler = error_handler, signal})
7
+ {
8
+ const out = [];
9
+ const queue = [path];
10
+ while (queue.length) {
11
+ if (signal) {
12
+ signal.throwIfAborted();
13
+ }
14
+ try {
15
+ const current_path = queue.pop();
16
+ user_friendly_status(`Reading ${current_path}`);
17
+ const lstat = await fs_lstat(current_path);
18
+ lstat.path = current_path;
19
+ out.push(lstat);
20
+ if (lstat.isDirectory()) {
21
+ if (current_path === '/dev' || current_path === '/proc') {
22
+ continue;
23
+ }
24
+ const basenames = await fs_readdir(current_path);
25
+ basenames.forEach(v => queue.push(fs_path_join(current_path, v)));
26
+ }
27
+ }
28
+ catch (error) {
29
+ error_handler(error);
30
+ }
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function error_handler(error)
36
+ {
37
+ console.log(`⚠️ ${error.message}`);
38
+ }
39
+
40
+ module.exports = fs_walk;
package/src/round.js CHANGED
@@ -1,3 +1,41 @@
1
+ function round(value, precision = 1)
2
+ {
3
+ if (precision === 0) {
4
+ return value;
5
+ }
6
+
7
+ const factor = 1 / precision;
8
+ const sign = Math.sign(value);
9
+
10
+ // Count decimal places for the cleanup (toFixed)
11
+ const exponent = get_decimal_places(precision);
12
+
13
+ // Small epsilon helps fix most floating point edge cases involving ties
14
+ const epsilon = 1e-12*sign;
15
+
16
+ // Scale → Nudge → Round → Unscale
17
+ const final = (Math.round(value*factor + epsilon) / factor);
18
+
19
+ // Cleanup trailing garbage such as: 34.038000000000004 → 34.038
20
+ return Number(final.toFixed(exponent));
21
+ }
22
+
23
+ function get_decimal_places(precision)
24
+ {
25
+ if (Math.floor(precision) === precision) {
26
+ return 0;
27
+ }
28
+
29
+ // Scientific notation support: e.g., 1e-20
30
+ const e = precision.toExponential().split('e');
31
+ if (e.length > 1) {
32
+ return Math.abs(parseInt(e[1], 10));
33
+ }
34
+
35
+ // Normal decimal notation (e.g., 0.001)
36
+ return precision.toString().split('.')[1]?.length || 0;
37
+ }
38
+
1
39
  // // round(34.037531, 0.001) -> 34.038000000000004
2
40
  // function round(value, precision = 1)
3
41
  // {
@@ -11,14 +49,53 @@
11
49
  // return Math.round(value * p) / p;
12
50
  // }
13
51
 
14
- function round(value, precision = 1)
15
- {
16
- if (precision === 0) {
17
- return value;
18
- }
19
- const factor = 1 / precision;
20
- const epsilon = 1e-12 * Math.sign(value);
21
- return Math.round((value * factor) + epsilon) / factor;
22
- }
52
+ // function round(value, precision = 1)
53
+ // {
54
+ // if (precision === 0) {
55
+ // return value;
56
+ // }
57
+ // const factor = 1 / precision;
58
+ // const epsilon = 1e-12 * Math.sign(value);
59
+ // return Number((Math.round((value * factor) + epsilon) / factor).toPrecision(15));
60
+ // }
61
+
62
+ // function round(value, precision = 1)
63
+ // {
64
+ // if (precision === 0) {
65
+ // return value;
66
+ // }
67
+ //
68
+ // const factor = 1 / precision;
69
+ //
70
+ // // The exponent is critical for defining the precision of the cleanup
71
+ // const exponent = Math.max(0, String(precision).length - 2);
72
+ //
73
+ // // 1. Calculate the scaled value.
74
+ // let scaled = value * factor;
75
+ //
76
+ // // 2. Strategic Nudge: Add a tiny, signed epsilon (1e-12) to correct floating-point
77
+ // // representation errors and force symmetric rounding (as Math.abs failed).
78
+ // const epsilon = 1e-12 * Math.sign(value);
79
+ //
80
+ // // 3. Round the nudged value.
81
+ // const roundedScaled = Math.round(scaled + epsilon);
82
+ //
83
+ // // 4. Unscale the value.
84
+ // const result = roundedScaled / factor;
85
+ //
86
+ // // 5. CRITICAL FINAL STEP: Use .toFixed() with the required decimal places for cleanup.
87
+ // // This stabilizes the result against the strict equality checks, including
88
+ // // the safe integer boundary errors caused by the final division.
89
+ // return Number(result.toFixed(exponent));
90
+ // }
91
+
92
+ // 💎 The most precise solution, but - extra dependency.
93
+ //
94
+ // const Decimal = require('decimal.js');
95
+ //
96
+ // function round(value, precision = 1)
97
+ // {
98
+ // return new Decimal(value).div(precision).round().times(precision).toNumber();
99
+ // }
23
100
 
24
101
  module.exports = round;
package/src/round.test.js CHANGED
@@ -6,6 +6,7 @@ const items = [
6
6
  [37.5, 25, 50],
7
7
  [34.037531, 0.001, 34.038],
8
8
  [1.005, 0.01, 1.01],
9
+ [0.99999, 0.00001, 0.99999],
9
10
 
10
11
  // Synthetic cases generated by DeepSeek
11
12
  [1.005, 0.001, 1.005], // More precise rounding
@@ -27,6 +28,23 @@ const items = [
27
28
  [999.999, 0.001, 999.999], // Rounding that causes carry-over
28
29
  [0.0000000000001, 0.01, 0], // Extremely small number
29
30
  [Number.MAX_SAFE_INTEGER, 1000, 9007199254741000], // Large numbers
31
+
32
+ [0.005, 0.01, 0.01],
33
+ [-1.5, 1, -2], // Symmetric negative rounding (Math.abs is critical here)
34
+ [-0.5, 1, -1], // Symmetric negative rounding for half
35
+ [123456789123.45, 1000, 123456789000], // Testing very large numbers
36
+ [1.2345e-20, 1e-20, 1e-20], // Testing very small numbers
37
+ [1.0000000000000001, 0.0000000000000001, 1.0000000000000000], // Testing rounding near Number.EPSILON range
38
+
39
+ [9007199254740992, 1, 9007199254740992],
40
+ [9007199254740991.5, 1, 9007199254740992],
41
+ [Number.MIN_SAFE_INTEGER, 1, Number.MIN_SAFE_INTEGER],
42
+ [Number.MAX_SAFE_INTEGER, 1, Number.MAX_SAFE_INTEGER],
43
+
44
+ // Out of scope of standard JS
45
+ // [0.004999999999999999, 0.01, 0.0],
46
+ // [Number.MIN_SAFE_INTEGER, 0.1, Number.MIN_SAFE_INTEGER],
47
+ // [Number.MAX_SAFE_INTEGER, 0.1, Number.MAX_SAFE_INTEGER],
30
48
  ];
31
49
 
32
50
  describe('round', function () {