@simplysm/core-common 13.0.69 → 13.0.71

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 (151) hide show
  1. package/README.md +66 -267
  2. package/dist/common.types.d.ts +14 -14
  3. package/dist/errors/argument-error.d.ts +10 -10
  4. package/dist/errors/argument-error.d.ts.map +1 -1
  5. package/dist/errors/argument-error.js +2 -2
  6. package/dist/errors/argument-error.js.map +1 -1
  7. package/dist/errors/not-implemented-error.d.ts +8 -8
  8. package/dist/errors/not-implemented-error.js +2 -2
  9. package/dist/errors/not-implemented-error.js.map +1 -1
  10. package/dist/errors/sd-error.d.ts +10 -10
  11. package/dist/errors/sd-error.d.ts.map +1 -1
  12. package/dist/errors/timeout-error.d.ts +10 -10
  13. package/dist/errors/timeout-error.js +3 -3
  14. package/dist/errors/timeout-error.js.map +1 -1
  15. package/dist/extensions/arr-ext.d.ts +2 -2
  16. package/dist/extensions/arr-ext.helpers.d.ts +8 -8
  17. package/dist/extensions/arr-ext.helpers.js +1 -1
  18. package/dist/extensions/arr-ext.helpers.js.map +1 -1
  19. package/dist/extensions/arr-ext.js +13 -13
  20. package/dist/extensions/arr-ext.js.map +1 -1
  21. package/dist/extensions/arr-ext.types.d.ts +57 -57
  22. package/dist/extensions/arr-ext.types.d.ts.map +1 -1
  23. package/dist/extensions/map-ext.d.ts +16 -16
  24. package/dist/extensions/set-ext.d.ts +11 -11
  25. package/dist/features/debounce-queue.d.ts +17 -15
  26. package/dist/features/debounce-queue.d.ts.map +1 -1
  27. package/dist/features/debounce-queue.js +6 -6
  28. package/dist/features/debounce-queue.js.map +1 -1
  29. package/dist/features/event-emitter.d.ts +20 -20
  30. package/dist/features/event-emitter.js +17 -17
  31. package/dist/features/serial-queue.d.ts +11 -11
  32. package/dist/features/serial-queue.js +5 -5
  33. package/dist/features/serial-queue.js.map +1 -1
  34. package/dist/globals.d.ts +4 -4
  35. package/dist/types/date-only.d.ts +64 -64
  36. package/dist/types/date-only.d.ts.map +1 -1
  37. package/dist/types/date-only.js +63 -63
  38. package/dist/types/date-time.d.ts +37 -37
  39. package/dist/types/date-time.d.ts.map +1 -1
  40. package/dist/types/date-time.js +54 -37
  41. package/dist/types/date-time.js.map +1 -1
  42. package/dist/types/lazy-gc-map.d.ts +26 -26
  43. package/dist/types/lazy-gc-map.d.ts.map +1 -1
  44. package/dist/types/lazy-gc-map.js +26 -26
  45. package/dist/types/lazy-gc-map.js.map +1 -1
  46. package/dist/types/time.d.ts +25 -25
  47. package/dist/types/time.d.ts.map +1 -1
  48. package/dist/types/time.js +25 -25
  49. package/dist/types/time.js.map +1 -1
  50. package/dist/types/uuid.d.ts +11 -11
  51. package/dist/types/uuid.d.ts.map +1 -1
  52. package/dist/types/uuid.js +12 -12
  53. package/dist/types/uuid.js.map +1 -1
  54. package/dist/utils/bytes.d.ts +17 -17
  55. package/dist/utils/bytes.js +4 -4
  56. package/dist/utils/bytes.js.map +1 -1
  57. package/dist/utils/date-format.d.ts +45 -45
  58. package/dist/utils/date-format.js +1 -1
  59. package/dist/utils/date-format.js.map +1 -1
  60. package/dist/utils/error.d.ts +4 -4
  61. package/dist/utils/json.d.ts +17 -17
  62. package/dist/utils/json.js +3 -3
  63. package/dist/utils/json.js.map +1 -1
  64. package/dist/utils/num.d.ts +23 -23
  65. package/dist/utils/obj.d.ts +111 -111
  66. package/dist/utils/obj.d.ts.map +1 -1
  67. package/dist/utils/obj.js +3 -3
  68. package/dist/utils/obj.js.map +1 -1
  69. package/dist/utils/path.d.ts +10 -10
  70. package/dist/utils/primitive.d.ts +5 -5
  71. package/dist/utils/primitive.js +1 -1
  72. package/dist/utils/primitive.js.map +1 -1
  73. package/dist/utils/str.d.ts +46 -46
  74. package/dist/utils/str.d.ts.map +1 -1
  75. package/dist/utils/str.js +5 -5
  76. package/dist/utils/str.js.map +1 -1
  77. package/dist/utils/template-strings.d.ts +26 -26
  78. package/dist/utils/transferable.d.ts +18 -18
  79. package/dist/utils/transferable.js +1 -1
  80. package/dist/utils/transferable.js.map +1 -1
  81. package/dist/utils/wait.d.ts +9 -9
  82. package/dist/utils/xml.d.ts +13 -13
  83. package/dist/utils/xml.d.ts.map +1 -1
  84. package/dist/utils/xml.js +1 -0
  85. package/dist/utils/xml.js.map +1 -1
  86. package/dist/zip/sd-zip.d.ts +22 -22
  87. package/dist/zip/sd-zip.js +16 -16
  88. package/package.json +4 -4
  89. package/src/common.types.ts +17 -17
  90. package/src/errors/argument-error.ts +15 -15
  91. package/src/errors/not-implemented-error.ts +9 -9
  92. package/src/errors/sd-error.ts +12 -12
  93. package/src/errors/timeout-error.ts +12 -12
  94. package/src/extensions/arr-ext.helpers.ts +10 -10
  95. package/src/extensions/arr-ext.ts +57 -57
  96. package/src/extensions/arr-ext.types.ts +59 -59
  97. package/src/extensions/map-ext.ts +16 -16
  98. package/src/extensions/set-ext.ts +11 -11
  99. package/src/features/debounce-queue.ts +21 -19
  100. package/src/features/event-emitter.ts +25 -25
  101. package/src/features/serial-queue.ts +13 -13
  102. package/src/globals.ts +4 -4
  103. package/src/index.ts +1 -1
  104. package/src/types/date-only.ts +83 -83
  105. package/src/types/date-time.ts +64 -44
  106. package/src/types/lazy-gc-map.ts +45 -45
  107. package/src/types/time.ts +34 -34
  108. package/src/types/uuid.ts +17 -17
  109. package/src/utils/bytes.ts +35 -35
  110. package/src/utils/date-format.ts +65 -65
  111. package/src/utils/error.ts +4 -4
  112. package/src/utils/json.ts +39 -39
  113. package/src/utils/num.ts +23 -23
  114. package/src/utils/obj.ts +138 -138
  115. package/src/utils/path.ts +10 -10
  116. package/src/utils/primitive.ts +6 -6
  117. package/src/utils/str.ts +260 -261
  118. package/src/utils/template-strings.ts +29 -29
  119. package/src/utils/transferable.ts +284 -284
  120. package/src/utils/wait.ts +10 -10
  121. package/src/utils/xml.ts +20 -19
  122. package/src/zip/sd-zip.ts +25 -25
  123. package/tests/errors/errors.spec.ts +80 -0
  124. package/tests/extensions/array-extension.spec.ts +796 -0
  125. package/tests/extensions/map-extension.spec.ts +147 -0
  126. package/tests/extensions/set-extension.spec.ts +74 -0
  127. package/tests/types/date-only.spec.ts +638 -0
  128. package/tests/types/date-time.spec.ts +391 -0
  129. package/tests/types/lazy-gc-map.spec.ts +692 -0
  130. package/tests/types/time.spec.ts +559 -0
  131. package/tests/types/uuid.spec.ts +74 -0
  132. package/tests/utils/bytes-utils.spec.ts +230 -0
  133. package/tests/utils/date-format.spec.ts +373 -0
  134. package/tests/utils/debounce-queue.spec.ts +272 -0
  135. package/tests/utils/json.spec.ts +486 -0
  136. package/tests/utils/number.spec.ts +157 -0
  137. package/tests/utils/object.spec.ts +829 -0
  138. package/tests/utils/path.spec.ts +78 -0
  139. package/tests/utils/primitive.spec.ts +43 -0
  140. package/tests/utils/sd-event-emitter.spec.ts +216 -0
  141. package/tests/utils/serial-queue.spec.ts +365 -0
  142. package/tests/utils/string.spec.ts +281 -0
  143. package/tests/utils/template-strings.spec.ts +57 -0
  144. package/tests/utils/transferable.spec.ts +703 -0
  145. package/tests/utils/wait.spec.ts +145 -0
  146. package/tests/utils/xml.spec.ts +146 -0
  147. package/tests/zip/sd-zip.spec.ts +238 -0
  148. package/docs/extensions.md +0 -503
  149. package/docs/features.md +0 -109
  150. package/docs/types.md +0 -486
  151. package/docs/utils.md +0 -780
