@gkucmierz/utils 5.1.0 → 5.4.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/main.mjs CHANGED
@@ -41,6 +41,9 @@ import {
41
41
  import {
42
42
 
43
43
  } from './src/developer-utils/get-type.mjs'
44
+ import {
45
+ detectGridColumns, navigateGrid
46
+ } from './src/developer-utils/grid-navigation.mjs'
44
47
  import {
45
48
  measurePerformance
46
49
  } from './src/developer-utils/measure-performance.mjs'
@@ -50,6 +53,9 @@ import {
50
53
  import {
51
54
  naturalSearch
52
55
  } from './src/developer-utils/natural-search.mjs'
56
+ import {
57
+ QUORUM_CONFIG, QUORUM_DEFAULT_K, QUORUM_DEFAULT_N, QUORUM_MAX_RELATIVE_ERROR, QUORUM_MAX_RELATIVE_ERROR_PCT, QUORUM_ZERO_FLOOR_EPSILON, computeRelativeError, evaluateQuorum, evaluateQuorum4of5, isQuorumEqual
58
+ } from './src/developer-utils/quorum.mjs'
53
59
  import {
54
60
  randNormal
55
61
  } from './src/developer-utils/rand-normal.mjs'
@@ -60,17 +66,20 @@ import {
60
66
  setSafeInterval
61
67
  } from './src/developer-utils/set-safe-interval.mjs'
62
68
  import {
63
- Type, Types, is, of, same
69
+ Type, Types, is, isCallable, of, same
64
70
  } from './src/developer-utils/type.mjs'
65
71
  import {
66
72
  barycentricCoordinates
67
73
  } from './src/geometry/barycentric.mjs'
68
74
  import {
69
- axisAngleToMatrix4, crossProduct, dotProduct, getRotationMatrixFromVectors, multiplyMatrix4, normalize, projectToTrackball
75
+ applyScreenDragQuat, axisAngleToMatrix4, axisAngleToQuat, crossProduct, dotProduct, eulerYXZToQuat, getRotationMatrixFromVectors, matToEulerYXZ, matToQuat, multiplyMatrix4, multiplyQuat, normalize, normalizeQuat, projectToTrackball, quatToMat, rotateVecByQuat, slerp
70
76
  } from './src/geometry/math3d.mjs'
71
77
  import {
72
78
  matrixAsArray
73
79
  } from './src/geometry/matrix.mjs'
80
+ import {
81
+ OrthoSet, containsPoint1D, containsPointNd, diffBox, diffRange, formatRange, measureBox, measureRange, mutualBox, mutualRange, parseRange, range, sharedMeasureNd, sumBox, sumRange, xorBox, xorRange
82
+ } from './src/geometry/range-set.mjs'
74
83
  import {
75
84
  egcd
76
85
  } from './src/number-theory/egcd.mjs'
@@ -140,6 +149,12 @@ import {
140
149
  import {
141
150
  formatBigNumber, formatBigNumberBI, wrapFn
142
151
  } from './src/string-arrays/format-big-number.mjs'
152
+ import {
153
+ parseHeading
154
+ } from './src/string-arrays/parse-heading.mjs'
155
+ import {
156
+ slugify
157
+ } from './src/string-arrays/slugify.mjs'
143
158
 
144
159
  export * from './src/automata/langtons-ant.mjs';
145
160
  export * from './src/combinatorics/combinations.mjs';
@@ -155,9 +170,11 @@ export * from './src/data-structures/list-node.mjs';
155
170
  export * from './src/developer-utils/binary-search.mjs';
156
171
  export * from './src/developer-utils/consume-iterator.mjs';
157
172
  export * from './src/developer-utils/get-type.mjs';
173
+ export * from './src/developer-utils/grid-navigation.mjs';
158
174
  export * from './src/developer-utils/measure-performance.mjs';
159
175
  export * from './src/developer-utils/memoize.mjs';
160
176
  export * from './src/developer-utils/natural-search.mjs';
177
+ export * from './src/developer-utils/quorum.mjs';
161
178
  export * from './src/developer-utils/rand-normal.mjs';
162
179
  export * from './src/developer-utils/range-array.mjs';
163
180
  export * from './src/developer-utils/set-safe-interval.mjs';
@@ -165,6 +182,7 @@ export * from './src/developer-utils/type.mjs';
165
182
  export * from './src/geometry/barycentric.mjs';
166
183
  export * from './src/geometry/math3d.mjs';
167
184
  export * from './src/geometry/matrix.mjs';
185
+ export * from './src/geometry/range-set.mjs';
168
186
  export * from './src/number-theory/egcd.mjs';
169
187
  export * from './src/number-theory/factors.mjs';
170
188
  export * from './src/number-theory/gcd.mjs';
@@ -188,7 +206,9 @@ export * from './src/string-arrays/bijective-numeration.mjs';
188
206
  export * from './src/string-arrays/chunks.mjs';
189
207
  export * from './src/string-arrays/copy-case.mjs';
190
208
  export * from './src/string-arrays/format-big-number.mjs';
209
+ export * from './src/string-arrays/parse-heading.mjs';
210
+ export * from './src/string-arrays/slugify.mjs';
191
211
 
192
212
  export default [
193
- createLangtonsAnt, createUnlimitedGrid, combinations, combinationsIterator, bin2gray, gray2bin, nChooseK, permutations, permutationsIterator, ConflatingQueue, SetCnt, Stack, Trie, Heap, ListNode, binarySearchArr, binarySearchGE, binarySearchLE, binarySearchRangeIncl, consumeIteratorNonBlocking, measurePerformance, memoize, naturalSearch, randNormal, array2range, range2array, setSafeInterval, Type, Types, is, of, same, barycentricCoordinates, axisAngleToMatrix4, crossProduct, dotProduct, getRotationMatrixFromVectors, multiplyMatrix4, normalize, projectToTrackball, matrixAsArray, egcd, factors, factorsBI, gcd, gcdBI, lcm, lcmBI, lucasLehmerBI, mobius, mobiusBI, mod, modBI, phi, phiBI, powMod, powModBI, tonelliShanksBI, nelderMead, particleSwarmOptimization, simulatedAnnealing, goldenRatio, goldenRatioBI, goldenRatioStr, gpn, gpnBI, heronsFormula, heronsFormulaBI, squareRoot, squareRootBI, arrayHistogram, fromBase64, fromBase64Url, toBase64, toBase64Url, bijective2num, bijective2numBI, num2bijective, num2bijectiveBI, chunks, chunksAsyncIterator, chunksIterator, copyCase, formatBigNumber, formatBigNumberBI, wrapFn
213
+ createLangtonsAnt, createUnlimitedGrid, combinations, combinationsIterator, bin2gray, gray2bin, nChooseK, permutations, permutationsIterator, ConflatingQueue, SetCnt, Stack, Trie, Heap, ListNode, binarySearchArr, binarySearchGE, binarySearchLE, binarySearchRangeIncl, consumeIteratorNonBlocking, detectGridColumns, navigateGrid, measurePerformance, memoize, naturalSearch, QUORUM_CONFIG, QUORUM_DEFAULT_K, QUORUM_DEFAULT_N, QUORUM_MAX_RELATIVE_ERROR, QUORUM_MAX_RELATIVE_ERROR_PCT, QUORUM_ZERO_FLOOR_EPSILON, computeRelativeError, evaluateQuorum, evaluateQuorum4of5, isQuorumEqual, randNormal, array2range, range2array, setSafeInterval, Type, Types, is, isCallable, of, same, barycentricCoordinates, applyScreenDragQuat, axisAngleToMatrix4, axisAngleToQuat, crossProduct, dotProduct, eulerYXZToQuat, getRotationMatrixFromVectors, matToEulerYXZ, matToQuat, multiplyMatrix4, multiplyQuat, normalize, normalizeQuat, projectToTrackball, quatToMat, rotateVecByQuat, slerp, matrixAsArray, OrthoSet, containsPoint1D, containsPointNd, diffBox, diffRange, formatRange, measureBox, measureRange, mutualBox, mutualRange, parseRange, range, sharedMeasureNd, sumBox, sumRange, xorBox, xorRange, egcd, factors, factorsBI, gcd, gcdBI, lcm, lcmBI, lucasLehmerBI, mobius, mobiusBI, mod, modBI, phi, phiBI, powMod, powModBI, tonelliShanksBI, nelderMead, particleSwarmOptimization, simulatedAnnealing, goldenRatio, goldenRatioBI, goldenRatioStr, gpn, gpnBI, heronsFormula, heronsFormulaBI, squareRoot, squareRootBI, arrayHistogram, fromBase64, fromBase64Url, toBase64, toBase64Url, bijective2num, bijective2numBI, num2bijective, num2bijectiveBI, chunks, chunksAsyncIterator, chunksIterator, copyCase, formatBigNumber, formatBigNumberBI, wrapFn, parseHeading, slugify
194
214
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gkucmierz/utils",
3
- "version": "5.1.0",
3
+ "version": "5.4.0",
4
4
  "type": "module",
5
5
  "description": "Usefull functions for solving programming tasks",
6
6
  "keywords": [
@@ -74,7 +74,7 @@
74
74
  "docs:build": "jsdoc -c jsdoc.json && cp -r docs/@gkucmierz/utils/*/* docs/ && rm -rf docs/@gkucmierz",
75
75
  "docs:open": "open docs/index.html",
76
76
  "lint": "eslint .",
77
- "prepare": "husky"
77
+ "prepare": "git config core.hooksPath .githooks || true"
78
78
  },
79
79
  "repository": {
80
80
  "type": "git",
@@ -88,7 +88,6 @@
88
88
  "docdash": "^2.0.2",
89
89
  "eslint": "^10.1.0",
90
90
  "globals": "^17.4.0",
91
- "husky": "^8.0.3",
92
91
  "jasmine": "^4.4.0",
93
92
  "jsdoc": "^4.0.4",
94
93
  "lz-string": "^1.5.0",
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @module developer-utils
3
+ */
4
+
5
+ /**
6
+ * Resolves the next 0-based index when navigating a 2D grid (matrix) that may contain
7
+ * trailing gaps in its last row (`totalItems` not divisible by `columns`).
8
+ *
9
+ * Pure mathematical/index function — completely independent of the DOM.
10
+ *
11
+ * Wrapping configuration hierarchy:
12
+ * - `wrap` (boolean, default `true`): master default for both axes.
13
+ * - `wrapHoriz` / `wrapHorizontal` (boolean, optional): overrides `wrap` for horizontal movement (`left` / `right`).
14
+ * - `wrapVert` / `wrapVertical` (boolean, optional): overrides `wrap` for vertical movement (`up` / `down`).
15
+ * - `rowStrict` (boolean, default `false`): when `true`, horizontal movement wraps/clamps strictly within the current row instead of reading order.
16
+ *
17
+ * When `columns === 1` (single-column layout), `right` behaves identically to `down`
18
+ * and `left` behaves identically to `up`.
19
+ *
20
+ * @param {number} currentIndex - Current active 0-based index (`0 <= currentIndex < totalItems`).
21
+ * @param {number} totalItems - Total number of items in the grid ($N \ge 0$).
22
+ * @param {'left'|'right'|'up'|'down'|'ArrowLeft'|'ArrowRight'|'ArrowUp'|'ArrowDown'} direction - Movement direction.
23
+ * @param {number|Object} [columnsOrOptions=1] - Column count ($C \ge 1$) or options object.
24
+ * @param {Object} [maybeOptions={}] - Optional configuration when 4th argument is a column count.
25
+ * @returns {number} Next 0-based index, or `-1` if `totalItems <= 0`.
26
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IHYEMBuIDm64CmA4jCACYIC+CAZjFGAgEQACOA1gK4DGYIBMAF4B6TnBAAbAM5MA3MGAZseQiXLygA|▶ Try it live in Instacode}
27
+ */
28
+ export const navigateGrid = (
29
+ currentIndex,
30
+ totalItems,
31
+ direction,
32
+ columnsOrOptions = 1,
33
+ maybeOptions = {}
34
+ ) => {
35
+ if (!Number.isFinite(totalItems) || totalItems <= 0) {
36
+ return -1;
37
+ }
38
+
39
+ const isObj = columnsOrOptions !== null && typeof columnsOrOptions === 'object';
40
+ const opts = isObj ? columnsOrOptions : (maybeOptions || {});
41
+ const rawCols = isObj ? opts.columns : columnsOrOptions;
42
+ const cols = Math.max(1, Math.floor(Number(rawCols) || 1));
43
+
44
+ const cur =
45
+ Number.isFinite(currentIndex) && currentIndex >= 0 && currentIndex < totalItems
46
+ ? Math.floor(currentIndex)
47
+ : 0;
48
+
49
+ const baseWrap = typeof opts.wrap === 'boolean' ? opts.wrap : true;
50
+ const wrapHoriz =
51
+ typeof opts.wrapHorizontal === 'boolean'
52
+ ? opts.wrapHorizontal
53
+ : typeof opts.wrapHoriz === 'boolean'
54
+ ? opts.wrapHoriz
55
+ : baseWrap;
56
+ const wrapVert =
57
+ typeof opts.wrapVertical === 'boolean'
58
+ ? opts.wrapVertical
59
+ : typeof opts.wrapVert === 'boolean'
60
+ ? opts.wrapVert
61
+ : baseWrap;
62
+ const rowStrict = Boolean(opts.rowStrict);
63
+
64
+ const row = Math.floor(cur / cols);
65
+ const col = cur % cols;
66
+
67
+ let deltaH = 0;
68
+ let deltaV = 0;
69
+
70
+ switch (direction) {
71
+ case 'right':
72
+ case 'ArrowRight':
73
+ deltaH = 1;
74
+ break;
75
+ case 'left':
76
+ case 'ArrowLeft':
77
+ deltaH = -1;
78
+ break;
79
+ case 'down':
80
+ case 'ArrowDown':
81
+ deltaV = 1;
82
+ break;
83
+ case 'up':
84
+ case 'ArrowUp':
85
+ deltaV = -1;
86
+ break;
87
+ default:
88
+ return cur;
89
+ }
90
+
91
+ // Horizontal movement (left / right)
92
+ if (deltaH !== 0) {
93
+ if (rowStrict) {
94
+ const rowStart = row * cols;
95
+ const rowLen = Math.min(cols, totalItems - rowStart);
96
+ if (rowLen <= 0) return cur;
97
+ const nextCol = col + deltaH;
98
+ if (nextCol >= 0 && nextCol < rowLen) {
99
+ return rowStart + nextCol;
100
+ }
101
+ if (wrapHoriz) {
102
+ return rowStart + (((nextCol % rowLen) + rowLen) % rowLen);
103
+ }
104
+ return cur;
105
+ }
106
+
107
+ const nextIdx = cur + deltaH;
108
+ if (nextIdx >= 0 && nextIdx < totalItems) {
109
+ return nextIdx;
110
+ }
111
+ if (wrapHoriz) {
112
+ return ((nextIdx % totalItems) + totalItems) % totalItems;
113
+ }
114
+ return cur;
115
+ }
116
+
117
+ // Vertical movement (up / down) along column `col` (handling incomplete last row gaps)
118
+ const colRows = col < totalItems ? Math.floor((totalItems - 1 - col) / cols) + 1 : 0;
119
+ if (colRows <= 0) return cur;
120
+
121
+ const nextRow = row + deltaV;
122
+ if (nextRow >= 0 && nextRow < colRows) {
123
+ return nextRow * cols + col;
124
+ }
125
+ if (wrapVert) {
126
+ const wrappedRow = ((nextRow % colRows) + colRows) % colRows;
127
+ return wrappedRow * cols + col;
128
+ }
129
+ return cur;
130
+ };
131
+
132
+ /**
133
+ * Pure higher-order geometric utility that detects the number of columns in the first row
134
+ * of a 2D layout from an iterable of items and a row-offset projector function.
135
+ *
136
+ * Counts how many consecutive leading items share the same row coordinate (`y` / `top`)
137
+ * within the given `tolerance`. Completely independent of the DOM.
138
+ *
139
+ * @param {Iterable<*>} items - Iterable of numbers (row offsets) or arbitrary items.
140
+ * @param {Function|number} [getRowOffset=(x) => x] - Pure projector `(item, index) => number` (or numeric tolerance if 2nd arg is a number).
141
+ * @param {number} [tolerance=0] - Maximum allowed absolute difference between an item's row offset and the first item's offset.
142
+ * @returns {number} Detected number of columns in the first row ($\ge 1$).
143
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4ICYFM7oMZwOIwioDCUANgK5gB2AzggL4IBmMUYCARAAIDmA1hWxgQ6GAC8A9BTggytTgG5gwDFlwEipSjVrKgA|▶ Try it live in Instacode}
144
+ */
145
+ export const detectGridColumns = (items, getRowOffset = (x) => x, tolerance = 0) => {
146
+ if (!items || typeof items[Symbol.iterator] !== 'function') {
147
+ return 1;
148
+ }
149
+
150
+ const isFn = typeof getRowOffset === 'function';
151
+ const projector = isFn ? getRowOffset : (x) => x;
152
+ const tol = Math.max(0, Number(isFn ? tolerance : getRowOffset) || 0);
153
+
154
+ let firstOffset = null;
155
+ let firstRowCount = 0;
156
+ let idx = 0;
157
+
158
+ for (const item of items) {
159
+ const raw = projector(item, idx++);
160
+ const offset = typeof raw === 'number' ? raw : Number(raw);
161
+ if (!Number.isFinite(offset)) {
162
+ continue;
163
+ }
164
+ if (firstOffset === null) {
165
+ firstOffset = offset;
166
+ firstRowCount = 1;
167
+ } else if (Math.abs(offset - firstOffset) <= tol) {
168
+ firstRowCount += 1;
169
+ } else {
170
+ break;
171
+ }
172
+ }
173
+
174
+ return Math.max(1, firstRowCount);
175
+ };
@@ -0,0 +1,345 @@
1
+ /**
2
+ * @module developer-utils
3
+ */
4
+
5
+ import { is } from './type.mjs';
6
+
7
+ /**
8
+ * Default required agreeing datapoints ($K = 4$) in a standard $4\text{-of-}5$ quorum check.
9
+ * @type {number}
10
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IIoFUDyAldBZAfQBEBRAMQEF0AZAFQIGkEBfBAMxijAQCIABAOYBrAK4BjMCACmMAF4B6EXBAAbAM48A3MGAYc+YuSp1G2oA|▶ Try it live in Instacode}
11
+ */
12
+ export const QUORUM_DEFAULT_K = 4;
13
+
14
+ /**
15
+ * Default total independent sources ($N = 5$) in a standard $4\text{-of-}5$ quorum check.
16
+ * @type {number}
17
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IIoFUDyAldBZAfQBEBRAMQEF0AZAFQIDkEBfBAMxijAQCIABAOYBrAK4BjMCACmMAF4B6EXBAAbAM48A3MGAYc+YuSp1G2oA|▶ Try it live in Instacode}
18
+ */
19
+ export const QUORUM_DEFAULT_N = 5;
20
+
21
+ /**
22
+ * Default maximum relative error threshold ($0.01\% = 0.0001 = 1\text{ bp}$) for two datapoints
23
+ * to be considered equal in a quorum evaluation.
24
+ * @type {number}
25
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IIoFUDyAldBZAfTwEEANA7AUQBliAVASQDVKDLtscEBfBAMxhQwCAEQABAOYBrAK4BjMCACmMAF4B6GXBAAbAM4iA3MGAYc+ImQo16zVu07ZjQA|▶ Try it live in Instacode}
26
+ */
27
+ export const QUORUM_MAX_RELATIVE_ERROR = 0.0001;
28
+
29
+ /**
30
+ * Default maximum relative error expressed in percent ($0.01\%$).
31
+ * @type {number}
32
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IIoFUDyAldBZAfTwEEANA7AUQBliAVASQDVKDLtscCAFAYToQBfBADMYUMAgBEAAQDmAawCuAYzAgApjABeAeiVwQAGwDOUgNzBgGHPiJkKNes1btO2Hv0tA|▶ Try it live in Instacode}
33
+ */
34
+ export const QUORUM_MAX_RELATIVE_ERROR_PCT = 0.01;
35
+
36
+ /**
37
+ * Safe denominator floor near zero to avoid division by zero when comparing $0$ vs $0$.
38
+ * @type {number}
39
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IIoFUDyAldBZAfQC0BRbTAgMQBlMcCSAFAZQElaA5BAXwQDMYUMAgBEAAQDmAawCuAYzAgApjABeAehlwQAGwDOIgNzBgGHPmJkKNOtgYt2mDsaA|▶ Try it live in Instacode}
40
+ */
41
+ export const QUORUM_ZERO_FLOOR_EPSILON = 1e-9;
42
+
43
+ /**
44
+ * Frozen SSoT configuration object for $K\text{-of-}N$ quorum evaluation.
45
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IIoFUDyAldBZAfQGFMA5AMQEkBxBAXwQDMYowEAiAAQHMBrAVwDGYEAFMYALwD0-OCAA2AZ3YBuYMAw58xMlWpqgA|▶ Try it live in Instacode}
46
+ */
47
+ export const QUORUM_CONFIG = Object.freeze({
48
+ DEFAULT_K: QUORUM_DEFAULT_K,
49
+ DEFAULT_N: QUORUM_DEFAULT_N,
50
+ MAX_RELATIVE_ERROR: QUORUM_MAX_RELATIVE_ERROR,
51
+ MAX_RELATIVE_ERROR_PCT: QUORUM_MAX_RELATIVE_ERROR_PCT,
52
+ ZERO_FLOOR_EPSILON: QUORUM_ZERO_FLOOR_EPSILON
53
+ });
54
+
55
+ const isFiniteScalar = (v) => is(v, Number) && Number.isFinite(v);
56
+
57
+ const isNumericVector = (v) =>
58
+ is(v, Array) && v.length > 0 && v.every((item) => isFiniteScalar(item));
59
+
60
+ const cleanFloat = (val) => Number(Number(val).toFixed(10));
61
+
62
+ /**
63
+ * Computes the symmetric relative error between two numbers or two equal-length numeric vectors:
64
+ * $$\delta_{\text{rel}}(a, b) = \frac{|a - b|}{\max(|a|, |b|, \varepsilon_{\text{floor}})}$$
65
+ *
66
+ * For vectors $\mathbf{a}, \mathbf{b} \in \mathbb{R}^D$, returns the worst-case component-wise
67
+ * relative error $\max_{d} \delta_{\text{rel}}(a_d, b_d)$.
68
+ *
69
+ * @param {number|number[]} a - First value or numeric vector.
70
+ * @param {number|number[]} b - Second value or numeric vector.
71
+ * @param {Object} [options={}] - Options object.
72
+ * @param {number} [options.zeroFloor=1e-9] - Denominator floor when both values are near zero.
73
+ * @returns {number} Relative error in $[0, \infty)$ (`0.0001` corresponds to `0.01%`).
74
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IMZUgVzgUwEo4A2AhnCAG44CiMMsCAvggGZ1gIBEAAgOYDWmVGBA4YALwD02EIQDOHANzBg6LLgIkylGnRhKgA|▶ Try it live in Instacode}
75
+ */
76
+ export const computeRelativeError = (
77
+ a,
78
+ b,
79
+ { zeroFloor = QUORUM_ZERO_FLOOR_EPSILON } = {}
80
+ ) => {
81
+ const safeFloor = isFiniteScalar(zeroFloor) && zeroFloor > 0 ? zeroFloor : QUORUM_ZERO_FLOOR_EPSILON;
82
+
83
+ if (isFiniteScalar(a) && isFiniteScalar(b)) {
84
+ if (a === b) return 0;
85
+ const denom = Math.max(Math.abs(a), Math.abs(b), safeFloor);
86
+ return Math.abs(a - b) / denom;
87
+ }
88
+
89
+ if (isNumericVector(a) && isNumericVector(b) && a.length === b.length) {
90
+ let maxErr = 0;
91
+ for (let i = 0; i < a.length; i++) {
92
+ const err = computeRelativeError(a[i], b[i], { zeroFloor: safeFloor });
93
+ if (err > maxErr) maxErr = err;
94
+ }
95
+ return maxErr;
96
+ }
97
+
98
+ return Infinity;
99
+ };
100
+
101
+ /**
102
+ * Checks whether two datapoints (`a` and `b`) are equal within the maximum relative error
103
+ * threshold (default `0.01%`, i.e. `maxRelativeError = 0.0001`).
104
+ *
105
+ * @param {number|number[]} a - First datapoint.
106
+ * @param {number|number[]} b - Second datapoint.
107
+ * @param {Object} [options={}] - Options object.
108
+ * @param {number} [options.maxRelativeError=0.0001] - Maximum allowed relative error (`0.0001` = `0.01%`).
109
+ * @param {number} [options.zeroFloor=1e-9] - Denominator floor near zero.
110
+ * @returns {boolean} True if and only if relative error $\le \text{maxRelativeError}$.
111
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4JAZwIoFdZbAUQEcsBDAGwQF8EAzGKMBAIgAEBzAaywGMwQBTGAC8A9FjggyaJgG5goTDhh4ipMnKA|▶ Try it live in Instacode}
112
+ */
113
+ export const isQuorumEqual = (
114
+ a,
115
+ b,
116
+ {
117
+ maxRelativeError = QUORUM_MAX_RELATIVE_ERROR,
118
+ zeroFloor = QUORUM_ZERO_FLOOR_EPSILON
119
+ } = {}
120
+ ) => {
121
+ const safeMax = isFiniteScalar(maxRelativeError) && maxRelativeError >= 0
122
+ ? maxRelativeError
123
+ : QUORUM_MAX_RELATIVE_ERROR;
124
+ return computeRelativeError(a, b, { zeroFloor }) <= safeMax + 1e-15;
125
+ };
126
+
127
+ const computeClusterMedianScalar = (values) => {
128
+ const sorted = [...values].sort((x, y) => x - y);
129
+ const mid = Math.floor(sorted.length / 2);
130
+ if (sorted.length % 2 === 1) {
131
+ return sorted[mid];
132
+ }
133
+ return cleanFloat((sorted[mid - 1] + sorted[mid]) / 2);
134
+ };
135
+
136
+ const computeClusterConsensusValue = (values) => {
137
+ if (values.length === 0) return null;
138
+ if (isFiniteScalar(values[0])) {
139
+ return computeClusterMedianScalar(values);
140
+ }
141
+ if (isNumericVector(values[0])) {
142
+ const dim = values[0].length;
143
+ const out = new Array(dim);
144
+ for (let d = 0; d < dim; d++) {
145
+ out[d] = computeClusterMedianScalar(values.map((v) => v[d]));
146
+ }
147
+ return out;
148
+ }
149
+ return null;
150
+ };
151
+
152
+ /**
153
+ * Evaluates a $K\text{-of-}N$ (default $4\text{-of-}5$) quorum across a set of datapoints.
154
+ *
155
+ * **Strict Equality-Clique Specification:**
156
+ * - Two datapoints $v_i, v_j$ are considered equal iff $\delta_{\text{rel}}(v_i, v_j) \le \text{maxRelativeError}$ (default `0.01%` = `0.0001`).
157
+ * - A subset of datapoints forms an agreeing cluster (clique) $C$ iff **every pair** $i, j \in C$
158
+ * is mutually equal within `maxRelativeError` ($\max_{i,j \in C} \delta_{\text{rel}}(v_i, v_j) \le \text{maxRelativeError}$).
159
+ * - `success: true` is returned **if and only if** the largest mutually-equal clique $C^*$ has size $|C^*| \ge K$
160
+ * (e.g., at least $4$ out of $5$ datapoints are mutually equal within $0.01\%$).
161
+ *
162
+ * @param {Array<number|number[]|{source?: string, id?: string, value: number|number[]}>} datapoints - Input datapoints.
163
+ * @param {Object} [options={}] - Quorum configuration options.
164
+ * @param {number} [options.k=4] - Minimum number of mutually-equal datapoints required for success ($K$).
165
+ * @param {number} [options.n=5] - Expected total number of independent sources ($N$).
166
+ * @param {number} [options.maxRelativeError=0.0001] - Maximum relative error tolerance (`0.0001` = `0.01%`).
167
+ * @param {number} [options.zeroFloor=1e-9] - Denominator floor near zero.
168
+ * @param {boolean} [options.strictN=false] - When `true`, requires `datapoints.length === n` in addition to $|C^*| \ge k$.
169
+ * @returns {Object} Quorum evaluation result with `success`, `consensusValue`, `maxRelativeError`, `agreeingSources`, and `outliers`.
170
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IKYDcCGAbArpuVARV1lzAQF8EAzGKCgIgAEBzAa1wGMwRUYAXgHpccENgDOjANzBgGHPkIkyYWUA|▶ Try it live in Instacode}
171
+ */
172
+ export const evaluateQuorum = (
173
+ datapoints = [],
174
+ {
175
+ k = QUORUM_DEFAULT_K,
176
+ n = QUORUM_DEFAULT_N,
177
+ maxRelativeError = QUORUM_MAX_RELATIVE_ERROR,
178
+ zeroFloor = QUORUM_ZERO_FLOOR_EPSILON,
179
+ strictN = false
180
+ } = {}
181
+ ) => {
182
+ const requiredK = Math.max(1, Math.floor(Number(k) || QUORUM_DEFAULT_K));
183
+ const expectedN = Math.max(requiredK, Math.floor(Number(n) || QUORUM_DEFAULT_N));
184
+ const safeMaxRelErr =
185
+ isFiniteScalar(maxRelativeError) && maxRelativeError >= 0
186
+ ? maxRelativeError
187
+ : QUORUM_MAX_RELATIVE_ERROR;
188
+
189
+ const list = is(datapoints, Array) ? datapoints : [];
190
+ const valid = [];
191
+
192
+ for (let idx = 0; idx < list.length; idx++) {
193
+ const item = list[idx];
194
+ const isEntryObj = is(item, Object) && !is(item, Array) && item !== null && 'value' in item;
195
+ const rawVal = isEntryObj ? item.value : item;
196
+ const source = isEntryObj && is(item.source || item.id, String)
197
+ ? String(item.source || item.id)
198
+ : `source-${idx + 1}`;
199
+
200
+ if (isFiniteScalar(rawVal) || isNumericVector(rawVal)) {
201
+ valid.push({
202
+ index: idx,
203
+ source,
204
+ value: isNumericVector(rawVal) ? [...rawVal] : rawVal
205
+ });
206
+ }
207
+ }
208
+
209
+ const m = valid.length;
210
+ const distMatrix = Array.from({ length: m }, () => new Array(m).fill(0));
211
+ const adjMatrix = Array.from({ length: m }, () => new Array(m).fill(false));
212
+
213
+ for (let i = 0; i < m; i++) {
214
+ adjMatrix[i][i] = true;
215
+ for (let j = i + 1; j < m; j++) {
216
+ const d = computeRelativeError(valid[i].value, valid[j].value, { zeroFloor });
217
+ distMatrix[i][j] = d;
218
+ distMatrix[j][i] = d;
219
+ const eq = d <= safeMaxRelErr + 1e-15;
220
+ adjMatrix[i][j] = eq;
221
+ adjMatrix[j][i] = eq;
222
+ }
223
+ }
224
+
225
+ // Exact maximum clique search (with min internal spread tie-breaker)
226
+ let bestClique = [];
227
+ let bestSpread = Infinity;
228
+
229
+ const computeCliqueSpread = (clique) => {
230
+ let maxD = 0;
231
+ for (let a = 0; a < clique.length; a++) {
232
+ for (let b = a + 1; b < clique.length; b++) {
233
+ const d = distMatrix[clique[a]][clique[b]];
234
+ if (d > maxD) maxD = d;
235
+ }
236
+ }
237
+ return maxD;
238
+ };
239
+
240
+ const searchClique = (startIdx, currentClique) => {
241
+ if (currentClique.length + (m - startIdx) < bestClique.length) {
242
+ return;
243
+ }
244
+
245
+ if (currentClique.length > 0) {
246
+ const spread = computeCliqueSpread(currentClique);
247
+ if (
248
+ currentClique.length > bestClique.length ||
249
+ (currentClique.length === bestClique.length && spread < bestSpread - 1e-15)
250
+ ) {
251
+ bestClique = [...currentClique];
252
+ bestSpread = spread;
253
+ }
254
+ }
255
+
256
+ for (let next = startIdx; next < m; next++) {
257
+ let compatible = true;
258
+ for (let c = 0; c < currentClique.length; c++) {
259
+ if (!adjMatrix[currentClique[c]][next]) {
260
+ compatible = false;
261
+ break;
262
+ }
263
+ }
264
+ if (compatible) {
265
+ currentClique.push(next);
266
+ searchClique(next + 1, currentClique);
267
+ currentClique.pop();
268
+ }
269
+ }
270
+ };
271
+
272
+ if (m > 0) {
273
+ searchClique(0, []);
274
+ } else {
275
+ bestSpread = 0;
276
+ }
277
+
278
+ const agreeingEntries = bestClique.map((idx) => valid[idx]);
279
+ const agreeingValues = agreeingEntries.map((e) => e.value);
280
+ const agreeingCount = agreeingEntries.length;
281
+ const nConditionMet = strictN ? list.length === expectedN && m === expectedN : true;
282
+ const success = Boolean(nConditionMet && agreeingCount >= requiredK);
283
+ const consensusValue = success ? computeClusterConsensusValue(agreeingValues) : null;
284
+ const referenceValue = consensusValue !== null
285
+ ? consensusValue
286
+ : computeClusterConsensusValue(agreeingValues);
287
+
288
+ const bestSet = new Set(bestClique);
289
+ const outliers = [];
290
+ for (let i = 0; i < m; i++) {
291
+ if (!bestSet.has(i)) {
292
+ const relErr = referenceValue !== null
293
+ ? computeRelativeError(valid[i].value, referenceValue, { zeroFloor })
294
+ : Infinity;
295
+ outliers.push({
296
+ index: valid[i].index,
297
+ source: valid[i].source,
298
+ value: valid[i].value,
299
+ relativeError: Number.isFinite(relErr) ? cleanFloat(relErr) : Infinity,
300
+ relativeErrorPct: Number.isFinite(relErr) ? Number((relErr * 100).toFixed(6)) : Infinity
301
+ });
302
+ }
303
+ }
304
+
305
+ const safeSpread = Number.isFinite(bestSpread) ? cleanFloat(bestSpread) : 0;
306
+
307
+ return {
308
+ success,
309
+ quorumReached: success,
310
+ status: success
311
+ ? agreeingCount >= expectedN
312
+ ? 'OK_UNANIMOUS'
313
+ : 'OK_QUORUM'
314
+ : 'REJECTED_NO_QUORUM',
315
+ consensusValue,
316
+ agreeingCount,
317
+ requiredK,
318
+ expectedN,
319
+ sampleCount: m,
320
+ maxRelativeError: safeSpread,
321
+ maxRelativeErrorPct: Number((safeSpread * 100).toFixed(6)),
322
+ thresholdRelativeError: safeMaxRelErr,
323
+ thresholdRelativeErrorPct: Number((safeMaxRelErr * 100).toFixed(4)),
324
+ agreeingIndices: agreeingEntries.map((e) => e.index),
325
+ agreeingSources: agreeingEntries.map((e) => e.source),
326
+ agreeingValues,
327
+ outliers
328
+ };
329
+ };
330
+
331
+ /**
332
+ * Convenience helper for the standard $4\text{-of-}5$ quorum check with $0.01\%$ (`0.0001`)
333
+ * maximum relative error tolerance.
334
+ *
335
+ * @param {Array<number|number[]|{source?: string, id?: string, value: number|number[]}>} datapoints - 5 source datapoints.
336
+ * @param {Object} [options={}] - Optional overrides (`maxRelativeError`, `strictN`, `zeroFloor`).
337
+ * @returns {Object} Quorum evaluation result.
338
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IKYDcCGAbArpuVARV1lzABYoAzAVgQF8FqYowEAiAAQHMBrXAGMwIVDABeAelxwQ2AM4cA3MGAYc+QiTKUatFUA|▶ Try it live in Instacode}
339
+ */
340
+ export const evaluateQuorum4of5 = (datapoints = [], options = {}) =>
341
+ evaluateQuorum(datapoints, {
342
+ ...options,
343
+ k: QUORUM_DEFAULT_K,
344
+ n: QUORUM_DEFAULT_N
345
+ });
@@ -21,6 +21,10 @@ export class Type {
21
21
  static OBJECT = 'object';
22
22
  static ARRAY = 'array';
23
23
  static FUNCTION = 'function';
24
+ static ASYNC_FUNCTION = 'asyncfunction';
25
+ static GENERATOR_FUNCTION = 'generatorfunction';
26
+ static ASYNC_GENERATOR_FUNCTION = 'asyncgeneratorfunction';
27
+ static CALLABLE = 'callable';
24
28
 
25
29
  // 3. Standard Built-ins
26
30
  static DATE = 'date';
@@ -56,6 +60,9 @@ export class Type {
56
60
  if (Array.isArray(expected)) {
57
61
  return expected.some(exp => Type.is(val, exp));
58
62
  }
63
+ if (expected === Type.CALLABLE || expected === 'callable') {
64
+ return Type.isCallable(val);
65
+ }
59
66
  if (typeof expected === 'function') {
60
67
  if (expected === Number) return typeof val === 'number' && !Number.isNaN(val);
61
68
  if (expected === Boolean) return typeof val === 'boolean' || val instanceof Boolean;
@@ -70,6 +77,16 @@ export class Type {
70
77
  return Type.of(val) === expected;
71
78
  }
72
79
 
80
+ /**
81
+ * Checks if a value is callable (regular function, async function, or generator).
82
+ * @param {*} val - Value to check.
83
+ * @returns {boolean} True if the value is any callable function.
84
+ */
85
+ static isCallable(val) {
86
+ const t = Type.of(val);
87
+ return t === 'function' || t === 'asyncfunction' || t === 'generatorfunction' || t === 'asyncgeneratorfunction';
88
+ }
89
+
73
90
  /**
74
91
  * Compares whether two values have the exact same canonical type.
75
92
  * Safely prevents JavaScript traps like typeof NaN === typeof 42 or typeof null === typeof {}.
@@ -109,3 +126,10 @@ export const of = Type.of;
109
126
  */
110
127
  export const same = Type.same;
111
128
 
129
+ /**
130
+ * Checks if a value is callable (regular function, async function, or generator).
131
+ * @see {@link Type.isCallable}
132
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4JAZwMIEMA2OsBGOApggL4IBmMUYCARAAIDmA1gK4DGYIxMAXgHp2cEDjT0A3MFCZc+IsWlA|▶ Try it live in Instacode}
133
+ */
134
+ export const isCallable = Type.isCallable;
135
+