@gkucmierz/utils 5.1.0 → 5.4.1

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.1",
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,291 @@
1
+ /**
2
+ * @module developer-utils
3
+ */
4
+
5
+ /**
6
+ * Builds a 2D occupancy matrix `rows[r][c] = itemIndex` (or `-1` for empty gap)
7
+ * when `spanLastRow` or `colSpans` is enabled.
8
+ *
9
+ * @param {number} totalItems
10
+ * @param {number} cols
11
+ * @param {Object} opts
12
+ * @returns {{ matrix: number[][], itemPos: Array<{ row: number, cols: number[] }> }}
13
+ */
14
+ const buildSpannedGridMatrix = (totalItems, cols, opts = {}) => {
15
+ const spanLastRow = Boolean(opts.spanLastRow);
16
+ const colSpans = opts.colSpans || null;
17
+ const matrix = [];
18
+ const itemPos = Array.from({ length: totalItems }, () => ({ row: 0, cols: [] }));
19
+
20
+ let curRow = new Array(cols).fill(-1);
21
+ let c = 0;
22
+
23
+ for (let i = 0; i < totalItems; i += 1) {
24
+ let span = 1;
25
+ if (colSpans) {
26
+ const rawSpan = Array.isArray(colSpans) ? colSpans[i] : colSpans[i];
27
+ if (Number.isFinite(Number(rawSpan)) && Number(rawSpan) >= 1) {
28
+ span = Math.min(cols, Math.floor(Number(rawSpan)));
29
+ }
30
+ }
31
+ if (c + span > cols) {
32
+ matrix.push(curRow);
33
+ curRow = new Array(cols).fill(-1);
34
+ c = 0;
35
+ }
36
+ if (spanLastRow && i === totalItems - 1 && c < cols) {
37
+ span = cols - c;
38
+ }
39
+ const rowIdx = matrix.length;
40
+ itemPos[i].row = rowIdx;
41
+ for (let k = 0; k < span && c < cols; k += 1) {
42
+ curRow[c] = i;
43
+ itemPos[i].cols.push(c);
44
+ c += 1;
45
+ }
46
+ if (c >= cols) {
47
+ matrix.push(curRow);
48
+ curRow = new Array(cols).fill(-1);
49
+ c = 0;
50
+ }
51
+ }
52
+
53
+ if (c > 0) {
54
+ matrix.push(curRow);
55
+ }
56
+
57
+ return { matrix, itemPos };
58
+ };
59
+
60
+ /**
61
+ * Resolves the next 0-based index when navigating a 2D grid (matrix) that may contain
62
+ * trailing gaps in its last row (`totalItems` not divisible by `columns`) or spanned cells
63
+ * (`spanLastRow: true` / `colSpans` / `preferredCol`).
64
+ *
65
+ * Pure mathematical/index function — completely independent of the DOM.
66
+ *
67
+ * Wrapping configuration hierarchy:
68
+ * - `wrap` (boolean, default `true`): master default for both axes.
69
+ * - `wrapHoriz` / `wrapHorizontal` (boolean, optional): overrides `wrap` for horizontal movement (`left` / `right`).
70
+ * - `wrapVert` / `wrapVertical` (boolean, optional): overrides `wrap` for vertical movement (`up` / `down`).
71
+ * - `rowStrict` (boolean, default `false`): when `true`, horizontal movement wraps/clamps strictly within the current row instead of reading order.
72
+ * - `spanLastRow` (boolean, default `false`): when `true`, the final item (`totalItems - 1`) spans all remaining columns in the last row (e.g. a 2x3 grid where row 2 has a full-width card `[4, 4]`).
73
+ * - `colSpans` (number[]|Object, optional): explicit column span per item index (`>= 1`).
74
+ * - `preferredCol` (number, optional): preferred column index (`0 <= preferredCol < columns`) preserved when stepping vertically out of a multi-column spanned item.
75
+ *
76
+ * When `columns === 1` (single-column layout), `right` behaves identically to `down`
77
+ * and `left` behaves identically to `up`.
78
+ *
79
+ * @param {number} currentIndex - Current active 0-based index (`0 <= currentIndex < totalItems`).
80
+ * @param {number} totalItems - Total number of items in the grid ($N \ge 0$).
81
+ * @param {'left'|'right'|'up'|'down'|'ArrowLeft'|'ArrowRight'|'ArrowUp'|'ArrowDown'} direction - Movement direction.
82
+ * @param {number|Object} [columnsOrOptions=1] - Column count ($C \ge 1$) or options object.
83
+ * @param {Object} [maybeOptions={}] - Optional configuration when 4th argument is a column count.
84
+ * @returns {number} Next 0-based index, or `-1` if `totalItems <= 0`.
85
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4IHYEMBuIDm64CmA4jCACYIC+CAZjFGAgEQACOA1gK4DGYIBMAF4B6TnBAAbAM5MA3MGAZseQiXLygA|▶ Try it live in Instacode}
86
+ */
87
+ export const navigateGrid = (
88
+ currentIndex,
89
+ totalItems,
90
+ direction,
91
+ columnsOrOptions = 1,
92
+ maybeOptions = {}
93
+ ) => {
94
+ if (!Number.isFinite(totalItems) || totalItems <= 0) {
95
+ return -1;
96
+ }
97
+
98
+ const isObj = columnsOrOptions !== null && typeof columnsOrOptions === 'object';
99
+ const opts = isObj ? columnsOrOptions : (maybeOptions || {});
100
+ const rawCols = isObj ? opts.columns : columnsOrOptions;
101
+ const cols = Math.max(1, Math.floor(Number(rawCols) || 1));
102
+
103
+ const cur =
104
+ Number.isFinite(currentIndex) && currentIndex >= 0 && currentIndex < totalItems
105
+ ? Math.floor(currentIndex)
106
+ : 0;
107
+
108
+ const baseWrap = typeof opts.wrap === 'boolean' ? opts.wrap : true;
109
+ const wrapHoriz =
110
+ typeof opts.wrapHorizontal === 'boolean'
111
+ ? opts.wrapHorizontal
112
+ : typeof opts.wrapHoriz === 'boolean'
113
+ ? opts.wrapHoriz
114
+ : baseWrap;
115
+ const wrapVert =
116
+ typeof opts.wrapVertical === 'boolean'
117
+ ? opts.wrapVertical
118
+ : typeof opts.wrapVert === 'boolean'
119
+ ? opts.wrapVert
120
+ : baseWrap;
121
+ const rowStrict = Boolean(opts.rowStrict);
122
+ const hasSpans = Boolean(opts.spanLastRow || opts.colSpans);
123
+
124
+ const row = Math.floor(cur / cols);
125
+ const col = cur % cols;
126
+
127
+ let deltaH = 0;
128
+ let deltaV = 0;
129
+
130
+ switch (direction) {
131
+ case 'right':
132
+ case 'ArrowRight':
133
+ deltaH = 1;
134
+ break;
135
+ case 'left':
136
+ case 'ArrowLeft':
137
+ deltaH = -1;
138
+ break;
139
+ case 'down':
140
+ case 'ArrowDown':
141
+ deltaV = 1;
142
+ break;
143
+ case 'up':
144
+ case 'ArrowUp':
145
+ deltaV = -1;
146
+ break;
147
+ default:
148
+ return cur;
149
+ }
150
+
151
+ // Horizontal movement (left / right)
152
+ if (deltaH !== 0) {
153
+ if (rowStrict) {
154
+ if (hasSpans && cols > 1) {
155
+ const { matrix, itemPos } = buildSpannedGridMatrix(totalItems, cols, opts);
156
+ const curRowIdx = itemPos[cur]?.row ?? 0;
157
+ const rowItems = [];
158
+ for (const cellItem of matrix[curRowIdx] || []) {
159
+ if (cellItem >= 0 && !rowItems.includes(cellItem)) {
160
+ rowItems.push(cellItem);
161
+ }
162
+ }
163
+ const posInRow = rowItems.indexOf(cur);
164
+ if (posInRow < 0 || rowItems.length <= 0) return cur;
165
+ const nextPos = posInRow + deltaH;
166
+ if (nextPos >= 0 && nextPos < rowItems.length) {
167
+ return rowItems[nextPos];
168
+ }
169
+ if (wrapHoriz) {
170
+ return rowItems[((nextPos % rowItems.length) + rowItems.length) % rowItems.length];
171
+ }
172
+ return cur;
173
+ }
174
+ const rowStart = row * cols;
175
+ const rowLen = Math.min(cols, totalItems - rowStart);
176
+ if (rowLen <= 0) return cur;
177
+ const nextCol = col + deltaH;
178
+ if (nextCol >= 0 && nextCol < rowLen) {
179
+ return rowStart + nextCol;
180
+ }
181
+ if (wrapHoriz) {
182
+ return rowStart + (((nextCol % rowLen) + rowLen) % rowLen);
183
+ }
184
+ return cur;
185
+ }
186
+
187
+ const nextIdx = cur + deltaH;
188
+ if (nextIdx >= 0 && nextIdx < totalItems) {
189
+ return nextIdx;
190
+ }
191
+ if (wrapHoriz) {
192
+ return ((nextIdx % totalItems) + totalItems) % totalItems;
193
+ }
194
+ return cur;
195
+ }
196
+
197
+ // Vertical movement (up / down) when spanned cells are configured
198
+ if (hasSpans && cols > 1) {
199
+ const { matrix, itemPos } = buildSpannedGridMatrix(totalItems, cols, opts);
200
+ const curInfo = itemPos[cur] || { row: 0, cols: [0] };
201
+ const occupiedCols = curInfo.cols.length > 0 ? curInfo.cols : [0];
202
+ let targetCol = occupiedCols[0];
203
+ if (occupiedCols.length > 1 && Number.isFinite(Number(opts.preferredCol))) {
204
+ const pref = Math.max(0, Math.min(cols - 1, Math.floor(Number(opts.preferredCol))));
205
+ if (occupiedCols.includes(pref)) {
206
+ targetCol = pref;
207
+ }
208
+ }
209
+
210
+ const validRowsForCol = [];
211
+ for (let r = 0; r < matrix.length; r += 1) {
212
+ if (matrix[r][targetCol] >= 0) {
213
+ validRowsForCol.push(r);
214
+ }
215
+ }
216
+ if (validRowsForCol.length === 0) return cur;
217
+
218
+ const curRowPos = validRowsForCol.indexOf(curInfo.row);
219
+ const baseRowPos = curRowPos >= 0 ? curRowPos : 0;
220
+ const nextRowPos = baseRowPos + deltaV;
221
+ if (nextRowPos >= 0 && nextRowPos < validRowsForCol.length) {
222
+ return matrix[validRowsForCol[nextRowPos]][targetCol];
223
+ }
224
+ if (wrapVert) {
225
+ const wrappedPos =
226
+ ((nextRowPos % validRowsForCol.length) + validRowsForCol.length) %
227
+ validRowsForCol.length;
228
+ return matrix[validRowsForCol[wrappedPos]][targetCol];
229
+ }
230
+ return cur;
231
+ }
232
+
233
+ // Vertical movement (up / down) along column `col` (handling incomplete last row gaps)
234
+ const colRows = col < totalItems ? Math.floor((totalItems - 1 - col) / cols) + 1 : 0;
235
+ if (colRows <= 0) return cur;
236
+
237
+ const nextRow = row + deltaV;
238
+ if (nextRow >= 0 && nextRow < colRows) {
239
+ return nextRow * cols + col;
240
+ }
241
+ if (wrapVert) {
242
+ const wrappedRow = ((nextRow % colRows) + colRows) % colRows;
243
+ return wrappedRow * cols + col;
244
+ }
245
+ return cur;
246
+ };
247
+
248
+ /**
249
+ * Pure higher-order geometric utility that detects the number of columns in the first row
250
+ * of a 2D layout from an iterable of items and a row-offset projector function.
251
+ *
252
+ * Counts how many consecutive leading items share the same row coordinate (`y` / `top`)
253
+ * within the given `tolerance`. Completely independent of the DOM.
254
+ *
255
+ * @param {Iterable<*>} items - Iterable of numbers (row offsets) or arbitrary items.
256
+ * @param {Function|number} [getRowOffset=(x) => x] - Pure projector `(item, index) => number` (or numeric tolerance if 2nd arg is a number).
257
+ * @param {number} [tolerance=0] - Maximum allowed absolute difference between an item's row offset and the first item's offset.
258
+ * @returns {number} Detected number of columns in the first row ($\ge 1$).
259
+ * @see {@link https://instacode.app/run/FASwtgDg9gTgLgAgN4ICYFM7oMZwOIwioDCUANgK5gB2AzggL4IBmMUYCARAAIDmA1hWxgQ6GAC8A9BTggytTgG5gwDFlwEipSjVrKgA|▶ Try it live in Instacode}
260
+ */
261
+ export const detectGridColumns = (items, getRowOffset = (x) => x, tolerance = 0) => {
262
+ if (!items || typeof items[Symbol.iterator] !== 'function') {
263
+ return 1;
264
+ }
265
+
266
+ const isFn = typeof getRowOffset === 'function';
267
+ const projector = isFn ? getRowOffset : (x) => x;
268
+ const tol = Math.max(0, Number(isFn ? tolerance : getRowOffset) || 0);
269
+
270
+ let firstOffset = null;
271
+ let firstRowCount = 0;
272
+ let idx = 0;
273
+
274
+ for (const item of items) {
275
+ const raw = projector(item, idx++);
276
+ const offset = typeof raw === 'number' ? raw : Number(raw);
277
+ if (!Number.isFinite(offset)) {
278
+ continue;
279
+ }
280
+ if (firstOffset === null) {
281
+ firstOffset = offset;
282
+ firstRowCount = 1;
283
+ } else if (Math.abs(offset - firstOffset) <= tol) {
284
+ firstRowCount += 1;
285
+ } else {
286
+ break;
287
+ }
288
+ }
289
+
290
+ return Math.max(1, firstRowCount);
291
+ };