@gkucmierz/utils 5.0.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 +28 -4
- package/package.json +2 -3
- package/src/data-structures/Stack.mjs +100 -0
- package/src/developer-utils/get-type.mjs +2 -16
- 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
|
@@ -20,6 +20,9 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
SetCnt
|
|
22
22
|
} from './src/data-structures/SetCnt.mjs'
|
|
23
|
+
import {
|
|
24
|
+
Stack
|
|
25
|
+
} from './src/data-structures/Stack.mjs'
|
|
23
26
|
import {
|
|
24
27
|
Trie
|
|
25
28
|
} from './src/data-structures/Trie.mjs'
|
|
@@ -36,8 +39,11 @@ import {
|
|
|
36
39
|
consumeIteratorNonBlocking
|
|
37
40
|
} from './src/developer-utils/consume-iterator.mjs'
|
|
38
41
|
import {
|
|
39
|
-
|
|
42
|
+
|
|
40
43
|
} from './src/developer-utils/get-type.mjs'
|
|
44
|
+
import {
|
|
45
|
+
detectGridColumns, navigateGrid
|
|
46
|
+
} from './src/developer-utils/grid-navigation.mjs'
|
|
41
47
|
import {
|
|
42
48
|
measurePerformance
|
|
43
49
|
} from './src/developer-utils/measure-performance.mjs'
|
|
@@ -47,6 +53,9 @@ import {
|
|
|
47
53
|
import {
|
|
48
54
|
naturalSearch
|
|
49
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'
|
|
50
59
|
import {
|
|
51
60
|
randNormal
|
|
52
61
|
} from './src/developer-utils/rand-normal.mjs'
|
|
@@ -57,17 +66,20 @@ import {
|
|
|
57
66
|
setSafeInterval
|
|
58
67
|
} from './src/developer-utils/set-safe-interval.mjs'
|
|
59
68
|
import {
|
|
60
|
-
Type, Types, is, of, same
|
|
69
|
+
Type, Types, is, isCallable, of, same
|
|
61
70
|
} from './src/developer-utils/type.mjs'
|
|
62
71
|
import {
|
|
63
72
|
barycentricCoordinates
|
|
64
73
|
} from './src/geometry/barycentric.mjs'
|
|
65
74
|
import {
|
|
66
|
-
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
|
|
67
76
|
} from './src/geometry/math3d.mjs'
|
|
68
77
|
import {
|
|
69
78
|
matrixAsArray
|
|
70
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'
|
|
71
83
|
import {
|
|
72
84
|
egcd
|
|
73
85
|
} from './src/number-theory/egcd.mjs'
|
|
@@ -137,6 +149,12 @@ import {
|
|
|
137
149
|
import {
|
|
138
150
|
formatBigNumber, formatBigNumberBI, wrapFn
|
|
139
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'
|
|
140
158
|
|
|
141
159
|
export * from './src/automata/langtons-ant.mjs';
|
|
142
160
|
export * from './src/combinatorics/combinations.mjs';
|
|
@@ -145,15 +163,18 @@ export * from './src/combinatorics/n-choose-k.mjs';
|
|
|
145
163
|
export * from './src/combinatorics/permutations.mjs';
|
|
146
164
|
export * from './src/data-structures/ConflatingQueue.mjs';
|
|
147
165
|
export * from './src/data-structures/SetCnt.mjs';
|
|
166
|
+
export * from './src/data-structures/Stack.mjs';
|
|
148
167
|
export * from './src/data-structures/Trie.mjs';
|
|
149
168
|
export * from './src/data-structures/heap.mjs';
|
|
150
169
|
export * from './src/data-structures/list-node.mjs';
|
|
151
170
|
export * from './src/developer-utils/binary-search.mjs';
|
|
152
171
|
export * from './src/developer-utils/consume-iterator.mjs';
|
|
153
172
|
export * from './src/developer-utils/get-type.mjs';
|
|
173
|
+
export * from './src/developer-utils/grid-navigation.mjs';
|
|
154
174
|
export * from './src/developer-utils/measure-performance.mjs';
|
|
155
175
|
export * from './src/developer-utils/memoize.mjs';
|
|
156
176
|
export * from './src/developer-utils/natural-search.mjs';
|
|
177
|
+
export * from './src/developer-utils/quorum.mjs';
|
|
157
178
|
export * from './src/developer-utils/rand-normal.mjs';
|
|
158
179
|
export * from './src/developer-utils/range-array.mjs';
|
|
159
180
|
export * from './src/developer-utils/set-safe-interval.mjs';
|
|
@@ -161,6 +182,7 @@ export * from './src/developer-utils/type.mjs';
|
|
|
161
182
|
export * from './src/geometry/barycentric.mjs';
|
|
162
183
|
export * from './src/geometry/math3d.mjs';
|
|
163
184
|
export * from './src/geometry/matrix.mjs';
|
|
185
|
+
export * from './src/geometry/range-set.mjs';
|
|
164
186
|
export * from './src/number-theory/egcd.mjs';
|
|
165
187
|
export * from './src/number-theory/factors.mjs';
|
|
166
188
|
export * from './src/number-theory/gcd.mjs';
|
|
@@ -184,7 +206,9 @@ export * from './src/string-arrays/bijective-numeration.mjs';
|
|
|
184
206
|
export * from './src/string-arrays/chunks.mjs';
|
|
185
207
|
export * from './src/string-arrays/copy-case.mjs';
|
|
186
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';
|
|
187
211
|
|
|
188
212
|
export default [
|
|
189
|
-
createLangtonsAnt, createUnlimitedGrid, combinations, combinationsIterator, bin2gray, gray2bin, nChooseK, permutations, permutationsIterator, ConflatingQueue, SetCnt, Trie, Heap, ListNode, binarySearchArr, binarySearchGE, binarySearchLE, binarySearchRangeIncl, consumeIteratorNonBlocking,
|
|
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
|
|
190
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,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module data-structures
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* High-performance, LIFO (Last-In-First-Out) Stack data structure.
|
|
7
|
+
* Encapsulates an internal private array (#items) to prevent index tampering
|
|
8
|
+
* and optimize V8 memory locality and fast push/pop operations.
|
|
9
|
+
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IMpwIYGMDWCC+CAZjFGAgEQACA5jgK5ZggCmMAXgPT1wgA2AZwoBuYMHTYcooA|▶ Try it live in Instacode}
|
|
10
|
+
*/
|
|
11
|
+
export class Stack {
|
|
12
|
+
#items = [];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a new Stack instance.
|
|
16
|
+
* @param {Iterable} [iter] - An optional iterable object to initialize the stack with.
|
|
17
|
+
*/
|
|
18
|
+
constructor(iter) {
|
|
19
|
+
for (const el of iter ?? []) {
|
|
20
|
+
this.push(el);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pushes one or more items onto the top of the stack.
|
|
26
|
+
* @param {...*} items - Elements to push onto the stack.
|
|
27
|
+
* @returns {Stack} The Stack instance for fluent method chaining.
|
|
28
|
+
*/
|
|
29
|
+
push(...items) {
|
|
30
|
+
this.#items.push(...items);
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Removes and returns the item from the top of the stack.
|
|
36
|
+
* @returns {*|undefined} The top item, or undefined if the stack is empty.
|
|
37
|
+
*/
|
|
38
|
+
pop() {
|
|
39
|
+
return this.#items.pop();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Returns the item from the top of the stack without removing it.
|
|
44
|
+
* @returns {*|undefined} The top item, or undefined if the stack is empty.
|
|
45
|
+
*/
|
|
46
|
+
peek() {
|
|
47
|
+
return this.#items[this.#items.length - 1];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Checks whether the stack is empty.
|
|
52
|
+
* @returns {boolean} True if the stack contains no items, false otherwise.
|
|
53
|
+
*/
|
|
54
|
+
isEmpty() {
|
|
55
|
+
return this.#items.length === 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Gets the number of items currently in the stack.
|
|
60
|
+
* @returns {number} The number of items in the stack.
|
|
61
|
+
*/
|
|
62
|
+
get size() {
|
|
63
|
+
return this.#items.length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Gets the number of items currently in the stack (Array length compatibility alias).
|
|
68
|
+
* @returns {number} The number of items in the stack.
|
|
69
|
+
*/
|
|
70
|
+
get length() {
|
|
71
|
+
return this.#items.length;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Removes all items from the stack.
|
|
76
|
+
* @returns {Stack} The Stack instance for fluent method chaining.
|
|
77
|
+
*/
|
|
78
|
+
clear() {
|
|
79
|
+
this.#items.length = 0;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns a shallow copy of the stack items as an Array from bottom to top.
|
|
85
|
+
* @returns {Array} Array of items from bottom to top.
|
|
86
|
+
*/
|
|
87
|
+
toArray() {
|
|
88
|
+
return [...this.#items];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Iterates through stack items from top to bottom (LIFO order).
|
|
93
|
+
* @returns {Iterator}
|
|
94
|
+
*/
|
|
95
|
+
*[Symbol.iterator]() {
|
|
96
|
+
for (let i = this.#items.length - 1; i >= 0; i--) {
|
|
97
|
+
yield this.#items[i];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -1,16 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Returns the type of the value as a lowercase string.
|
|
8
|
-
* More specific than the typeof operator (e.g., distinguishes 'array', 'null', 'date').
|
|
9
|
-
* @param {*} val - The value to check.
|
|
10
|
-
* @returns {string} The type of the value.
|
|
11
|
-
* @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IOYFM4BUCeF0IC+CAZjFGAgEQACqA1gK4DGYI6MAXgPSNwgAbAM5UA3MGAZsedOKA|▶ Try it live in Instacode}
|
|
12
|
-
*/
|
|
13
|
-
export const getType = val => {
|
|
14
|
-
const str = Object.prototype.toString.call(val);
|
|
15
|
-
return str.slice(8, -1).toLowerCase();
|
|
16
|
-
};
|
|
1
|
+
// Deprecated - removed in v5.0.0 in favor of ./type.mjs
|
|
2
|
+
export {};
|
|
@@ -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
|
+
};
|