@wopjs/cast 0.1.11 → 0.1.13
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/dist/index.d.mts +59 -19
- package/dist/index.d.ts +59 -19
- package/dist/index.js +52 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +50 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/array.test.ts +538 -0
- package/src/array.ts +129 -0
- package/src/index.ts +1 -0
- package/src/is-to-as.ts +24 -21
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { inertFilter, inertFilterMap, coalesce } from "./array";
|
|
4
|
+
import { isNumber, isString } from "./is-to-as";
|
|
5
|
+
|
|
6
|
+
/** Bypass TypeScript static type inference. */
|
|
7
|
+
const castType = <T>(x: T): T => x;
|
|
8
|
+
|
|
9
|
+
describe("array.ts", () => {
|
|
10
|
+
describe("inertFilterMap", () => {
|
|
11
|
+
it("returns undefined for undefined input", () => {
|
|
12
|
+
const result = inertFilterMap(undefined, x => x);
|
|
13
|
+
expect(result).toBe(undefined);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("returns original array for empty array", () => {
|
|
17
|
+
const arr: number[] = [];
|
|
18
|
+
const result = inertFilterMap(arr, x => x);
|
|
19
|
+
expect(result).toBe(arr);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns original array when all items map to themselves", () => {
|
|
23
|
+
const arr = [1, 2, 3];
|
|
24
|
+
const result = inertFilterMap(arr, x => x);
|
|
25
|
+
expect(result).toBe(arr);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns original array when callback returns same reference for objects", () => {
|
|
29
|
+
const obj1 = { a: 1 };
|
|
30
|
+
const obj2 = { b: 2 };
|
|
31
|
+
const arr = [obj1, obj2];
|
|
32
|
+
const result = inertFilterMap(arr, x => x);
|
|
33
|
+
expect(result).toBe(arr);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns new array when at least one item is mapped to different value", () => {
|
|
37
|
+
const arr = [1, 2, 3];
|
|
38
|
+
const result = inertFilterMap(arr, x => x * 2);
|
|
39
|
+
expect(result).not.toBe(arr);
|
|
40
|
+
expect(result).toEqual([2, 4, 6]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns new array when at least one item is filtered out", () => {
|
|
44
|
+
const arr = [1, 2, 3];
|
|
45
|
+
const result = inertFilterMap(arr, x => (x === 2 ? undefined : x));
|
|
46
|
+
expect(result).not.toBe(arr);
|
|
47
|
+
expect(result).toEqual([1, 3]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("filters out all items when callback always returns undefined", () => {
|
|
51
|
+
const arr = [1, 2, 3];
|
|
52
|
+
const result = inertFilterMap(arr, () => undefined);
|
|
53
|
+
expect(result).not.toBe(arr);
|
|
54
|
+
expect(result).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("maps all items correctly", () => {
|
|
58
|
+
const arr = ["a", "b", "c"];
|
|
59
|
+
const result = inertFilterMap(arr, x => x.toUpperCase());
|
|
60
|
+
expect(result).toEqual(["A", "B", "C"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("filters and maps simultaneously", () => {
|
|
64
|
+
const arr = [1, 2, 3, 4, 5];
|
|
65
|
+
const result = inertFilterMap(arr, x => (x % 2 === 0 ? x * 10 : undefined));
|
|
66
|
+
expect(result).toEqual([20, 40]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("handles mixed filter and identity mapping", () => {
|
|
70
|
+
const arr = [1, 2, 3, 4, 5];
|
|
71
|
+
// Filter out even numbers, keep odd numbers as-is
|
|
72
|
+
const result = inertFilterMap(arr, x => (x % 2 === 0 ? undefined : x));
|
|
73
|
+
expect(result).not.toBe(arr);
|
|
74
|
+
expect(result).toEqual([1, 3, 5]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("uses Object.is for equality comparison", () => {
|
|
78
|
+
// NaN is equal to NaN with Object.is
|
|
79
|
+
const arr = [NaN, NaN];
|
|
80
|
+
const result = inertFilterMap(arr, x => x);
|
|
81
|
+
expect(result).toBe(arr);
|
|
82
|
+
|
|
83
|
+
// +0 and -0 are different with Object.is
|
|
84
|
+
const arr2 = [0];
|
|
85
|
+
const result2 = inertFilterMap(arr2, x => (x === 0 ? -0 : x));
|
|
86
|
+
expect(result2).not.toBe(arr2);
|
|
87
|
+
expect(Object.is(result2[0], -0)).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("provides correct index to callback", () => {
|
|
91
|
+
const arr = ["a", "b", "c"];
|
|
92
|
+
const indices: number[] = [];
|
|
93
|
+
inertFilterMap(arr, (_, index) => {
|
|
94
|
+
indices.push(index);
|
|
95
|
+
return undefined;
|
|
96
|
+
});
|
|
97
|
+
expect(indices).toEqual([0, 1, 2]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("provides array reference to callback", () => {
|
|
101
|
+
const arr = [1, 2, 3];
|
|
102
|
+
let receivedArr: number[] | undefined;
|
|
103
|
+
inertFilterMap(arr, (_, __, a) => {
|
|
104
|
+
receivedArr = a;
|
|
105
|
+
return undefined;
|
|
106
|
+
});
|
|
107
|
+
expect(receivedArr).toBe(arr);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("respects thisArg", () => {
|
|
111
|
+
const arr = [1, 2, 3];
|
|
112
|
+
const context = { multiplier: 10 };
|
|
113
|
+
const result = inertFilterMap(
|
|
114
|
+
arr,
|
|
115
|
+
function (this: typeof context, x) {
|
|
116
|
+
return x * this.multiplier;
|
|
117
|
+
},
|
|
118
|
+
context
|
|
119
|
+
);
|
|
120
|
+
expect(result).toEqual([10, 20, 30]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("handles single element array - identity", () => {
|
|
124
|
+
const arr = [42];
|
|
125
|
+
const result = inertFilterMap(arr, x => x);
|
|
126
|
+
expect(result).toBe(arr);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("handles single element array - mapped", () => {
|
|
130
|
+
const arr = [42];
|
|
131
|
+
const result = inertFilterMap(arr, x => x * 2);
|
|
132
|
+
expect(result).not.toBe(arr);
|
|
133
|
+
expect(result).toEqual([84]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("handles single element array - filtered", () => {
|
|
137
|
+
const arr = [42];
|
|
138
|
+
const result = inertFilterMap(arr, () => undefined);
|
|
139
|
+
expect(result).not.toBe(arr);
|
|
140
|
+
expect(result).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("returns new array when first item changes", () => {
|
|
144
|
+
const arr = [1, 2, 3];
|
|
145
|
+
const result = inertFilterMap(arr, (x, i) => (i === 0 ? x * 2 : x));
|
|
146
|
+
expect(result).not.toBe(arr);
|
|
147
|
+
expect(result).toEqual([2, 2, 3]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("returns new array when last item changes", () => {
|
|
151
|
+
const arr = [1, 2, 3];
|
|
152
|
+
const result = inertFilterMap(arr, (x, i) => (i === 2 ? x * 2 : x));
|
|
153
|
+
expect(result).not.toBe(arr);
|
|
154
|
+
expect(result).toEqual([1, 2, 6]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("returns new array when middle item is filtered", () => {
|
|
158
|
+
const arr = [1, 2, 3];
|
|
159
|
+
const result = inertFilterMap(arr, (x, i) => (i === 1 ? undefined : x));
|
|
160
|
+
expect(result).not.toBe(arr);
|
|
161
|
+
expect(result).toEqual([1, 3]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("handles readonly arrays", () => {
|
|
165
|
+
const arr: readonly number[] = [1, 2, 3];
|
|
166
|
+
const result = inertFilterMap(arr, x => x);
|
|
167
|
+
expect(result).toBe(arr);
|
|
168
|
+
|
|
169
|
+
const result2 = inertFilterMap(arr, x => x * 2);
|
|
170
|
+
expect(result2).toEqual([2, 4, 6]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("handles arrays with undefined values - identity", () => {
|
|
174
|
+
const arr = [1, undefined, 3];
|
|
175
|
+
// When callback returns the original undefined, it gets filtered out
|
|
176
|
+
const result = inertFilterMap(arr, x => x);
|
|
177
|
+
expect(result).not.toBe(arr);
|
|
178
|
+
expect(result).toEqual([1, 3]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("handles sparse arrays", () => {
|
|
182
|
+
const arr = [1, , 3]; // eslint-disable-line no-sparse-arrays
|
|
183
|
+
const result = inertFilterMap(arr, x => x);
|
|
184
|
+
expect(result).not.toBe(arr);
|
|
185
|
+
// Sparse slots become undefined, which gets filtered out
|
|
186
|
+
expect(result).toEqual([1, 3]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Type narrowing tests
|
|
190
|
+
{
|
|
191
|
+
it("type narrowing - mutable array input returns mutable array", () => {
|
|
192
|
+
const arr = castType<number[]>([1, 2, 3]);
|
|
193
|
+
const result = inertFilterMap(arr, x => x.toString());
|
|
194
|
+
const _check: string[] = result;
|
|
195
|
+
expect(_check).toEqual(["1", "2", "3"]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("type narrowing - readonly array input returns readonly array", () => {
|
|
199
|
+
const arr = castType<readonly number[]>([1, 2, 3]);
|
|
200
|
+
const result = inertFilterMap(arr, x => x.toString());
|
|
201
|
+
const _check: readonly string[] = result;
|
|
202
|
+
expect(_check).toEqual(["1", "2", "3"]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("type narrowing - undefined input returns undefined", () => {
|
|
206
|
+
const arr = castType<number[] | undefined>(undefined);
|
|
207
|
+
const result = inertFilterMap(arr, x => x);
|
|
208
|
+
const _check: number[] | undefined = result;
|
|
209
|
+
expect(_check).toBe(undefined);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("type narrowing - excludes undefined from result type", () => {
|
|
213
|
+
const arr = castType<(number | undefined)[]>([1, undefined, 3]);
|
|
214
|
+
const result = inertFilterMap(arr, x => x);
|
|
215
|
+
// Result type should be number[] (Defined<number | undefined> = number)
|
|
216
|
+
const _check: number[] = result;
|
|
217
|
+
expect(_check).toEqual([1, 3]);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("inertFilter", () => {
|
|
223
|
+
it("returns undefined for undefined input", () => {
|
|
224
|
+
const result = inertFilter(undefined, () => true);
|
|
225
|
+
expect(result).toBe(undefined);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("returns original array for empty array", () => {
|
|
229
|
+
const arr: number[] = [];
|
|
230
|
+
const result = inertFilter(arr, () => true);
|
|
231
|
+
expect(result).toBe(arr);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("returns original array when all items pass predicate", () => {
|
|
235
|
+
const arr = [1, 2, 3];
|
|
236
|
+
const result = inertFilter(arr, () => true);
|
|
237
|
+
expect(result).toBe(arr);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("returns original array when all items pass actual condition", () => {
|
|
241
|
+
const arr = [2, 4, 6];
|
|
242
|
+
const result = inertFilter(arr, x => x % 2 === 0);
|
|
243
|
+
expect(result).toBe(arr);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("returns new array when at least one item is filtered out", () => {
|
|
247
|
+
const arr = [1, 2, 3];
|
|
248
|
+
const result = inertFilter(arr, x => x !== 2);
|
|
249
|
+
expect(result).not.toBe(arr);
|
|
250
|
+
expect(result).toEqual([1, 3]);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("returns empty array when all items are filtered out", () => {
|
|
254
|
+
const arr = [1, 2, 3];
|
|
255
|
+
const result = inertFilter(arr, () => false);
|
|
256
|
+
expect(result).not.toBe(arr);
|
|
257
|
+
expect(result).toEqual([]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("filters correctly based on predicate", () => {
|
|
261
|
+
const arr = [1, 2, 3, 4, 5, 6];
|
|
262
|
+
const result = inertFilter(arr, x => x % 2 === 0);
|
|
263
|
+
expect(result).toEqual([2, 4, 6]);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("provides correct index to predicate", () => {
|
|
267
|
+
const arr = ["a", "b", "c"];
|
|
268
|
+
const indices: number[] = [];
|
|
269
|
+
inertFilter(arr, (_, index) => {
|
|
270
|
+
indices.push(index);
|
|
271
|
+
return true;
|
|
272
|
+
});
|
|
273
|
+
expect(indices).toEqual([0, 1, 2]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("provides array reference to predicate", () => {
|
|
277
|
+
const arr = [1, 2, 3];
|
|
278
|
+
let receivedArr: number[] | undefined;
|
|
279
|
+
inertFilter(arr, (_, __, a) => {
|
|
280
|
+
receivedArr = a;
|
|
281
|
+
return true;
|
|
282
|
+
});
|
|
283
|
+
expect(receivedArr).toBe(arr);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("respects thisArg", () => {
|
|
287
|
+
const arr = [1, 2, 3, 4, 5];
|
|
288
|
+
const context = { threshold: 3 };
|
|
289
|
+
const result = inertFilter(
|
|
290
|
+
arr,
|
|
291
|
+
function (this: typeof context, x) {
|
|
292
|
+
return x > this.threshold;
|
|
293
|
+
},
|
|
294
|
+
context
|
|
295
|
+
);
|
|
296
|
+
expect(result).toEqual([4, 5]);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("handles single element array - kept", () => {
|
|
300
|
+
const arr = [42];
|
|
301
|
+
const result = inertFilter(arr, () => true);
|
|
302
|
+
expect(result).toBe(arr);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("handles single element array - filtered", () => {
|
|
306
|
+
const arr = [42];
|
|
307
|
+
const result = inertFilter(arr, () => false);
|
|
308
|
+
expect(result).not.toBe(arr);
|
|
309
|
+
expect(result).toEqual([]);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("returns new array when first item is filtered", () => {
|
|
313
|
+
const arr = [1, 2, 3];
|
|
314
|
+
const result = inertFilter(arr, (_, i) => i !== 0);
|
|
315
|
+
expect(result).not.toBe(arr);
|
|
316
|
+
expect(result).toEqual([2, 3]);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("returns new array when last item is filtered", () => {
|
|
320
|
+
const arr = [1, 2, 3];
|
|
321
|
+
const result = inertFilter(arr, (_, i) => i !== 2);
|
|
322
|
+
expect(result).not.toBe(arr);
|
|
323
|
+
expect(result).toEqual([1, 2]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("returns new array when middle item is filtered", () => {
|
|
327
|
+
const arr = [1, 2, 3];
|
|
328
|
+
const result = inertFilter(arr, (_, i) => i !== 1);
|
|
329
|
+
expect(result).not.toBe(arr);
|
|
330
|
+
expect(result).toEqual([1, 3]);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("handles readonly arrays", () => {
|
|
334
|
+
const arr: readonly number[] = [1, 2, 3];
|
|
335
|
+
const result = inertFilter(arr, () => true);
|
|
336
|
+
expect(result).toBe(arr);
|
|
337
|
+
|
|
338
|
+
const result2 = inertFilter(arr, x => x !== 2);
|
|
339
|
+
expect(result2).toEqual([1, 3]);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("handles arrays with falsy values correctly", () => {
|
|
343
|
+
const arr = [0, 1, "", "hello", false, true, null, undefined];
|
|
344
|
+
const result = inertFilter(arr, () => true);
|
|
345
|
+
expect(result).toBe(arr);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("handles sparse arrays", () => {
|
|
349
|
+
const arr = [1, , 3]; // eslint-disable-line no-sparse-arrays
|
|
350
|
+
const result = inertFilter(arr, x => x !== undefined);
|
|
351
|
+
expect(result).not.toBe(arr);
|
|
352
|
+
expect(result).toEqual([1, 3]);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("filters multiple consecutive items", () => {
|
|
356
|
+
const arr = [1, 2, 3, 4, 5];
|
|
357
|
+
const result = inertFilter(arr, x => x === 1 || x === 5);
|
|
358
|
+
expect(result).toEqual([1, 5]);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("filters all but one item", () => {
|
|
362
|
+
const arr = [1, 2, 3, 4, 5];
|
|
363
|
+
const result = inertFilter(arr, x => x === 3);
|
|
364
|
+
expect(result).toEqual([3]);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Type narrowing tests with type guard predicate
|
|
368
|
+
{
|
|
369
|
+
it("type narrowing - narrows type with type guard predicate", () => {
|
|
370
|
+
const arr = castType<(string | number)[]>([1, "a", 2, "b"]);
|
|
371
|
+
const result = inertFilter(arr, isString);
|
|
372
|
+
const _check: string[] = result;
|
|
373
|
+
expect(_check).toEqual(["a", "b"]);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("type narrowing - narrows to number type", () => {
|
|
377
|
+
const arr = castType<(string | number)[]>([1, "a", 2, "b"]);
|
|
378
|
+
const result = inertFilter(arr, isNumber);
|
|
379
|
+
const _check: number[] = result;
|
|
380
|
+
expect(_check).toEqual([1, 2]);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("type narrowing - mutable array input returns mutable array", () => {
|
|
384
|
+
const arr = castType<number[]>([1, 2, 3]);
|
|
385
|
+
const result = inertFilter(arr, x => x > 1);
|
|
386
|
+
const _check: number[] = result;
|
|
387
|
+
expect(_check).toEqual([2, 3]);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("type narrowing - readonly array input returns readonly array", () => {
|
|
391
|
+
const arr = castType<readonly number[]>([1, 2, 3]);
|
|
392
|
+
const result = inertFilter(arr, x => x > 1);
|
|
393
|
+
const _check: readonly number[] = result;
|
|
394
|
+
expect(_check).toEqual([2, 3]);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it("type narrowing - undefined input returns undefined", () => {
|
|
398
|
+
const arr = castType<number[] | undefined>(undefined);
|
|
399
|
+
const result = inertFilter(arr, x => x > 1);
|
|
400
|
+
const _check: number[] | undefined = result;
|
|
401
|
+
expect(_check).toBe(undefined);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it("type narrowing - type guard with readonly array", () => {
|
|
405
|
+
const arr = castType<readonly (string | number)[]>([1, "a", 2, "b"]);
|
|
406
|
+
const result = inertFilter(arr, isString);
|
|
407
|
+
const _check: readonly string[] = result;
|
|
408
|
+
expect(_check).toEqual(["a", "b"]);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe("coalesce", () => {
|
|
414
|
+
it("returns original array when all items are truthy", () => {
|
|
415
|
+
const arr = [1, 2, 3];
|
|
416
|
+
const result = coalesce(arr);
|
|
417
|
+
expect(result).toBe(arr);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("returns original array for array of truthy strings", () => {
|
|
421
|
+
const arr = ["a", "b", "c"];
|
|
422
|
+
const result = coalesce(arr);
|
|
423
|
+
expect(result).toBe(arr);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("returns original array for array of truthy objects", () => {
|
|
427
|
+
const arr = [{}, [], () => {}];
|
|
428
|
+
const result = coalesce(arr);
|
|
429
|
+
expect(result).toBe(arr);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("filters out null values", () => {
|
|
433
|
+
const arr = [1, null, 3];
|
|
434
|
+
const result = coalesce(arr);
|
|
435
|
+
expect(result).not.toBe(arr);
|
|
436
|
+
expect(result).toEqual([1, 3]);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("filters out undefined values", () => {
|
|
440
|
+
const arr = [1, undefined, 3];
|
|
441
|
+
const result = coalesce(arr);
|
|
442
|
+
expect(result).not.toBe(arr);
|
|
443
|
+
expect(result).toEqual([1, 3]);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("filters out false", () => {
|
|
447
|
+
const arr = [true, false, true];
|
|
448
|
+
const result = coalesce(arr);
|
|
449
|
+
expect(result).not.toBe(arr);
|
|
450
|
+
expect(result).toEqual([true, true]);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it("filters out 0", () => {
|
|
454
|
+
const arr = [1, 0, 2];
|
|
455
|
+
const result = coalesce(arr);
|
|
456
|
+
expect(result).not.toBe(arr);
|
|
457
|
+
expect(result).toEqual([1, 2]);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("filters out empty string", () => {
|
|
461
|
+
const arr = ["a", "", "b"];
|
|
462
|
+
const result = coalesce(arr);
|
|
463
|
+
expect(result).not.toBe(arr);
|
|
464
|
+
expect(result).toEqual(["a", "b"]);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("filters out NaN", () => {
|
|
468
|
+
const arr = [1, NaN, 2];
|
|
469
|
+
const result = coalesce(arr);
|
|
470
|
+
expect(result).not.toBe(arr);
|
|
471
|
+
expect(result).toEqual([1, 2]);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("filters out all falsy values", () => {
|
|
475
|
+
const arr = [1, null, undefined, false, 0, "", NaN, 2];
|
|
476
|
+
const result = coalesce(arr);
|
|
477
|
+
expect(result).toEqual([1, 2]);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("returns empty array when all values are falsy", () => {
|
|
481
|
+
const arr = [null, undefined, false, 0, "", NaN];
|
|
482
|
+
const result = coalesce(arr);
|
|
483
|
+
expect(result).toEqual([]);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("returns original empty array", () => {
|
|
487
|
+
const arr: unknown[] = [];
|
|
488
|
+
const result = coalesce(arr);
|
|
489
|
+
expect(result).toBe(arr);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it("handles readonly arrays", () => {
|
|
493
|
+
const arr: readonly (number | null)[] = [1, null, 3];
|
|
494
|
+
const result = coalesce(arr);
|
|
495
|
+
expect(result).toEqual([1, 3]);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it("preserves truthy falsy-looking values", () => {
|
|
499
|
+
// These are all truthy
|
|
500
|
+
const arr = ["0", "false", "null", "undefined", " ", []];
|
|
501
|
+
const result = coalesce(arr);
|
|
502
|
+
expect(result).toBe(arr);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// Type narrowing tests
|
|
506
|
+
{
|
|
507
|
+
it("type narrowing - removes null and undefined from type", () => {
|
|
508
|
+
const arr = castType<(string | null | undefined)[]>(["a", null, "b", undefined]);
|
|
509
|
+
const result = coalesce(arr);
|
|
510
|
+
// Result type should be string[] (Truthy removes null, undefined, etc.)
|
|
511
|
+
const _check: string[] = result as string[];
|
|
512
|
+
expect(_check).toEqual(["a", "b"]);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("type narrowing - removes false from boolean union", () => {
|
|
516
|
+
const arr = castType<(string | boolean)[]>(["a", true, false, "b"]);
|
|
517
|
+
const result = coalesce(arr);
|
|
518
|
+
expect(result).toEqual(["a", true, "b"]);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("type narrowing - handles mixed types", () => {
|
|
522
|
+
const arr = castType<(number | string | null | undefined | false | 0 | "")[]>([
|
|
523
|
+
1,
|
|
524
|
+
"hello",
|
|
525
|
+
null,
|
|
526
|
+
undefined,
|
|
527
|
+
false,
|
|
528
|
+
0,
|
|
529
|
+
"",
|
|
530
|
+
2,
|
|
531
|
+
"world",
|
|
532
|
+
]);
|
|
533
|
+
const result = coalesce(arr);
|
|
534
|
+
expect(result).toEqual([1, "hello", 2, "world"]);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
});
|
package/src/array.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Defined, Truthy } from "./is-to-as";
|
|
2
|
+
|
|
3
|
+
import { isTruthy } from "./is-to-as";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lazy filterMap that avoids creating a new array when possible.
|
|
7
|
+
*
|
|
8
|
+
* @returns the original array if `callbackfn` returns the same value for all items, otherwise a new array with the mapped items.
|
|
9
|
+
*
|
|
10
|
+
* @param arr
|
|
11
|
+
* @param callbackfn Return `undefined` to filter out the item, or return a value to map the item.
|
|
12
|
+
* @param thisArg
|
|
13
|
+
*/
|
|
14
|
+
export function inertFilterMap<T, U = T>(
|
|
15
|
+
arr: T[],
|
|
16
|
+
callbackfn: (value: T, index: number, arr: T[]) => U | undefined,
|
|
17
|
+
thisArg?: any
|
|
18
|
+
): Defined<U>[];
|
|
19
|
+
export function inertFilterMap<T, U = T>(
|
|
20
|
+
arr: T[] | undefined,
|
|
21
|
+
callbackfn: (value: T, index: number, arr: T[]) => U | undefined,
|
|
22
|
+
thisArg?: any
|
|
23
|
+
): Defined<U>[] | undefined;
|
|
24
|
+
export function inertFilterMap<T, U = T>(
|
|
25
|
+
arr: readonly T[],
|
|
26
|
+
callbackfn: (value: T, index: number, arr: readonly T[]) => U | undefined,
|
|
27
|
+
thisArg?: any
|
|
28
|
+
): readonly Defined<U>[];
|
|
29
|
+
export function inertFilterMap<T, U = T>(
|
|
30
|
+
arr: readonly T[] | undefined,
|
|
31
|
+
callbackfn: (value: T, index: number, arr: readonly T[]) => U | undefined,
|
|
32
|
+
thisArg?: any
|
|
33
|
+
): readonly Defined<U>[] | undefined;
|
|
34
|
+
export function inertFilterMap<T, U = T>(
|
|
35
|
+
arr: readonly T[] | undefined,
|
|
36
|
+
callbackfn: (value: T, index: number, arr: T[]) => U | undefined,
|
|
37
|
+
thisArg?: any
|
|
38
|
+
): readonly Defined<U>[] | undefined {
|
|
39
|
+
if (arr) {
|
|
40
|
+
let result: Defined<U>[] | undefined;
|
|
41
|
+
let index = -1;
|
|
42
|
+
let length = arr.length;
|
|
43
|
+
while (++index < length) {
|
|
44
|
+
const newItem = callbackfn.call(thisArg, arr[index], index, arr as T[]);
|
|
45
|
+
if (newItem === undefined || !Object.is(newItem, arr[index])) {
|
|
46
|
+
result ??= arr.slice(0, index) as Defined<U>[];
|
|
47
|
+
}
|
|
48
|
+
if (newItem !== undefined) {
|
|
49
|
+
result?.push(newItem as Defined<U>);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result || (arr as readonly Defined<U>[]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Lazy filter that avoids creating a new array when possible.
|
|
58
|
+
*
|
|
59
|
+
* @returns the original array if `predicate` returns `true` for all items, otherwise a new array with the filtered items.
|
|
60
|
+
*
|
|
61
|
+
* @param arr
|
|
62
|
+
* @param predicate Return `false` to filter out the item, or `true` to keep it.
|
|
63
|
+
* @param thisArg
|
|
64
|
+
*/
|
|
65
|
+
export function inertFilter<T, U extends T>(
|
|
66
|
+
arr: T[],
|
|
67
|
+
predicate: (value: T, index: number, arr: T[]) => value is U,
|
|
68
|
+
thisArg?: any
|
|
69
|
+
): U[];
|
|
70
|
+
export function inertFilter<T, U extends T>(
|
|
71
|
+
arr: T[] | undefined,
|
|
72
|
+
predicate: (value: T, index: number, arr: T[]) => value is U,
|
|
73
|
+
thisArg?: any
|
|
74
|
+
): U[] | undefined;
|
|
75
|
+
export function inertFilter<T, U extends T>(
|
|
76
|
+
arr: readonly T[],
|
|
77
|
+
predicate: (value: T, index: number, arr: readonly T[]) => value is U,
|
|
78
|
+
thisArg?: any
|
|
79
|
+
): readonly U[];
|
|
80
|
+
export function inertFilter<T, U extends T>(
|
|
81
|
+
arr: readonly T[] | undefined,
|
|
82
|
+
predicate: (value: T, index: number, arr: readonly T[]) => value is U,
|
|
83
|
+
thisArg?: any
|
|
84
|
+
): readonly U[] | undefined;
|
|
85
|
+
export function inertFilter<T>(arr: T[], predicate: (value: T, index: number, arr: T[]) => boolean, thisArg?: any): T[];
|
|
86
|
+
export function inertFilter<T>(
|
|
87
|
+
arr: T[] | undefined,
|
|
88
|
+
predicate: (value: T, index: number, arr: T[]) => boolean,
|
|
89
|
+
thisArg?: any
|
|
90
|
+
): T[] | undefined;
|
|
91
|
+
export function inertFilter<T>(
|
|
92
|
+
arr: readonly T[],
|
|
93
|
+
predicate: (value: T, index: number, arr: readonly T[]) => boolean,
|
|
94
|
+
thisArg?: any
|
|
95
|
+
): readonly T[];
|
|
96
|
+
export function inertFilter<T>(
|
|
97
|
+
arr: readonly T[] | undefined,
|
|
98
|
+
predicate: (value: T, index: number, arr: readonly T[]) => boolean,
|
|
99
|
+
thisArg?: any
|
|
100
|
+
): readonly T[] | undefined;
|
|
101
|
+
export function inertFilter<T>(
|
|
102
|
+
arr: readonly T[] | undefined,
|
|
103
|
+
predicate: (value: T, index: number, arr: T[]) => boolean,
|
|
104
|
+
thisArg?: any
|
|
105
|
+
): readonly T[] | undefined {
|
|
106
|
+
if (arr) {
|
|
107
|
+
let result: T[] | undefined;
|
|
108
|
+
let index = -1;
|
|
109
|
+
let length = arr.length;
|
|
110
|
+
while (++index < length) {
|
|
111
|
+
if (predicate.call(thisArg, arr[index], index, arr as T[])) {
|
|
112
|
+
result?.push(arr[index]);
|
|
113
|
+
} else {
|
|
114
|
+
result ??= arr.slice(0, index);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return result || arr;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface Coalesce {
|
|
122
|
+
<T>(arr: T[]): Truthy<T>[];
|
|
123
|
+
<T>(arr: readonly T[]): readonly Truthy<T>[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @returns the same array if all its items are truthy, otherwise a new array with all falsy items removed.
|
|
128
|
+
*/
|
|
129
|
+
export const coalesce: Coalesce = arr => inertFilter(arr, isTruthy) as unknown[];
|
package/src/index.ts
CHANGED