@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 +23 -3
- package/package.json +2 -3
- package/src/developer-utils/grid-navigation.mjs +175 -0
- package/src/developer-utils/quorum.mjs +345 -0
- package/src/developer-utils/type.mjs +24 -0
- package/src/geometry/math3d.mjs +164 -0
- package/src/geometry/range-set.mjs +748 -0
- package/src/string-arrays/parse-heading.mjs +38 -0
- package/src/string-arrays/slugify.mjs +74 -0
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.
|
|
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": "
|
|
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
|
+
|