ts-data-forge 1.0.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.
Files changed (143) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +534 -0
  3. package/package.json +101 -0
  4. package/src/array/array-utils-creation.test.mts +443 -0
  5. package/src/array/array-utils-modification.test.mts +197 -0
  6. package/src/array/array-utils-overload-type-error.test.mts +149 -0
  7. package/src/array/array-utils-reducing-value.test.mts +425 -0
  8. package/src/array/array-utils-search.test.mts +169 -0
  9. package/src/array/array-utils-set-op.test.mts +335 -0
  10. package/src/array/array-utils-slice-clamped.test.mts +113 -0
  11. package/src/array/array-utils-slicing.test.mts +316 -0
  12. package/src/array/array-utils-transformation.test.mts +790 -0
  13. package/src/array/array-utils-validation.test.mts +492 -0
  14. package/src/array/array-utils.mts +4000 -0
  15. package/src/array/array.test.mts +146 -0
  16. package/src/array/index.mts +2 -0
  17. package/src/array/tuple-utils.mts +519 -0
  18. package/src/array/tuple-utils.test.mts +518 -0
  19. package/src/collections/imap-mapped.mts +801 -0
  20. package/src/collections/imap-mapped.test.mts +860 -0
  21. package/src/collections/imap.mts +651 -0
  22. package/src/collections/imap.test.mts +932 -0
  23. package/src/collections/index.mts +6 -0
  24. package/src/collections/iset-mapped.mts +889 -0
  25. package/src/collections/iset-mapped.test.mts +1187 -0
  26. package/src/collections/iset.mts +682 -0
  27. package/src/collections/iset.test.mts +1084 -0
  28. package/src/collections/queue.mts +390 -0
  29. package/src/collections/queue.test.mts +282 -0
  30. package/src/collections/stack.mts +423 -0
  31. package/src/collections/stack.test.mts +225 -0
  32. package/src/expect-type.mts +206 -0
  33. package/src/functional/index.mts +4 -0
  34. package/src/functional/match.mts +300 -0
  35. package/src/functional/match.test.mts +177 -0
  36. package/src/functional/optional.mts +733 -0
  37. package/src/functional/optional.test.mts +619 -0
  38. package/src/functional/pipe.mts +212 -0
  39. package/src/functional/pipe.test.mts +85 -0
  40. package/src/functional/result.mts +1134 -0
  41. package/src/functional/result.test.mts +777 -0
  42. package/src/globals.d.mts +38 -0
  43. package/src/guard/has-key.mts +119 -0
  44. package/src/guard/has-key.test.mts +219 -0
  45. package/src/guard/index.mts +7 -0
  46. package/src/guard/is-non-empty-string.mts +108 -0
  47. package/src/guard/is-non-empty-string.test.mts +91 -0
  48. package/src/guard/is-non-null-object.mts +106 -0
  49. package/src/guard/is-non-null-object.test.mts +90 -0
  50. package/src/guard/is-primitive.mts +165 -0
  51. package/src/guard/is-primitive.test.mts +102 -0
  52. package/src/guard/is-record.mts +153 -0
  53. package/src/guard/is-record.test.mts +112 -0
  54. package/src/guard/is-type.mts +450 -0
  55. package/src/guard/is-type.test.mts +496 -0
  56. package/src/guard/key-is-in.mts +163 -0
  57. package/src/guard/key-is-in.test.mts +19 -0
  58. package/src/index.mts +10 -0
  59. package/src/iterator/index.mts +1 -0
  60. package/src/iterator/range.mts +120 -0
  61. package/src/iterator/range.test.mts +33 -0
  62. package/src/json/index.mts +1 -0
  63. package/src/json/json.mts +711 -0
  64. package/src/json/json.test.mts +628 -0
  65. package/src/number/branded-types/finite-number.mts +354 -0
  66. package/src/number/branded-types/finite-number.test.mts +135 -0
  67. package/src/number/branded-types/index.mts +26 -0
  68. package/src/number/branded-types/int.mts +278 -0
  69. package/src/number/branded-types/int.test.mts +140 -0
  70. package/src/number/branded-types/int16.mts +192 -0
  71. package/src/number/branded-types/int16.test.mts +170 -0
  72. package/src/number/branded-types/int32.mts +193 -0
  73. package/src/number/branded-types/int32.test.mts +170 -0
  74. package/src/number/branded-types/non-negative-finite-number.mts +223 -0
  75. package/src/number/branded-types/non-negative-finite-number.test.mts +188 -0
  76. package/src/number/branded-types/non-negative-int16.mts +187 -0
  77. package/src/number/branded-types/non-negative-int16.test.mts +201 -0
  78. package/src/number/branded-types/non-negative-int32.mts +187 -0
  79. package/src/number/branded-types/non-negative-int32.test.mts +204 -0
  80. package/src/number/branded-types/non-zero-finite-number.mts +229 -0
  81. package/src/number/branded-types/non-zero-finite-number.test.mts +198 -0
  82. package/src/number/branded-types/non-zero-int.mts +167 -0
  83. package/src/number/branded-types/non-zero-int.test.mts +177 -0
  84. package/src/number/branded-types/non-zero-int16.mts +196 -0
  85. package/src/number/branded-types/non-zero-int16.test.mts +195 -0
  86. package/src/number/branded-types/non-zero-int32.mts +196 -0
  87. package/src/number/branded-types/non-zero-int32.test.mts +197 -0
  88. package/src/number/branded-types/non-zero-safe-int.mts +196 -0
  89. package/src/number/branded-types/non-zero-safe-int.test.mts +232 -0
  90. package/src/number/branded-types/non-zero-uint16.mts +189 -0
  91. package/src/number/branded-types/non-zero-uint16.test.mts +199 -0
  92. package/src/number/branded-types/non-zero-uint32.mts +189 -0
  93. package/src/number/branded-types/non-zero-uint32.test.mts +199 -0
  94. package/src/number/branded-types/positive-finite-number.mts +241 -0
  95. package/src/number/branded-types/positive-finite-number.test.mts +204 -0
  96. package/src/number/branded-types/positive-int.mts +304 -0
  97. package/src/number/branded-types/positive-int.test.mts +176 -0
  98. package/src/number/branded-types/positive-int16.mts +188 -0
  99. package/src/number/branded-types/positive-int16.test.mts +197 -0
  100. package/src/number/branded-types/positive-int32.mts +188 -0
  101. package/src/number/branded-types/positive-int32.test.mts +197 -0
  102. package/src/number/branded-types/positive-safe-int.mts +187 -0
  103. package/src/number/branded-types/positive-safe-int.test.mts +210 -0
  104. package/src/number/branded-types/positive-uint16.mts +188 -0
  105. package/src/number/branded-types/positive-uint16.test.mts +203 -0
  106. package/src/number/branded-types/positive-uint32.mts +188 -0
  107. package/src/number/branded-types/positive-uint32.test.mts +203 -0
  108. package/src/number/branded-types/safe-int.mts +291 -0
  109. package/src/number/branded-types/safe-int.test.mts +170 -0
  110. package/src/number/branded-types/safe-uint.mts +187 -0
  111. package/src/number/branded-types/safe-uint.test.mts +176 -0
  112. package/src/number/branded-types/uint.mts +179 -0
  113. package/src/number/branded-types/uint.test.mts +158 -0
  114. package/src/number/branded-types/uint16.mts +186 -0
  115. package/src/number/branded-types/uint16.test.mts +170 -0
  116. package/src/number/branded-types/uint32.mts +218 -0
  117. package/src/number/branded-types/uint32.test.mts +170 -0
  118. package/src/number/enum/index.mts +2 -0
  119. package/src/number/enum/int8.mts +344 -0
  120. package/src/number/enum/int8.test.mts +180 -0
  121. package/src/number/enum/uint8.mts +293 -0
  122. package/src/number/enum/uint8.test.mts +164 -0
  123. package/src/number/index.mts +4 -0
  124. package/src/number/num.mts +604 -0
  125. package/src/number/num.test.mts +242 -0
  126. package/src/number/refined-number-utils.mts +566 -0
  127. package/src/object/index.mts +1 -0
  128. package/src/object/object.mts +447 -0
  129. package/src/object/object.test.mts +124 -0
  130. package/src/others/cast-mutable.mts +113 -0
  131. package/src/others/cast-readonly.mts +192 -0
  132. package/src/others/cast-readonly.test.mts +89 -0
  133. package/src/others/if-then.mts +98 -0
  134. package/src/others/if-then.test.mts +75 -0
  135. package/src/others/index.mts +7 -0
  136. package/src/others/map-nullable.mts +172 -0
  137. package/src/others/map-nullable.test.mts +297 -0
  138. package/src/others/memoize-function.mts +196 -0
  139. package/src/others/memoize-function.test.mts +168 -0
  140. package/src/others/tuple.mts +160 -0
  141. package/src/others/tuple.test.mts +11 -0
  142. package/src/others/unknown-to-string.mts +215 -0
  143. package/src/others/unknown-to-string.test.mts +114 -0
