smol-range v0.0.3
smol-range
smol-range contains a range utility function that aims to replicate Python's range function. This range function has a few interesting properties:
- Uses iterators (not arrays) for iteration, so no unnecessary memory usage.
- Uses
Proxyto add some custom behavior, such as handling theinkeyword (to test if a number is "in" the range) and adding functionality to look up range values by step (e.g.,range(2, 5)[1] === 3). - Supports negative steps and non-integer start/end/step values.
- Added
.forEachmethod likeArray.prototype.forEachfor easier iteration.
Installation and Basic Example
Install smol-range via yarn or NPM:
npm install smol-range # NPM
yarn add smol-range # Or YarnThen, import the range function and go to town!
import { range } from 'smol-range';
// Iterate over a range...
for (const x of range(12)) {
// Do something with x
}
// Or using .forEach
range(12).forEach(x => { /* ... */ });
// Or maybe even generate an array from a range...
const arr = Array.from(range(10, 20, 2)); // [10, 12, 14, 16, 18]
[...range(10, 20, 2)]; // [10, 12, 14, 16, 18]NOTE: if used in a TypeScript project, this package requires "down-level iteration" to be enabled by setting "downlevelIteration": true in your tsconfig.json.
API
range is the sole function exported from smol-range, but contains a few different signatures:
export function range(end: number): Range;
export function range(start: number, end: number): Range;
export function range(start: number, end: number, step: number): Range;The following sections discuss each signature.
Signature range(end: number): Range
The simplest usage of range is to provide a single argument, which acts as an ending value for a range that starts at 0. E.g., range(3) starts at 0, ends before 3, and increments in steps of 1.
range(3); // -> 0, 1, 2This is useful if you want to have a loop with a certain number of iterations:
// Execute block 3 times
for (const x of range(3)) {
// Do something...
}Signature range(start: number, end: number): Range
If you don't want your range to start at 0, you can provide both a starting and ending value. E.g., range(3, 7) starts at 3 and ends before 7 and still increments by 1:
range(3, 7); // -> 3, 4, 5, 6When providing a start and end value, it's intended for end > start since we're incrementing by a positive amount. However, negative values are fair game!
range(-2, 4); // -> -2, -1, 0, 1, 2, 3Signature range(start: number, end: number, step: number): Range
If you don't want your range to increment/step by 1, you can provide a third argument to indicate a step value! E.g., range(3, 9, 2) starts at 3, ends before 9, and increments by 2.
range(3, 9, 2); // -> 3, 5, 7It's worth noting that the step value can be negative if you want to increment "backwards", but in this case you'll need start > end because the iterator always moves from start to end. Here's an example:
range(5, 0, -1); // -> 5, 4, 3, 2, 1
range(-3, -12, -2); // -> -3, -5, -7, -9, -11Custom in Operator
Using Proxy API, this library adds custom behavior to the in operation for range outputs which allows you to test if a value is in a range. Simple example:
const myRange = range(2, 5); // -> 2, 3, 4
2 in myRange; // -> true
4 in myRange; // -> true
5 in myRange; // -> falseAny value that should be able to be iterated over by the range output should test true via the in operation.
It's worth noting that this library uses math to test this, and does not actually attempt to iterate over the range to find the value – ensuring O(1) efficiency.
Custom lookups
Using Proxy API, this library adds custom behavior for property lookups on the range output, so that you can get a specific range value without having to iterate over the range. For example:
const myRange = range(4, Infinity, 3);
myRange[0]; // -> 4
myRange[1]; // -> 7
myRange[272]; // -> 820
myRange[1369]; // -> 4111The lookup also supports negative values (when there's a finite end to the range):
const myRange = range(1, 9, 2); // -> 1, 3, 5, 7
myRange[-1]; // -> 7
myRange[-4]; // -> 1
myRange[-5]; // -> undefinedAgain, the library uses math to determine these values, once again ensuring O(1) efficiency.
Custom .forEach method
A generated Range has a .forEach method that allows you to easily iterate through a range, similar to Array.prototype.forEach.
type Range = {
// ...
forEach: (fn: (x: number, i: number) => void) => void;
}You pass in a callback that can (optionally) accept a value (current iteration value from the range) and an iteration count (what step in the iteration currently on). Here's an example:
range(2, 6).forEach(x => {
// x: 2, 3, 4, 6
});
range(2, 6).forEach((x, i) => {
console.log(`${i}th call, current value is ${x}`);
});Non-integer values
Non-integer values are fair game for start, end, and step values! E.g., there's nothing stopping you from doing something like range(1.2, 7.4, 1.7). However, the range function generates values $y$ via this basic formula:
$$y = \text{start} + \text{step} \cdot n$$
where $n$ varies from a starting value of $0$ to an ending value $N$ where:
$$\text{start} + \text{step} \cdot N \lt \text{end}$$
Now, if you've been working with JavaScript for long enough, you might smell a little bit of floating-point shenanigans lurking! This library does not try to mitigate floating-point errors, and therefore you might end up in some weird situations like the following:
3.3 in range(0, 11, 1.1); // -> false, because 3 * 1.1 === 3.3000000000000003 in JSThis is a hard problem to solve in this context, and to keep this library bloat-free, we do not try to solve this problem here.
Examples
Single-argument range generates from 0 to arg-1
for (const x of range(3)) {
console.log(x); // log: 0, 1, 2
}Use Array.from or Array spread ([...]) to generate an array, if need be.
Array.from(range(3)); // -> [0, 1, 2]
[...range(3)]; // -> [0, 1, 2]Double-argument range to provide start and end value
Will have a default step-value of 1.
[...range(2, 6)]; // -> [2, 3, 4, 5];
[...range(-4, -1)]; // -> [-4, -3, -2];Triple-argument range to provide start, end, and step values.
[...range(1, 9, 2)]; // -> [1, 3, 5, 7];
[...range(2, 5, 1.2)]; // -> [2, 3.2, 4.4];Use a negative step-value to traverse backwards:
[...range(10, 6, -1)]; // -> [10, 9, 8, 7];
[...range(24, 18, -2)]; // -> [24, 22, 20];Can also use Infinity as an upper bound, but watch out for infinite loop!
for (const x of range(Infinity)) {
console.log(x); // log: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
if (x >= 10) break;
}