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

I think you missed my point - I was going for exact === precision, which is a bit different than "almost exact".

There is no such thing as exact 0.1 or exact 0.3 in binary float.

0.1 is rounded to S := 3602879701896397 / 36028797018963968
0.3 is rounded to A := 5404319552844595 / 18014398509481984
0.30000000000000004 is rounded to B := 1351079888211149 / 4503599627370496

For the 4th step of Range(0, 1, 0.1):
R := 3 * S == 10808639105689191 / 36028797018963968.

R needs one more bit than would fit into float64, and lies halfway between A and B, thus both are equally correct roundings of R.

In other words, 0.3 is not better than 0.30000000000000004, they are both half ulp off, so why would you spend extra effort meddling with it?

Just to be clear, I'm just offering solutions to @tabatkins' complaints. I personally disagree about it even being important - you shouldn't be using floats like that anyways.

1 Like

Guess I'm a little late to comment here😅. From your question: Why Number.range instead of a new object like Range?
The Range object already exists as part of the DOM api. It's used to represent dom nodes. So the name is already taken😉

1 Like

I'm a bit late here, but I'd like to explain why I made all those decisions.

cc @NickGard

  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)
  1. It might be possible to have a String.range or Temporal.range in the future. Mix all of them in a Range will cause confusion when there is a type mismatch.
  2. Mixing number and bigint is somehow convenient for cases like Number.range(0, 10n). I think it should work. I have opened an issue in the proposal repo. Mixing Number and Bigint · Issue #49 · tc39/proposal-iterator.range · GitHub
  3. Unfortunately, the Range is a global API on the Web. There was also an issue for this: Consider first-class type (i.e. 'NumberRange' or 'Interval') vs. a 'range' method · Issue #22 · tc39/proposal-iterator.range · GitHub
  4. About 1.b, I don't think it's a strong conversion in the past API designs so I guess it's ok to do that.
  1. Why return an iterator instead of an iterable?

Yes, this is a serious problem that is discussed at https://github.com/tc39/proposal-Number.range/issues/17. I want to convert to iterable but many others don't agree, I think we are stuck here.

There is some research of this by @yulia on this topic.

  1. 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.

To support this operation, we need to overwrite [[has]], this make the range object no longer a normal object but a extoic object.

Furthermore, in the JavaScript history we don't have this pattern, for example, "a" in "ab" results in TypeError.

So I don't think it should be included.

And as @tabatkins said, it's really subtle to decide which semantics to use. Should 5 be in the range of -10 to 10 with step 10? (And more problem with floating-point numbers).

  1. Why return undefined for bad inputs rather than an empty iterable?

Actually, we throw TypeError for bad input. There is a playground you can try it https://tc39.es/proposal-Number.range/playground.html.

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

This is designed for the future. For example, imagine we add a contains() method in the future, with those metadata, polyfill authors can simply add this contains() to the prototype.

If we don't expose these, they will need to overwrite the range method itself to collect the initial parameters and re-implement the normalization steps to the parameters in the specification.

What does the current proposal/polyfill do for 2^53?

Good question, I don't know :joy:


Reply to @tabatkins:

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

Yeah, but as @NickGard said,

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)) .)

I agree with this argument. It might be surprising.

2 Likes