Skip to content

Commit dac2f0f

Browse files
committed
Change IntegerRange to an iterable class with step method
1 parent 9b2ea23 commit dac2f0f

File tree

5 files changed

+148
-133
lines changed

5 files changed

+148
-133
lines changed

math/integer_range.ts

Lines changed: 103 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,124 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22
// This module is browser compatible.
33

4-
/**
5-
* Options for {@linkcode integerRange}.
6-
*/
4+
/** Options for {@linkcode IntegerRange}. */
75
export type IntegerRangeOptions = {
8-
/**
9-
* The step between each number in the range.
10-
* @default {1}
11-
*/
12-
step?: number;
13-
/**
14-
* Whether to include the start value in the range.
15-
* @default {true}
16-
*/
17-
includeStart?: boolean;
186
/**
197
* Whether to include the end value in the range.
208
* @default {false}
219
*/
2210
includeEnd?: boolean;
2311
};
2412

25-
/**
26-
* Creates a generator that yields integers in a range from `start` to `end`.
27-
*
28-
* Using the default options, yielded numbers are in the interval `[start, end)` with step size `1`.
29-
*
30-
* @param start The start of the range (inclusive by default)
31-
* @param end The end of the range (exclusive by default)
32-
* @param options Options for the range
33-
* @returns A generator yielding integers in the specified range
34-
*
35-
* @example Usage
36-
* ```ts
37-
* import { integerRange } from "@std/math/integer-range";
38-
* import { assertEquals } from "@std/assert";
39-
* assertEquals([...integerRange(1, 5)], [1, 2, 3, 4]);
40-
* assertEquals([...integerRange(1, 5, { step: 2 })], [1, 3]);
41-
* assertEquals(
42-
* [...integerRange(1, 5, { includeStart: false, includeEnd: true })],
43-
* [2, 3, 4, 5],
44-
* );
45-
* assertEquals([...integerRange(5, 1)], []);
46-
* assertEquals([...integerRange(5, 1, { step: -1 })], [5, 4, 3, 2]);
47-
* ```
48-
*/
49-
export function integerRange(
50-
start: number,
51-
end: number,
52-
options?: IntegerRangeOptions,
53-
): Generator<number, undefined, undefined>;
13+
export class IntegerRange {
14+
readonly start: number;
15+
readonly end: number;
16+
readonly includeEnd: boolean;
5417

55-
/**
56-
* Creates a generator that yields integers in a range from 0 to `end`.
57-
*
58-
* Using the default options, yielded numbers are in the interval `[0, end)` with step size `1`.
59-
*
60-
* @param end The end of the range (exclusive by default)
61-
* @param options Options for the range
62-
* @returns A generator yielding integers in the specified range
63-
*
64-
* @example Usage
65-
* ```ts
66-
* import { integerRange } from "@std/math/integer-range";
67-
* import { assertEquals } from "@std/assert";
68-
* assertEquals([...integerRange(5)], [0, 1, 2, 3, 4]);
69-
* ```
70-
*/
71-
export function integerRange(
72-
end: number,
73-
options?: IntegerRangeOptions,
74-
): Generator<number, undefined, undefined>;
75-
// deno-lint-ignore deno-style-guide/exported-function-args-maximum
76-
export function* integerRange(
77-
startOrEnd: number,
78-
endOrOptions?: number | IntegerRangeOptions,
79-
maybeOptions?: IntegerRangeOptions,
80-
): Generator<number, undefined, undefined> {
81-
const hasStart = typeof endOrOptions === "number";
82-
const [start, end, options] = [
83-
hasStart ? startOrEnd : 0,
84-
hasStart ? endOrOptions : startOrEnd,
85-
hasStart ? maybeOptions : endOrOptions,
86-
];
18+
/**
19+
* Creates an iterable that yields integers in a range from `start` to `end`.
20+
*
21+
* Using the default options, yielded numbers are in the interval `[start, end)`.
22+
*
23+
* @param start The start of the range (inclusive by default)
24+
* @param end The end of the range (exclusive by default)
25+
* @param options Options for the range
26+
*
27+
* @example Usage
28+
* ```ts
29+
* import { IntegerRange } from "@std/math/integer-range";
30+
* import { assertEquals } from "@std/assert";
31+
* assertEquals([...new IntegerRange(1, 5)], [1, 2, 3, 4]);
32+
* ```
33+
*/
34+
constructor(
35+
start: number,
36+
end: number,
37+
options?: IntegerRangeOptions,
38+
);
39+
/**
40+
* Creates an iterable that yields integers in a range from `0` to `end`.
41+
*
42+
* Using the default options, yielded numbers are in the interval `[0, end)`.
43+
*
44+
* @param end The end of the range (exclusive by default)
45+
* @param options Options for the range
46+
*
47+
* @example Usage
48+
* ```ts
49+
* import { IntegerRange } from "@std/math/integer-range";
50+
* import { assertEquals } from "@std/assert";
51+
* assertEquals([...new IntegerRange(5)], [0, 1, 2, 3, 4]);
52+
* ```
53+
*/
54+
constructor(end: number, options?: IntegerRangeOptions);
55+
constructor(
56+
startOrEnd: number,
57+
endOrOptions?: number | IntegerRangeOptions,
58+
maybeOptions?: IntegerRangeOptions,
59+
) {
60+
const hasStart = typeof endOrOptions === "number";
61+
this.start = hasStart ? startOrEnd : 0;
62+
this.end = hasStart ? endOrOptions : startOrEnd;
63+
const options = hasStart ? maybeOptions : endOrOptions;
64+
this.includeEnd = options?.includeEnd ?? false;
8765

88-
const { step = 1, includeStart = true, includeEnd = false } = options ?? {};
89-
if (step === 0) throw new RangeError("`step` must not be zero");
90-
for (const [k, v] of Object.entries({ start, end, step })) {
91-
if (!Number.isSafeInteger(v)) {
92-
throw new RangeError(`\`${k}\` must be a safe integer`);
66+
for (const k of ["start", "end"] as const) {
67+
if (!Number.isSafeInteger(this[k])) {
68+
throw new RangeError(`\`${k}\` must be a safe integer`);
69+
}
9370
}
9471
}
9572

96-
if (start === end && !(includeStart && includeEnd)) return;
73+
/**
74+
* Generates numbers in the range with the default step size of 1.
75+
*
76+
* @returns A generator yielding numbers in the specified range with step size 1.
77+
*
78+
* @example Usage
79+
* ```ts
80+
* import { IntegerRange } from "@std/math/integer-range";
81+
* import { assertEquals } from "@std/assert";
82+
* assertEquals([...new IntegerRange(1, 5)], [1, 2, 3, 4]);
83+
* assertEquals([...new IntegerRange(1, 5, { includeEnd: true })], [1, 2, 3, 4, 5]);
84+
* assertEquals([...new IntegerRange(5, 1)], []);
85+
* ```
86+
*/
87+
*[Symbol.iterator](): Generator<number, undefined, undefined> {
88+
yield* this.step(1);
89+
}
9790

98-
const limitsSign = Math.sign(end - start);
99-
const stepSign = Math.sign(step);
100-
if (limitsSign !== 0 && limitsSign !== stepSign) return;
91+
/**
92+
* Generates numbers in the range with the specified step size.
93+
*
94+
* @param step The step size between yielded numbers.
95+
* @returns A generator yielding numbers in the specified range with the given step size.
96+
*
97+
* @example Usage
98+
* ```ts
99+
* import { IntegerRange } from "@std/math/integer-range";
100+
* import { assertEquals } from "@std/assert";
101+
* assertEquals([...new IntegerRange(1, 5).step(2)], [1, 3]);
102+
* assertEquals([...new IntegerRange(1, 5, { includeEnd: true }).step(2)], [1, 3, 5]);
103+
* assertEquals([...new IntegerRange(5, 1).step(-1)], [5, 4, 3, 2]);
104+
* ```
105+
*/
106+
*step(step: number): Generator<number, undefined, undefined> {
107+
if (!Number.isSafeInteger(step) || step === 0) {
108+
throw new RangeError("`step` must be a safe, non-zero integer");
109+
}
101110

102-
if (includeStart) yield start;
111+
const { start, end, includeEnd } = this;
112+
if (start === end && !includeEnd) return;
103113

104-
let i = 0;
105-
const delta = Math.abs(step);
106-
const maxDelta = Math.abs(end - start);
107-
for (i += delta; i < maxDelta; i += delta) yield start + (i * stepSign);
114+
const limitsSign = Math.sign(end - start);
115+
const stepSign = Math.sign(step);
116+
if (limitsSign !== 0 && limitsSign !== stepSign) return;
108117

109-
if (includeEnd && (i * stepSign) + start === end) yield end;
118+
let i = 0;
119+
const delta = Math.abs(step);
120+
const maxDelta = Math.abs(end - start);
121+
for (; i < maxDelta; i += delta) yield start + (i * stepSign);
122+
if (includeEnd && i === maxDelta) yield end;
123+
}
110124
}

math/integer_range_test.ts

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,88 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
2-
import { integerRange } from "@std/math/integer-range";
2+
import { IntegerRange } from "@std/math/integer-range";
33
import { assertEquals, assertThrows } from "@std/assert";
44

5-
Deno.test("integerRange()", async (t) => {
5+
Deno.test("new IntegerRange()", async (t) => {
66
await t.step("basic", () => {
7-
const range = integerRange(1, 5);
7+
const range = new IntegerRange(1, 5);
88
assertEquals([...range], [1, 2, 3, 4]);
9-
// already consumed iterator
10-
assertEquals([...range], []);
9+
// can be re-consumed multiple times
10+
assertEquals([...range], [1, 2, 3, 4]);
11+
12+
const iter = range[Symbol.iterator]();
13+
assertEquals([...iter], [1, 2, 3, 4]);
14+
// already consumed, yields nothing
15+
assertEquals([...iter], []);
1116
});
1217

1318
await t.step("only include end (start defaulting to `0`)", () => {
14-
const range = integerRange(5);
19+
const range = new IntegerRange(5);
1520
assertEquals([...range], [0, 1, 2, 3, 4]);
1621
});
1722

1823
await t.step("`step`", () => {
19-
assertEquals([...integerRange(1, 5, { step: 2 })], [1, 3]);
24+
assertEquals([...new IntegerRange(1, 5).step(2)], [1, 3]);
2025
});
2126

22-
await t.step("`includeStart`, `includeEnd`", () => {
27+
await t.step("`includeEnd`", () => {
2328
assertEquals(
24-
[...integerRange(1, 5, { includeStart: false, includeEnd: true })],
25-
[2, 3, 4, 5],
29+
[...new IntegerRange(1, 5, { includeEnd: true })],
30+
[1, 2, 3, 4, 5],
2631
);
2732
});
2833

2934
await t.step(
3035
"`start` and `end` in opposite order to `step` yield no results",
3136
() => {
32-
assertEquals([...integerRange(5, 1)], []);
33-
assertEquals([...integerRange(1, 5, { step: -1 })], []);
37+
assertEquals([...new IntegerRange(5, 1)], []);
38+
assertEquals([...new IntegerRange(1, 5).step(-1)], []);
3439
},
3540
);
3641

3742
await t.step("decreasing range with negative step", () => {
38-
assertEquals([...integerRange(5, 1, { step: -1 })], [5, 4, 3, 2]);
43+
assertEquals([...new IntegerRange(5, 1).step(-1)], [5, 4, 3, 2]);
3944
});
4045

4146
await t.step("`start` == `end`", () => {
42-
assertEquals([...integerRange(0, 0)], []);
43-
assertEquals([...integerRange(0, 0, { includeEnd: true })], [0]);
44-
assertEquals([...integerRange(0, 0, { includeStart: false })], []);
45-
46-
// if _both_ are false, nothing is yielded
47-
assertEquals([
48-
...integerRange(0, 0, { includeStart: false, includeEnd: false }),
49-
], []);
47+
assertEquals([...new IntegerRange(0, 0)], []);
48+
// only yield anything if `includeEnd` is `true`
49+
assertEquals([...new IntegerRange(0, 0, { includeEnd: true })], [0]);
5050
});
5151

5252
await t.step("errors", () => {
5353
assertThrows(
54-
() => [...integerRange(1.5, 5)],
54+
() => [...new IntegerRange(1.5, 5)],
5555
RangeError,
5656
"`start` must be a safe integer",
5757
);
5858
assertThrows(
59-
() => [...integerRange(1, 5, { step: 1.5 })],
59+
() => [...new IntegerRange(1, 5.5)],
6060
RangeError,
61-
"`step` must be a safe integer",
61+
"`end` must be a safe integer",
6262
);
6363
assertThrows(
64-
() => [...integerRange(1, 5.5)],
64+
() => [...new IntegerRange(1, 5).step(1.5)],
6565
RangeError,
66-
"`end` must be a safe integer",
66+
"`step` must be a safe, non-zero integer",
6767
);
68+
6869
assertThrows(
69-
() => [...integerRange(1, 5, { step: 0 })],
70+
() => [...new IntegerRange(1, 5).step(0)],
7071
RangeError,
71-
"`step` must not be zero",
72+
"`step` must be a safe, non-zero integer",
7273
);
7374
});
7475

7576
await t.step("`includeEnd` with `step`", () => {
76-
assertEquals([...integerRange(0, 2, { step: 2 })], [0]);
77-
assertEquals([...integerRange(0, 2, { step: 2, includeEnd: true })], [
78-
0,
79-
2,
80-
]);
81-
assertEquals([...integerRange(0, 3, { step: 2 })], [0, 2]);
82-
assertEquals([...integerRange(0, 3, { step: 2, includeEnd: true })], [
83-
0,
84-
2,
85-
]);
77+
assertEquals([...new IntegerRange(0, 2).step(2)], [0]),
78+
assertEquals(
79+
[...new IntegerRange(0, 2, { includeEnd: true }).step(2)],
80+
[0, 2],
81+
);
82+
assertEquals([...new IntegerRange(0, 3).step(2)], [0, 2]),
83+
assertEquals(
84+
[...new IntegerRange(0, 3, { includeEnd: true }).step(2)],
85+
[0, 2],
86+
);
8687
});
8788
});

math/mod.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
* ```ts
88
* import { modulo } from "@std/math/modulo";
99
* import { assertEquals } from "@std/assert";
10-
* import { integerRange } from "@std/math/integer-range";
10+
* import { IntegerRange } from "@std/math/integer-range";
1111
*
1212
* // 5 o'clock is always 5 o'clock, no matter how many twelve-hour cycles you add or remove
13-
* for (const n of integerRange(-3, 3, { includeEnd: true })) {
13+
* for (const n of new IntegerRange(-3, 3, { includeEnd: true })) {
1414
* const val = n * 12 + 5
1515
* assertEquals(modulo(val, 12), 5);
1616
* }

math/modulo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
* ```ts
1313
* import { modulo } from "@std/math/modulo";
1414
* import { assertEquals } from "@std/assert";
15-
* import { integerRange } from "@std/math/integer-range";
15+
* import { IntegerRange } from "@std/math/integer-range";
1616
*
1717
* // 5 o'clock is always 5 o'clock, no matter how many twelve-hour cycles you add or remove
18-
* for (const n of integerRange(-3, 3, { includeEnd: true })) {
18+
* for (const n of new IntegerRange(-3, 3, { includeEnd: true })) {
1919
* const val = n * 12 + 5
2020
* assertEquals(modulo(val, 12), 5);
2121
* }

math/modulo_test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22
import { modulo } from "./modulo.ts";
33
import { assert, assertEquals } from "@std/assert";
4-
import { integerRange } from "./integer_range.ts";
4+
import { IntegerRange } from "./integer_range.ts";
55

66
Deno.test("modulo()", async (t) => {
77
await t.step("basic functionality", async (t) => {
8-
for (const n of integerRange(-3, 3, { includeEnd: true })) {
8+
for (const n of new IntegerRange(-3, 3, { includeEnd: true })) {
99
const val = n * 12 + 5;
1010
await t.step(`modulo(${val}, 12) == 5`, () => {
1111
assertEquals(modulo(val, 12), 5);

0 commit comments

Comments
 (0)