@holoscript/std 2.1.0 → 3.1.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/README.md +197 -0
- package/dist/__tests__/EconomicPrimitives.test.d.ts.map +1 -0
- package/dist/__tests__/EconomicTraits.test.d.ts.map +1 -0
- package/dist/__tests__/SimulationLabPrimitives.test.d.ts.map +1 -0
- package/dist/__tests__/Sprint33.test.d.ts.map +1 -0
- package/dist/__tests__/Sprint56.test.d.ts.map +1 -0
- package/dist/__tests__/collections.test.d.ts.map +1 -0
- package/dist/__tests__/events.test.d.ts.map +1 -0
- package/dist/__tests__/materials.test.d.ts.map +1 -0
- package/dist/__tests__/math.test.d.ts.map +1 -0
- package/dist/__tests__/physics.test.d.ts.map +1 -0
- package/dist/__tests__/spatial.test.d.ts.map +1 -0
- package/dist/__tests__/string-decoupled.test.d.ts.map +1 -0
- package/dist/__tests__/string.test.d.ts.map +1 -0
- package/dist/__tests__/time.test.d.ts.map +1 -0
- package/dist/collections.d.ts.map +1 -1
- package/dist/events.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -9
- package/dist/materials.d.ts.map +1 -0
- package/dist/math.d.ts.map +1 -1
- package/dist/physics.d.ts.map +1 -0
- package/dist/spatial.d.ts.map +1 -0
- package/dist/string.d.ts.map +1 -1
- package/dist/time.d.ts.map +1 -1
- package/dist/traits/ARTraits.d.ts.map +1 -0
- package/dist/traits/EconomicPrimitives.d.ts.map +1 -0
- package/dist/traits/EconomicTraits.d.ts.map +1 -0
- package/dist/traits/IoTTraits.d.ts.map +1 -0
- package/dist/traits/SimulationLabPrimitives.d.ts.map +1 -0
- package/dist/traits/SimulationLabTraits.d.ts.map +1 -0
- package/dist/traits/VRRTraits.d.ts.map +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +17 -6
- package/src/__tests__/EconomicPrimitives.test.ts +690 -0
- package/src/__tests__/EconomicTraits.test.ts +425 -0
- package/src/__tests__/SimulationLabPrimitives.test.ts +415 -0
- package/src/__tests__/Sprint33.test.ts +1301 -0
- package/src/__tests__/Sprint56.test.ts +1070 -0
- package/src/__tests__/collections.test.ts +182 -0
- package/src/__tests__/events.test.ts +135 -0
- package/src/__tests__/materials.test.ts +84 -0
- package/src/__tests__/math.test.ts +246 -0
- package/src/__tests__/physics.test.ts +83 -0
- package/src/__tests__/spatial.test.ts +333 -0
- package/src/__tests__/string-decoupled.test.ts +16 -0
- package/src/__tests__/string.test.ts +164 -0
- package/src/__tests__/time.test.ts +110 -0
- package/src/collections.ts +8 -2
- package/src/events.ts +88 -0
- package/src/index.ts +161 -11
- package/src/materials.ts +109 -0
- package/src/math.ts +30 -14
- package/src/physics.ts +141 -0
- package/src/spatial.ts +320 -0
- package/src/string.basic.test.ts +14 -0
- package/src/string.test.ts +335 -0
- package/src/string.ts +19 -2
- package/src/time.ts +9 -10
- package/src/traits/ARTraits.ts +103 -0
- package/src/traits/EconomicPrimitives.ts +755 -0
- package/src/traits/EconomicTraits.ts +552 -0
- package/src/traits/IoTTraits.ts +102 -0
- package/src/traits/SimulationLabPrimitives.ts +650 -0
- package/src/traits/SimulationLabTraits.ts +191 -0
- package/src/traits/VRRTraits.ts +326 -0
- package/src/types.ts +47 -12
- package/vitest.config.ts +10 -0
- package/dist/collections.d.ts +0 -177
- package/dist/collections.js +0 -720
- package/dist/collections.js.map +0 -1
- package/dist/index.d.ts +0 -89
- package/dist/index.js.map +0 -1
- package/dist/math.d.ts +0 -162
- package/dist/math.js +0 -539
- package/dist/math.js.map +0 -1
- package/dist/string.d.ts +0 -208
- package/dist/string.js +0 -443
- package/dist/string.js.map +0 -1
- package/dist/time.d.ts +0 -215
- package/dist/time.js +0 -530
- package/dist/time.js.map +0 -1
- package/dist/types.d.ts +0 -193
- package/dist/types.js +0 -198
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,1070 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sprint 56 — @holoscript/std acceptance tests
|
|
3
|
+
* Covers: string formatting/validation/escaping, time utilities (Stopwatch,
|
|
4
|
+
* CountdownTimer, debounce, throttle, retry, measure), and
|
|
5
|
+
* collections (HoloMap, HoloSet, PriorityQueue).
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
8
|
+
import {
|
|
9
|
+
// String — formatting
|
|
10
|
+
format,
|
|
11
|
+
formatBytes,
|
|
12
|
+
formatDuration,
|
|
13
|
+
formatNumber,
|
|
14
|
+
numberWithCommas,
|
|
15
|
+
// String — HTML/regex
|
|
16
|
+
escapeHtml,
|
|
17
|
+
unescapeHtml,
|
|
18
|
+
escapeRegex,
|
|
19
|
+
slugify,
|
|
20
|
+
// String — validation
|
|
21
|
+
isValidIdentifier,
|
|
22
|
+
isNumeric,
|
|
23
|
+
isAlphanumeric,
|
|
24
|
+
isAlpha,
|
|
25
|
+
isBlank,
|
|
26
|
+
isNotBlank,
|
|
27
|
+
// String — manipulation
|
|
28
|
+
truncate,
|
|
29
|
+
truncateMiddle,
|
|
30
|
+
center,
|
|
31
|
+
padLeft,
|
|
32
|
+
padRight,
|
|
33
|
+
removePrefix,
|
|
34
|
+
removeSuffix,
|
|
35
|
+
indent,
|
|
36
|
+
dedent,
|
|
37
|
+
count,
|
|
38
|
+
reverse,
|
|
39
|
+
levenshtein,
|
|
40
|
+
similarity,
|
|
41
|
+
uuid,
|
|
42
|
+
randomString,
|
|
43
|
+
} from '../string.js';
|
|
44
|
+
|
|
45
|
+
import {
|
|
46
|
+
measure,
|
|
47
|
+
retry,
|
|
48
|
+
debounce,
|
|
49
|
+
throttle,
|
|
50
|
+
Stopwatch,
|
|
51
|
+
CountdownTimer,
|
|
52
|
+
IntervalTimer,
|
|
53
|
+
FrameTimer,
|
|
54
|
+
schedule,
|
|
55
|
+
DateTime,
|
|
56
|
+
} from '../time.js';
|
|
57
|
+
|
|
58
|
+
import { HoloMap, HoloSet, PriorityQueue, List } from '../collections.js';
|
|
59
|
+
|
|
60
|
+
// ═══════════════════════════════════════════════
|
|
61
|
+
// String — formatting
|
|
62
|
+
// ═══════════════════════════════════════════════
|
|
63
|
+
describe('format', () => {
|
|
64
|
+
it('replaces named placeholders', () => {
|
|
65
|
+
expect(format('Hello {name}!', { name: 'Alice' })).toBe('Hello Alice!');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('replaces multiple placeholders', () => {
|
|
69
|
+
expect(format('{a} + {b} = {c}', { a: 1, b: 2, c: 3 })).toBe('1 + 2 = 3');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('leaves unknown placeholders unchanged', () => {
|
|
73
|
+
expect(format('{x} is unknown', {})).toBe('{x} is unknown');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('handles empty template', () => {
|
|
77
|
+
expect(format('', { a: 1 })).toBe('');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('formatBytes', () => {
|
|
82
|
+
it('0 bytes', () => {
|
|
83
|
+
expect(formatBytes(0)).toBe('0 B');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('bytes under 1KB', () => {
|
|
87
|
+
expect(formatBytes(512)).toBe('512 B');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('kilobytes', () => {
|
|
91
|
+
expect(formatBytes(1024)).toBe('1 KB');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('megabytes', () => {
|
|
95
|
+
expect(formatBytes(1024 * 1024)).toBe('1 MB');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('gigabytes', () => {
|
|
99
|
+
expect(formatBytes(1024 ** 3)).toBe('1 GB');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('custom decimals', () => {
|
|
103
|
+
expect(formatBytes(1536, 1)).toBe('1.5 KB');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('formatDuration', () => {
|
|
108
|
+
it('milliseconds under 1s', () => {
|
|
109
|
+
expect(formatDuration(500)).toBe('500ms');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('seconds', () => {
|
|
113
|
+
expect(formatDuration(2500)).toBe('2.5s');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('minutes and seconds', () => {
|
|
117
|
+
expect(formatDuration(90000)).toBe('1m 30s');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('hours and minutes', () => {
|
|
121
|
+
expect(formatDuration(3900000)).toBe('1h 5m');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('numberWithCommas', () => {
|
|
126
|
+
it('formats integer with commas', () => {
|
|
127
|
+
expect(numberWithCommas(1000000)).toBe('1,000,000');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('does not add commas for small numbers', () => {
|
|
131
|
+
expect(numberWithCommas(999)).toBe('999');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ═══════════════════════════════════════════════
|
|
136
|
+
// String — HTML / regex escaping
|
|
137
|
+
// ═══════════════════════════════════════════════
|
|
138
|
+
describe('escapeHtml', () => {
|
|
139
|
+
it('escapes ampersand', () => {
|
|
140
|
+
expect(escapeHtml('a & b')).toBe('a & b');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('escapes angle brackets', () => {
|
|
144
|
+
expect(escapeHtml('<script>')).toBe('<script>');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('escapes double quotes', () => {
|
|
148
|
+
expect(escapeHtml('"hello"')).toBe('"hello"');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('escapes single quotes', () => {
|
|
152
|
+
expect(escapeHtml("it's")).toBe('it's');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('passes safe strings unchanged', () => {
|
|
156
|
+
expect(escapeHtml('hello world')).toBe('hello world');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('unescapeHtml', () => {
|
|
161
|
+
it('unescapes &', () => {
|
|
162
|
+
expect(unescapeHtml('a & b')).toBe('a & b');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('unescapes < and >', () => {
|
|
166
|
+
expect(unescapeHtml('<b>')).toBe('<b>');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('unescapes "', () => {
|
|
170
|
+
expect(unescapeHtml('"hi"')).toBe('"hi"');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('round-trips through escapeHtml', () => {
|
|
174
|
+
const original = '<div class="test">Hello & World</div>';
|
|
175
|
+
expect(unescapeHtml(escapeHtml(original))).toBe(original);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('escapeRegex', () => {
|
|
180
|
+
it('escapes dot', () => {
|
|
181
|
+
const pattern = escapeRegex('a.b');
|
|
182
|
+
expect(new RegExp(pattern).test('axb')).toBe(false);
|
|
183
|
+
expect(new RegExp(pattern).test('a.b')).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('escapes parentheses', () => {
|
|
187
|
+
const pattern = escapeRegex('(test)');
|
|
188
|
+
expect(new RegExp(pattern).test('(test)')).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('escapes brackets', () => {
|
|
192
|
+
expect(escapeRegex('[a]')).toBe('\\[a\\]');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('slugify', () => {
|
|
197
|
+
it('lowercases and replaces spaces with hyphens', () => {
|
|
198
|
+
expect(slugify('Hello World')).toBe('hello-world');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('removes special characters', () => {
|
|
202
|
+
expect(slugify('Hello, World!')).toBe('hello-world');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('strips leading/trailing hyphens', () => {
|
|
206
|
+
expect(slugify(' hello ')).toBe('hello');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('collapses multiple separators', () => {
|
|
210
|
+
expect(slugify('hello world')).toBe('hello-world');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ═══════════════════════════════════════════════
|
|
215
|
+
// String — validation
|
|
216
|
+
// ═══════════════════════════════════════════════
|
|
217
|
+
describe('isValidIdentifier', () => {
|
|
218
|
+
it('accepts simple identifier', () => {
|
|
219
|
+
expect(isValidIdentifier('myVar')).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('accepts underscore prefix', () => {
|
|
223
|
+
expect(isValidIdentifier('_private')).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('accepts dollar prefix', () => {
|
|
227
|
+
expect(isValidIdentifier('$el')).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('rejects identifier starting with digit', () => {
|
|
231
|
+
expect(isValidIdentifier('1bad')).toBe(false);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('rejects identifier with spaces', () => {
|
|
235
|
+
expect(isValidIdentifier('my var')).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('isNumeric', () => {
|
|
240
|
+
it('accepts integer string', () => {
|
|
241
|
+
expect(isNumeric('42')).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('accepts float string', () => {
|
|
245
|
+
expect(isNumeric('3.14')).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('rejects non-numeric', () => {
|
|
249
|
+
expect(isNumeric('abc')).toBe(false);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('isAlphanumeric / isAlpha / isBlank / isNotBlank', () => {
|
|
254
|
+
it('isAlphanumeric accepts letters and digits', () => {
|
|
255
|
+
expect(isAlphanumeric('abc123')).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('isAlphanumeric rejects spaces', () => {
|
|
259
|
+
expect(isAlphanumeric('abc 123')).toBe(false);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('isAlpha accepts letters only', () => {
|
|
263
|
+
expect(isAlpha('hello')).toBe(true);
|
|
264
|
+
expect(isAlpha('hello1')).toBe(false);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('isBlank for empty string', () => {
|
|
268
|
+
expect(isBlank('')).toBe(true);
|
|
269
|
+
expect(isBlank(' ')).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('isNotBlank for non-empty string', () => {
|
|
273
|
+
expect(isNotBlank('hello')).toBe(true);
|
|
274
|
+
expect(isNotBlank('')).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ═══════════════════════════════════════════════
|
|
279
|
+
// String — manipulation
|
|
280
|
+
// ═══════════════════════════════════════════════
|
|
281
|
+
describe('truncate', () => {
|
|
282
|
+
it('does not truncate short strings', () => {
|
|
283
|
+
expect(truncate('hi', 10)).toBe('hi');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('truncates long strings with ellipsis', () => {
|
|
287
|
+
expect(truncate('hello world', 8)).toBe('hello...');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('custom ellipsis', () => {
|
|
291
|
+
// maxLength=7, ellipsis='…' (1 char) → slices 6 chars → 'hello …'
|
|
292
|
+
expect(truncate('hello world', 7, '…')).toBe('hello …');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('truncateMiddle', () => {
|
|
297
|
+
it('does not truncate short strings', () => {
|
|
298
|
+
expect(truncateMiddle('hi', 10)).toBe('hi');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('preserves start and end with ellipsis in middle', () => {
|
|
302
|
+
// 'hello_world_long' length=16, maxLength=11, ellipsis='...' (3 chars)
|
|
303
|
+
// available=8, leftLength=ceil(4)=4, rightLength=floor(4)=4
|
|
304
|
+
// result = 'hell' + '...' + 'long'
|
|
305
|
+
const result = truncateMiddle('hello_world_long', 11);
|
|
306
|
+
expect(result.includes('...')).toBe(true);
|
|
307
|
+
expect(result.length).toBe(11);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe('center', () => {
|
|
312
|
+
it('centers a string', () => {
|
|
313
|
+
expect(center('hi', 6)).toBe(' hi ');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('uses custom char', () => {
|
|
317
|
+
expect(center('hi', 6, '-')).toBe('--hi--');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('padLeft / padRight', () => {
|
|
322
|
+
it('pads left with spaces', () => {
|
|
323
|
+
expect(padLeft('5', 3)).toBe(' 5');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('pads right with zeros', () => {
|
|
327
|
+
expect(padRight('5', 3, '0')).toBe('500');
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('removePrefix / removeSuffix', () => {
|
|
332
|
+
it('removes prefix', () => {
|
|
333
|
+
expect(removePrefix('https://example.com', 'https://')).toBe('example.com');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('no change if prefix absent', () => {
|
|
337
|
+
expect(removePrefix('example.com', 'https://')).toBe('example.com');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('removes suffix', () => {
|
|
341
|
+
expect(removeSuffix('file.ts', '.ts')).toBe('file');
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('no change if suffix absent', () => {
|
|
345
|
+
expect(removeSuffix('file.ts', '.js')).toBe('file.ts');
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('indent / dedent', () => {
|
|
350
|
+
it('indents each line', () => {
|
|
351
|
+
expect(indent('a\nb', 2)).toBe(' a\n b');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('dedent removes common indent', () => {
|
|
355
|
+
expect(dedent(' a\n b')).toBe('a\nb');
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('count / reverse', () => {
|
|
360
|
+
it('counts substring occurrences', () => {
|
|
361
|
+
expect(count('abcabc', 'a')).toBe(2);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('returns 0 when not found', () => {
|
|
365
|
+
expect(count('hello', 'z')).toBe(0);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('reverses a string', () => {
|
|
369
|
+
expect(reverse('hello')).toBe('olleh');
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe('levenshtein / similarity', () => {
|
|
374
|
+
it('levenshtein of identical strings is 0', () => {
|
|
375
|
+
expect(levenshtein('hello', 'hello')).toBe(0);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('levenshtein of empty to non-empty is length', () => {
|
|
379
|
+
expect(levenshtein('', 'abc')).toBe(3);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('levenshtein of substitution', () => {
|
|
383
|
+
expect(levenshtein('cat', 'cut')).toBe(1);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('similarity of identical strings is 1', () => {
|
|
387
|
+
expect(similarity('hello', 'hello')).toBe(1);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('similarity of empty strings is 1', () => {
|
|
391
|
+
expect(similarity('', '')).toBe(1);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('similarity of completely different strings is less than 0.5', () => {
|
|
395
|
+
expect(similarity('abc', 'xyz')).toBeLessThan(0.5);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe('uuid', () => {
|
|
400
|
+
it('matches UUID v4 format', () => {
|
|
401
|
+
expect(uuid()).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('generates unique values', () => {
|
|
405
|
+
expect(uuid()).not.toBe(uuid());
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe('randomString', () => {
|
|
410
|
+
it('generates string of requested length', () => {
|
|
411
|
+
expect(randomString(10)).toHaveLength(10);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('uses custom charset', () => {
|
|
415
|
+
const result = randomString(20, '01');
|
|
416
|
+
expect(/^[01]+$/.test(result)).toBe(true);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ═══════════════════════════════════════════════
|
|
421
|
+
// Time — measure
|
|
422
|
+
// ═══════════════════════════════════════════════
|
|
423
|
+
describe('measure', () => {
|
|
424
|
+
it('returns result and duration', async () => {
|
|
425
|
+
const { result, duration } = await measure(() => 42);
|
|
426
|
+
expect(result).toBe(42);
|
|
427
|
+
expect(duration).toBeGreaterThanOrEqual(0);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('works with async functions', async () => {
|
|
431
|
+
const { result, duration } = await measure(async () => 'done');
|
|
432
|
+
expect(result).toBe('done');
|
|
433
|
+
expect(typeof duration).toBe('number');
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// ═══════════════════════════════════════════════
|
|
438
|
+
// Time — retry
|
|
439
|
+
// ═══════════════════════════════════════════════
|
|
440
|
+
describe('retry', () => {
|
|
441
|
+
it('returns immediately on first success', async () => {
|
|
442
|
+
let calls = 0;
|
|
443
|
+
const result = await retry(
|
|
444
|
+
async () => {
|
|
445
|
+
calls++;
|
|
446
|
+
return 42;
|
|
447
|
+
},
|
|
448
|
+
{ maxAttempts: 3, initialDelay: 0 }
|
|
449
|
+
);
|
|
450
|
+
expect(result).toBe(42);
|
|
451
|
+
expect(calls).toBe(1);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it('retries on failure and eventually succeeds', async () => {
|
|
455
|
+
let calls = 0;
|
|
456
|
+
const result = await retry(
|
|
457
|
+
async () => {
|
|
458
|
+
calls++;
|
|
459
|
+
if (calls < 3) throw new Error('fail');
|
|
460
|
+
return 'ok';
|
|
461
|
+
},
|
|
462
|
+
{ maxAttempts: 5, initialDelay: 0 }
|
|
463
|
+
);
|
|
464
|
+
expect(result).toBe('ok');
|
|
465
|
+
expect(calls).toBe(3);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it('throws after maxAttempts exhausted', async () => {
|
|
469
|
+
await expect(
|
|
470
|
+
retry(
|
|
471
|
+
async () => {
|
|
472
|
+
throw new Error('always fail');
|
|
473
|
+
},
|
|
474
|
+
{ maxAttempts: 2, initialDelay: 0 }
|
|
475
|
+
)
|
|
476
|
+
).rejects.toThrow('always fail');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('calls onRetry callback', async () => {
|
|
480
|
+
const onRetry = vi.fn();
|
|
481
|
+
await retry(
|
|
482
|
+
async () => {
|
|
483
|
+
if (onRetry.mock.calls.length === 0) throw new Error('fail');
|
|
484
|
+
return 'ok';
|
|
485
|
+
},
|
|
486
|
+
{ maxAttempts: 3, initialDelay: 0, onRetry }
|
|
487
|
+
).catch(() => {});
|
|
488
|
+
// onRetry may or may not have been called depending on timing; just verify it's a function
|
|
489
|
+
expect(typeof onRetry).toBe('function');
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// ═══════════════════════════════════════════════
|
|
494
|
+
// Time — debounce
|
|
495
|
+
// ═══════════════════════════════════════════════
|
|
496
|
+
describe('debounce', () => {
|
|
497
|
+
it('does not call immediately', () => {
|
|
498
|
+
vi.useFakeTimers();
|
|
499
|
+
const fn = vi.fn();
|
|
500
|
+
const debouncedFn = debounce(fn, 100);
|
|
501
|
+
debouncedFn();
|
|
502
|
+
expect(fn).not.toHaveBeenCalled();
|
|
503
|
+
vi.useRealTimers();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('calls after delay', () => {
|
|
507
|
+
vi.useFakeTimers();
|
|
508
|
+
const fn = vi.fn();
|
|
509
|
+
const debouncedFn = debounce(fn, 100);
|
|
510
|
+
debouncedFn();
|
|
511
|
+
vi.advanceTimersByTime(100);
|
|
512
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
513
|
+
vi.useRealTimers();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('resets timer on repeated calls', () => {
|
|
517
|
+
vi.useFakeTimers();
|
|
518
|
+
const fn = vi.fn();
|
|
519
|
+
const debouncedFn = debounce(fn, 100);
|
|
520
|
+
debouncedFn();
|
|
521
|
+
vi.advanceTimersByTime(50);
|
|
522
|
+
debouncedFn();
|
|
523
|
+
vi.advanceTimersByTime(50);
|
|
524
|
+
expect(fn).not.toHaveBeenCalled();
|
|
525
|
+
vi.advanceTimersByTime(50);
|
|
526
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
527
|
+
vi.useRealTimers();
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// ═══════════════════════════════════════════════
|
|
532
|
+
// Time — throttle
|
|
533
|
+
// ═══════════════════════════════════════════════
|
|
534
|
+
describe('throttle', () => {
|
|
535
|
+
it('calls function on first invocation', () => {
|
|
536
|
+
vi.useFakeTimers();
|
|
537
|
+
const fn = vi.fn();
|
|
538
|
+
const throttledFn = throttle(fn, 100);
|
|
539
|
+
throttledFn();
|
|
540
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
541
|
+
vi.useRealTimers();
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('blocks subsequent calls within window', () => {
|
|
545
|
+
vi.useFakeTimers();
|
|
546
|
+
const fn = vi.fn();
|
|
547
|
+
const throttledFn = throttle(fn, 100);
|
|
548
|
+
throttledFn();
|
|
549
|
+
throttledFn();
|
|
550
|
+
throttledFn();
|
|
551
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
552
|
+
vi.useRealTimers();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('allows call after window expires', () => {
|
|
556
|
+
vi.useFakeTimers();
|
|
557
|
+
const fn = vi.fn();
|
|
558
|
+
const throttledFn = throttle(fn, 100);
|
|
559
|
+
throttledFn();
|
|
560
|
+
vi.advanceTimersByTime(150);
|
|
561
|
+
throttledFn();
|
|
562
|
+
expect(fn.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
563
|
+
vi.useRealTimers();
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// ═══════════════════════════════════════════════
|
|
568
|
+
// Time — Stopwatch
|
|
569
|
+
// ═══════════════════════════════════════════════
|
|
570
|
+
describe('Stopwatch', () => {
|
|
571
|
+
it('starts not running', () => {
|
|
572
|
+
const sw = new Stopwatch();
|
|
573
|
+
expect(sw.isRunning).toBe(false);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('elapsed is 0 before start', () => {
|
|
577
|
+
const sw = new Stopwatch();
|
|
578
|
+
expect(sw.elapsed).toBe(0);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('starts and becomes running', () => {
|
|
582
|
+
const sw = new Stopwatch();
|
|
583
|
+
sw.start();
|
|
584
|
+
expect(sw.isRunning).toBe(true);
|
|
585
|
+
sw.stop();
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('elapsed is positive after start', async () => {
|
|
589
|
+
const sw = new Stopwatch();
|
|
590
|
+
sw.start();
|
|
591
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
592
|
+
expect(sw.elapsed).toBeGreaterThan(0);
|
|
593
|
+
sw.stop();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('stop pauses elapsed time', async () => {
|
|
597
|
+
const sw = new Stopwatch();
|
|
598
|
+
sw.start();
|
|
599
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
600
|
+
sw.stop();
|
|
601
|
+
const elapsed = sw.elapsed;
|
|
602
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
603
|
+
expect(sw.elapsed).toBeCloseTo(elapsed, 0);
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
it('reset clears elapsed and running state', () => {
|
|
607
|
+
const sw = new Stopwatch();
|
|
608
|
+
sw.start();
|
|
609
|
+
sw.reset();
|
|
610
|
+
expect(sw.elapsed).toBe(0);
|
|
611
|
+
expect(sw.isRunning).toBe(false);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it('restart resets and starts again', async () => {
|
|
615
|
+
const sw = new Stopwatch();
|
|
616
|
+
sw.start();
|
|
617
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
618
|
+
sw.restart();
|
|
619
|
+
expect(sw.isRunning).toBe(true);
|
|
620
|
+
expect(sw.elapsed).toBeLessThan(15);
|
|
621
|
+
sw.stop();
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('lap returns elapsed time and restarts', async () => {
|
|
625
|
+
const sw = new Stopwatch();
|
|
626
|
+
sw.start();
|
|
627
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
628
|
+
const lapTime = sw.lap();
|
|
629
|
+
expect(typeof lapTime).toBe('number');
|
|
630
|
+
expect(lapTime).toBeGreaterThan(0);
|
|
631
|
+
// After lap(), stopwatch restarted — elapsed should be less than before
|
|
632
|
+
expect(sw.elapsed).toBeLessThan(lapTime);
|
|
633
|
+
sw.stop();
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// ═══════════════════════════════════════════════
|
|
638
|
+
// Time — CountdownTimer
|
|
639
|
+
// ═══════════════════════════════════════════════
|
|
640
|
+
describe('CountdownTimer', () => {
|
|
641
|
+
it('starts not running', () => {
|
|
642
|
+
const ct = new CountdownTimer(1000);
|
|
643
|
+
expect(ct.isRunning).toBe(false);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('remaining equals duration before start', () => {
|
|
647
|
+
const ct = new CountdownTimer(5000);
|
|
648
|
+
expect(ct.remaining).toBe(5000);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it('isComplete is false before start', () => {
|
|
652
|
+
const ct = new CountdownTimer(1000);
|
|
653
|
+
expect(ct.isComplete).toBe(false);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('starts and becomes running', () => {
|
|
657
|
+
const ct = new CountdownTimer(1000);
|
|
658
|
+
ct.start();
|
|
659
|
+
expect(ct.isRunning).toBe(true);
|
|
660
|
+
ct.stop();
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('pause stops running', () => {
|
|
664
|
+
const ct = new CountdownTimer(1000);
|
|
665
|
+
ct.start();
|
|
666
|
+
ct.pause();
|
|
667
|
+
expect(ct.isRunning).toBe(false);
|
|
668
|
+
ct.stop();
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
it('reset restores remaining', () => {
|
|
672
|
+
const ct = new CountdownTimer(1000);
|
|
673
|
+
ct.start();
|
|
674
|
+
ct.reset(2000);
|
|
675
|
+
expect(ct.remaining).toBe(2000);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('fires onComplete callback', async () => {
|
|
679
|
+
const onComplete = vi.fn();
|
|
680
|
+
const ct = new CountdownTimer(50, { onComplete });
|
|
681
|
+
ct.start();
|
|
682
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
683
|
+
expect(onComplete).toHaveBeenCalledOnce();
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// ═══════════════════════════════════════════════
|
|
688
|
+
// Time — IntervalTimer
|
|
689
|
+
// ═══════════════════════════════════════════════
|
|
690
|
+
describe('IntervalTimer', () => {
|
|
691
|
+
it('starts not running', () => {
|
|
692
|
+
const it = new IntervalTimer(() => {}, 1000);
|
|
693
|
+
expect(it.running).toBe(false);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('start makes it running', () => {
|
|
697
|
+
const timer = new IntervalTimer(() => {}, 1000);
|
|
698
|
+
timer.start();
|
|
699
|
+
expect(timer.running).toBe(true);
|
|
700
|
+
timer.stop();
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('stop clears running', () => {
|
|
704
|
+
const timer = new IntervalTimer(() => {}, 1000);
|
|
705
|
+
timer.start();
|
|
706
|
+
timer.stop();
|
|
707
|
+
expect(timer.running).toBe(false);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('fires callback at interval', async () => {
|
|
711
|
+
const fn = vi.fn();
|
|
712
|
+
const timer = new IntervalTimer(fn, 10);
|
|
713
|
+
timer.start();
|
|
714
|
+
await new Promise((r) => setTimeout(r, 80));
|
|
715
|
+
timer.stop();
|
|
716
|
+
expect(fn.mock.calls.length).toBeGreaterThanOrEqual(2);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it('pause stops the interval without clearing isPaused', () => {
|
|
720
|
+
const timer = new IntervalTimer(() => {}, 1000);
|
|
721
|
+
timer.start();
|
|
722
|
+
timer.pause();
|
|
723
|
+
expect(timer.running).toBe(false);
|
|
724
|
+
expect(timer.paused).toBe(true);
|
|
725
|
+
timer.stop();
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// ═══════════════════════════════════════════════
|
|
730
|
+
// Time — schedule
|
|
731
|
+
// ═══════════════════════════════════════════════
|
|
732
|
+
describe('schedule', () => {
|
|
733
|
+
it('fires callback after delay', async () => {
|
|
734
|
+
const fn = vi.fn();
|
|
735
|
+
schedule(fn, 20);
|
|
736
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
737
|
+
expect(fn).toHaveBeenCalledOnce();
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it('cancel function prevents callback', async () => {
|
|
741
|
+
const fn = vi.fn();
|
|
742
|
+
const cancel = schedule(fn, 50);
|
|
743
|
+
cancel();
|
|
744
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
745
|
+
expect(fn).not.toHaveBeenCalled();
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
// ═══════════════════════════════════════════════
|
|
750
|
+
// Time — DateTime
|
|
751
|
+
// ═══════════════════════════════════════════════
|
|
752
|
+
describe('DateTime', () => {
|
|
753
|
+
it('toISO returns ISO string', () => {
|
|
754
|
+
const iso = DateTime.toISO(new Date(2024, 0, 15));
|
|
755
|
+
expect(iso).toMatch(/^2024/);
|
|
756
|
+
expect(iso).toContain('T');
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('format with YYYY pattern', () => {
|
|
760
|
+
const result = DateTime.format(new Date(2024, 5, 15), 'YYYY-MM-DD');
|
|
761
|
+
expect(result).toBe('2024-06-15');
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it('diff in milliseconds (d1 - d2)', () => {
|
|
765
|
+
// diff returns d1.getTime() - d2.getTime()
|
|
766
|
+
const d1 = new Date(2024, 0, 2);
|
|
767
|
+
const d2 = new Date(2024, 0, 1);
|
|
768
|
+
expect(DateTime.diff(d1, d2, 'ms')).toBe(86400000);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
it('isToday returns true for today', () => {
|
|
772
|
+
expect(DateTime.isToday(new Date())).toBe(true);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('isToday returns false for other day', () => {
|
|
776
|
+
expect(DateTime.isToday(new Date(2000, 0, 1))).toBe(false);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it('startOfDay returns midnight', () => {
|
|
780
|
+
const start = DateTime.startOfDay(new Date(2024, 5, 15, 12, 30));
|
|
781
|
+
expect(start.getHours()).toBe(0);
|
|
782
|
+
expect(start.getMinutes()).toBe(0);
|
|
783
|
+
expect(start.getSeconds()).toBe(0);
|
|
784
|
+
});
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// ═══════════════════════════════════════════════
|
|
788
|
+
// Collections — HoloMap
|
|
789
|
+
// ═══════════════════════════════════════════════
|
|
790
|
+
describe('HoloMap', () => {
|
|
791
|
+
it('creates empty map', () => {
|
|
792
|
+
const m = HoloMap.of<string, number>();
|
|
793
|
+
expect(m.size).toBe(0);
|
|
794
|
+
expect(m.isEmpty).toBe(true);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('set and get', () => {
|
|
798
|
+
const m = HoloMap.of<string, number>().set('a', 1);
|
|
799
|
+
expect(m.get('a')).toBe(1);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('set returns new map (immutable)', () => {
|
|
803
|
+
const m1 = HoloMap.of<string, number>();
|
|
804
|
+
const m2 = m1.set('x', 10);
|
|
805
|
+
expect(m1.has('x')).toBe(false);
|
|
806
|
+
expect(m2.has('x')).toBe(true);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
it('delete removes entry', () => {
|
|
810
|
+
const m = HoloMap.of<string, number>().set('a', 1).delete('a');
|
|
811
|
+
expect(m.has('a')).toBe(false);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
it('has returns true for existing key', () => {
|
|
815
|
+
const m = HoloMap.of<string, number>().set('z', 99);
|
|
816
|
+
expect(m.has('z')).toBe(true);
|
|
817
|
+
expect(m.has('w')).toBe(false);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('getOrDefault returns default for missing key', () => {
|
|
821
|
+
const m = HoloMap.of<string, number>();
|
|
822
|
+
expect(m.getOrDefault('missing', 42)).toBe(42);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it('from creates from entries', () => {
|
|
826
|
+
const m = HoloMap.from([
|
|
827
|
+
['a', 1],
|
|
828
|
+
['b', 2],
|
|
829
|
+
]);
|
|
830
|
+
expect(m.size).toBe(2);
|
|
831
|
+
expect(m.get('b')).toBe(2);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
it('merge combines two maps', () => {
|
|
835
|
+
const m1 = HoloMap.from([['a', 1]]);
|
|
836
|
+
const m2 = HoloMap.from([['b', 2]]);
|
|
837
|
+
const merged = m1.merge(m2);
|
|
838
|
+
expect(merged.size).toBe(2);
|
|
839
|
+
expect(merged.get('a')).toBe(1);
|
|
840
|
+
expect(merged.get('b')).toBe(2);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it('map transforms values', () => {
|
|
844
|
+
const m = HoloMap.from([
|
|
845
|
+
['a', 1],
|
|
846
|
+
['b', 2],
|
|
847
|
+
]).map((v) => v * 10);
|
|
848
|
+
expect(m.get('a')).toBe(10);
|
|
849
|
+
expect(m.get('b')).toBe(20);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('filter keeps matching entries', () => {
|
|
853
|
+
const m = HoloMap.from([
|
|
854
|
+
['a', 1],
|
|
855
|
+
['b', 2],
|
|
856
|
+
['c', 3],
|
|
857
|
+
]).filter((v) => v > 1);
|
|
858
|
+
expect(m.size).toBe(2);
|
|
859
|
+
expect(m.has('a')).toBe(false);
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('keys, values, entries iteration', () => {
|
|
863
|
+
const m = HoloMap.from([
|
|
864
|
+
['x', 1],
|
|
865
|
+
['y', 2],
|
|
866
|
+
]);
|
|
867
|
+
expect([...m.keys()].sort()).toEqual(['x', 'y']);
|
|
868
|
+
expect([...m.values()].sort()).toEqual([1, 2]);
|
|
869
|
+
expect([...m.entries()].length).toBe(2);
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it('toObject converts to plain object', () => {
|
|
873
|
+
const m = HoloMap.from([
|
|
874
|
+
['a', 1],
|
|
875
|
+
['b', 2],
|
|
876
|
+
]);
|
|
877
|
+
const obj = (m as HoloMap<string, number>).toObject();
|
|
878
|
+
expect(obj).toEqual({ a: 1, b: 2 });
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
// ═══════════════════════════════════════════════
|
|
883
|
+
// Collections — HoloSet
|
|
884
|
+
// ═══════════════════════════════════════════════
|
|
885
|
+
describe('HoloSet', () => {
|
|
886
|
+
it('creates empty set', () => {
|
|
887
|
+
const s = HoloSet.of<number>();
|
|
888
|
+
expect(s.size).toBe(0);
|
|
889
|
+
expect(s.isEmpty).toBe(true);
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
it('add and has', () => {
|
|
893
|
+
const s = HoloSet.of<number>().add(1);
|
|
894
|
+
expect(s.has(1)).toBe(true);
|
|
895
|
+
expect(s.has(2)).toBe(false);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
it('add is immutable', () => {
|
|
899
|
+
const s1 = HoloSet.of<number>();
|
|
900
|
+
const s2 = s1.add(5);
|
|
901
|
+
expect(s1.has(5)).toBe(false);
|
|
902
|
+
expect(s2.has(5)).toBe(true);
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('delete removes element', () => {
|
|
906
|
+
const s = HoloSet.of<number>().add(1).delete(1);
|
|
907
|
+
expect(s.has(1)).toBe(false);
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it('from creates from array', () => {
|
|
911
|
+
const s = HoloSet.from([1, 2, 3]);
|
|
912
|
+
expect(s.size).toBe(3);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it('union combines elements', () => {
|
|
916
|
+
const a = HoloSet.from([1, 2]);
|
|
917
|
+
const b = HoloSet.from([2, 3]);
|
|
918
|
+
const u = a.union(b);
|
|
919
|
+
expect(u.size).toBe(3);
|
|
920
|
+
expect(u.has(1)).toBe(true);
|
|
921
|
+
expect(u.has(3)).toBe(true);
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
it('intersection keeps common elements', () => {
|
|
925
|
+
const a = HoloSet.from([1, 2, 3]);
|
|
926
|
+
const b = HoloSet.from([2, 3, 4]);
|
|
927
|
+
const inter = a.intersection(b);
|
|
928
|
+
expect(inter.size).toBe(2);
|
|
929
|
+
expect(inter.has(1)).toBe(false);
|
|
930
|
+
expect(inter.has(2)).toBe(true);
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
it('difference keeps elements not in other', () => {
|
|
934
|
+
const a = HoloSet.from([1, 2, 3]);
|
|
935
|
+
const b = HoloSet.from([2]);
|
|
936
|
+
const diff = a.difference(b);
|
|
937
|
+
expect(diff.size).toBe(2);
|
|
938
|
+
expect(diff.has(2)).toBe(false);
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it('isSubsetOf / isSupersetOf', () => {
|
|
942
|
+
const small = HoloSet.from([1, 2]);
|
|
943
|
+
const big = HoloSet.from([1, 2, 3]);
|
|
944
|
+
expect(small.isSubsetOf(big)).toBe(true);
|
|
945
|
+
expect(big.isSupersetOf(small)).toBe(true);
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it('toArray converts to array', () => {
|
|
949
|
+
const s = HoloSet.from([10, 20, 30]);
|
|
950
|
+
expect(s.toArray().sort((a, b) => a - b)).toEqual([10, 20, 30]);
|
|
951
|
+
});
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
// ═══════════════════════════════════════════════
|
|
955
|
+
// Collections — PriorityQueue
|
|
956
|
+
// ═══════════════════════════════════════════════
|
|
957
|
+
describe('PriorityQueue', () => {
|
|
958
|
+
it('creates empty queue', () => {
|
|
959
|
+
const pq = new PriorityQueue<number>();
|
|
960
|
+
expect(pq.size).toBe(0);
|
|
961
|
+
expect(pq.isEmpty).toBe(true);
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it('enqueue and size', () => {
|
|
965
|
+
const pq = new PriorityQueue<number>();
|
|
966
|
+
pq.enqueue(5, 1);
|
|
967
|
+
pq.enqueue(3, 0);
|
|
968
|
+
expect(pq.size).toBe(2);
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
it('dequeue returns item with highest priority (min-heap)', () => {
|
|
972
|
+
const pq = PriorityQueue.minHeap<string>();
|
|
973
|
+
pq.enqueue('low', 10);
|
|
974
|
+
pq.enqueue('high', 1);
|
|
975
|
+
pq.enqueue('mid', 5);
|
|
976
|
+
expect(pq.dequeue()).toBe('high');
|
|
977
|
+
expect(pq.dequeue()).toBe('mid');
|
|
978
|
+
expect(pq.dequeue()).toBe('low');
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
it('maxHeap dequeues highest priority first', () => {
|
|
982
|
+
const pq = PriorityQueue.maxHeap<string>();
|
|
983
|
+
pq.enqueue('low', 1);
|
|
984
|
+
pq.enqueue('high', 10);
|
|
985
|
+
expect(pq.dequeue()).toBe('high');
|
|
986
|
+
expect(pq.dequeue()).toBe('low');
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
it('peek does not remove item', () => {
|
|
990
|
+
const pq = PriorityQueue.minHeap<number>();
|
|
991
|
+
pq.enqueue(42, 1);
|
|
992
|
+
expect(pq.peek()).toBe(42);
|
|
993
|
+
expect(pq.size).toBe(1);
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
it('dequeue on empty returns undefined', () => {
|
|
997
|
+
const pq = new PriorityQueue<number>();
|
|
998
|
+
expect(pq.dequeue()).toBeUndefined();
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
it('clear empties the queue', () => {
|
|
1002
|
+
const pq = PriorityQueue.minHeap<number>();
|
|
1003
|
+
pq.enqueue(1, 1);
|
|
1004
|
+
pq.enqueue(2, 2);
|
|
1005
|
+
pq.clear();
|
|
1006
|
+
expect(pq.size).toBe(0);
|
|
1007
|
+
expect(pq.isEmpty).toBe(true);
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
// ═══════════════════════════════════════════════
|
|
1012
|
+
// Collections — List (extended)
|
|
1013
|
+
// ═══════════════════════════════════════════════
|
|
1014
|
+
describe('List (extended)', () => {
|
|
1015
|
+
it('range creates sequential list (exclusive end)', () => {
|
|
1016
|
+
const l = List.range(1, 6);
|
|
1017
|
+
expect(l.toArray()).toEqual([1, 2, 3, 4, 5]);
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
it('groupBy groups elements', () => {
|
|
1021
|
+
const l = List.of(1, 2, 3, 4);
|
|
1022
|
+
const grouped = l.groupBy((n) => (n % 2 === 0 ? 'even' : 'odd'));
|
|
1023
|
+
expect(grouped.get('even')?.toArray().sort()).toEqual([2, 4]);
|
|
1024
|
+
expect(grouped.get('odd')?.toArray().sort()).toEqual([1, 3]);
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
it('partition splits by predicate', () => {
|
|
1028
|
+
const l = List.of(1, 2, 3, 4, 5);
|
|
1029
|
+
const [evens, odds] = l.partition((n) => n % 2 === 0);
|
|
1030
|
+
expect(evens.toArray()).toEqual([2, 4]);
|
|
1031
|
+
expect(odds.toArray()).toEqual([1, 3, 5]);
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
it('zip pairs two lists', () => {
|
|
1035
|
+
const a = List.of(1, 2, 3);
|
|
1036
|
+
const b = List.of('a', 'b', 'c');
|
|
1037
|
+
const zipped = a.zip(b);
|
|
1038
|
+
expect(zipped.toArray()).toEqual([
|
|
1039
|
+
[1, 'a'],
|
|
1040
|
+
[2, 'b'],
|
|
1041
|
+
[3, 'c'],
|
|
1042
|
+
]);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it('unique removes duplicates', () => {
|
|
1046
|
+
const l = List.of(1, 2, 1, 3, 2);
|
|
1047
|
+
expect(l.unique().toArray().sort()).toEqual([1, 2, 3]);
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
it('takeLast returns last N elements', () => {
|
|
1051
|
+
const l = List.of(1, 2, 3, 4, 5);
|
|
1052
|
+
expect(l.takeLast(3).toArray()).toEqual([3, 4, 5]);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
it('dropLast removes last N elements', () => {
|
|
1056
|
+
const l = List.of(1, 2, 3, 4, 5);
|
|
1057
|
+
expect(l.dropLast(2).toArray()).toEqual([1, 2, 3]);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it('average of numeric list', () => {
|
|
1061
|
+
const l = List.of(2, 4, 6);
|
|
1062
|
+
expect((l as List<number>).average()).toBe(4);
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
it('none returns true when predicate never satisfied', () => {
|
|
1066
|
+
const l = List.of(1, 2, 3);
|
|
1067
|
+
expect(l.none((n) => n > 10)).toBe(true);
|
|
1068
|
+
expect(l.none((n) => n > 1)).toBe(false);
|
|
1069
|
+
});
|
|
1070
|
+
});
|