Skip to content

Commit 29d1698

Browse files
perf(collections): optimize minOf/maxOf with fast array paths (#6936)
1 parent 94295db commit 29d1698

File tree

4 files changed

+146
-16
lines changed

4 files changed

+146
-16
lines changed

collections/max_of.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,43 @@ export function maxOf<T, S extends ((el: T) => number) | ((el: T) => bigint)>(
7171
array: Iterable<T>,
7272
selector: S,
7373
): ReturnType<S> | undefined {
74-
let maximumValue: ReturnType<S> | undefined;
74+
if (Array.isArray(array)) {
75+
const length = array.length;
76+
if (length === 0) return undefined;
7577

76-
for (const element of array) {
77-
const currentValue = selector(element) as ReturnType<S>;
78+
let max = selector(array[0]!) as ReturnType<S>;
79+
if (Number.isNaN(max)) return max;
7880

79-
if (maximumValue === undefined || currentValue > maximumValue) {
80-
maximumValue = currentValue;
81-
continue;
81+
for (let i = 1; i < length; i++) {
82+
const currentValue = selector(array[i]!) as ReturnType<S>;
83+
if (currentValue > max) {
84+
max = currentValue;
85+
} else if (Number.isNaN(currentValue)) {
86+
return currentValue;
87+
}
8288
}
8389

84-
if (Number.isNaN(currentValue)) {
90+
return max;
91+
}
92+
93+
const iter = array[Symbol.iterator]();
94+
const first = iter.next();
95+
96+
if (first.done) return undefined;
97+
98+
let max = selector(first.value) as ReturnType<S>;
99+
if (Number.isNaN(max)) return max;
100+
101+
let next = iter.next();
102+
while (!next.done) {
103+
const currentValue = selector(next.value) as ReturnType<S>;
104+
if (currentValue > max) {
105+
max = currentValue;
106+
} else if (Number.isNaN(currentValue)) {
85107
return currentValue;
86108
}
109+
next = iter.next();
87110
}
88111

89-
return maximumValue;
112+
return max;
90113
}

collections/max_of_test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ Deno.test("maxOf() handles regular max", () => {
99
assertEquals(actual, 120);
1010
});
1111

12+
Deno.test("maxOf() handles single element", () => {
13+
const actual = maxOf([42], (i) => i);
14+
assertEquals(actual, 42);
15+
});
16+
1217
Deno.test("maxOf() handles mixed negatives and positives numbers", () => {
1318
const array = [-32, -18, 140, 36];
1419

@@ -67,6 +72,36 @@ Deno.test("maxOf() handles no mutation", () => {
6772
assertEquals(array, [1, 2, 3, 4]);
6873
});
6974

75+
Deno.test("maxOf() handles iterable input", () => {
76+
function* generate() {
77+
yield 5;
78+
yield 2;
79+
yield 8;
80+
yield 1;
81+
}
82+
83+
const actual = maxOf(generate(), (i) => i);
84+
assertEquals(actual, 8);
85+
});
86+
87+
Deno.test("maxOf() handles empty iterable", () => {
88+
function* generate(): Generator<number> {}
89+
90+
const actual = maxOf(generate(), (i) => i);
91+
assertEquals(actual, undefined);
92+
});
93+
94+
Deno.test("maxOf() handles NaN in iterable", () => {
95+
function* generate() {
96+
yield 1;
97+
yield NaN;
98+
yield 3;
99+
}
100+
101+
const actual = maxOf(generate(), (i) => i);
102+
assertEquals(actual, NaN);
103+
});
104+
70105
Deno.test("maxOf() handles empty array results in undefined", () => {
71106
const array: number[] = [];
72107

@@ -93,6 +128,13 @@ Deno.test("maxOf() handles NaN and Infinity", () => {
93128
assertEquals(actual, NaN);
94129
});
95130

131+
Deno.test("maxOf() handles NaN as first element", () => {
132+
const array = [NaN, 1, 2, 3];
133+
134+
const actual = maxOf(array, (i) => i);
135+
assertEquals(actual, NaN);
136+
});
137+
96138
Deno.test("maxOf() handles infinity", () => {
97139
const array = [1, 2, Infinity, 3, 4, 5, 6, 7, 8];
98140

collections/min_of.ts

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,43 @@ export function minOf<T, S extends ((el: T) => number) | ((el: T) => bigint)>(
7171
array: Iterable<T>,
7272
selector: S,
7373
): ReturnType<S> | undefined {
74-
let minimumValue: ReturnType<S> | undefined;
74+
if (Array.isArray(array)) {
75+
const length = array.length;
76+
if (length === 0) return undefined;
7577

76-
for (const element of array) {
77-
const currentValue = selector(element) as ReturnType<S>;
78+
let min = selector(array[0]!) as ReturnType<S>;
79+
if (Number.isNaN(min)) return min;
7880

79-
if (minimumValue === undefined || currentValue < minimumValue) {
80-
minimumValue = currentValue;
81-
continue;
81+
for (let i = 1; i < length; i++) {
82+
const currentValue = selector(array[i]!) as ReturnType<S>;
83+
if (currentValue < min) {
84+
min = currentValue;
85+
} else if (Number.isNaN(currentValue)) {
86+
return currentValue;
87+
}
8288
}
8389

84-
if (Number.isNaN(currentValue)) {
90+
return min;
91+
}
92+
93+
const iter = array[Symbol.iterator]();
94+
const first = iter.next();
95+
96+
if (first.done) return undefined;
97+
98+
let min = selector(first.value) as ReturnType<S>;
99+
if (Number.isNaN(min)) return min;
100+
101+
let next = iter.next();
102+
while (!next.done) {
103+
const currentValue = selector(next.value) as ReturnType<S>;
104+
if (currentValue < min) {
105+
min = currentValue;
106+
} else if (Number.isNaN(currentValue)) {
85107
return currentValue;
86108
}
109+
next = iter.next();
87110
}
88111

89-
return minimumValue;
112+
return min;
90113
}

collections/min_of_test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ Deno.test("minOf() handles regular min", () => {
99
assertEquals(actual, 5);
1010
});
1111

12+
Deno.test("minOf() handles single element", () => {
13+
const actual = minOf([42], (i) => i);
14+
assertEquals(actual, 42);
15+
});
16+
1217
Deno.test("minOf() handles mixed negatives and positives numbers", () => {
1318
const array = [-32, -18, 140, 36];
1419

@@ -67,6 +72,36 @@ Deno.test("minOf() handles no mutation", () => {
6772
assertEquals(array, [1, 2, 3, 4]);
6873
});
6974

75+
Deno.test("minOf() handles iterable input", () => {
76+
function* generate() {
77+
yield 5;
78+
yield 2;
79+
yield 8;
80+
yield 1;
81+
}
82+
83+
const actual = minOf(generate(), (i) => i);
84+
assertEquals(actual, 1);
85+
});
86+
87+
Deno.test("minOf() handles empty iterable", () => {
88+
function* generate(): Generator<number> {}
89+
90+
const actual = minOf(generate(), (i) => i);
91+
assertEquals(actual, undefined);
92+
});
93+
94+
Deno.test("minOf() handles NaN in iterable", () => {
95+
function* generate() {
96+
yield 1;
97+
yield NaN;
98+
yield 3;
99+
}
100+
101+
const actual = minOf(generate(), (i) => i);
102+
assertEquals(actual, NaN);
103+
});
104+
70105
Deno.test("minOf() handles empty array results in undefined", () => {
71106
const array: number[] = [];
72107

@@ -93,6 +128,13 @@ Deno.test("minOf() handles NaN and Infinity", () => {
93128
assertEquals(actual, NaN);
94129
});
95130

131+
Deno.test("minOf() handles NaN as first element", () => {
132+
const array = [NaN, 1, 2, 3];
133+
134+
const actual = minOf(array, (i) => i);
135+
assertEquals(actual, NaN);
136+
});
137+
96138
Deno.test("minOf() handles minus infinity", () => {
97139
const array = [1, 2, -Infinity, 3, 4, 5, 6, 7, 8];
98140

0 commit comments

Comments
 (0)