smol-range
v0.0.3
Published
Small range utility function, like Python's but with some sugar on top.
Downloads
2
Maintainers
Keywords
Readme
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
Proxy
to add some custom behavior, such as handling thein
keyword (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
.forEach
method likeArray.prototype.forEach
for easier iteration.
Installation and Basic Example
Install smol-range
via yarn or NPM:
npm install smol-range # NPM
yarn add smol-range # Or Yarn
Then, 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, 2
This 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, 6
When 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, 3
Signature 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, 7
It'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, -11
Custom 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; // -> false
Any 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]; // -> 4111
The 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]; // -> undefined
Again, 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 JS
This 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;
}