@vbarbarosh/node-helpers 3.72.5 → 3.73.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.73.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_jobs.js": "./src/array_chunk_jobs.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,83 @@ function array_chunk(array = [], limit = 1)
201
203
  module.exports = array_chunk;
202
204
 
203
205
 
206
+ /***/ }),
207
+
208
+ /***/ "./src/array_chunk_jobs.js":
209
+ /*!*********************************!*\
210
+ !*** ./src/array_chunk_jobs.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 of jobs between up to `max_threads`, while trying to put at least
218
+ * `min_jobs_per_thread` items into each chunk.
219
+ *
220
+ * - If an array is empty → returns [].
221
+ * - If there are too few items to satisfy `min_items_per_chunk`, even for 1 chunk,
222
+ * everything goes into a single chunk.
223
+ *
224
+ * Alternative names:
225
+ * - array_chunk_balanced
226
+ *
227
+ * @param array
228
+ * @param max_threads
229
+ * @param min_jobs_per_thread
230
+ * @returns {*[]}
231
+ */
232
+ function array_chunk_jobs(array = [], max_threads = 1, min_jobs_per_thread = 1)
233
+ {
234
+ const total_threads = clamp(1, max_threads, Math.floor(array.length / min_jobs_per_thread));
235
+ const jobs_per_thread = Math.floor(array.length / total_threads);
236
+ let extra = array.length % total_threads;
237
+
238
+ const out = [];
239
+ for (let i = 0; i < array.length; ) {
240
+ const size = jobs_per_thread + (extra ? 1 : 0);
241
+ out.push(array.slice(i, i + size));
242
+ i += size;
243
+ if (extra) {
244
+ extra--;
245
+ }
246
+ }
247
+ return out;
248
+ }
249
+
250
+ module.exports = array_chunk_jobs;
251
+
252
+
253
+ /***/ }),
254
+
255
+ /***/ "./src/array_fps.js":
256
+ /*!**************************!*\
257
+ !*** ./src/array_fps.js ***!
258
+ \**************************/
259
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
260
+
261
+ const round = __webpack_require__(/*! ./round */ "./src/round.js");
262
+
263
+ const EPS = 0.00001;
264
+
265
+ function array_fps(duration_sec, fps)
266
+ {
267
+ const total_frames = Math.floor(duration_sec * fps);
268
+
269
+ if (total_frames <= 1) {
270
+ return [0];
271
+ }
272
+
273
+ const out = [];
274
+ for (let i = 0; i < total_frames; ++i) {
275
+ out.push(round((i / (total_frames - 1)) * (duration_sec - EPS), EPS));
276
+ }
277
+ return out;
278
+ }
279
+
280
+ module.exports = array_fps;
281
+
282
+
204
283
  /***/ }),
205
284
 
206
285
  /***/ "./src/array_gcd.js":
@@ -644,6 +723,22 @@ function array_unique_last(array, fn = identity)
644
723
  module.exports = array_unique_last;
645
724
 
646
725
 
726
+ /***/ }),
727
+
728
+ /***/ "./src/clamp.js":
729
+ /*!**********************!*\
730
+ !*** ./src/clamp.js ***!
731
+ \**********************/
732
+ /***/ ((module) => {
733
+
734
+ function clamp(min, max, value)
735
+ {
736
+ return Math.max(min, Math.min(max, value));
737
+ }
738
+
739
+ module.exports = clamp;
740
+
741
+
647
742
  /***/ }),
648
743
 
649
744
  /***/ "./src/date_add_hours.js":
@@ -2124,6 +2219,117 @@ function random_int(min, max)
2124
2219
  module.exports = random_int;
2125
2220
 
2126
2221
 
