Skip to content

Commit

Permalink
Add peekable() iterators.
Browse files Browse the repository at this point in the history
  • Loading branch information
Cody Casterline committed Apr 25, 2023
1 parent 1aacf19 commit 19072f8
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 1 deletion.
83 changes: 83 additions & 0 deletions _src/peek.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@

/** An iterator that allows peeking at the next item without removing it. */
export class Peekable<T, TReturn=unknown, TNext = undefined> implements Iterator<T, TReturn, TNext> {

static from<T>(inner: Iterable<T>): Peekable<T> {
return new Peekable(inner[Symbol.iterator]())
}

#inner: Iterator<T,TReturn,TNext>;
#peeked?: IteratorResult<T, TReturn>;


constructor(inner: Iterator<T, TReturn, TNext>) {
this.#inner = inner
this.return = inner.return?.bind(inner)
this.throw = inner.throw?.bind(inner)
}

/** Returns the value that the next call to {@link #next} would yield. */
peek(): IteratorResult<T, TReturn> {
if (this.#peeked === undefined) {
this.#peeked = this.#inner.next()
}
return this.#peeked;
}

next(): IteratorResult<T, TReturn> {
if (this.#peeked !== undefined) {
let next = this.#peeked
this.#peeked = undefined
return next
}

return this.#inner.next()
}

// Delegate to inner:
return?: ((value?: TReturn) => IteratorResult<T,TReturn>);
throw?: ((e?: unknown) => IteratorResult<T,TReturn>);
}




/** An iterator that allows peeking at the next item without removing it. */
export class PeekableAsync<T, TReturn=unknown, TNext = undefined> implements AsyncIterator<T, TReturn, TNext> {

static from<T>(inner: AsyncIterable<T>): PeekableAsync<T> {
return new PeekableAsync(inner[Symbol.asyncIterator]())
}

#inner: AsyncIterator<T,TReturn,TNext>;
#peeked?: Promise<IteratorResult<T, TReturn>>;


constructor(inner: AsyncIterator<T, TReturn, TNext>) {
this.#inner = inner
this.return = inner.return?.bind(inner)
this.throw = inner.throw?.bind(inner)
}

/** Returns the value that the next call to {@link #next} would yield. */
peek(): Promise<IteratorResult<T, TReturn>> {
if (this.#peeked === undefined) {
this.#peeked = this.#inner.next()
}
return this.#peeked;
}

next(): Promise<IteratorResult<T, TReturn>> {
if (this.#peeked !== undefined) {
let next = this.#peeked
this.#peeked = undefined
return next
}

return this.#inner.next()
}

// Delegate to inner:
return?: ((value?: TReturn) => Promise<IteratorResult<T,TReturn>>);
throw?: ((e?: unknown) => Promise<IteratorResult<T,TReturn>>);
}
10 changes: 10 additions & 0 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,15 @@
"explicit-module-boundary-types"
]
}
},
"test": {
"files": {
"exclude": [
// test --doc tests my README.md files too. But the README has a code example
// of importing the mod from deno.land, so I was getting circular dependencies
// captured into my deno.lock.
"README.md"
]
}
}
}
24 changes: 24 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
* @module
*/

import { Peekable, PeekableAsync } from "./_src/peek.ts";
import { stateful, StatefulPromise } from "./_src/promise.ts";
import { Queue } from "./_src/queue.ts";

Expand Down Expand Up @@ -242,6 +243,11 @@ interface LazyShared<T> {
*/
avg(): Awaitable<T extends number ? number : never>

/**
* Get a "peekable" iterator, which lets you peek() at the next item without
* removing it from the iterator.
*/
peekable(): Peekable<T> | PeekableAsync<T>
}

export class Lazy<T> implements Iterable<T>, LazyShared<T> {
Expand Down Expand Up @@ -473,6 +479,15 @@ export class Lazy<T> implements Iterable<T>, LazyShared<T> {
let sum = this.also( () => { count += 1 } ).sum()
return sum / count as (T extends number ? number : never)
}

/**
* Get a "peekable" iterator, which lets you peek() at the next item without
* removing it from the iterator.
*/
peekable(): Peekable<T> {
let inner = this.#inner
return Peekable.from(inner)
}
}


Expand Down Expand Up @@ -768,6 +783,15 @@ export class LazyAsync<T> implements AsyncIterable<T>, LazyShared<T> {
let sum = await this.also( () => { count += 1 } ).sum()
return sum / count as (T extends number ? number : never)
}

/**
* Get a "peekable" iterator, which lets you peek() at the next item without
* removing it from the iterator.
*/
peekable(): PeekableAsync<T> {
let inner = this.#inner
return PeekableAsync.from(inner)
}
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// deno-lint-ignore-file explicit-module-boundary-types

export { assertEquals, assertIsError, assertThrows } from "https://deno.land/[email protected]/testing/asserts.ts";
export { assert, assertEquals, assertIsError, assertThrows } from "https://deno.land/[email protected]/testing/asserts.ts";
export { delay } from "https://deno.land/[email protected]/async/delay.ts";


Expand Down
60 changes: 60 additions & 0 deletions tests/peekable_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { range } from "../mod.ts";
import { assertEquals } from "./helpers.ts";

Deno.test(function peekableSync() {
let iter = range({to: 100, step: 3})
.filter(it => it % 2 == 0)
.peekable();

assertEquals(iter.peek().value, 0)
assertEquals(iter.peek().value, 0)
assertEquals(iter.next().value, 0)

assertEquals(iter.next().value, 6)

assertEquals(iter.peek().value, 12)
assertEquals(iter.peek().value, 12)
assertEquals(iter.next().value, 12)

for (let next = iter.peek(); !next.done && next.value < 96 ; iter.next(), next = iter.peek()) {
// console.log(iter.peek())
}

assertEquals(iter.peek().value, 96)
assertEquals(iter.next().value, 96)

assertEquals(iter.peek().done, true)
assertEquals(iter.peek().done, true)
assertEquals(iter.next().done, true)
assertEquals(iter.next().done, true)
})


Deno.test(async function peekableAsync() {
let iter = range({to: 100, step: 3})
.filter(it => it % 2 == 0)
.toAsync()
.peekable();

assertEquals((await iter.peek()).value, 0)
assertEquals((await iter.peek()).value, 0)
assertEquals((await iter.next()).value, 0)

assertEquals((await iter.next()).value, 6)

assertEquals((await iter.peek()).value, 12)
assertEquals((await iter.peek()).value, 12)
assertEquals((await iter.next()).value, 12)

for (let next = await iter.peek(); !next.done && next.value < 96 ; iter.next(), next = await iter.peek()) {
// console.log(iter.peek())
}

assertEquals((await iter.peek()).value, 96)
assertEquals((await iter.next()).value, 96)

assertEquals((await iter.peek()).done, true)
assertEquals((await iter.peek()).done, true)
assertEquals((await iter.next()).done, true)
assertEquals((await iter.next()).done, true)
})

0 comments on commit 19072f8

Please sign in to comment.