Skip to content

Commit

Permalink
Remove range rounding hack.
Browse files Browse the repository at this point in the history
It may have been well-intentioned, but it is not a perfect solution, and the
simpler solution has well-defined behavior: start + i * step.
  • Loading branch information
mbostock committed Oct 26, 2015
1 parent 5909c42 commit d2364d4
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 21 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,24 @@ merge([[1], [2, 3]]); // returns [1, 2, 3]

<a name="range" href="#range">#</a> <b>range</b>([<i>start</i>, ]<i>stop</i>[, <i>step</i>])

Generates an array containing an arithmetic progression, similar to the Python built-in [range](http://docs.python.org/library/functions.html#range). This method is often used to iterate over a sequence of numeric or integer values, such as the indexes into an array. Unlike the Python version, the arguments are not required to be integers, though the results are more predictable if they are due to floating point precision. If the generated array is required to have a specific length, consider using [array.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) on an integer range. For example:
Returns an array containing an arithmetic progression, similar to the Python built-in [range](http://docs.python.org/library/functions.html#range). This method is often used to iterate over a sequence of regularly-spaced numeric values, such as the indexes of an array or the ticks of a linear scale.

If *step* is omitted, it defaults to 1. If *start* is omitted, it defaults to 0. The *stop* value is exclusive; it is not included in the result. If *step* is positive, the last element is the largest *start* + *i* \* *step* less than *stop*; if *step* is negative, the last element is the smallest *start* + *i* \* *step* greater than *stop*. If the returned array would contain an infinite number of values, an empty range is returned.

The arguments are not required to be integers; however, the results are more predictable if they are. The values in the returned array are defined as *start* + *i* \* *step*, where *i* is an integer from zero to one minus the total number of elements in the returned array. For example:

```js
range(0, 1, 1/49); // BAD: returns 50 elements!
range(49).map(function(d) { return d / 49; }); // GOOD: returns 49 elements.
range(0, 1, 0.2) // [0, 0.2, 0.4, 0.6000000000000001, 0.8]
```

If *step* is omitted, it defaults to 1. If *start* is omitted, it defaults to 0. The *stop* value is not included in the result. The full form returns an array of numbers [*start*, *start* + *step*, *start* + 2 \* *step*, …]. If *step* is positive, the last element is the largest *start* + *i* \* *step* less than *stop*; if *step* is negative, the last element is the smallest *start* + *i* \* *step* greater than *stop*. If the returned array would contain an infinite number of values, an empty range is returned.
This unexpected behavior is due to IEEE 754 double-precision floating point, which defines 0.2 * 3 = 0.6000000000000001. Use [d3-format](https://github.com/d3/d3-format) to format numbers for human consumption with appropriate rounding; see also [linear.tickFormat](https://github.com/d3/d3-scale#linear_tickFormat) in [d3-scale](https://github.com/d3/d3-scale).

Likewise, if the returned array should have a specific length, consider using [array.map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) on an integer range. For example:

```js
range(0, 1, 1 / 49); // BAD: returns 50 elements!
range(49).map(function(d) { return d / 49; }); // GOOD: returns 49 elements.
```

<a name="permute" href="#permute">#</a> <b>permute</b>(<i>array</i>, <i>indexes</i>)

Expand Down
11 changes: 1 addition & 10 deletions src/range.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,11 @@ export default function(start, stop, step) {

var i = -1,
n = Math.max(0, Math.ceil((stop - start) / step)) | 0,
k = scale(Math.abs(step)),
range = new Array(n);

start *= k;
step *= k;
while (++i < n) {
range[i] = (start + i * step) / k;
range[i] = start + i * step;
}

return range;
};

function scale(x) {
var k = 1;
while (x * k % 1) k *= 10;
return k;
}
18 changes: 11 additions & 7 deletions test/range-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,18 +118,22 @@ tape("range(start, stop, step) returns an empty array if step is zero", function
test.end();
});

tape("range(start, stop, step) handles fractional steps without rounding errors", function(test) {
test.deepEqual(arrays.range(0, 0.5, 0.1), [0, 0.1, 0.2, 0.3, 0.4]);
test.deepEqual(arrays.range(-2, -1.2, 0.1), [-2, -1.9, -1.8, -1.7, -1.6, -1.5, -1.4, -1.3]);
tape("range(start, stop, step) returns exactly [start + step * i, …] for fractional steps", function(test) {
test.deepEqual(arrays.range(0, 0.5, 0.1), [0 + 0.1 * 0, 0 + 0.1 * 1, 0 + 0.1 * 2, 0 + 0.1 * 3, 0 + 0.1 * 4]);
test.deepEqual(arrays.range(0.5, 0, -0.1), [0.5 - 0.1 * 0, 0.5 - 0.1 * 1, 0.5 - 0.1 * 2, 0.5 - 0.1 * 3, 0.5 - 0.1 * 4]);
test.deepEqual(arrays.range(-2, -1.2, 0.1), [-2 + 0.1 * 0, -2 + 0.1 * 1, -2 + 0.1 * 2, -2 + 0.1 * 3, -2 + 0.1 * 4, -2 + 0.1 * 5, -2 + 0.1 * 6, -2 + 0.1 * 7]);
test.deepEqual(arrays.range(-1.2, -2, -0.1), [-1.2 - 0.1 * 0, -1.2 - 0.1 * 1, -1.2 - 0.1 * 2, -1.2 - 0.1 * 3, -1.2 - 0.1 * 4, -1.2 - 0.1 * 5, -1.2 - 0.1 * 6, -1.2 - 0.1 * 7]);
test.end();
});

tape("range(start, stop, step) handles extremely small steps without rounding errors", function(test) {
test.deepEqual(arrays.range(2.1e-31, 5e-31, 1.1e-31), [2.1e-31, 3.2e-31, 4.3e-31]);
tape("range(start, stop, step) returns exactly [start + step * i, …] for very small fractional steps", function(test) {
test.deepEqual(arrays.range(2.1e-31, 5e-31, 1.1e-31), [2.1e-31 + 1.1e-31 * 0, 2.1e-31 + 1.1e-31 * 1, 2.1e-31 + 1.1e-31 * 2]);
test.deepEqual(arrays.range(5e-31, 2.1e-31, -1.1e-31), [5e-31 - 1.1e-31 * 0, 5e-31 - 1.1e-31 * 1, 5e-31 - 1.1e-31 * 2]);
test.end();
});

tape("range(start, stop, step) handles extremely large steps without rounding errors", function(test) {
test.deepEqual(arrays.range(1e300, 2e300, 0.3e300), [1e300, 1.3e300, 1.6e300, 1.9e300]);
tape("range(start, stop, step) returns exactly [start + step * i, …] for very large fractional steps", function(test) {
test.deepEqual(arrays.range(1e300, 2e300, 0.3e300), [1e300 + 0.3e300 * 0, 1e300 + 0.3e300 * 1, 1e300 + 0.3e300 * 2, 1e300 + 0.3e300 * 3]);
test.deepEqual(arrays.range(2e300, 1e300, -0.3e300), [2e300 - 0.3e300 * 0, 2e300 - 0.3e300 * 1, 2e300 - 0.3e300 * 2, 2e300 - 0.3e300 * 3]);
test.end();
});

0 comments on commit d2364d4

Please sign in to comment.