# Why Number.range instead of a new object like Range?

**URL:** <https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979>\
**Category:** I have questions\
**Created:** [September 8, 2021, 5:32pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979 "2021-09-08T17:32:43Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [September 8, 2021, 5:32pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/1 "2021-09-08T17:32:43Z")

</div>

I've read through the [proposal](https://tc39.es/proposal-Number.range/), the [notes from the April TC39 meeting](https://github.com/tc39/notes/blob/33dd18f94b976536f8d6228afe87f7f2117f5bc0/meetings/2020-03/april-1.md#numberrange-and-bigintrange-for-stage-1), and the [core-js polyfill](https://github.com/zloirock/core-js/#numberrange) and I have several questions:

1. Why split constructors for BigInt and Number ranges? (Number.range / BigInt.range)  
a. Is there a reason that Range(\<number|bigint\>, \<number|bigint\>) isn't a good idea as long as the types match?  
b. None of the other methods on the Number object return new types. They either return a number (e.g. parseInt) or a primitive (e.g. isFinite, toLocaleString)

2. Why return an iterator instead of an iterable?  
a. Unexpectedly stateful objects like iterators have been a footgun in the past. For instance, RegExp objects with the global flag maintain state and the rule of thumb is to create them as needed instead of reusing them (e.g. `if (/foo/g.test(str))` instead of `const FOO = /foo/g; /* ...later */ if (FOO.test(str))`.)  
b. It would be simpler to call `Range(1, 5).toIterator()` than to construct an iterable from an iterator.

3. The proposal explicitly calls out `if (x in Range(0, 30))` as a non-goal, but this seems like a common use-case of ranges in other languages.

4. Why return `undefined` for bad inputs rather than an empty iterable?  
a. `for (let i of Range(0, 5, 10) { /*...*/ }` throws if the bad range returns undefined, but iterates zero times if an empty iterable is returned.

5. Why store `start`, `end`, `step`, and `inclusiveEnd` on the range? No other iterator/iterable stores metadata like this.

---

<div class="post-metadata">

**Author:** ![ljharb](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/ljharb/32/8_2.png) [@ljharb](https://es.discourse.group/u/ljharb)\
**Post date:** [September 8, 2021, 5:48pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/2 "2021-09-08T17:48:43Z")

</div>

1b. “A number or a primitive” is already “a number or not a number”; whether “not a number” includes only primitives or not doesn’t seem relevant to me.

1. There’s no such thing as an iterable by itself - “iterable” is a protocol, that a number of things implement. All built-in iterators also are iterable, so the proposal as-is does return an iterable - just, an iterable iterator, versus an iterable something else.

2. It seems like not throwing here would be most likely to be hiding a bug - the cases where returning an empty iterator is a convenience seem like they’d be an extreme minority.

3. I agree; it seems kind of strange to attach all this metadata publicly to the iterator.

---

<div class="post-metadata">

**Author:** ![aclaymore](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/aclaymore/32/501_2.png) [@aclaymore](https://es.discourse.group/u/aclaymore)\
**Post date:** [September 8, 2021, 6:03pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/3 "2021-09-08T18:03:07Z")

</div>

`if (x in Number.range(0, 10)` would require range being a proxy and subverting the `Reflect.has` trap to have a different semantic meaning than checking the existence of a property. Which is likely why it’s under the “‘magic” section.

Maybe there is a way of providing a range check api as a method instead of with syntax

---

<div class="post-metadata">

**Author:** ![tabatkins](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/tabatkins/32/170_2.png) [@tabatkins](https://es.discourse.group/u/tabatkins)\
**Post date:** [September 8, 2021, 6:05pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/4 "2021-09-08T18:05:03Z")

</div>

1a) We expect that we'll have more types of ranges in the future, for things like dates/times, strings, etc. These may or may not be distinguishable purely from argument types, and they might require or allow additional metadata beyond what numeric ranges do, so stacking them all in one Range object didn't seem great.

2b) If you need a reusable Range, we already have a way to do that - `()=>Range(1,5)`. Each invocation will produce an iterator.

1. The "in" operator already has a meaning on objects, including Range objects - whether or not the object contains a particular key. We can't overload that with a notion of range-containment without causing ambiguity.

Also, unfortunately, the notion of what's in a range is fraught for Number ranges, at least, if the step is not an integer or dyadic fraction - you have to explicitly perform the iteration to figure out precisely what floats are in it. For example, is `0.3` in `Number.Range(0, 1, .1)`? Depends! `0 + .1 + .1 + .1` certainly does not quite equal `.3`.

You might be thinking instead of _intervals_, which have an easy and well-defined notion of what's inside of them, based on just comparing the value to the start/end and taking inclusive/exclusive into account. They still wouldn't be able to override the `in` operator, but they could at least have a well-behaved `.contains()` method.

---

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [September 8, 2021, 6:33pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/5 "2021-09-08T18:33:00Z")

</div>

I wasn't thinking of an interval for this question, but checking if a number is a member of the range (set), like  
`2 in Number.range(0, 10, 2) === true`  
and  
`3 in Number.range(0, 10, 2) === false`

I hadn't considered the non-dyadic fraction cases, but that is clearly an issue with using the `in` operator. How does the current proposal handle iterating over `Number.range(0, 1, 0.1)`? Will it give `0`, `0.1`, `0.2`, `0.30000000000000004`?

---

<div class="post-metadata">

**Author:** ![tabatkins](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/tabatkins/32/170_2.png) [@tabatkins](https://es.discourse.group/u/tabatkins)\
**Post date:** [September 8, 2021, 6:53pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/6 "2021-09-08T18:53:49Z")

</div>

As it happens, no, it'll give you the expected `.3` answer. The range values are explicitly specified as `start + n*step` (rather than an accumulating sum) precisely to minimize the chance of floating-point value drift. (And to avoid issues with precision loss as your value gets large, so you don't infinite-loop accidentally.)

But that does still mean that you can't reliably tell whether a value is in the range or not without iterating it. BigInt.Range() doesn't have this problem, tho.

---

<div class="post-metadata">

**Author:** ![aclaymore](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/aclaymore/32/501_2.png) [@aclaymore](https://es.discourse.group/u/aclaymore)\
**Post date:** [September 8, 2021, 9:54pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/7 "2021-09-08T21:54:12Z")

</div>

> [@tabatkins](#):
>
> As it happens, no, it'll give you the expected `.3` answer.

Though `0.3` is `0.299999999999999988897769753748434595763683319091796875` and `0.1 * 3 === 0.3` is false.

---

<div class="post-metadata">

**Author:** ![tabatkins](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/tabatkins/32/170_2.png) [@tabatkins](https://es.discourse.group/u/tabatkins)\
**Post date:** [September 8, 2021, 10:21pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/8 "2021-09-08T22:21:06Z")

</div>

Dang, I should have actually tested that, huh.

Well, just illustrates the problem beautifully. `Number.Range(0, 1, .1).contains(.3)`, which one would absolutely reasonably expect to return `true`, actually returns `false`.

---

<div class="post-metadata">

**Author:** ![lightmare](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/lightmare/32/843_2.png) [@lightmare](https://es.discourse.group/u/lightmare)\
**Post date:** [September 8, 2021, 10:50pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/9 "2021-09-08T22:50:18Z")

</div>

> [@tabatkins](#):
>
> `Number.Range(0, 1, .1).contains(.3)`, which one would absolutely reasonably expect to return `true`, actually returns `false`.

Well, reasonably... based on incorrectly assuming exact, or decimal floating point arithmetic. JS Number is specified in binary, hence it cannot represent `.1` or `.3` exactly. It is not reasonable to assume `.1±rounding * 3` === `.3±rounding`

Anyway, a range iterator with inexact floating-point step should not be encouraged, it's a terrible misfeature that nobody wants. Use `linspace` instead.

---

<div class="post-metadata">

**Author:** ![claudiameadows](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/claudiameadows/32/126_2.png) [@claudiameadows](https://es.discourse.group/u/claudiameadows)\
**Post date:** [September 9, 2021, 7:07pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/10 "2021-09-09T19:07:17Z")

</div>

I did some quick playing around in a Node REPL, and here's what I've found: `0.1 + 0.2` and `0.1 * 3` both evaluate to `0.3 + Number.EPSILON/3`.

So it _might_ be possible to figure out a way to get it correctly rounded, but it'll take a lot more work. (For one, `1.1 * 14` evaluates to `1.1*14 - Number.EPSILON*5`.)

---

<div class="post-metadata">

**Author:** ![claudiameadows](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/claudiameadows/32/126_2.png) [@claudiameadows](https://es.discourse.group/u/claudiameadows)\
**Post date:** [September 9, 2021, 7:08pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/11 "2021-09-09T19:08:14Z")

</div>

Floating-point range iterators are useful provided they're close enough. It's just not a common use case.

---

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [September 9, 2021, 7:20pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/12 "2021-09-09T19:20:36Z")

</div>

I've been thinking about the "`in`" operator in regards to ranges, and I realized that an object with both keys and values as the numbers of the range would work for this. I know it's not a good polyfill (for that we'd definitely need the `Proxy` for `has`) but I still think the following syntaxes would be convenient:

- `for (let i in range)`
- `for (let i of range)`
- `if (i in range)`

```javascript
for (let i of Number.range(10, -Infinity, -1)) {
  announceCountdownNumber(i);
 }

for (let i in Number.range(10, -Infinity, -1)) {
  /* I can never remember if I should use 'in' or 'of' */
  announceCountdownNumber(i);
}

// announcing countdown
function announceCountdownNumber(x) {
  if (x in Number.range(10, 0, {inclusive: true, step: -1}) {
    console.log(x);
  } else {
    console.log('T+', Math.abs(x))
  }
}

```

## _crappy polyfill that allows all these syntaxes:_

```javascript
function kindaRange(start, end, step) {
  // doesn't validate args or provide defaults or allow infinite ranges
  var result = {};

  // allows for..in and "x in range" syntaxes
  for (let i = start; i < end; i += step) {
    result[i] = i;
  }

  // allows for..of syntax
  result[Symbol.iterator] = function*() {
    yield* Object.keys(result);
  };

  return result;
}

```

---

<div class="post-metadata">

**Author:** ![ljharb](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/ljharb/32/8_2.png) [@ljharb](https://es.discourse.group/u/ljharb)\
**Post date:** [September 9, 2021, 7:31pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/13 "2021-09-09T19:31:37Z")

</div>

What happens when i have a range from `-2 **53` to `2** 53`?

---

<div class="post-metadata">

**Author:** ![lightmare](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/lightmare/32/843_2.png) [@lightmare](https://es.discourse.group/u/lightmare)\
**Post date:** [September 9, 2021, 9:02pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/14 "2021-09-09T21:02:53Z")

</div>

> [@claudiameadows](#):
>
> So it _might_ be possible to figure out a way to get it correctly rounded, but it'll take a lot more work. (For one, `1.1 * 14` evaluates to `1.1*14 - Number.EPSILON*5`.)

How do you define "correctly rounded"? When the range constructor receives (start, stop, step), how can you tell whether I wrote `Range(0, 20, 1.1)` or `Range(0, 20, 1.1000000000000001)` or `Range(0, 20, 2476979795053773 / 2251799813685248)`? The value of `step` is the same in all cases, because the first two have infinite binary representation, and get rounded to the third. You cannot know whether I expect to hit `15.4` after 14 steps, if you round to `15.4` it's equally likely you're "correct" as it is you're "incorrect".

Really, the only time you would want a non-integer step is when you know it can be represented exactly, such as 0.125 for example. In most of cases, though, you want `Range(start, end, numSteps)`.

---

<div class="post-metadata">

**Author:** ![NickGard](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/nickgard/32/419_2.png) [@NickGard](https://es.discourse.group/u/NickGard)\
**Post date:** [September 10, 2021, 2:55am UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/15 "2021-09-10T02:55:31Z")

</div>

What does the current proposal/polyfill do for 2^53? I know it doesn't address "`in`" syntax or behavior, but if the iterator gets to (or past) the maximum safe integer, what should the user expect to happen? Wouldn't the same behavior apply to the "`in`" syntax? (I assume that the behavior is as wonky as trying to do math in that range of numbers.)

---

<div class="post-metadata">

**Author:** ![ljharb](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/ljharb/32/8_2.png) [@ljharb](https://es.discourse.group/u/ljharb)\
**Post date:** [September 10, 2021, 3:48am UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/16 "2021-09-10T03:48:25Z")

</div>

The range i specified includes only safe integers - i was only using it to illustrate that you’d have to have an object with 2\*\*54 properties on it for `in` to work properly.

---

<div class="post-metadata">

**Author:** ![claudiameadows](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/claudiameadows/32/126_2.png) [@claudiameadows](https://es.discourse.group/u/claudiameadows)\
**Post date:** [September 10, 2021, 8:15pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/17 "2021-09-10T20:15:00Z")

</div>

It's the standard definition used within floating point math contexts: "correctly rounded" = within 0.5 ulps of the result (ulps being a technical term). In this case, it'd mean returning 0.3 for step 4 of `Range(0, 1, 0.1)

Those familiar with floating point math implementation would know what I'm talking about here.

---

<div class="post-metadata">

**Author:** ![lightmare](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/lightmare/32/843_2.png) [@lightmare](https://es.discourse.group/u/lightmare)\
**Post date:** [September 10, 2021, 9:03pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/18 "2021-09-10T21:03:08Z")

</div>

> [@claudiameadows](#):
>
> It's the standard definition used within floating point math contexts: "correctly rounded" = within 0.5 ulps of the result (ulps being a technical term). In this case, it'd mean returning 0.3 for step 4 of `Range(0, 1, 0.1)

What's the point of doing some shenanigans to arrive at `0.3`, though? Simple `3*0.1` gives `0.30000000000000004` which is correctly rounded, too.

```javascript
>>> Fraction(0.3) - 3 * Fraction(0.1)
Fraction(-1, 36028797018963968)
// -0.5 ulp

>>> Fraction(0.30000000000000004) - 3 * Fraction(0.1)
Fraction(1, 36028797018963968)
// +0.5 ulp

```

---

<div class="post-metadata">

**Author:** ![claudiameadows](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/claudiameadows/32/126_2.png) [@claudiameadows](https://es.discourse.group/u/claudiameadows)\
**Post date:** [September 10, 2021, 9:06pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/19 "2021-09-10T21:06:59Z")

</div>

It doesn't return the same number. Run `0.1 * 3 === 0.3` in a REPL.

> [@claudiameadows](#):
>
> I did some quick playing around in a Node REPL, and here's what I've found: `0.1 + 0.2` and `0.1 * 3` both evaluate to `0.3 + Number.EPSILON/3`.

---

<div class="post-metadata">

**Author:** ![lightmare](https://yyz2.discourse-cdn.com/free1/user_avatar/es.discourse.group/lightmare/32/843_2.png) [@lightmare](https://es.discourse.group/u/lightmare)\
**Post date:** [September 10, 2021, 9:17pm UTC](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979/20 "2021-09-10T21:17:24Z")

</div>

> [@claudiameadows](#):
>
> Run `0.1 * 3 === 0.3` in a REPL.

Obviously, I already wrote that `3*0.1` gives `0.30000000000000004`. Both `float64(0.3)` and `float64(0.30000000000000004)` are good approximations of `3*float64(0.1)`.

[Next page](https://es.discourse.group/t/why-number-range-instead-of-a-new-object-like-range/979.md?page=2)
