Skip to content

Commit ee94413

Browse files
authored
Added toMapNotNull operator and fix mapNotNull to filter undefined
2 parents 6a97a68 + e08f3bb commit ee94413

12 files changed

Lines changed: 489 additions & 18 deletions

File tree

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,6 @@ jobs:
8989
9090
# Commit and push version increment
9191
git add package.json
92-
git commit -m "chore: increment version after release"
92+
git commit -m "Incremented version after release"
9393
git push
9494

src/AsyncSequence.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {Take as TakeOp} from "./operators/async/take";
6868
import {TakeWhile as TakeWhileOp} from "./operators/async/takeWhile";
6969
import {ToArray as ToArrayOp} from "./operators/async/toArray";
7070
import {ToMap as ToMapOp} from "./operators/async/toMap";
71+
import {ToMapNotNull as ToMapNotNullOp} from "./operators/async/toMapNotNull";
7172
import {ToSequence as ToSequenceOp} from "./operators/async/toSequence";
7273
import {ToSet as ToSetOp} from "./operators/async/toSet";
7374
import {Unzip as UnzipOp} from "./operators/async/unzip";
@@ -91,7 +92,7 @@ export interface AsyncSequenceOperators<T> extends AllOp, AnyOp, AsIterableOp, A
9192
ElementAtOp, ElementAtOrElseOp, ElementAtOrNullOp, FilterOp, FilterIndexedOp, FilterNotOp, FilterNotNullOp, FirstOp, FirstOrNullOp, FlatMapOp, FlattenOp, FoldOp,
9293
FoldIndexedOp, ForEachOp, ForEachIndexedOp, GroupByOp, IndexOfOp, IndexOfFirstOp, IndexOfLastOp, IsEmptyOp, IsNotEmptyOp, JoinToStringOp, LastOp, LastOrNullOp, MapOp, MapIndexedOp, MapNotNullOp, MaxOp, MaxByOp,
9394
MaxWithOp, MergeOp, MinOp, MinByOp, MinusOp, MinWithOp, NoneOp, OnEachOp, OnEachIndexedOp, PartitionOp, PlusOp, ReduceOp, ReduceIndexedOp, ReverseOp, SingleOp, SingleOrNullOp, SortedOp,
94-
SortedByOp, SortedByDescendingOp, SortedDescendingOp, SortedWithOp, SumOp, SumByOp, TakeOp, TakeWhileOp, ToArrayOp, ToMapOp, ToSetOp, ToSequenceOp, UnzipOp, WithIndexOp, ZipOp {
95+
SortedByOp, SortedByDescendingOp, SortedDescendingOp, SortedWithOp, SumOp, SumByOp, TakeOp, TakeWhileOp, ToArrayOp, ToMapOp, ToMapNotNullOp, ToSetOp, ToSequenceOp, UnzipOp, WithIndexOp, ZipOp {
9596
}
9697

9798
export class AsyncSequenceImpl<T> {
@@ -103,4 +104,4 @@ applyMixins(AsyncSequenceImpl, [AllOp, AnyOp, AsIterableOp, AssociateOp, Associa
103104
ElementAtOp, ElementAtOrElseOp, ElementAtOrNullOp, FilterOp, FilterIndexedOp, FilterNotOp, FilterNotNullOp, FirstOp, FirstOrNullOp, FlatMapOp, FlattenOp, FoldOp,
104105
FoldIndexedOp, ForEachOp, ForEachIndexedOp, GroupByOp, IndexOfOp, IndexOfFirstOp, IndexOfLastOp, IsEmptyOp, IsNotEmptyOp, JoinToStringOp, LastOp, LastOrNullOp, MapOp, MapIndexedOp, MapNotNullOp, MaxOp, MaxByOp,
105106
MaxWithOp, MergeOp, MinOp, MinByOp, MinusOp, MinWithOp, NoneOp, OnEachOp, OnEachIndexedOp, PartitionOp, PlusOp, ReduceOp, ReduceIndexedOp, ReverseOp, SingleOp, SingleOrNullOp, SortedOp,
106-
SortedByOp, SortedByDescendingOp, SortedDescendingOp, SortedWithOp, SumOp, SumByOp, TakeOp, TakeWhileOp, ToArrayOp, ToMapOp, ToSetOp, ToSequenceOp, UnzipOp, WithIndexOp, ZipOp]);
107+
SortedByOp, SortedByDescendingOp, SortedDescendingOp, SortedWithOp, SumOp, SumByOp, TakeOp, TakeWhileOp, ToArrayOp, ToMapOp, ToMapNotNullOp, ToSetOp, ToSequenceOp, UnzipOp, WithIndexOp, ZipOp]);