2222
+ /***/ }),
2223
+
2224
+ /***/ "./src/round.js":
2225
+ /*!**********************!*\
2226
+ !*** ./src/round.js ***!
2227
+ \**********************/
2228
+ /***/ ((module) => {
2229
+
2230
+ function round(value, precision = 1)
2231
+ {
2232
+ if (precision === 0) {
2233
+ return value;
2234
+ }
2235
+
2236
+ const factor = 1 / precision;
2237
+ const sign = Math.sign(value);
2238
+
2239
+ // Count decimal places for the cleanup (toFixed)
2240
+ const exponent = get_decimal_places(precision);
2241
+
2242
+ // Small epsilon helps fix most floating point edge cases involving ties
2243
+ const epsilon = 1e-12*sign;
2244
+
2245
+ // Scale → Nudge → Round → Unscale
2246
+ const final = (Math.round(value*factor + epsilon) / factor);
2247
+
2248
+ // Cleanup trailing garbage such as: 34.038000000000004 → 34.038
2249
+ return Number(final.toFixed(exponent));
2250
+ }
2251
+
2252
+ function get_decimal_places(precision)
2253
+ {
2254
+ if (Math.floor(precision) === precision) {
2255
+ return 0;
2256
+ }
2257
+
2258
+ // Scientific notation support: e.g., 1e-20
2259
+ const e = precision.toExponential().split('e');
2260
+ if (e.length > 1) {
2261
+ return Math.abs(parseInt(e[1], 10));
2262
+ }
2263
+
2264
+ // Normal decimal notation (e.g., 0.001)
2265
+ return precision.toString().split('.')[1]?.length || 0;
2266
+ }
2267
+
2268
+ // // round(34.037531, 0.001) -> 34.038000000000004
2269
+ // function round(value, precision = 1)
2270
+ // {
2271
+ // return Math.round(value / precision) * precision;
2272
+ // }
2273
+
2274
+ // // round(1.005, 0.01) -> 1
2275
+ // function round(value, precision = 1)
2276
+ // {
2277
+ // const p = 1 / precision;
2278
+ // return Math.round(value * p) / p;
2279
+ // }
2280
+
2281
+ // function round(value, precision = 1)
2282
+ // {
2283
+ // if (precision === 0) {
2284
+ // return value;
2285
+ // }
2286
+ // const factor = 1 / precision;
2287
+ // const epsilon = 1e-12 * Math.sign(value);
2288
+ // return Number((Math.round((value * factor) + epsilon) / factor).toPrecision(15));
2289
+ // }
2290
+
2291
+ // function round(value, precision = 1)
2292
+ // {
2293
+ // if (precision === 0) {
2294
+ // return value;
2295
+ // }
2296
+ //
2297
+ // const factor = 1 / precision;
2298
+ //
2299
+ // // The exponent is critical for defining the precision of the cleanup
2300
+ // const exponent = Math.max(0, String(precision).length - 2);
2301
+ //
2302
+ // // 1. Calculate the scaled value.
2303
+ // let scaled = value * factor;
2304
+ //
2305
+ // // 2. Strategic Nudge: Add a tiny, signed epsilon (1e-12) to correct floating-point
2306
+ // // representation errors and force symmetric rounding (as Math.abs failed).
2307
+ // const epsilon = 1e-12 * Math.sign(value);
2308
+ //
2309
+ // // 3. Round the nudged value.
2310
+ // const roundedScaled = Math.round(scaled + epsilon);
2311
+ //
2312
+ // // 4. Unscale the value.
2313
+ // const result = roundedScaled / factor;
2314
+ //
2315
+ // // 5. CRITICAL FINAL STEP: Use .toFixed() with the required decimal places for cleanup.
2316
+ // // This stabilizes the result against the strict equality checks, including
2317
+ // // the safe integer boundary errors caused by the final division.
2318
+ // return Number(result.toFixed(exponent));
2319
+ // }
2320
+
2321
+ // 💎 The most precise solution, but - extra dependency.
2322
+ //
2323
+ // const Decimal = require('decimal.js');
2324
+ //
2325
+ // function round(value, precision = 1)
2326
+ // {
2327
+ // return new Decimal(value).div(precision).round().times(precision).toNumber();
2328
+ // }
2329
+
2330
+ module.exports = round;
2331
+
2332
+
2127
2333
  /***/ }),
