Skip to content

Commit 3106d1c

Browse files
fix(Array): range fn based on the iterator range proposal
1 parent 9348a92 commit 3106d1c

File tree

8 files changed

+273
-49
lines changed

8 files changed

+273
-49
lines changed

.changeset/beige-turkeys-repeat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opentf/utils": minor
3+
---
4+
5+
Fixed range fn based on the iterator range proposal.

apps/docs/pages/Array/range.mdx

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,73 @@
1+
import { Callout } from "nextra/components";
12
import REPL from "../../components/REPL";
23

3-
> It generates a sequence of numbers starting at the first argument, progressing by the third argument, and stopping at the second argument.
4+
> Generates a sequence of numbers starting at the `first` argument, progressing by the `step` argument, and stopping at the `second` argument.
5+
6+
<Callout emoji="" type="info">
7+
It is based on the [Range proposal](https://tc39.es/proposal-iterator.range/) & [Iterator.range](https://github.com/tc39/proposal-iterator.range).
8+
</Callout>
49

510
## Syntax
611

712
```ts
8-
range(start: number, stop: number, step?: number): number[];
13+
range(
14+
start: number,
15+
end: number,
16+
options?: number | { step: number; inclusive: false }
17+
): number[]
918
```
19+
20+
<Callout type="info">
21+
The `step` value can be auto infered based on the `start` & `end` values.
22+
</Callout>
23+
24+
<Callout type="info">
25+
The `third` argument can be either a `step` value or `options` object.
26+
</Callout>
27+
1028
## Usage
1129

1230
```ts
1331
import { range } from '@opentf/utils';
1432

15-
range();
33+
range(start, stop);
1634
```
1735

1836
## Examples
1937

2038
```ts
21-
range() //=> []
22-
23-
range(0) //=> []
39+
range() // It throws error
2440

25-
range(1) //=> []
41+
range(0) // It throws error
2642

27-
range(1, 2) //=> [1, 2]
43+
range(NaN, 0) // It throws error
2844

29-
range(10, 50, 10) //=> [10, 20, 30, 40, 50]
45+
range(0, 0) //=> []
3046

31-
range(10, 12, 0.5) //=> [10, 10.5, 11, 11.5, 12]
47+
range(0, 0, { inclusive: true }) //=> [0]
3248

33-
range(-10, -5) //=> [-10, -9, -8, -7, -6, -5]
49+
range(0, 5) //=> [0, 1, 2, 3, 4]
3450

35-
range(0, -4, -1) //=> [0, -1, -2, -3, -4]
51+
range(0, 5, { inclusive: true }) //=> [0, 1, 2, 3, 4, 5]
3652

37-
range(-4) //=> []
53+
range(3, 9, { step: 3 }) //=> [3, 6]
3854

39-
range(1, 2, 0) //=> []
55+
range(0, -5) //=> [0, -1, -2, -3, -4]
4056

41-
range(-1, -2, 1) //=> []
57+
range(0, -4, { step: -1 }) //=> [0, -1, -2, -3]
4258

43-
range(-1, -2, -1) //=> [-1, -2]
44-
45-
range(1, 2, 1) //=> [1, 2]
46-
47-
range(0, 5) //=> [0, 1, 2, 3, 4, 5]
48-
49-
range(3, 9, 3) //=> [3, 6, 9]
59+
range(-10, -5) //=> [-10, -9, -8, -7, -6]
5060

61+
range(10, 12, { step: 0.5 }) //=> [10, 10.5, 11, 11.5]
5162
```
5263

5364
## Try
5465

5566
<REPL code={`const { range } = require('@opentf/utils');
5667
57-
range(1, 5);`} />
68+
log(range(1, 5));
69+
70+
for (const item of range(1, 5, {inclusive: true})) {
71+
log(item);
72+
}
73+
`} />
Lines changed: 100 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,104 @@
11
import { range } from '../../src';
22

3-
describe('Array', () => {
4-
test('range', () => {
5-
expect(range()).toEqual([]);
6-
expect(range(0)).toEqual([]);
7-
expect(range(1)).toEqual([]);
8-
expect(range(1, 2)).toEqual([1, 2]);
9-
expect(range(10, 50, 10)).toEqual([10, 20, 30, 40, 50]);
10-
expect(range(10, 12, 0.5)).toEqual([10, 10.5, 11, 11.5, 12]);
11-
expect(range(-10, -5)).toEqual([-10, -9, -8, -7, -6, -5]);
12-
expect(range(0, -4, -1)).toEqual([0, -1, -2, -3, -4]);
13-
expect(range(-4)).toEqual([]);
14-
expect(range(1, 2, 0)).toEqual([]);
15-
expect(range(-1, -2, 1)).toEqual([]);
16-
expect(range(-1, -2, -1)).toEqual([-1, -2]);
17-
expect(range(1, 2, 1)).toEqual([1, 2]);
18-
expect(range(0, 5)).toEqual([0, 1, 2, 3, 4, 5]);
19-
expect(range(3, 9, 3)).toEqual([3, 6, 9]);
3+
describe('Array > range', () => {
4+
test('Error handling: Type Mismatch', () => {
5+
const sharedMatrix = [
6+
[],
7+
[0],
8+
[0n],
9+
[0, 1, function () {}],
10+
[0n, 1n, function () {}],
11+
[0, function () {}, 2],
12+
[function () {}, 2, 2],
13+
[0n, 1],
14+
[0n, 1, 1],
15+
[0n, 1, { step: 1 }],
16+
[0, 1n],
17+
[0, 1n, 1],
18+
[0, 1n, { step: 1 }],
19+
[0, 1, 1n],
20+
[0, 1, { step: 1n }],
21+
];
22+
for (const each of sharedMatrix) {
23+
expect(() => range(...each)).toThrowError();
24+
}
25+
});
26+
27+
test('empty', () => {
28+
expect(range(0, 0)).toEqual([]);
29+
expect(range(0, -5, 1)).toEqual([]);
30+
});
31+
32+
test('positive integers', () => {
33+
expect(range(0, 5)).toEqual([0, 1, 2, 3, 4]);
34+
expect(range(1, 2)).toEqual([1]);
35+
expect(range(1, 2, { step: 1 })).toEqual([1]);
36+
expect(range(10, 50, { step: 10 })).toEqual([10, 20, 30, 40]);
37+
expect(range(10, 50, 10)).toEqual([10, 20, 30, 40]);
38+
expect(range(3, 9, { step: 3 })).toEqual([3, 6]);
39+
});
40+
41+
test('negative integers', () => {
42+
expect(range(0, -5)).toEqual([0, -1, -2, -3, -4]);
43+
expect(range(0, -4, { step: -1 })).toEqual([0, -1, -2, -3]);
44+
expect(range(-1, 5)).toEqual([-1, 0, 1, 2, 3, 4]);
45+
expect(range(0, -5, { step: -1 })).toEqual([0, -1, -2, -3, -4]);
46+
expect(range(-1, -2)).toEqual([-1]);
47+
expect(range(-1, -2, { step: -1 })).toEqual([-1]);
48+
expect(range(-10, -5)).toEqual([-10, -9, -8, -7, -6]);
49+
});
50+
51+
test('decimal', () => {
52+
expect(range(10, 12, { step: 0.5 })).toEqual([10, 10.5, 11, 11.5]);
53+
expect(range(0, 1, { step: 0.1 })).toEqual([
54+
0, 0.1, 0.2, 0.30000000000000004, 0.4, 0.5, 0.6000000000000001,
55+
0.7000000000000001, 0.8, 0.9,
56+
]);
57+
});
58+
59+
test('inclusive true', () => {
60+
expect(range(0, 5, { inclusive: true })).toEqual([0, 1, 2, 3, 4, 5]);
61+
expect(range(-1, -2, { step: -1, inclusive: true })).toEqual([-1, -2]);
62+
});
63+
64+
test('Use with for of loop', () => {
65+
const res = [];
66+
for (const item of range(1, 5)) {
67+
res.push(item);
68+
}
69+
expect(res).toEqual([1, 2, 3, 4]);
70+
});
71+
72+
test('NaN', () => {
73+
expect(() => range(NaN, 0)).toThrowError();
74+
expect(() => range(0, NaN)).toThrowError();
75+
expect(() => range(NaN, NaN)).toThrowError();
76+
77+
expect(() => range(0, 0, { step: NaN })).toThrowError();
78+
expect(() => range(0, 5, NaN)).toThrowError();
79+
});
80+
81+
test('Step infer', () => {
82+
expect(range(0, -2)).toEqual([0, -1]);
83+
expect(range(0, -2, { inclusive: true })).toEqual([0, -1, -2]);
84+
});
85+
86+
test('Error: Zero as step', () => {
87+
expect(() => range(0, 10, 0)).toThrowError();
88+
expect(() => range(0, 10, { step: 0 })).toThrowError();
89+
expect(() => range(0n, 10n, 0n)).toThrowError();
90+
expect(() => range(0n, 10n, { step: 0n })).toThrowError();
91+
});
92+
93+
test('Error: Infinity as start / step', () => {
94+
expect(() => range(Infinity, 10, 0)).toThrowError();
95+
expect(() => range(-Infinity, 10, 0)).toThrowError();
96+
expect(() => range(0, 10, Infinity)).toThrowError();
97+
expect(() => range(0, 10, { step: Infinity })).toThrowError();
98+
});
99+
100+
test('Inclusive on same start-end', () => {
101+
expect(range(0, 0, { inclusive: false })).toEqual([]);
102+
expect(range(0, 0, { inclusive: true })).toEqual([0]);
20103
});
21104
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { isInfinity } from '../../src';
2+
3+
describe('Types > isInfinity', () => {
4+
test('invalid cases', () => {
5+
expect(isInfinity()).toBe(false);
6+
expect(isInfinity('')).toBe(false);
7+
expect(isInfinity(0)).toBe(false);
8+
expect(isInfinity(false)).toBe(false);
9+
expect(isInfinity(null)).toBe(false);
10+
expect(isInfinity(NaN)).toBe(false);
11+
expect(isInfinity('Infinity')).toBe(false);
12+
expect(isInfinity('1/0')).toBe(false);
13+
});
14+
15+
test('valid cases', () => {
16+
expect(isInfinity(Infinity)).toBe(true);
17+
expect(isInfinity(Infinity)).toBe(true);
18+
expect(isInfinity(-Infinity)).toBe(true);
19+
});
20+
});

packages/utils/src/array/range.ts

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,99 @@
1+
import isInfinity from '../types/isInfinity';
2+
import isNum from '../types/isNum';
3+
14
/**
25
* It generates a sequence of numbers starting at the first argument,
36
* progressing by the third argument, and stopping at the second argument.
47
*
58
* @example
69
*
7-
* range(0, 5) // [0, 1, 2, 3, 4, 5]
8-
* range(10, 50, 10) // [10, 20, 30, 40, 50]
10+
* range(0, 5) // [0, 1, 2, 3, 4]
911
*/
12+
export default function range(
13+
start: number,
14+
end: number,
15+
options?: number | { step: number; inclusive: boolean }
16+
) {
17+
if (Number.isNaN(start) || Number.isNaN(end)) {
18+
throw new RangeError();
19+
}
20+
21+
if (!isNum(start) || !isNum(end)) {
22+
throw new TypeError();
23+
}
24+
25+
if (isInfinity(start)) {
26+
throw RangeError();
27+
}
28+
29+
const ifIncrease = end > start;
30+
31+
let inclusiveEnd = false;
32+
let step;
33+
34+
if (options === undefined || options === null) {
35+
step = undefined;
36+
} else if (typeof options === 'object') {
37+
step = options.step;
38+
inclusiveEnd = Boolean(options.inclusive);
39+
} else if (isNum(options)) {
40+
step = options;
41+
} else {
42+
throw new TypeError();
43+
}
44+
45+
if (Number.isNaN(step)) {
46+
throw new RangeError();
47+
}
48+
49+
if (step === undefined || step === null) {
50+
step = ifIncrease ? 1 : -1;
51+
}
52+
53+
if (typeof step !== 'number') {
54+
throw new TypeError();
55+
}
56+
57+
if (isInfinity(step)) {
58+
throw RangeError();
59+
}
60+
61+
if (step === 0 && start !== end) {
62+
throw new RangeError();
63+
}
64+
65+
const arr: number[] = [];
66+
const ifStepIncrease = step > 0;
67+
68+
if (ifIncrease !== ifStepIncrease) {
69+
return arr;
70+
}
71+
72+
let hitsEnd = false;
73+
let curCount = 0;
74+
75+
while (hitsEnd === false) {
76+
const val = start + step * curCount;
77+
78+
if (val === end) {
79+
hitsEnd = true;
80+
}
81+
82+
curCount = curCount + 1;
83+
let endCondition = false;
84+
85+
if (ifIncrease) {
86+
endCondition = inclusiveEnd ? val > end : val >= end;
87+
} else {
88+
endCondition = inclusiveEnd ? end > val : end >= val;
89+
}
90+
91+
if (endCondition) {
92+
return arr;
93+
}
94+
95+
arr.push(val);
96+
}
1097

11-
export default function range(start = 0, stop: number, step = 1): number[] {
12-
if (step === 0) return [];
13-
return Array.from(
14-
{ length: (stop - start) / step + 1 },
15-
(_, i) => start + i * step
16-
);
98+
return arr;
1799
}

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export { default as isBlob } from './types/isBlob';
7373
export { default as isPromise } from './types/isPromise';
7474
export { default as isPureObj } from './types/isPureObj';
7575
export { default as isDef } from './types/isDef';
76+
export { default as isInfinity } from './types/isInfinity';
7677

7778
// Object
7879
export { default as has } from './object/has';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Checks if the given value is Infinity.
3+
*
4+
* @example
5+
*
6+
* isInfinity(0) //=> false
7+
*
8+
* isInfinity(NaN) //=> false
9+
*
10+
* isInfinity(Infinity) //=> true
11+
*/
12+
export default function isInfinity(x: unknown) {
13+
if (typeof x !== 'number') return false;
14+
if (Number.isNaN(x)) return false;
15+
if (Number.isFinite(x)) return false;
16+
17+
return true;
18+
}

packages/utils/src/types/isNum.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
* isNum(Infinity) //=> false
1515
*
1616
*/
17-
1817
export default function isNum(val: unknown, coerce = false): val is number {
1918
let n = val;
2019
if (coerce && typeof val === 'string' && val.trim().length) {

0 commit comments

Comments
 (0)