src/Sequence.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {Take as TakeOp} from "./operators/sync/take";
6969
import {TakeWhile as TakeWhileOp} from "./operators/sync/takeWhile";
7070
import {ToArray as ToArrayOp} from "./operators/sync/toArray";
7171
import {ToMap as ToMapOp} from "./operators/sync/toMap";
72+
import {ToMapNotNull as ToMapNotNullOp} from "./operators/sync/toMapNotNull";
7273
import {ToSet as ToSetOp} from "./operators/sync/toSet";
7374
import {Unzip as UnzipOp} from "./operators/sync/unzip";
7475
import {WithIndex as WithIndexOp} from "./operators/sync/withIndex";
@@ -91,7 +92,7 @@ export interface SequenceOperators<T> extends AllOp, AnyOp, AsIterableOp, Associ
9192
ElementAtOp, ElementAtOrElseOp, ElementAtOrNullOp, FilterOp, FilterIndexedOp, FilterNotOp, FilterNotNullOp, FirstOp, FirstOrNullOp, FlatMapOp, FlattenOp, FoldOp,
9293
FoldIndexedOp, ForEachOp, ForEachIndexedOp, GroupByOp, IndexOfOp, IndexOfFirstOp, IndexOfLastOp, IsEmptyOp, IsNotEmptyOp, JoinToStringOp, LastOp, LastOrNullOp, MapOp, MapIndexedOp, MapNotNullOp, MaxOp, MaxByOp,
9394
MaxWithOp, MergeOp, MinOp, MinByOp, MinusOp, MinWithOp, NoneOp, OnEachOp, OnEachIndexedOp, PartitionOp, PlusOp, ReduceOp, ReduceIndexedOp, ReverseOp, SingleOp, SingleOrNullOp, SortedOp,
94-
SortedByOp, SortedByDescendingOp, SortedDescendingOp, SortedWithOp, SumOp, SumByOp, TakeOp, TakeWhileOp, ToArrayOp, ToAsyncSequenceOp, ToMapOp, ToSetOp, UnzipOp, WithIndexOp, ZipOp {
95+
SortedByOp, SortedByDescendingOp, SortedDescendingOp, SortedWithOp, SumOp, SumByOp, TakeOp, TakeWhileOp, ToArrayOp, ToAsyncSequenceOp, ToMapOp, ToMapNotNullOp, ToSetOp, UnzipOp, WithIndexOp, ZipOp {
9596
}
9697

9798
export class SequenceImpl<T> {
@@ -103,4 +104,4 @@ applyMixins(SequenceImpl, [AllOp, AnyOp, AsIterableOp, AssociateOp, AssociateByO
103104
ElementAtOp, ElementAtOrElseOp, ElementAtOrNullOp, FilterOp, FilterIndexedOp, FilterNotOp, FilterNotNullOp, FirstOp, FirstOrNullOp, FlatMapOp, FlattenOp, FoldOp,
104105
FoldIndexedOp, ForEachOp, ForEachIndexedOp, GroupByOp, IndexOfOp, IndexOfFirstOp, IndexOfLastOp, IsEmptyOp, IsNotEmptyOp, JoinToStringOp, LastOp, LastOrNullOp, MapOp, MapIndexedOp, MapNotNullOp, MaxOp, MaxByOp,
105106
MaxWithOp, MergeOp, MinOp, MinByOp, MinusOp, MinWithOp, NoneOp, OnEachOp, OnEachIndexedOp, PartitionOp, PlusOp, ReduceOp, ReduceIndexedOp, ReverseOp, SingleOp, SingleOrNullOp, SortedOp,
106-
SortedByOp, SortedByDescendingOp, SortedDescendingOp, SortedWithOp, SumOp, SumByOp, TakeOp, TakeWhileOp, ToArrayOp, ToAsyncSequenceOp, ToMapOp, ToSetOp, UnzipOp, WithIndexOp, ZipOp]);
107+
SortedByOp, SortedByDescendingOp, SortedDescendingOp, SortedWithOp, SumOp, SumByOp, TakeOp, TakeWhileOp, ToArrayOp, ToAsyncSequenceOp, ToMapOp, ToMapNotNullOp, ToSetOp, UnzipOp, WithIndexOp, ZipOp]);

