@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 +1 -1
- package/dist/browser.js +208 -2
- package/package.json +1 -1
- package/src/array_chunk_jobs.js +37 -0
- package/src/array_chunk_jobs.test.js +37 -0
- package/src/array_fps.js +20 -0
- package/src/array_fps.test.js +11 -0
- package/src/fs_lsr.js +3 -0
- package/src/fs_walk.js +40 -0
- package/src/round.js +86 -9
- package/src/round.test.js +18 -0
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
});
|
package/src/array_fps.js
ADDED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 () {
|