@@ -0,0 +1,790 @@
1
+ import { IMap } from '../collections/index.mjs';
2
+ import { expectType } from '../expect-type.mjs';
3
+ import { Optional } from '../functional/optional.mjs';
4
+ import { Arr } from './array-utils.mjs';
5
+
6
+ describe('Arr transformations', () => {
7
+ describe('partition', () => {
8
+ const xs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] as const;
9
+
10
+ {
11
+ const result = Arr.partition(xs, 4);
12
+
13
+ expectType<
14
+ typeof result,
15
+ readonly (readonly (
16
+ | 1
17
+ | 2
18
+ | 3
19
+ | 4
20
+ | 5
21
+ | 6
22
+ | 7
23
+ | 8
24
+ | 9
25
+ | 10
26
+ | 11
27
+ | 12
28
+ )[])[]
29
+ >('=');
30
+
31
+ test('case 1', () => {
32
+ expect(result).toStrictEqual([
33
+ [1, 2, 3, 4],
34
+ [5, 6, 7, 8],
35
+ [9, 10, 11, 12],
36
+ ]);
37
+ });
38
+ }
39
+
40
+ {
41
+ const result = Arr.partition(xs, 3);
42
+
43
+ expectType<
44
+ typeof result,
45
+ readonly (readonly (
46
+ | 1
47
+ | 2
48
+ | 3
49
+ | 4
50
+ | 5
51
+ | 6
52
+ | 7
53
+ | 8
54
+ | 9
55
+ | 10
56
+ | 11
57
+ | 12
58
+ )[])[]
59
+ >('=');
60
+
61
+ test('case 2', () => {
62
+ expect(result).toStrictEqual([
63
+ [1, 2, 3],
64
+ [4, 5, 6],
65
+ [7, 8, 9],
66
+ [10, 11, 12],
67
+ ]);
68
+ });
69
+ }
70
+
71
+ {
72
+ const result = Arr.partition(xs, 5);
73
+
74
+ expectType<
75
+ typeof result,
76
+ readonly (readonly (
77
+ | 1
78
+ | 2
79
+ | 3
80
+ | 4
81
+ | 5
82
+ | 6
83
+ | 7
84
+ | 8
85
+ | 9
86
+ | 10
87
+ | 11
88
+ | 12
89
+ )[])[]
90
+ >('=');
91
+
92
+ test('case 3', () => {
93
+ expect(result).toStrictEqual([
94
+ [1, 2, 3, 4, 5],
95
+ [6, 7, 8, 9, 10],
96
+ [11, 12],
97
+ ]);
98
+ });
99
+ }
100
+
101
+ test('should partition array into chunks', () => {
102
+ const numbers = [1, 2, 3, 4, 5, 6];
103
+ const result = Arr.partition(numbers, 2);
104
+
105
+ expect(result).toStrictEqual([
106
+ [1, 2],
107
+ [3, 4],
108
+ [5, 6],
109
+ ]);
110
+ });
111
+
112
+ test('should handle arrays not evenly divisible by chunk size', () => {
113
+ const numbers = [1, 2, 3, 4, 5];
114
+ const result = Arr.partition(numbers, 2);
115
+
116
+ expect(result).toStrictEqual([[1, 2], [3, 4], [5]]);
117
+ });
118
+
119
+ test('should work with chunk size < 2 (returns empty)', () => {
120
+ const numbers = [1, 2, 3];
121
+ const result = Arr.partition(numbers, 1);
122
+
123
+ // According to docs, returns empty array if chunkSize < 2
124
+ expect(result).toStrictEqual([]);
125
+ });
126
+
127
+ test('should work with chunk size larger than array', () => {
128
+ const numbers = [1, 2];
129
+ const result = Arr.partition(numbers, 5);
130
+
131
+ expect(result).toStrictEqual([[1, 2]]);
132
+ });
133
+
134
+ test('should work with empty array', () => {
135
+ const empty: readonly number[] = [];
136
+ const result = Arr.partition(empty, 2);
137
+
138
+ expect(result).toStrictEqual([]);
139
+ });
140
+
141
+ test('should partition array into chunks', () => {
142
+ const numbers = [1, 2, 3, 4, 5, 6];
143
+ const result = Arr.partition(numbers, 2);
144
+
145
+ expect(result).toStrictEqual([
146
+ [1, 2],
147
+ [3, 4],
148
+ [5, 6],
149
+ ]);
150
+ });
151
+
152
+ test('should handle arrays not evenly divisible by chunk size', () => {
153
+ const numbers = [1, 2, 3, 4, 5];
154
+ const result = Arr.partition(numbers, 2);
155
+
156
+ expect(result).toStrictEqual([[1, 2], [3, 4], [5]]);
157
+ });
158
+
159
+ test('should work with chunk size < 2 (returns empty)', () => {
160
+ const numbers = [1, 2, 3];
161
+ const result = Arr.partition(numbers, 1);
162
+
163
+ // According to docs, returns empty array if chunkSize < 2
164
+ expect(result).toStrictEqual([]);
165
+ });
166
+
167
+ test('should work with chunk size larger than array', () => {
168
+ const numbers = [1, 2];
169
+ const result = Arr.partition(numbers, 5);
170
+
171
+ expect(result).toStrictEqual([[1, 2]]);
172
+ });
173
+
174
+ test('should work with empty array', () => {
175
+ const empty: readonly number[] = [];
176
+ const result = Arr.partition(empty, 2);
177
+
178
+ expect(result).toStrictEqual([]);
179
+ });
180
+ });
181
+
182
+ describe('toReversed', () => {
183
+ {
184
+ const xs = [1, 2, 3] as const;
185
+ const result = xs.toReversed();
186
+
187
+ expectType<typeof result, (1 | 2 | 3)[]>('=');
188
+
189
+ test('case 1', () => {
190
+ expect(result).toStrictEqual([3, 2, 1]);
191
+ });
192
+ }
193
+ });
194
+
195
+ describe('toSorted', () => {
196
+ {
197
+ const xs = [2, 1, 3] as const;
198
+ const result = xs.toSorted();
199
+
200
+ expectType<typeof result, (1 | 2 | 3)[]>('=');
201
+
202
+ test('case 1', () => {
203
+ expect(result).toStrictEqual([1, 2, 3]);
204
+ });
205
+ }
206
+ {
207
+ const xs = [2, 1, 3] as const;
208
+ const result = xs.toSorted((a, b) => a - b);
209
+
210
+ expectType<typeof result, (1 | 2 | 3)[]>('=');
211
+
212
+ test('case 2', () => {
213
+ expect(result).toStrictEqual([1, 2, 3]);
214
+ });
215
+ }
216
+ {
217
+ const xs = [2, 1, 3] as const;
218
+ const result = xs.toSorted((a, b) => b - a);
219
+
220
+ expectType<typeof result, (1 | 2 | 3)[]>('=');
221
+
222
+ test('case 3', () => {
223
+ expect(result).toStrictEqual([3, 2, 1]);
224
+ });
225
+ }
226
+ });
227
+
228
+ describe('toSortedBy', () => {
229
+ {
230
+ const xs = [{ v: 2 }, { v: 1 }, { v: 3 }] as const;
231
+ const sorted = Arr.toSortedBy(xs, (x) => x.v);
232
+
233
+ expectType<
234
+ typeof sorted,
235
+ readonly (
236
+ | Readonly<{ v: 1 }>
237
+ | Readonly<{ v: 2 }>
238
+ | Readonly<{ v: 3 }>
239
+ )[]
240
+ >('=');
241
+
242
+ test('case 1', () => {
243
+ expect(sorted).toStrictEqual([{ v: 1 }, { v: 2 }, { v: 3 }]);
244
+ });
245
+ }
246
+ {
247
+ const xs = [{ v: 2 }, { v: 1 }, { v: 3 }] as const;
248
+ const sorted = Arr.toSortedBy(
249
+ xs,
250
+ (x) => x.v,
251
+ (a, b) => a - b,
252
+ );
253
+
254
+ expectType<
255
+ typeof sorted,
256
+ readonly (
257
+ | Readonly<{ v: 1 }>
258
+ | Readonly<{ v: 2 }>
259
+ | Readonly<{ v: 3 }>
260
+ )[]
261
+ >('=');
262
+
263
+ test('case 2', () => {
264
+ expect(sorted).toStrictEqual([{ v: 1 }, { v: 2 }, { v: 3 }]);
265
+ });
266
+ }
267
+
268
+ test('should sort by key function', () => {
269
+ const people = [
270
+ { name: 'Alice', age: 30 },
271
+ { name: 'Bob', age: 20 },
272
+ { name: 'Charlie', age: 25 },
273
+ ];
274
+ const result = Arr.toSortedBy(people, (person) => person.age);
275
+
276
+ expect(result).toHaveLength(3);
277
+ expect(result[0]?.name).toBe('Bob');
278
+ expect(result[1]?.name).toBe('Charlie');
279
+ expect(result[2]?.name).toBe('Alice');
280
+ });
281
+
282
+ test('should work with string sorting', () => {
283
+ const words = ['banana', 'apple', 'cherry'];
284
+ const result = Arr.toSortedBy(
285
+ words,
286
+ (word: string) => word,
287
+ (a: string, b: string) => a.localeCompare(b),
288
+ );
289
+ expect(result).toStrictEqual(['apple', 'banana', 'cherry']);
290
+ });
291
+
292
+ test('should work with custom key extraction', () => {
293
+ const items = ['hello', 'hi', 'welcome', 'bye'];
294
+ const result = Arr.toSortedBy(items, (item) => item.length);
295
+ expect(result).toStrictEqual(['hi', 'bye', 'hello', 'welcome']);
296
+ });
297
+
298
+ test('should work with empty array', () => {
299
+ const empty: readonly { value: number }[] = [];
300
+ const result = Arr.toSortedBy(empty, (item) => item.value);
301
+ expect(result).toStrictEqual([]);
302
+ });
303
+
304
+ test('should sort by key function', () => {
305
+ const people = [
306
+ { name: 'Alice', age: 30 },
307
+ { name: 'Bob', age: 20 },
308
+ { name: 'Charlie', age: 25 },
309
+ ];
310
+ const result = Arr.toSortedBy(people, (person) => person.age);
311
+
312
+ expect(result).toHaveLength(3);
313
+ expect(result[0]?.name).toBe('Bob');
314
+ expect(result[1]?.name).toBe('Charlie');
315
+ expect(result[2]?.name).toBe('Alice');
316
+ });
317
+
318
+ test('should work with string sorting', () => {
319
+ const words = ['banana', 'apple', 'cherry'];
320
+ const result = Arr.toSortedBy(
321
+ words,
322
+ (word: string) => word,
323
+ (a: string, b: string) => a.localeCompare(b),
324
+ );
325
+ expect(result).toStrictEqual(['apple', 'banana', 'cherry']);
326
+ });
327
+
328
+ test('should work with custom key extraction', () => {
329
+ const items = ['hello', 'hi', 'welcome', 'bye'];
330
+ const result = Arr.toSortedBy(items, (item) => item.length);
331
+ expect(result).toStrictEqual(['hi', 'bye', 'hello', 'welcome']);
332
+ });
333
+
334
+ test('should work with empty array', () => {
335
+ const empty: readonly { value: number }[] = [];
336
+ const result = Arr.toSortedBy(empty, (item) => item.value);
337
+ expect(result).toStrictEqual([]);
338
+ });
339
+ });
340
+
341
+ describe('groupBy', () => {
342
+ const xs = [
343
+ { x: 1, y: 1 },
344
+ { x: 2, y: 1 },
345
+ { x: 3, y: 1 },
346
+ { x: 1, y: 2 },
347
+ { x: 2, y: 2 },
348
+ { x: 1, y: 3 },
349
+ ] as const;
350
+
351
+ const result = Arr.groupBy(xs, (a) => a.x);
352
+
353
+ expectType<
354
+ typeof result,
355
+ IMap<
356
+ 1 | 2 | 3,
357
+ readonly (
358
+ | Readonly<{ x: 1; y: 1 }>
359
+ | Readonly<{ x: 1; y: 2 }>
360
+ | Readonly<{ x: 1; y: 3 }>
361
+ | Readonly<{ x: 2; y: 1 }>
362
+ | Readonly<{ x: 2; y: 2 }>
363
+ | Readonly<{ x: 3; y: 1 }>
364
+ )[]
365
+ >
366
+ >('=');
367
+
368
+ test('case 1', () => {
369
+ expect(result).toStrictEqual(
370
+ IMap.create<
371
+ 1 | 2 | 3,
372
+ readonly (
373
+ | Readonly<{ x: 1; y: 1 }>
374
+ | Readonly<{ x: 1; y: 2 }>
375
+ | Readonly<{ x: 1; y: 3 }>
376
+ | Readonly<{ x: 2; y: 1 }>
377
+ | Readonly<{ x: 2; y: 2 }>
378
+ | Readonly<{ x: 3; y: 1 }>
379
+ )[]
380
+ >([
381
+ [
382
+ 1,
383
+ [
384
+ { x: 1, y: 1 },
385
+ { x: 1, y: 2 },
386
+ { x: 1, y: 3 },
387
+ ],
388
+ ],
389
+ [
390
+ 2,
391
+ [
392
+ { x: 2, y: 1 },
393
+ { x: 2, y: 2 },
394
+ ],
395
+ ],
396
+ [3, [{ x: 3, y: 1 }]],
397
+ ]),
398
+ );
399
+ });
400
+
401
+ test('should group elements by key', () => {
402
+ const array = [
403
+ { type: 'fruit', name: 'apple' },
404
+ { type: 'vegetable', name: 'carrot' },
405
+ { type: 'fruit', name: 'banana' },
406
+ ];
407
+ const grouped = Arr.groupBy(array, (item) => item.type);
408
+
409
+ expect(grouped.size).toBe(2);
410
+ const fruits = grouped.get('fruit');
411
+ const vegetables = grouped.get('vegetable');
412
+
413
+ expect(Optional.isSome(fruits)).toBe(true);
414
+ expect(Optional.isSome(vegetables)).toBe(true);
415
+
416
+ if (Optional.isSome(fruits)) {
417
+ expect(fruits.value).toHaveLength(2);
418
+ expect(fruits.value[0]?.name).toBe('apple');
419
+ expect(fruits.value[1]?.name).toBe('banana');
420
+ }
421
+
422
+ if (Optional.isSome(vegetables)) {
423
+ expect(vegetables.value).toHaveLength(1);
424
+ expect(vegetables.value[0]?.name).toBe('carrot');
425
+ }
426
+ });
427
+
428
+ test('should work with numeric keys', () => {
429
+ const numbers = [1, 2, 3, 4, 5, 6];
430
+ const grouped = Arr.groupBy(numbers, (n) => n % 2);
431
+
432
+ expect(grouped.size).toBe(2);
433
+ const evens = grouped.get(0);
434
+ const odds = grouped.get(1);
435
+
436
+ if (Optional.isSome(evens)) {
437
+ expect(evens.value).toStrictEqual([2, 4, 6]);
438
+ }
439
+
440
+ if (Optional.isSome(odds)) {
441
+ expect(odds.value).toStrictEqual([1, 3, 5]);
442
+ }
443
+ });
444
+
445
+ test('should work with empty array', () => {
446
+ const empty: readonly number[] = [];
447
+ const grouped = Arr.groupBy(empty, (n) => n % 2);
448
+ expect(grouped.size).toBe(0);
449
+ });
450
+
451
+ test('should handle all elements in same group', () => {
452
+ const array = [1, 2, 3, 4];
453
+ const grouped = Arr.groupBy(array, () => 'all');
454
+
455
+ expect(grouped.size).toBe(1);
456
+ const all = grouped.get('all');
457
+
458
+ if (Optional.isSome(all)) {
459
+ expect(all.value).toStrictEqual([1, 2, 3, 4]);
460
+ }
461
+ });
462
+
463
+ test('should group elements by key', () => {
464
+ const array = [
465
+ { type: 'fruit', name: 'apple' },
466
+ { type: 'vegetable', name: 'carrot' },
467
+ { type: 'fruit', name: 'banana' },
468
+ ];
469
+ const grouped = Arr.groupBy(array, (item) => item.type);
470
+
471
+ expect(grouped.size).toBe(2);
472
+ const fruits = grouped.get('fruit');
473
+ const vegetables = grouped.get('vegetable');
474
+
475
+ expect(Optional.isSome(fruits)).toBe(true);
476
+ expect(Optional.isSome(vegetables)).toBe(true);
477
+
478
+ if (Optional.isSome(fruits)) {
479
+ expect(fruits.value).toHaveLength(2);
480
+ expect(fruits.value[0]?.name).toBe('apple');
481
+ expect(fruits.value[1]?.name).toBe('banana');
482
+ }
483
+
484
+ if (Optional.isSome(vegetables)) {
485
+ expect(vegetables.value).toHaveLength(1);
486
+ expect(vegetables.value[0]?.name).toBe('carrot');
487
+ }
488
+ });
489
+
490
+ test('should work with numeric keys', () => {
491
+ const numbers = [1, 2, 3, 4, 5, 6];
492
+ const grouped = Arr.groupBy(numbers, (n) => n % 2);
493
+
494
+ expect(grouped.size).toBe(2);
495
+ const evens = grouped.get(0);
496
+ const odds = grouped.get(1);
497
+
498
+ if (Optional.isSome(evens)) {
499
+ expect(evens.value).toStrictEqual([2, 4, 6]);
500
+ }
501
+
502
+ if (Optional.isSome(odds)) {
503
+ expect(odds.value).toStrictEqual([1, 3, 5]);
504
+ }
505
+ });
506
+
507
+ test('should work with empty array', () => {
508
+ const empty: readonly number[] = [];
509
+ const grouped = Arr.groupBy(empty, (n) => n % 2);
510
+ expect(grouped.size).toBe(0);
511
+ });
512
+
513
+ test('should handle all elements in same group', () => {
514
+ const array = [1, 2, 3, 4];
515
+ const grouped = Arr.groupBy(array, () => 'all');
516
+
517
+ expect(grouped.size).toBe(1);
518
+ const all = grouped.get('all');
519
+
520
+ if (Optional.isSome(all)) {
521
+ expect(all.value).toStrictEqual([1, 2, 3, 4]);
522
+ }
523
+ });
524
+ });
525
+
526
+ describe('zip', () => {
527
+ {
528
+ const xs = [1, 2, 3] as const;
529
+ const ys = [4, 5, 6] as const;
530
+ const zipped = Arr.zip(xs, ys);
531
+
532
+ expectType<
533
+ typeof zipped,
534
+ readonly [readonly [1, 4], readonly [2, 5], readonly [3, 6]]
535
+ >('=');
536
+
537
+ test('case 1', () => {
538
+ expect(zipped).toStrictEqual([
539
+ [1, 4],
540
+ [2, 5],
541
+ [3, 6],
542
+ ]);
543
+ });
544
+ }
545
+ {
546
+ const xs: readonly number[] = [1, 2, 3];
547
+ const ys: readonly number[] = [4];
548
+ const zipped = Arr.zip(xs, ys);
549
+
550
+ expectType<typeof zipped, readonly (readonly [number, number])[]>('=');
551
+
552
+ test('case 2', () => {
553
+ expect(zipped).toStrictEqual([[1, 4]]);
554
+ });
555
+ }
556
+ {
557
+ const xs = [1] as const;
558
+ const ys: readonly number[] = [4, 5, 6];
559
+ const zipped = Arr.zip(xs, ys);
560
+
561
+ expectType<typeof zipped, readonly [readonly [1, number]]>('=');
562
+
563
+ test('case 3', () => {
564
+ expect(zipped).toStrictEqual([[1, 4]]);
565
+ });
566
+ }
567
+
568
+ // testArrayEquality({
569
+ // testName: 'zip',
570
+ // target: zip([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]),
571
+ // toBe: [
572
+ // [0, 5],
573
+ // [1, 6],
574
+ // [2, 7],
575
+ // [3, 8],
576
+ // [4, 9],
577
+ // ],
578
+ // });
579
+
580
+ // testArrayEquality({
581
+ // testName: 'zipArrays 2 arrays',
582
+ // target: zipArrays([0, 1, 2, 3, 4], [5, 6, 7, 8, 9]),
583
+ // toBe: [
584
+ // [0, 5],
585
+ // [1, 6],
586
+ // [2, 7],
587
+ // [3, 8],
588
+ // [4, 9],
589
+ // ],
590
+ // });
591
+
592
+ // testArrayEquality({
593
+ // testName: 'zipArrays 3 arrays',
594
+ // target: zipArrays(
595
+ // [0, 1, 2, 3, 4],
596
+ // [5, 6, 7, 8, 9, 999, 999],
597
+ // [10, 11, 12, 13, 14, 999]
598
+ // ),
599
+ // toBe: [
600
+ // [0, 5, 10],
601
+ // [1, 6, 11],
602
+ // [2, 7, 12],
603
+ // [3, 8, 13],
604
+ // [4, 9, 14],
605
+ // ],
606
+ // });
607
+
608
+ test('should zip two arrays', () => {
609
+ const arr1 = [1, 2, 3];
610
+ const arr2 = ['a', 'b', 'c'];
611
+ const result = Arr.zip(arr1, arr2);
612
+ expect(result).toStrictEqual([
613
+ [1, 'a'],
614
+ [2, 'b'],
615
+ [3, 'c'],
616
+ ]);
617
+ });
618
+
619
+ test('should handle arrays of different lengths', () => {
620
+ const arr1 = [1, 2, 3, 4];
621
+ const arr2 = ['a', 'b'];
622
+ const result = Arr.zip(arr1, arr2);
623
+ expect(result).toStrictEqual([
624
+ [1, 'a'],
625
+ [2, 'b'],
626
+ ]);
627
+ });
628
+
629
+ test('should work with empty arrays', () => {
630
+ const arr1: readonly number[] = [];
631
+ const arr2: readonly string[] = [];
632
+ const result = Arr.zip(arr1, arr2);
633
+ expect(result).toStrictEqual([]);
634
+ });
635
+
636
+ test('should handle one empty array', () => {
637
+ const arr1 = [1, 2, 3];
638
+ const arr2: readonly string[] = [];
639
+ const result = Arr.zip(arr1, arr2);
640
+ expect(result).toStrictEqual([]);
641
+ });
642
+
643
+ test('should zip two arrays', () => {
644
+ const arr1 = [1, 2, 3];
645
+ const arr2 = ['a', 'b', 'c'];
646
+ const result = Arr.zip(arr1, arr2);
647
+ expect(result).toStrictEqual([
648
+ [1, 'a'],
649
+ [2, 'b'],
650
+ [3, 'c'],
651
+ ]);
652
+ });
653
+
654
+ test('should handle arrays of different lengths', () => {
655
+ const arr1 = [1, 2, 3, 4];
656
+ const arr2 = ['a', 'b'];
657
+ const result = Arr.zip(arr1, arr2);
658
+ expect(result).toStrictEqual([
659
+ [1, 'a'],
660
+ [2, 'b'],
661
+ ]);
662
+ });
663
+
664
+ test('should work with empty arrays', () => {
665
+ const arr1: readonly number[] = [];
666
+ const arr2: readonly string[] = [];
667
+ const result = Arr.zip(arr1, arr2);
668
+ expect(result).toStrictEqual([]);
669
+ });
670
+
671
+ test('should handle one empty array', () => {
672
+ const arr1 = [1, 2, 3];
673
+ const arr2: readonly string[] = [];
674
+ const result = Arr.zip(arr1, arr2);
675
+ expect(result).toStrictEqual([]);
676
+ });
677
+ });
678
+
679
+ describe('filterNot', () => {
680
+ const xs = [1, 2, 3] as const;
681
+ const filtered = Arr.filterNot(xs, (x) => x % 2 === 0);
682
+
683
+ expectType<typeof filtered, readonly (1 | 2 | 3)[]>('=');
684
+
685
+ test('case 1', () => {
686
+ expect(filtered).toStrictEqual([1, 3]);
687
+ });
688
+ });
689
+
690
+ describe('concat', () => {
691
+ const xs = [1, 2, 3] as const;
692
+ const ys = [4, 5] as const;
693
+ const result = Arr.concat(xs, ys);
694
+
695
+ expectType<typeof result, readonly [1, 2, 3, 4, 5]>('=');
696
+
697
+ test('case 1', () => {
698
+ expect(result).toStrictEqual([1, 2, 3, 4, 5]);
699
+ });
700
+
701
+ // testArrayEquality({
702
+ // testName: 'concat 2 arrays',
703
+ // target: concat([1, 2, 3], [4, 5, 6]),
704
+ // toBe: [1, 2, 3, 4, 5, 6],
705
+ // });
706
+
707
+ // testArrayEquality({
708
+ // testName: 'concat 2 arrays',
709
+ // target: concat([1, 2, 3], []),
710
+ // toBe: [1, 2, 3],
711
+ // });
712
+
713
+ // testArrayEquality({
714
+ // testName: 'concat 2 arrays',
715
+ // target: concat([], [4, 5, 6]),
716
+ // toBe: [4, 5, 6],
717
+ // });
718
+
719
+ // testArrayEquality({
720
+ // testName: 'concat 2 arrays',
721
+ // target: concat([], []),
722
+ // toBe: [],
723
+ // });
724
+
725
+ // testArrayEquality({
726
+ // testName: 'concat 2 arrays',
727
+ // target: concat(['1', '2', '3'], [4, 5, 6]),
728
+ // toBe: ['1', '2', '3', 4, 5, 6],
729
+ // });
730
+ });
731
+
732
+ describe('uniq', () => {
733
+ test('should remove duplicate primitives', () => {
734
+ const array = [1, 2, 2, 3, 1, 4, 3];
735
+ const result = Arr.uniq(array);
736
+ expect(result).toStrictEqual([1, 2, 3, 4]);
737
+ });
738
+
739
+ test('should work with strings', () => {
740
+ const array = ['a', 'b', 'a', 'c', 'b'];
741
+ const result = Arr.uniq(array);
742
+ expect(result).toStrictEqual(['a', 'b', 'c']);
743
+ });
744
+
745
+ test('should work with empty array', () => {
746
+ const array: readonly number[] = [];
747
+ const result = Arr.uniq(array);
748
+ expect(result).toStrictEqual([]);
749
+ });
750
+
751
+ test('should preserve order of first occurrence', () => {
752
+ const array = [3, 1, 2, 1, 3, 2];
753
+ const result = Arr.uniq(array);
754
+ expect(result).toStrictEqual([3, 1, 2]);
755
+ });
756
+ });
757
+
758
+ describe('uniqBy', () => {
759
+ test('should remove duplicates based on key function', () => {
760
+ const array = [
761
+ { id: 1, name: 'Alice' },
762
+ { id: 2, name: 'Bob' },
763
+ { id: 1, name: 'Alice Duplicate' },
764
+ { id: 3, name: 'Charlie' },
765
+ ];
766
+ const result = Arr.uniqBy(array, (item) => item.id);
767
+
768
+ expect(result).toHaveLength(3);
769
+ expect(result[0]).toStrictEqual({ id: 1, name: 'Alice' });
770
+ expect(result[1]).toStrictEqual({ id: 2, name: 'Bob' });
771
+ expect(result[2]).toStrictEqual({ id: 3, name: 'Charlie' });
772
+ });
773
+
774
+ test('should work with string key function', () => {
775
+ const words = ['hello', 'world', 'hi', 'welcome'];
776
+ const result = Arr.uniqBy(words, (word) => word.length);
777
+
778
+ expect(result).toHaveLength(3);
779
+ expect(result).toContain('hello'); // length 5
780
+ expect(result).toContain('hi'); // length 2
781
+ expect(result).toContain('welcome'); // length 7
782
+ });
783
+
784
+ test('should work with empty array', () => {
785
+ const empty: readonly { id: number }[] = [];
786
+ const result = Arr.uniqBy(empty, (item) => item.id);
787
+ expect(result).toStrictEqual([]);
788
+ });
789
+ });
790
+ });