src/operators/async/mapNotNull.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ export class MapNotNull {
44

55
/**
66
* Transforms each element into another value by applying the given `transform` function and returns a new sequence.
7-
* Transformations into `null` values are discarded.
7+
* Transformations into `null` or `undefined` values are discarded.
88
*
9-
* @param {(value: T) => R | null} transform
10-
* @returns {AsyncSequence<R>}
9+
* @param {(value: T) => Promise<R | null | undefined> | R | null | undefined} transform
10+
* @returns {AsyncSequence<NonNullable<R>>}
1111
*/
12-
mapNotNull<T, R>(this: AsyncSequence<T>, transform: (value: T) => Promise<R | null> | R | null): AsyncSequence<R> {
12+
mapNotNull<T, R>(this: AsyncSequence<T>, transform: (value: T) => Promise<R | null | undefined> | R | null | undefined): AsyncSequence<NonNullable<R>> {
1313
return this.flatMap(async (value: T) => {
1414
const item = await transform(value);
15-
return item !== null
16-
? asyncSequenceOf(item)
15+
return item != null
16+
? asyncSequenceOf(item as NonNullable<R>)
1717
: emptyAsyncSequence();
1818
});
1919
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {AsyncSequence} from "../../sequency";
2+
3+
export class ToMapNotNull {
4+
5+
/**
6+
* Returns a map consisting of each key-value pair. Pairs with `null` or `undefined` keys or values are discarded.
7+
* If a `map` is passed the pairs are set on this map. Duplicate keys override each other.
8+
*
9+
* @param {Map<NonNullable<K>, NonNullable<V>>} map
10+
* @returns {Promise<Map<NonNullable<K>, NonNullable<V>>>}
11+
*/
12+
async toMapNotNull<K, V>(this: AsyncSequence<[K, V]>, map?: Map<NonNullable<K>, NonNullable<V>>): Promise<Map<NonNullable<K>, NonNullable<V>>> {
13+
const result = map ?? new Map<NonNullable<K>, NonNullable<V>>();
14+
for (let item = await this.iterator.next(); !item.done; item = await this.iterator.next()) {
15+
const pair = item.value;
16+
const key = pair[0];
17+
const value = pair[1];
18+
if (key != null && value != null) {
19+
result.set(key as NonNullable<K>, value as NonNullable<V>);
20+
}
21+
}
22+
return result;
23+
}
24+
25+
}

src/operators/sync/mapNotNull.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ export class MapNotNull {
44

55
/**
66
* Transforms each element into another value by applying the given `transform` function and returns a new sequence.
7-
* Transformations into `null` values are discarded.
7+
* Transformations into `null` or `undefined` values are discarded.
88
*
9-
* @param {(value: T) => R} transform
10-
* @returns {Sequence<R>}
9+
* @param {(value: T) => R | null | undefined} transform
10+
* @returns {Sequence<NonNullable<R>>}
1111
*/
12-
mapNotNull<T, R>(this: Sequence<T>, transform: (value: T) => R | null): Sequence<R> {
12+
mapNotNull<T, R>(this: Sequence<T>, transform: (value: T) => R | null | undefined): Sequence<NonNullable<R>> {
1313
return this.flatMap((value: T) => {
1414
const item = transform(value);
15-
return item !== null
16-
? sequenceOf(item)
15+
return item != null
16+
? sequenceOf(item as NonNullable<R>)
1717
: emptySequence();
1818
});
1919
}

src/operators/sync/toMapNotNull.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {Sequence} from "../../sequency";
2+
3+
export class ToMapNotNull {
4+
5+
/**
6+
* Returns a map consisting of each key-value pair. Pairs with `null` or `undefined` keys or values are discarded.
7+
* If a `map` is passed the pairs are set on this map. Duplicate keys override each other.
8+
*
9+
* @param {Map<NonNullable<K>, NonNullable<V>>} map
10+
* @returns {Map<NonNullable<K>, NonNullable<V>>}
11+
*/
12+
toMapNotNull<K, V>(this: Sequence<[K, V]>, map?: Map<NonNullable<K>, NonNullable<V>>): Map<NonNullable<K>, NonNullable<V>> {
13+
const result = map ?? new Map<NonNullable<K>, NonNullable<V>>();
14+
for (let item = this.iterator.next(); !item.done; item = this.iterator.next()) {
15+
const pair = item.value;
16+
const key = pair[0];
17+
const value = pair[1];
18+
if (key != null && value != null) {
19+
result.set(key as NonNullable<K>, value as NonNullable<V>);
20+
}
21+
}
22+
return result;
23+
}
24+
25+
}

src/sequency.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,4 +265,19 @@ export function range(start: number, endInclusive: number, step: number = 1): Se
265265
current += step;
266266
}
267267
});
268-
}
268+
}
269+
270+
/**
271+
* Creates a tuple from the given arguments, preserving tuple types for better type inference.
272+
* Useful when working with sequences that need to maintain tuple types, especially when using `toMap`.
273+
*
274+
* @param {...T} args Arguments to create a tuple from
275+
* @returns Tuple of the arguments
276+
* @example
277+
* // Preserves tuple type [string, number] instead of (string | number)[]
278+
* const result = sequenceOf(["a", 1], ["b", 2])
279+
* .mapNotNull(([key, value]) => value > 0 ? tuple(key, value) : null)
280+
* .toMap();
281+
* // result: Map<string, number>
282+
*/
283+
export const tuple = <T extends any[]>(...args: T) => args;

