Skip to content

Commit

Permalink
Make all predicates take an index param
Browse files Browse the repository at this point in the history
  • Loading branch information
nvie committed Feb 18, 2025
1 parent 6e4bf22 commit c413636
Show file tree
Hide file tree
Showing 8 changed files with 60 additions and 33 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
## [Unreleased]

- Add second param `index` to all predicates. This will make operations like
partitioning a list based on the element position as easy as partitioning
based on the element value, for example:
```ts
const items = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const [thirds, rest] = partition(items, (item, index) => index % 3 === 0);
console.log(thirds); // [1, 4, 7]
console.log(rest); // [2, 3, 5, 6, 8, 9]
```
- Officially drop Node 16 support (it may still work)

## [2.3.2] - 2024-05-27
Expand Down
35 changes: 11 additions & 24 deletions src/builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ import { identityPredicate, keyToCmp, numberIdentity, primitiveIdentity } from "
* any. If no predicate is given, it will return the first value returned by
* the iterable.
*/
export function find<T>(iterable: Iterable<T>, keyFn?: Predicate<T>): T | undefined {
export function find<T>(iterable: Iterable<T>, predicate?: Predicate<T>): T | undefined {
const it = iter(iterable);
if (keyFn === undefined) {
if (predicate === undefined) {
const value = it.next();
return value.done ? value.value : value.value;
} else {
let res: IteratorResult<T>;
let i = 0;
while (!(res = it.next()).done) {
const value = res.value;
if (keyFn(value)) {
if (predicate(value, i++)) {
return value;
}
}
Expand All @@ -42,13 +43,13 @@ export function find<T>(iterable: Iterable<T>, keyFn?: Predicate<T>): T | undefi
* all([2, 4, 5], n => n % 2 === 0) // => false
*
*/
export function every<T>(iterable: Iterable<T>, keyFn: Predicate<T> = identityPredicate): boolean {
export function every<T>(iterable: Iterable<T>, predicate: Predicate<T> = identityPredicate): boolean {
let index = 0;
for (const item of iterable) {
if (!keyFn(item)) {
if (!predicate(item, index++)) {
return false;
}
}

return true;
}

Expand All @@ -69,13 +70,13 @@ export function every<T>(iterable: Iterable<T>, keyFn: Predicate<T> = identityPr
* some([{name: 'Bob'}, {name: 'Alice'}], person => person.name.startsWith('C')) // => false
*
*/
export function some<T>(iterable: Iterable<T>, keyFn: Predicate<T> = identityPredicate): boolean {
export function some<T>(iterable: Iterable<T>, predicate: Predicate<T> = identityPredicate): boolean {
let index = 0;
for (const item of iterable) {
if (keyFn(item)) {
if (predicate(item, index++)) {
return true;
}
}

return false;
}

Expand Down Expand Up @@ -127,7 +128,7 @@ export function* enumerate<T>(iterable: Iterable<T>, start = 0): IterableIterato
/**
* Non-lazy version of ifilter().
*/
export function filter<T, N extends T>(iterable: Iterable<T>, predicate: (item: T) => item is N): N[];
export function filter<T, N extends T>(iterable: Iterable<T>, predicate: (item: T, index: number) => item is N): N[];
export function filter<T>(iterable: Iterable<T>, predicate: Predicate<T>): T[];
export function filter<T>(iterable: Iterable<T>, predicate: Predicate<T>): T[] {
return Array.from(ifilter(iterable, predicate));
Expand All @@ -140,20 +141,6 @@ export function filter<T>(iterable: Iterable<T>, predicate: Predicate<T>): T[] {
* state, think of it as a "cursor") which can only be consumed once.
*/
export function iter<T>(iterable: Iterable<T>): IterableIterator<T> {
// class SelfIter implements IterableIterator<T> {
// #iterator: Iterator<T>;
// constructor(orig: Iterable<T>) {
// this.#iterator = orig[Symbol.iterator]();
// }
// [Symbol.iterator]() {
// return this;
// }
// next() {
// return this.#iterator.next();
// }
// }
// return new SelfIter(iterable);

return iterable[Symbol.iterator]() as IterableIterator<T>;
// ^^^^^^^^^^^^^^^^^^^^^^ Not safe!
}
Expand Down
9 changes: 6 additions & 3 deletions src/itertools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@ export function* cycle<T>(iterable: Iterable<T>): IterableIterator<T> {
* false.
*/
export function* dropwhile<T>(iterable: Iterable<T>, predicate: Predicate<T>): IterableIterator<T> {
let index = 0;
const it = iter(iterable);
let res: IteratorResult<T>;
while (!(res = it.next()).done) {
const value = res.value;
if (!predicate(value)) {
if (!predicate(value, index++)) {
yield value;
break; // we break, so we cannot use a for..of loop!
}
Expand Down Expand Up @@ -135,8 +136,9 @@ export function* icompress<T>(data: Iterable<T>, selectors: Iterable<boolean>):
export function ifilter<T, N extends T>(iterable: Iterable<T>, predicate: (item: T) => item is N): IterableIterator<N>;
export function ifilter<T>(iterable: Iterable<T>, predicate: Predicate<T>): IterableIterator<T>;
export function* ifilter<T>(iterable: Iterable<T>, predicate: Predicate<T>): IterableIterator<T> {
let index = 0;
for (const value of iterable) {
if (predicate(value)) {
if (predicate(value, index++)) {
yield value;
}
}
Expand Down Expand Up @@ -404,11 +406,12 @@ export function* repeat<T>(thing: T, times?: number): IterableIterator<T> {
* predicate is true.
*/
export function* takewhile<T>(iterable: Iterable<T>, predicate: Predicate<T>): IterableIterator<T> {
let index = 0;
const it = iter(iterable);
let res: IteratorResult<T>;
while (!(res = it.next()).done) {
const value = res.value;
if (!predicate(value)) return; // early return, so we cannot use for..of loop!
if (!predicate(value, index++)) return; // early return, so we cannot use for..of loop!
yield value;
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/more-itertools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { iter, map } from "./builtins";
import { enumerate, iter, map } from "./builtins";
import { izip, repeat } from "./itertools";
import type { Predicate, Primitive } from "./types";
import { primitiveIdentity } from "./utils";
Expand Down Expand Up @@ -117,15 +117,17 @@ export function* pairwise<T>(iterable: Iterable<T>): IterableIterator<[T, T]> {
*/
export function partition<T, N extends T>(
iterable: Iterable<T>,
predicate: (item: T) => item is N,
predicate: (item: T, index: number) => item is N,
): [N[], Exclude<T, N>[]];
export function partition<T>(iterable: Iterable<T>, predicate: Predicate<T>): [T[], T[]];
export function partition<T>(iterable: Iterable<T>, predicate: Predicate<T>): [T[], T[]];
export function partition<T>(iterable: Iterable<T>, predicate: Predicate<T>): [T[], T[]] {
const good = [];
const bad = [];

let index = 0;
for (const item of iterable) {
if (predicate(item)) {
if (predicate(item, index++)) {
good.push(item);
} else {
bad.push(item);
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type Predicate<T> = (value: T) => boolean;
export type Predicate<T> = (value: T, index: number) => boolean;
export type Primitive = string | number | boolean;
4 changes: 4 additions & 0 deletions test/builtins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from "~";

const isEven = (n: number) => n % 2 === 0;
const isEvenIndex = (_, index: number) => index % 2 === 0;

Check failure on line 23 in test/builtins.test.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Parameter '_' implicitly has an 'any' type.

Check failure on line 23 in test/builtins.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Parameter '_' implicitly has an 'any' type.

Check failure on line 23 in test/builtins.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Parameter '_' implicitly has an 'any' type.

Check failure on line 23 in test/builtins.test.ts

View workflow job for this annotation

GitHub Actions / test (latest)

Parameter '_' implicitly has an 'any' type.

function isNum(value: unknown): value is number {
return typeof value === "number";
Expand Down Expand Up @@ -169,10 +170,13 @@ describe("enumerate", () => {
describe("filter", () => {
it("filters empty list", () => {
expect(filter([], isEven)).toEqual([]);
expect(filter([], isEvenIndex)).toEqual([]);
});

it("ifilter works like Array.filter, but lazy", () => {
expect(filter([0, 1, 2, 3], isEven)).toEqual([0, 2]);
expect(filter([0, 1, 2, 3], isEvenIndex)).toEqual([0, 2]);
expect(filter([9, 0, 1, 2, 3], isEvenIndex)).toEqual([9, 1, 3]);
});

it("filter retains rich type info", () => {
Expand Down
20 changes: 18 additions & 2 deletions test/itertools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from "~";

const isEven = (x: number) => x % 2 === 0;
const isEvenIndex = (_, index: number) => index % 2 === 0;

Check failure on line 24 in test/itertools.test.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Parameter '_' implicitly has an 'any' type.

Check failure on line 24 in test/itertools.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Parameter '_' implicitly has an 'any' type.

Check failure on line 24 in test/itertools.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Parameter '_' implicitly has an 'any' type.

Check failure on line 24 in test/itertools.test.ts

View workflow job for this annotation

GitHub Actions / test (latest)

Parameter '_' implicitly has an 'any' type.
const isPositive = (x: number) => x >= 0;

function isNum(value: unknown): value is number {
Expand Down Expand Up @@ -95,19 +96,24 @@ describe("cycle", () => {
describe("dropwhile", () => {
it("dropwhile on empty list", () => {
expect(Array.from(dropwhile([], isEven))).toEqual([]);
expect(Array.from(dropwhile([], isEvenIndex))).toEqual([]);
expect(Array.from(dropwhile([], isPositive))).toEqual([]);
});

it("dropwhile on list", () => {
expect(Array.from(dropwhile([1], isEven))).toEqual([1]);
expect(Array.from(dropwhile([1], isEvenIndex))).toEqual([]);
expect(Array.from(dropwhile([1], isPositive))).toEqual([]);

expect(Array.from(dropwhile([-1, 0, 1], isEven))).toEqual([-1, 0, 1]);
expect(Array.from(dropwhile([4, -1, 0, 1], isEven))).toEqual([-1, 0, 1]);
expect(Array.from(dropwhile([-1, 0, 1], isEvenIndex))).toEqual([0, 1]);
expect(Array.from(dropwhile([4, -1, 0, 1], isEvenIndex))).toEqual([-1, 0, 1]);
expect(Array.from(dropwhile([-1, 0, 1], isPositive))).toEqual([-1, 0, 1]);
expect(Array.from(dropwhile([7, -1, 0, 1], isPositive))).toEqual([-1, 0, 1]);

expect(Array.from(dropwhile([0, 2, 4, 6, 7, 8, 10], isEven))).toEqual([7, 8, 10]);
expect(Array.from(dropwhile([0, 2, 4, 6, 7, 8, 10], isEvenIndex))).toEqual([2, 4, 6, 7, 8, 10]);
expect(Array.from(dropwhile([0, 1, 2, -2, 3, 4, 5, 6, 7], isPositive))).toEqual([-2, 3, 4, 5, 6, 7]);
});

Expand Down Expand Up @@ -182,6 +188,7 @@ describe("ifilter", () => {

it("ifilter can handle infinite inputs", () => {
expect(take(5, ifilter(range(9999), isEven))).toEqual([0, 2, 4, 6, 8]);
expect(take(5, ifilter(range(9999), isEvenIndex))).toEqual([0, 2, 4, 6, 8]);
});

it("ifilter retains rich type info", () => {
Expand Down Expand Up @@ -338,30 +345,39 @@ describe("repeat", () => {
describe("takewhile", () => {
it("takewhile on empty list", () => {
expect(Array.from(takewhile([], isEven))).toEqual([]);
expect(Array.from(takewhile([], isEvenIndex))).toEqual([]);
expect(Array.from(takewhile([], isPositive))).toEqual([]);
});

it("takewhile on list", () => {
expect(Array.from(takewhile([1], isEven))).toEqual([]);
expect(Array.from(takewhile([1], isEvenIndex))).toEqual([1]);
expect(Array.from(takewhile([1], isPositive))).toEqual([1]);

expect(Array.from(takewhile([-1, 0, 1], isEven))).toEqual([]);
expect(Array.from(takewhile([-1, 0, 1], isEvenIndex))).toEqual([-1]);
expect(Array.from(takewhile([-1, 0, 1], isPositive))).toEqual([]);

expect(Array.from(takewhile([0, 2, 4, 6, 7, 8, 10], isEven))).toEqual([0, 2, 4, 6]);
expect(Array.from(takewhile([0, 2, 4, 6, 7, 8, 10], isEvenIndex))).toEqual([0]);
expect(Array.from(takewhile([0, 1, 2, -2, 3, 4, 5, 6, 7], isPositive))).toEqual([0, 1, 2]);
});

it("takewhile on lazy iterable", () => {
const lazy = gen([0, 1, 2, -2, 4, 6, 8, 7]);
const lazy1 = takewhile(lazy, isPositive);
const lazy2 = takewhile(lazy, isEven);
const lazy2 = takewhile(lazy, isEvenIndex);
const lazy3 = takewhile(lazy, isEven);

expect(Array.from(lazy1)).toEqual([0, 1, 2]);

// By now, the -2 from the original input has been consumed, but we should
// be able to continue pulling more values from the same input
expect(Array.from(lazy2)).toEqual([4, 6, 8]);
expect(Array.from(lazy2)).toEqual([4]);

// By now, the 6 from the original input has been consumed, but we should
// be able to continue pulling more values from the same input
expect(Array.from(lazy3)).toEqual([8]);
});
});

Expand Down
6 changes: 6 additions & 0 deletions test/more-itertools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import * as fc from "fast-check";

const isEven = (x: number) => x % 2 === 0;
const isEvenIndex = (_, index: number) => index % 2 === 0;

Check failure on line 20 in test/more-itertools.test.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Parameter '_' implicitly has an 'any' type.

Check failure on line 20 in test/more-itertools.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

Parameter '_' implicitly has an 'any' type.

Check failure on line 20 in test/more-itertools.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

Parameter '_' implicitly has an 'any' type.

Check failure on line 20 in test/more-itertools.test.ts

View workflow job for this annotation

GitHub Actions / test (latest)

Parameter '_' implicitly has an 'any' type.
const isPositive = (x: number) => x >= 0;

function isNum(value: unknown): value is number {
Expand Down Expand Up @@ -246,6 +247,7 @@ describe("pairwise", () => {
describe("partition", () => {
it("partition empty list", () => {
expect(partition([], isEven)).toEqual([[], []]);
expect(partition([], isEvenIndex)).toEqual([[], []]);
});

it("partition splits input list into two lists", () => {
Expand All @@ -254,6 +256,10 @@ describe("partition", () => {
[-2, 4, 6, 8, 8, 0, -2],
[1, 3, 5, -3],
]);
expect(partition(values, isEvenIndex)).toEqual([
[1, 3, 5, 8, 0, -3],
[-2, 4, 6, 8, -2],
]);
expect(partition(values, isPositive)).toEqual([
[1, 3, 4, 5, 6, 8, 8, 0],
[-2, -2, -3],
Expand Down

0 comments on commit c413636

Please sign in to comment.