2128
2334
 
2129
2335
  /***/ "./src/urlmod.js":
@@ -2273,10 +2479,10 @@ var __webpack_exports__ = {};
2273
2479
  \******************************/
2274
2480
  const ns = new URL(document.currentScript.src).searchParams.get('var') ?? 'h';
2275
2481
  if (typeof window[ns] !== 'undefined') {
2276
- console.log(`❌ @vbarbarosh/node-helpers@${"3.72.5"} was not injected — window.${ns} is already in use`);
2482
+ console.log(`❌ @vbarbarosh/node-helpers@${"3.73.0"} was not injected — window.${ns} is already in use`);
2277
2483
  }
2278
2484
  else {
2279
- console.log(`🎉 @vbarbarosh/node-helpers@${"3.72.5"} successfully exposed as window.${ns}`);
2485
+ console.log(`🎉 @vbarbarosh/node-helpers@${"3.73.0"} successfully exposed as window.${ns}`);
2280
2486
  window[ns] = {};
2281
2487
  // https://github.com/webpack/webpack/issues/625
2282
2488
  // 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.73.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,37 @@
1
+ const clamp = require('./clamp');
2
+
3
+ /**
4
+ * Split an array of jobs between up to `max_threads`, while trying to put at least
5
+ * `min_jobs_per_thread` items into each chunk.
6
+ *
7
+ * - If an array is empty → returns [].
8
+ * - If there are too few items to satisfy `min_items_per_chunk`, even for 1 chunk,
9
+ * everything goes into a single chunk.
10
+ *
11
+ * Alternative names:
12
+ * - array_chunk_balanced
13
+ *
14
+ * @param array
15
+ * @param max_threads
16
+ * @param min_jobs_per_thread
17
+ * @returns {*[]}
18
+ */
19
+ function array_chunk_jobs(array = [], max_threads = 1, min_jobs_per_thread = 1)
20
+ {
21
+ const total_threads = clamp(1, max_threads, Math.floor(array.length / min_jobs_per_thread));
22
+ const jobs_per_thread = Math.floor(array.length / total_threads);
23
+ let extra = array.length % total_threads;
24
+
25
+ const out = [];
26
+ for (let i = 0; i < array.length; ) {
27
+ const size = jobs_per_thread + (extra ? 1 : 0);
28
+ out.push(array.slice(i, i + size));
29
+ i += size;
30
+ if (extra) {
31
+ extra--;
32
+ }
33
+ }
34
+ return out;
35
+ }
36
+
37
+ module.exports = array_chunk_jobs;
@@ -0,0 +1,37 @@
1
+ const array_chunk_jobs = require('./array_chunk_jobs');
2
+ const assert = require('assert');
3
+
4
+ describe('array_chunk_jobs', function () {
5
+ it('should handle basic input', function () {
6
+ assert.deepStrictEqual(array_chunk_jobs([1]), [[1]]);
7
+ assert.deepStrictEqual(array_chunk_jobs([1,2]), [[1,2]]);
8
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3]), [[1,2,3]]);
9
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3,4,5], 3, 2), [[1,2,3],[4,5]]);
10
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3,4,5,6], 3, 2), [[1,2],[3,4],[5,6]]);
11
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3,4,5,6,7], 3, 2), [[1,2,3],[4,5],[6,7]]);
12
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3,4,5], 3, 3), [[1,2,3,4,5]]);
13
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3,4,5,6], 3, 3), [[1,2,3],[4,5,6]]);
14
+ assert.deepStrictEqual(array_chunk_jobs([1,2,3,4,5,6,7], 3, 3), [[1,2,3,4],[5,6,7]]);
15
+ assert.deepStrictEqual(array_chunk_jobs([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_jobs([]), []);
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_jobs([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_jobs([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,11 @@
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
+ });
11
+ });
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 () {