test/operators/async/mapNotNull.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,40 @@ describe("mapNotNull", () => {
1313

1414
expect(array).toEqual([1, 4]);
1515
});
16+
17+
it("should filter out undefined values", async () => {
18+
const a1 = {a: 1};
19+
const a2 = {a: undefined};
20+
const a3 = {a: undefined};
21+
const a4 = {a: 4};
22+
23+
const array = await asyncSequenceOf<{a: number | undefined}>(a1, a2, a3, a4)
24+
.mapNotNull(async it => it.a)
25+
.toArray();
26+
27+
expect(array).toEqual([1, 4]);
28+
});
29+
30+
it("should filter out both null and undefined values", async () => {
31+
const a1 = {a: 1};
32+
const a2 = {a: null};
33+
const a3 = {a: undefined};
34+
const a4 = {a: 4};
35+
const a5 = {a: null};
36+
const a6 = {a: undefined};
37+
38+
const array = await asyncSequenceOf<{a: number | null | undefined}>(a1, a2, a3, a4, a5, a6)
39+
.mapNotNull(async it => it.a)
40+
.toArray();
41+
42+
expect(array).toEqual([1, 4]);
43+
});
44+
45+
it("should preserve falsy but non-null values", async () => {
46+
const array = await asyncSequenceOf<number | boolean | string | null | undefined>(0, false, "", null, undefined, 1)
47+
.mapNotNull(async it => it)
48+
.toArray();
49+
50+
expect(array).toEqual([0, false, "", 1]);
51+
});
1652
});
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import {asAsyncSequence} from "../../../src/sequency";
2+
3+
describe("toMapNotNull", () => {
4+
it("should return items as new map, filtering out null and undefined values", async () => {
5+
const key1 = {k: 1};
6+
const key2 = {k: 2};
7+
const key3 = {k: 3};
8+
const array: [object, string | null | undefined][] = [[key1, "a"], [key2, null], [key3, "c"], [key1, undefined]];
9+
const map = await asAsyncSequence(array)
10+
.toMapNotNull();
11+
12+
expect(map).toEqual(
13+
new Map([
14+
[key1, "a"],
15+
[key3, "c"]
16+
])
17+
);
18+
});
19+
20+
it("should filter out null and undefined keys", async () => {
21+
const key1 = {k: 1};
22+
const key2 = {k: 2};
23+
const array: [object | null | undefined, string][] = [[key1, "a"], [null, "b"], [key2, "c"], [undefined, "d"]];
24+
const map = await asAsyncSequence(array)
25+
.toMapNotNull();
26+
27+
expect(map).toEqual(
28+
new Map([
29+
[key1, "a"],
30+
[key2, "c"]
31+
])
32+
);
33+
});
34+
35+
it("should append items to passed map, filtering out null and undefined", async () => {
36+
const key0 = {k: 0};
37+
const key1 = {k: 1};
38+
const key2 = {k: 2};
39+
const key3 = {k: 3};
40+
41+
const existingMap = new Map();
42+
existingMap.set(key0, "_");
43+
44+
const array: [object, string | null | undefined][] = [[key1, "a"], [key2, null], [key3, "c"]];
45+
const result = await asAsyncSequence(array)
46+
.toMapNotNull(existingMap);
47+
48+
expect(result).toEqual(
49+
new Map([
50+
[key0, "_"],
51+
[key1, "a"],
52+
[key3, "c"]
53+
])
54+
);
55+
});
56+
57+
it("should handle all null/undefined pairs", async () => {
58+
const array: [string | null | undefined, string | null | undefined][] = [
59+
[null, "a"],
60+
["b", null],
61+
[undefined, "c"],
62+
["d", undefined],
63+
[null, null],
64+
[undefined, undefined]
65+
];
66+
const map = await asAsyncSequence(array)
67+
.toMapNotNull();
68+
69+
expect(map.size).toBe(0);
70+
});
71+
72+
it("should handle empty sequence", async () => {
73+
const array: [string, number][] = [];
74+
const map = await asAsyncSequence(array)
75+
.toMapNotNull();
76+
77+
expect(map.size).toBe(0);
78+
});
79+
80+
it("should preserve falsy but non-null values", async () => {
81+
const array: [string, number | null | undefined | boolean | string][] = [
82+
["a", 0],
83+
["b", false],
84+
["c", ""],
85+
["d", null],
86+
["e", undefined]
87+
];
88+
const map = await asAsyncSequence(array)
89+
.toMapNotNull();
90+
91+
expect(map.size).toBe(3);
92+
expect(map.get("a")).toBe(0);
93+
expect(map.get("b")).toBe(false);
94+
expect(map.get("c")).toBe("");
95+
});
96+
97+
it("should handle duplicate keys, keeping the last non-null value", async () => {
98+
const key1 = {k: 1};
99+
const array: [object, string | null | undefined][] = [
100+
[key1, "first"],
101+
[key1, null],
102+
[key1, "second"],
103+
[key1, undefined],
104+
[key1, "third"]
105+
];
106+
const map = await asAsyncSequence(array)
107+
.toMapNotNull();
108+
109+
expect(map).toEqual(
110+
new Map([
111+
[key1, "third"]
112+
])
113+
);
114+
});
115+
116+
it("should handle mixed key and value types", async () => {
117+
const key1 = {k: 1};
118+
const key2 = "key2";
119+
const array: [object | string | number | null | undefined, string | number | null | undefined][] = [
120+
[key1, "value1"],
121+
[key2, 42],
122+
[null, "value3"],
123+
[3, null],
124+
[key1, undefined],
125+
[undefined, "value6"],
126+
[5, 55]
127+
];
128+
const map = await asAsyncSequence(array)
129+
.toMapNotNull();
130+
131+
expect(map.size).toBe(3);
132+
expect(map.get(key1)).toBe("value1");
133+
expect(map.get(key2)).toBe(42);
134+
expect(map.get(5)).toBe(55);
135+
});
136+
137+
it("should return the same map instance when passed as parameter", async () => {
138+
const key1 = {k: 1};
139+
const existingMap = new Map();
140+
existingMap.set(key1, "existing");
141+
142+
const array: [object, string][] = [[key1, "new"]];
143+
const result = await asAsyncSequence(array)
144+
.toMapNotNull(existingMap);
145+
146+
expect(result).toBe(existingMap);
147+
});
148+
149+
it("should handle numeric keys and values", async () => {
150+
const array: [number | null | undefined, number | null | undefined][] = [
151+
[1, 10],
152+
[2, null],
153+
[null, 30],
154+
[4, undefined],
155+
[undefined, 50]
156+
];
157+
const map = await asAsyncSequence(array)
158+
.toMapNotNull();
159+
160+
expect(map).toEqual(
161+
new Map([
162+
[1, 10]
163+
])
164+
);
165+
});
166+
});

0 commit comments

Comments
 (0)