@@ -0,0 +1,796 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import "@simplysm/core-common"; // Enable $ extension
3
+
4
+ describe("Array prototype extensions", () => {
5
+ //#region Basic chaining
6
+
7
+ describe("Basic chaining", () => {
8
+ it("Can chain existing array methods", () => {
9
+ const result = [1, 2, 3, 4, 5].filter((x) => x > 2).map((x) => x * 10);
10
+
11
+ expect(result).toEqual([30, 40, 50]);
12
+ });
13
+
14
+ it("Can chain extension methods", () => {
15
+ const result = [
16
+ { id: 1, name: "a" },
17
+ { id: 2, name: "b" },
18
+ ].toMap((x) => x.id);
19
+
20
+ expect(result.get(1)).toEqual({ id: 1, name: "a" });
21
+ expect(result.get(2)).toEqual({ id: 2, name: "b" });
22
+ });
23
+
24
+ it("Can mix array and extension methods chaining", () => {
25
+ const users = [
26
+ { id: 1, name: "Kim", active: true },
27
+ { id: 2, name: "Lee", active: false },
28
+ { id: 3, name: "Park", active: true },
29
+ ];
30
+
31
+ const result = users.filter((u) => u.active).toMap((u) => u.id);
32
+
33
+ expect(result.size).toBe(2);
34
+ expect(result.has(1)).toBe(true);
35
+ expect(result.has(3)).toBe(true);
36
+ expect(result.has(2)).toBe(false);
37
+ });
38
+
39
+ it("Can chain multiple steps", () => {
40
+ const result = [1, 2, 3, 4, 5]
41
+ .filter((x) => x > 1)
42
+ .map((x) => x * 2)
43
+ .filter((x) => x < 10)
44
+ .toMap((x) => x);
45
+
46
+ expect(result.size).toBe(3);
47
+ expect(result.has(4)).toBe(true);
48
+ expect(result.has(6)).toBe(true);
49
+ expect(result.has(8)).toBe(true);
50
+ });
51
+
52
+ it("Can access array properties", () => {
53
+ const arr = [1, 2, 3];
54
+
55
+ expect(arr.length).toBe(3);
56
+ expect(arr[0]).toBe(1);
57
+ expect(arr[1]).toBe(2);
58
+ expect(arr[2]).toBe(3);
59
+ });
60
+ });
61
+
62
+ //#endregion
63
+
64
+ //#region single
65
+
66
+ describe("single()", () => {
67
+ it("Returns single matching element", () => {
68
+ const result = [1, 2, 3].single((x) => x === 2);
69
+
70
+ expect(result).toBe(2);
71
+ });
72
+
73
+ it("Returns undefined if no matching element", () => {
74
+ const result = [1, 2, 3].single((x) => x === 4);
75
+
76
+ expect(result).toBe(undefined);
77
+ });
78
+
79
+ it("Throws error if multiple matching elements", () => {
80
+ expect(() => [1, 1, 2].single((x) => x === 1)).toThrow();
81
+ });
82
+
83
+ it("Without condition, targets entire array", () => {
84
+ expect([1].single()).toBe(1);
85
+ expect(([] as number[]).single()).toBe(undefined);
86
+ expect(() => [1, 2].single()).toThrow();
87
+ });
88
+
89
+ it("Can use single after chaining", () => {
90
+ const result = [1, 2, 3, 4, 5].filter((x) => x > 3).single((x) => x === 4);
91
+
92
+ expect(result).toBe(4);
93
+ });
94
+ });
95
+
96
+ //#endregion
97
+
98
+ //#region Async methods
99
+
100
+ describe("parallelAsync()", () => {
101
+ it("Performs parallel async execution", async () => {
102
+ const result = await [1, 2, 3].parallelAsync(async (x) => Promise.resolve(x * 2));
103
+
104
+ expect(result).toEqual([2, 4, 6]);
105
+ });
106
+ });
107
+
108
+ describe("mapAsync()", () => {
109
+ it("Performs sequential async mapping", async () => {
110
+ const result = await [1, 2, 3].mapAsync(async (x) => Promise.resolve(x * 2));
111
+
112
+ expect(result).toEqual([2, 4, 6]);
113
+ });
114
+
115
+ it("Can use mapAsync after chaining", async () => {
116
+ const result = await [1, 2, 3, 4, 5]
117
+ .filter((x) => x > 2)
118
+ .mapAsync(async (x) => Promise.resolve(x * 10));
119
+
120
+ expect(result).toEqual([30, 40, 50]);
121
+ });
122
+ });
123
+
124
+ describe("filterAsync()", () => {
125
+ it("Performs async filtering", async () => {
126
+ const result = await [1, 2, 3, 4, 5].filterAsync(async (x) => Promise.resolve(x > 2));
127
+
128
+ expect(result).toEqual([3, 4, 5]);
129
+ });
130
+ });
131
+
132
+ //#endregion
133
+
134
+ //#region Map conversion
135
+
136
+ describe("toMap()", () => {
137
+ it("Creates Map with key function", () => {
138
+ const result = [
139
+ { id: 1, name: "a" },
140
+ { id: 2, name: "b" },
141
+ ].toMap((x) => x.id);
142
+
143
+ expect(result.get(1)).toEqual({ id: 1, name: "a" });
144
+ expect(result.get(2)).toEqual({ id: 2, name: "b" });
145
+ });
146
+
147
+ it("Transforms values with value function", () => {
148
+ const result = [
149
+ { id: 1, name: "a" },
150
+ { id: 2, name: "b" },
151
+ ].toMap(
152
+ (x) => x.id,
153
+ (x) => x.name,
154
+ );
155
+
156
+ expect(result.get(1)).toBe("a");
157
+ expect(result.get(2)).toBe("b");
158
+ });
159
+
160
+ it("Throws error on duplicate keys", () => {
161
+ expect(() =>
162
+ [
163
+ { id: 1, name: "a" },
164
+ { id: 1, name: "b" },
165
+ ].toMap((x) => x.id),
166
+ ).toThrow("Duplicated key");
167
+ });
168
+ });
169
+
170
+ describe("toMapAsync()", () => {
171
+ it("Creates Map with async key/value functions", async () => {
172
+ const result = await [
173
+ { id: 1, name: "a" },
174
+ { id: 2, name: "b" },
175
+ ].toMapAsync(async (x) => Promise.resolve(x.id));
176
+
177
+ expect(result.get(1)).toEqual({ id: 1, name: "a" });
178
+ expect(result.get(2)).toEqual({ id: 2, name: "b" });
179
+ });
180
+ });
181
+
182
+ describe("toArrayMap()", () => {
183
+ it("Creates Map with array values", () => {
184
+ const result = [
185
+ { type: "a", v: 1 },
186
+ { type: "b", v: 2 },
187
+ { type: "a", v: 3 },
188
+ ].toArrayMap((x) => x.type);
189
+
190
+ expect(result.get("a")).toHaveLength(2);
191
+ expect(result.get("b")).toHaveLength(1);
192
+ });
193
+
194
+ it("Transforms values with value function", () => {
195
+ const result = [
196
+ { type: "a", v: 1 },
197
+ { type: "a", v: 2 },
198
+ ].toArrayMap(
199
+ (x) => x.type,
200
+ (x) => x.v,
201
+ );
202
+
203
+ expect(result.get("a")).toEqual([1, 2]);
204
+ });
205
+ });
206
+
207
+ describe("toSetMap()", () => {
208
+ it("Creates Map with Set values", () => {
209
+ const result = [
210
+ { type: "a", v: 1 },
211
+ { type: "a", v: 1 }, // duplicate
212
+ { type: "a", v: 2 },
213
+ ].toSetMap(
214
+ (x) => x.type,
215
+ (x) => x.v,
216
+ );
217
+
218
+ expect(result.get("a")?.size).toBe(2); // duplicates removed
219
+ });
220
+ });
221
+
222
+ describe("toMapValues()", () => {
223
+ it("Creates Map with aggregated results per group", () => {
224
+ const result = [
225
+ { type: "a", v: 10 },
226
+ { type: "b", v: 20 },
227
+ { type: "a", v: 30 },
228
+ ].toMapValues(
229
+ (x) => x.type,
230
+ (items) => items.reduce((sum, x) => sum + x.v, 0),
231
+ );
232
+
233
+ expect(result.get("a")).toBe(40);
234
+ expect(result.get("b")).toBe(20);
235
+ });
236
+ });
237
+
238
+ //#endregion
239
+
240
+ //#region Tree conversion
241
+
242
+ describe("toTree()", () => {
243
+ it("Converts to tree structure", () => {
244
+ interface Item {
245
+ id: number;
246
+ parentId?: number;
247
+ name: string;
248
+ }
249
+
250
+ const items: Item[] = [
251
+ { id: 1, name: "root" },
252
+ { id: 2, parentId: 1, name: "child1" },
253
+ { id: 3, parentId: 1, name: "child2" },
254
+ { id: 4, parentId: 2, name: "grandchild" },
255
+ ];
256
+
257
+ const result = items.toTree("id", "parentId");
258
+
259
+ expect(result).toHaveLength(1);
260
+ expect(result[0].children).toHaveLength(2);
261
+ expect(result[0].children[0].children).toHaveLength(1);
262
+ });
263
+ });
264
+
265
+ //#endregion
266
+
267
+ //#region Array comparison
268
+
269
+ describe("diffs()", () => {
270
+ it("Analyzes differences between arrays", () => {
271
+ interface Item {
272
+ id: number;
273
+ value: string;
274
+ }
275
+
276
+ const source: Item[] = [
277
+ { id: 1, value: "a" },
278
+ { id: 2, value: "b" },
279
+ { id: 3, value: "c" },
280
+ ];
281
+
282
+ const target: Item[] = [
283
+ { id: 2, value: "b" },
284
+ { id: 3, value: "changed" },
285
+ { id: 4, value: "d" },
286
+ ];
287
+
288
+ const result = source.diffs(target, { keys: ["id"] });
289
+
290
+ const deleted = result.find((d) => d.source?.id === 1);
291
+ expect(deleted?.target).toBe(undefined);
292
+
293
+ const updated = result.find((d) => d.source?.id === 3);
294
+ expect(updated?.target?.value).toBe("changed");
295
+
296
+ const inserted = result.find((d) => d.target?.id === 4);
297
+ expect(inserted?.source).toBe(undefined);
298
+ });
299
+ });
300
+
301
+ describe("oneWayDiffs()", () => {
302
+ it("Analyzes one-way differences", () => {
303
+ interface Item {
304
+ id: number;
305
+ value: string;
306
+ }
307
+
308
+ const orgItems: Item[] = [
309
+ { id: 1, value: "a" },
310
+ { id: 2, value: "b" },
311
+ ];
312
+
313
+ const items: Item[] = [
314
+ { id: 2, value: "changed" },
315
+ { id: 3, value: "c" },
316
+ ];
317
+
318
+ const result = items.oneWayDiffs(orgItems, "id");
319
+
320
+ const updated = result.find((d) => d.item.id === 2);
321
+ expect(updated?.type).toBe("update");
322
+
323
+ const created = result.find((d) => d.item.id === 3);
324
+ expect(created?.type).toBe("create");
325
+ });
326
+
327
+ it("Includes unchanged items when includeSame=true", () => {
328
+ interface Item {
329
+ id: number;
330
+ value: string;
331
+ }
332
+
333
+ const orgItems: Item[] = [
334
+ { id: 1, value: "a" },
335
+ { id: 2, value: "b" },
336
+ ];
337
+
338
+ const items: Item[] = [
339
+ { id: 1, value: "a" }, // unchanged
340
+ { id: 2, value: "changed" },
341
+ ];
342
+
343
+ const result = items.oneWayDiffs(orgItems, "id", { includeSame: true });
344
+
345
+ const same = result.find((d) => d.item.id === 1);
346
+ expect(same?.type).toBe("same");
347
+
348
+ const updated = result.find((d) => d.item.id === 2);
349
+ expect(updated?.type).toBe("update");
350
+ });
351
+ });
352
+
353
+ describe("merge()", () => {
354
+ it("Merges modified items", () => {
355
+ interface Item {
356
+ id: number;
357
+ value: string;
358
+ }
359
+
360
+ const source: Item[] = [
361
+ { id: 1, value: "a" },
362
+ { id: 2, value: "b" },
363
+ ];
364
+ const target: Item[] = [
365
+ { id: 1, value: "a" },
366
+ { id: 2, value: "changed" },
367
+ ];
368
+
369
+ const result = source.merge(target, { keys: ["id"] });
370
+
371
+ expect(result).toHaveLength(2);
372
+ expect(result.find((r) => r.id === 2)?.value).toBe("changed");
373
+ });
374
+ });
375
+
376
+ //#endregion
377
+
378
+ //#region ReadonlyArray support
379
+
380
+ describe("ReadonlyArray support", () => {
381
+ it("$ can be used with readonly array", () => {
382
+ const arr: readonly number[] = [1, 2, 3];
383
+ const result = arr.filter((x) => x > 1).toMap((x) => x);
384
+
385
+ expect(result.size).toBe(2);
386
+ expect(result.has(2)).toBe(true);
387
+ expect(result.has(3)).toBe(true);
388
+ });
389
+ });
390
+
391
+ //#endregion
392
+
393
+ //#region Various array method chaining
394
+
395
+ describe("Various array method chaining", () => {
396
+ it("flatMap can be chained", () => {
397
+ const result = [
398
+ [1, 2],
399
+ [3, 4],
400
+ ]
401
+ .flatMap((x) => x)
402
+ .toMap((x) => x);
403
+
404
+ expect(result.size).toBe(4);
405
+ });
406
+
407
+ it("slice can be chained", () => {
408
+ const result = [1, 2, 3, 4, 5].slice(1, 4).toMap((x) => x);
409
+
410
+ expect(result.size).toBe(3);
411
+ expect(result.has(2)).toBe(true);
412
+ expect(result.has(3)).toBe(true);
413
+ expect(result.has(4)).toBe(true);
414
+ });
415
+
416
+ it("concat can be chained", () => {
417
+ const result = [1, 2].concat([3, 4]).toMap((x) => x);
418
+
419
+ expect(result.size).toBe(4);
420
+ });
421
+
422
+ it("sort can be chained", () => {
423
+ const result = [3, 1, 2].sort((a, b) => a - b).toMap((x, i) => i);
424
+
425
+ expect(result.get(0)).toBe(1);
426
+ expect(result.get(1)).toBe(2);
427
+ expect(result.get(2)).toBe(3);
428
+ });
429
+ });
430
+
431
+ //#endregion
432
+
433
+ //#region first, last
434
+
435
+ describe("first()", () => {
436
+ it("Returns first element", () => {
437
+ expect([1, 2, 3].first()).toBe(1);
438
+ });
439
+
440
+ it("Returns first matching element", () => {
441
+ expect([1, 2, 3, 4, 5].first((x) => x > 3)).toBe(4);
442
+ });
443
+
444
+ it("Returns undefined for empty array", () => {
445
+ expect(([] as number[]).first()).toBe(undefined);
446
+ });
447
+ });
448
+
449
+ describe("last()", () => {
450
+ it("Returns last element", () => {
451
+ expect([1, 2, 3].last()).toBe(3);
452
+ });
453
+
454
+ it("Returns last matching element", () => {
455
+ expect([1, 2, 3, 4, 5].last((x) => x < 4)).toBe(3);
456
+ });
457
+
458
+ it("Returns undefined for empty array", () => {
459
+ expect(([] as number[]).last()).toBe(undefined);
460
+ });
461
+ });
462
+
463
+ //#endregion
464
+
465
+ //#region filterExists, ofType
466
+
467
+ describe("filterExists()", () => {
468
+ it("Removes null/undefined", () => {
469
+ const arr = [1, null, 2, undefined, 3];
470
+ const result = arr.filterExists();
471
+ expect(result).toEqual([1, 2, 3]);
472
+ });
473
+
474
+ it("Can be chained", () => {
475
+ const arr = [1, null, 2, undefined, 3];
476
+ const result = arr.filterExists().map((x) => x * 2);
477
+ expect(result).toEqual([2, 4, 6]);
478
+ });
479
+ });
480
+
481
+ describe("ofType()", () => {
482
+ it("Filters string type elements only", () => {
483
+ const arr = [1, "a", 2, "b", true];
484
+ const result = arr.ofType("string");
485
+ expect(result).toEqual(["a", "b"]);
486
+ });
487
+
488
+ it("Filters number type elements only", () => {
489
+ const arr = [1, "a", 2, "b", 3];
490
+ const result = arr.ofType("number");
491
+ expect(result).toEqual([1, 2, 3]);
492
+ });
493
+
494
+ it("Filters boolean type elements only", () => {
495
+ const arr = [1, "a", true, false, 2];
496
+ const result = arr.ofType("boolean");
497
+ expect(result).toEqual([true, false]);
498
+ });
499
+ });
500
+
501
+ //#endregion
502
+
503
+ //#region mapMany
504
+
505
+ describe("mapMany()", () => {
506
+ it("Maps then flattens", () => {
507
+ const result = [1, 2, 3].mapMany((x) => [x, x * 10]);
508
+ expect(result).toEqual([1, 10, 2, 20, 3, 30]);
509
+ });
510
+ });
511
+
512
+ describe("mapManyAsync()", () => {
513
+ it("Async maps then flattens", async () => {
514
+ const result = await [1, 2, 3].mapManyAsync(async (x) => Promise.resolve([x, x * 10]));
515
+ expect(result).toEqual([1, 10, 2, 20, 3, 30]);
516
+ });
517
+
518
+ it("Async maps nested Promise array then flattens", async () => {
519
+ const result = await [1, 2].mapManyAsync(async (x) => Promise.resolve([x, x + 1, x + 2]));
520
+ expect(result).toEqual([1, 2, 3, 2, 3, 4]);
521
+ });
522
+ });
523
+
524
+ //#endregion
525
+
526
+ //#region groupBy
527
+
528
+ describe("groupBy()", () => {
529
+ it("Groups by key", () => {
530
+ const items = [
531
+ { type: "a", value: 1 },
532
+ { type: "b", value: 2 },
533
+ { type: "a", value: 3 },
534
+ ];
535
+ const result = items.groupBy((x) => x.type);
536
+
537
+ expect(result).toHaveLength(2);
538
+ expect(result.find((g) => g.key === "a")?.values).toHaveLength(2);
539
+ expect(result.find((g) => g.key === "b")?.values).toHaveLength(1);
540
+ });
541
+ });
542
+
543
+ //#endregion
544
+
545
+ //#region toObject
546
+
547
+ describe("toObject()", () => {
548
+ it("Converts array to object", () => {
549
+ const items = [
550
+ { key: "a", value: 1 },
551
+ { key: "b", value: 2 },
552
+ ];
553
+ const result = items.toObject(
554
+ (x) => x.key,
555
+ (x) => x.value,
556
+ );
557
+
558
+ expect(result).toEqual({ a: 1, b: 2 });
559
+ });
560
+
561
+ it("Throws error on duplicate keys", () => {
562
+ const items = [
563
+ { key: "a", value: 1 },
564
+ { key: "a", value: 2 },
565
+ ];
566
+ expect(() => items.toObject((x) => x.key)).toThrow();
567
+ });
568
+ });
569
+
570
+ //#endregion
571
+
572
+ //#region distinct
573
+
574
+ describe("distinct()", () => {
575
+ it("Removes duplicates", () => {
576
+ expect([1, 2, 2, 3, 3, 3].distinct()).toEqual([1, 2, 3]);
577
+ });
578
+
579
+ it("Removes duplicates from object array", () => {
580
+ const arr = [{ a: 1 }, { a: 2 }, { a: 1 }];
581
+ const result = arr.distinct();
582
+ expect(result).toHaveLength(2);
583
+ });
584
+
585
+ it("Can be chained", () => {
586
+ const result = [1, 2, 2, 3].distinct().map((x) => x * 2);
587
+ expect(result).toEqual([2, 4, 6]);
588
+ });
589
+
590
+ it("Can use custom key with keyFn", () => {
591
+ const arr = [
592
+ { id: 1, name: "a" },
593
+ { id: 2, name: "b" },
594
+ { id: 1, name: "c" },
595
+ ];
596
+ const result = arr.distinct({ keyFn: (x) => x.id });
597
+ expect(result).toHaveLength(2);
598
+ });
599
+
600
+ it("Removes duplicates by reference with matchAddress=true", () => {
601
+ const obj1 = { a: 1 };
602
+ const obj2 = { a: 1 }; // same value but different reference
603
+ const arr = [obj1, obj1, obj2];
604
+ const result = arr.distinct({ matchAddress: true });
605
+ expect(result).toHaveLength(2);
606
+ expect(result).toContain(obj1);
607
+ expect(result).toContain(obj2);
608
+ });
609
+ });
610
+
611
+ //#endregion
612
+
613
+ //#region orderBy, orderByDesc
614
+
615
+ describe("orderBy()", () => {
616
+ it("Sorts in ascending order", () => {
617
+ expect([3, 1, 2].orderBy()).toEqual([1, 2, 3]);
618
+ });
619
+
620
+ it("Can specify sort criteria with selector", () => {
621
+ const items = [
622
+ { name: "b", age: 30 },
623
+ { name: "a", age: 20 },
624
+ { name: "c", age: 25 },
625
+ ];
626
+ const result = items.orderBy((x) => x.age);
627
+ expect(result.map((x) => x.age)).toEqual([20, 25, 30]);
628
+ });
629
+
630
+ it("Can be chained", () => {
631
+ const result = [3, 1, 2].orderBy().map((x) => x * 2);
632
+ expect(result).toEqual([2, 4, 6]);
633
+ });
634
+ });
635
+
636
+ describe("orderByDesc()", () => {
637
+ it("Sorts in descending order", () => {
638
+ expect([1, 3, 2].orderByDesc()).toEqual([3, 2, 1]);
639
+ });
640
+ });
641
+
642
+ //#endregion
643
+
644
+ //#region sum, min, max
645
+
646
+ describe("sum()", () => {
647
+ it("Returns sum", () => {
648
+ expect([1, 2, 3, 4, 5].sum()).toBe(15);
649
+ });
650
+
651
+ it("Can extract values with selector", () => {
652
+ const items = [{ value: 10 }, { value: 20 }, { value: 30 }];
653
+ expect(items.sum((x) => x.value)).toBe(60);
654
+ });
655
+
656
+ it("Returns 0 for empty array", () => {
657
+ expect(([] as number[]).sum()).toBe(0);
658
+ });
659
+
660
+ it("Throws error for non-number type", () => {
661
+ expect(() => (["a", "b"] as unknown as number[]).sum()).toThrow("sum can only be used with numbers");
662
+ });
663
+ });
664
+
665
+ describe("min()", () => {
666
+ it("Returns minimum value", () => {
667
+ expect([3, 1, 2].min()).toBe(1);
668
+ });
669
+
670
+ it("Returns undefined for empty array", () => {
671
+ expect(([] as number[]).min()).toBe(undefined);
672
+ });
673
+
674
+ it("Throws error for non-number/string type", () => {
675
+ expect(() => ([true, false] as unknown as number[]).min()).toThrow(
676
+ "min can only be used with numbers/strings",
677
+ );
678
+ });
679
+ });
680
+
681
+ describe("max()", () => {
682
+ it("Returns maximum value", () => {
683
+ expect([1, 3, 2].max()).toBe(3);
684
+ });
685
+
686
+ it("Returns undefined for empty array", () => {
687
+ expect(([] as number[]).max()).toBe(undefined);
688
+ });
689
+
690
+ it("Throws error for non-number/string type", () => {
691
+ expect(() => ([{}, {}] as unknown as number[]).max()).toThrow(
692
+ "max can only be used with numbers/strings",
693
+ );
694
+ });
695
+ });
696
+
697
+ //#endregion
698
+
699
+ //#region shuffle
700
+
701
+ describe("shuffle()", () => {
702
+ it("Shuffles array (preserves original)", () => {
703
+ const original = [1, 2, 3, 4, 5];
704
+ const shuffled = original.shuffle();
705
+
706
+ // original unchanged
707
+ expect(original).toEqual([1, 2, 3, 4, 5]);
708
+ // has same elements
709
+ expect(shuffled.sort()).toEqual([1, 2, 3, 4, 5]);
710
+ });
711
+ });
712
+
713
+ //#endregion
714
+
715
+ //#region Mutating methods
716
+
717
+ describe("distinctThis()", () => {
718
+ it("Removes duplicates from original array", () => {
719
+ const arr = [1, 2, 2, 3, 3, 3];
720
+ const result = arr.distinctThis();
721
+
722
+ expect(arr).toEqual([1, 2, 3]);
723
+ expect(result).toEqual([1, 2, 3]);
724
+ });
725
+ });
726
+
727
+ describe("orderByThis()", () => {
728
+ it("Sorts original array in ascending order", () => {
729
+ const arr = [3, 1, 2];
730
+ arr.orderByThis();
731
+
732
+ expect(arr).toEqual([1, 2, 3]);
733
+ });
734
+ });
735
+
736
+ describe("orderByDescThis()", () => {
737
+ it("Sorts original array in descending order", () => {
738
+ const arr = [1, 3, 2];
739
+ arr.orderByDescThis();
740
+
741
+ expect(arr).toEqual([3, 2, 1]);
742
+ });
743
+ });
744
+
745
+ describe("insert()", () => {
746
+ it("Inserts item to original array", () => {
747
+ const arr = [1, 3];
748
+ arr.insert(1, 2);
749
+
750
+ expect(arr).toEqual([1, 2, 3]);
751
+ });
752
+ });
753
+
754
+ describe("remove()", () => {
755
+ it("Removes item from original array", () => {
756
+ const arr = [1, 2, 3];
757
+ arr.remove(2);
758
+
759
+ expect(arr).toEqual([1, 3]);
760
+ });
761
+
762
+ it("Removes item with condition function", () => {
763
+ const arr = [1, 2, 3, 4];
764
+ arr.remove((x) => x % 2 === 0);
765
+
766
+ expect(arr).toEqual([1, 3]);
767
+ });
768
+ });
769
+
770
+ describe("toggle()", () => {
771
+ it("Removes item if exists", () => {
772
+ const arr = [1, 2, 3];
773
+ arr.toggle(2);
774
+
775
+ expect(arr).toEqual([1, 3]);
776
+ });
777
+
778
+ it("Adds item if not exists", () => {
779
+ const arr = [1, 3];
780
+ arr.toggle(2);
781
+
782
+ expect(arr).toEqual([1, 3, 2]);
783
+ });
784
+ });
785
+
786
+ describe("clear()", () => {
787
+ it("Clears original array", () => {
788
+ const arr = [1, 2, 3];
789
+ arr.clear();
790
+
791
+ expect(arr).toEqual([]);
792
+ });
793
+ });
794
+
795
+ //#endregion
796
+ });