Skip to content

Commit

Permalink
Range
Browse files Browse the repository at this point in the history
  • Loading branch information
dahlia committed Nov 22, 2023
1 parent a73cf65 commit 6cbe516
Show file tree
Hide file tree
Showing 5 changed files with 573 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.cov/
docs/
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Version 0.5.0

To be released. Unreleased versions are available on [nest.land].

- Added *range.ts* module.
- Added `range()` function.
- Added `Range` class.


Version 0.4.0
-------------
Expand Down
1 change: 1 addition & 0 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { filter } from "./filter.ts";
export { reduce } from "./fold.ts";
export { count, cycle, repeat } from "./infinite.ts";
export { map } from "./map.ts";
export { Range, range } from "./range.ts";
export { take, takeWhile } from "./take.ts";
export { tee } from "./tee.ts";
export { assertStreams, assertStreamStartsWith } from "./testing.ts";
Expand Down
185 changes: 185 additions & 0 deletions range.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* Creates a {@link Range} of numbers from 0 to `stop` with step 1.
* @param stop The stop of the range.
*/
export function range(stop: number): Range<number>;

/**
* Creates a {@link Range} of bigints from 0 to `stop` with step 1.
* @param stop The stop of the range.
*/
export function range(stop: bigint): Range<bigint>;

/**
* Creates a {@link Range} of numbers with step 1.
* @param start The start of the range.
* @param stop The stop of the range.
*/
export function range(start: number, stop: number): Range<number>;

/**
* Creates a {@link Range} of bigints with step 1.
* @param start The start of the range.
* @param stop The stop of the range.
*/
export function range(start: bigint, stop: bigint): Range<bigint>;

/**
* Creates a {@link Range} of numbers.
* @param start The start of the range. It must be a finite number.
* @param stop The stop of the range. It must be a finite number.
* @param step The step of the range. It must be a finite number,
* and cannot be zero.
*/
export function range(start: number, stop: number, step: number): Range<number>;

/**
* Creates a {@link Range} of bigints.
* @param start The start of the range.
* @param stop The stop of the range.
* @param step The step of the range. Cannot be zero.
* @returns A {@link Range} of bigints.
*/
export function range(start: bigint, stop: bigint, step: bigint): Range<bigint>;

export function range<T extends number | bigint>(start: T, stop?: T, step?: T) {
if (step === 0 || step === 0n) throw new RangeError("step cannot be zero");
else if (typeof step === "undefined") {
step = (typeof start === "bigint" ? 1n : 1) as T;
}
if (typeof stop === "undefined") {
stop = start;
if (typeof start === "bigint") start = 0n as T;
else start = 0 as T;
}

return new Range(start, stop, step);
}

function validateNumber(n: number | bigint): boolean {
return typeof n === "bigint" || Number.isFinite(n);
}

/**
* An immutable sequence of numbers. It implements both `Iterable` and
* `AsyncIterable`.
*
* It is similar to Python's `range()` function.
* @template T The type of the elements in the range. It must be either
* `number` or `bigint`.
*/
export class Range<T extends number | bigint>
implements Iterable<T>, AsyncIterable<T> {
/**
* The start of the range. It must be a finite number.
*/
readonly start: T;

/**
* The stop of the range. It must be a finite number.
*/
readonly stop: T;

/**
* The step of the range. It must be a finite number, and cannot be zero.
*/
readonly step: T;

/**
* Constructs a new `Range` object.
* @param start The start of the range. It must be a finite number.
* @param stop The stop of the range. It must be a finite number.
* @param step The step of the range. It must be a finite number,
* and cannot be zero.
*/
constructor(start: T, stop: T, step: T) {
if (step === 0 || step === 0n) throw new RangeError("step cannot be zero");
else if (!validateNumber(start)) throw new RangeError("start is invalid");
else if (!validateNumber(stop)) throw new RangeError("stop is invalid");
else if (!validateNumber(step)) throw new RangeError("step is invalid");
else if (typeof start !== typeof stop || typeof start !== typeof step) {
throw new TypeError("start, stop, and step must be the same type");
}

this.start = start;
this.stop = stop;
this.step = step;
}

#stepIsNegative(): boolean {
return typeof this.step === "bigint" ? this.step < 0n : this.step < 0;
}

#stepIsPositive(): boolean {
return typeof this.step === "bigint" ? this.step > 0n : this.step > 0;
}

/**
* The length of the range. Note that it guarantees to return the same value
* as `Array.from(range).length`.
* @returns The number of elements in the range.
*/
get length(): number {
if (this.#stepIsNegative() && this.start <= this.stop) return 0;
else if (this.#stepIsPositive() && this.start >= this.stop) return 0;
const amount = this.stop - this.start;
return typeof amount == "number" ? Math.ceil(amount / this.step) : Number(
amount / (this.step as bigint) +
(amount % (this.step as bigint) === 0n ? 0n : 1n),
);
}

/**
* Iterates over the elements of the range.
* @return An iterator that iterates over the elements of the range.
*/
*[Symbol.iterator](): Iterator<T> {
if (this.#stepIsNegative() && this.start <= this.stop) return;
else if (this.#stepIsPositive() && this.start >= this.stop) return;
let i = typeof this.start === "bigint" ? 0n : 0, v = this.start;
const length = this.length;
while (i < length) {
v = (this.start as number) + (this.step as number) * (i as number) as T;
yield v as T;
i++;
}
}

/**
* Iterates over the elements of the range, in an asynchronous manner.
* @return An async iterator that iterates over the elements of the range.
*/
async *[Symbol.asyncIterator](): AsyncIterator<T> {
for (const value of this) yield value;
}

/**
* Returns the element at the specified index in the range. Note that it
* guarantees to return the same value as `Array.from(range).at(index)`.
* @param index The index of the element to return. If it is negative, it
* counts from the end of the range.
* @returns The element at the specified index in the range. If the index is
* out of range, `undefined` is returned.
*/
at(index: number): T | undefined {
if (!Number.isSafeInteger(index)) return undefined;
if (this.#stepIsNegative() && this.start <= this.stop) return undefined;
if (this.#stepIsPositive() && this.start >= this.stop) return undefined;
if (index < 0) index += this.length;
if (index >= this.length || index < 0) return undefined;
if (typeof this.start === "bigint" && typeof this.step === "bigint") {
return this.start + this.step * BigInt(index) as T;
}
return (this.start as number) + (this.step as number) * index as T;
}

/**
* Represents the range as a string.
* @returns A string representation of the range.
*/
toString(): string {
const suffix = typeof this.start === "bigint" ? "n" : "";
return `[object Range(${this.start}${suffix}, ${this.stop}${suffix}, ` +
`${this.step}${suffix})]`;
}
}
Loading

0 comments on commit 6cbe516

Please sign in to comment.