Ranges

# kdex (4 months ago)

A few months ago, there was a thread on ESDiscuss [1] where somebody asked for native Range support, i.e. numeric ranges, string ranges, etc.

It seems like it didn't receive much attention, so I'd like to bring this up for discussion again.

If you were to iterate from X to Y, you would usually use a for loop that uses a loop variable i, which you would declare with let. You can't use const, since you'll have to increment it, and you don't want to use var, because it makes no sense to make a reference to i outside of the loop, unless you were to re-use the loop variable for further loops.

In other words, you're forced to choose a scope where other scopes barely make any sense. Having a for-loop that abstracts over this needless explicitness could help make the concept of iteration more declarative.

Python has range() (but no for loops that create ranges) [2], and Scala introduced two for-loop syntaxes that will create ranges for you:

for (i <- 1 to 10) { … } // 10 inclusive
for (i <- 1 until 10) { … } // 10 exclusive

There's a plethora of npm packages that implement ranges, the most popular one apparently being fill-range with 7.4 million downloads a month, so it seems that the community would appreciate native support a lot.

I've searched through some proposals, but couldn't find anyone currently writing a spec for it. Is there general interest for ranges at TC39? It struck me as odd that the only thing I found about them in ES was [1].

[1] esdiscuss.org/topic/feature-request-add-range-type-data [2] docs.python.org/3/library/functions.html#func-range [3] www.npmjs.com/package/fill

# Hikaru Nakashima (4 months ago)

How about this

for ( i of Array.range(1, 10) ) { ... }
// OR
for ( i of [1..10] )  { ... }
# Isiah Meadows (4 months ago)

I'll note, just for clarity, that Scala's 1 to 10 is technically just a normal method call equivalent to (1).to(10), with optional parentheses removed.

Also, I'd prefer this to be a generator instead, so infinite ranges are also possible, and so it doesn't have to be eager.

# kdex (4 months ago)

Agreed. There's no reason why Array.range or [1..10] couldn't just return a generator or at least something that extends a generator, though. I wonder if it's viable to implement something akin to .length on ranges, which could be natural numbers or Infinity.

As for numbers, I don't see any issues. One issue that came up in the original thread was that string ranges may need a better definition, as ["A".."C"] might not necessarily transpile to be a generator that yields "A", "B" and "C".

# Isiah Meadows (4 months ago)

If string ranges are based on character codes, it will (for the Latin alphabet, at least, not necessarily for other languages).

I would prefer a function over syntax, though, since it would be more easily adopted (polyfill > syntax), and it would fit more idiomatically

with the rest of the language (which also uses functions for most utilities).

Maybe a Number.range would work?

Number.range = function *range(start, end=undefined, step=1) {
  if (end === undefined) [start, end] = [0, start];
  if (end === undefined) end = Infinity;
  for (let i = 0; i < end; i += step) {
    yield i;
  }
};
# Michael J. Ryan (4 months ago)

If there's a Number.range, if suggest a corresponding String.range for character ranges... Agreed on it being a utility function over me syntax.

# Viktor Kronvall (4 months ago)

For String.range what would the expected result of String.range('A','zzz') be?

Is an exhaustive pattern expected?

['A','B','C',...'Z','a','b','c',...,'z','AA','AB',...] 2016年11月3日(木) 19:21 Michael J. Ryan <tracker1 at gmail.com>:

# kdex (4 months ago)

About the code points: String.range should also handle surrogate pairs, similar to for..of does it. About String.range("A", "zzz"): Do any other possibilities even make sense?

# Viktor Kronvall (4 months ago)

Actually, after giving it some more thought for that case there is just that one possibility that makes sense.

However, there are more ambiguous cases such as String.range("AAA", "ZZZ") (should all letters increase at once or should the rightmost letter be incremented first)

Also, how would range handle the arguments in inverted order? Should there be a decreasing range or should it terminate with no elements in the iterator? 2016年11月3日(木) 21:05 kdex <kdex at kdex.de>:

# Viktor Kronvall (4 months ago)

Even more interestingly what would String.range("","zzz") produce. From what code point is the range started? Will this throw? Is the empty string included in the iterator? 2016年11月3日(木) 21:18 Viktor Kronvall <viktor.kronvall at gmail.com>:

# Isiah Meadows (4 months ago)

Could we just not have a String.range?

  1. Needing a list of alphabetically sorted characters is a bit niche, even in the Latin alphabet.
  2. It's ambiguous whether it should include symbols or not. (think: String.range("A", "z"))
  3. If it's Unicode-aware, how should it act with, say, String.range("A", "π")?
# kdex (4 months ago)

I don't think we should give the Latin alphabet special treatment. This is solely about Unicode codepoints.

I'd expect…

- `String.range("AAA", "ZZZ")` to generate ["AAA", "AAB", …, "ABA", … "ZZZ"].
- `String.range("", "zzz")` to either throw or be equivalent to `String.range("\u0000", "zzz")`.
- `String.range("z", "A")` to either throw or wrap around at "\u{ffffff}" and therefore include
	all codepoints.
- `String.range("A", "z")` to include symbols, too. If you only need letters in latin-1, you should
	either write a custom latin-1 generator or skip these values with a RegExp when iterating
	over them. Or just use two ranges, one for A-Z and one for a-z.
- `String.range("A", "π")` to include all characters from "\u0065" (A) to "\u03c0" (π).
- `String.range("😀", "😂")` to generate ["😀", "😬", "😁", "😂"] (surrogate pairs being respected)

For more complicated cases, it might make more sense to make users pass a function to String.range for fine-grained control, or maybe just make users create their own generators.

# Zach Lym (4 months ago)

With regard to syntax, Rust's is the best that I've seen: '0 ... 10' inclusive and '1 .. 9' exclusive.

for ( i of [1..10] ) { ... }

Why do we need to clutter the syntax with brackets?

Strings are something that's needed, I've been tinkering with a LINQ syntax for IndexedDB and a Range type would make it easier to take advantage of indexing.

However, there is not right answer here: is "10A" > "001B"? What if I'm

storing DNS labels, where mail.example.com < *.example.com?

I think there should be a sane default (perhaps with syntactic sugar to specify region encoding and Unicode normalization) but just people to create extend a Range object so they can build their own semantics.

Thank you, -Zach Lym

# Alexander Jones (4 months ago)

+1000

Classic feature creep with basically zero application.

# Michael J. Ryan (4 months ago)

Your right... The more I think about it...

Number.range(from=0, to=Number. MAX_SAFE_INTEGER)

Is probably best, as a signature, then using .map(...) If you want a string representation...

For that matter, wouldn't mind seeing more of the array utility methods, like map, added to strings, iterating through each character, same as .split('').map, but that's another discussion.

On Nov 4, 2016 3:11 AM, "Alexander Jones" <alex at weej.com> wrote:

+1000

Classic feature creep with basically zero application.

# Jordan Harband (4 months ago)

Here you go:

  1. function* range(start, end) { for (const i = +start; i < end; ++i) { yield i; } }
  2. function range(start, end) { return Array.from({ length: start - end }, (_, i) => start + i); }
# Mathias Bynens (4 months ago)

On Fri, Nov 4, 2016 at 6:24 PM, Jordan Harband <ljharb at gmail.com> wrote:

Here you go:

  1. function* range(start, end) { for (const i = +start; i < end; ++i) { yield i; } }

For future reference: ++i throws when i is a const binding. The intended example uses let instead.

# Andrea Giammarchi (4 months ago)

FWIW: got bitten from same thing too ... the fact for(const x of ...) works would always mislead me to think a for(const i; ... i++) would too, even if it's completely different meaning/issue/matter.

Oh well

# Jordan Harband (4 months ago)

lol thanks, good call. too much habit typing "const" :-p s/const/let there

# Andy Earnshaw (3 months ago)

On Thu, 3 Nov 2016 at 22:11 Zach Lym <zachlym at indolering.com> wrote:

With regard to syntax, Rust's is the best that I've seen: '0 ... 10' inclusive and '1 .. 9' exclusive.

I love Rust's syntax too, but unfortunately 1..<something> is already valid

ES syntax:

1..Infinity
//-> undefined

Number.prototype.foo = 'bar'
1..foo
//-> 'bar'

You'd have to enforce the spaces around the operator, which would be weird IMO. ... would be ok though.

# Hikaru Nakashima (2 months ago)

My idia is as follows:

[1..5]  //-> [1,2,3,4,5]

(1..5)   //-> iterate 1, 2, 3, 4, 5


[1..Infinity]  // -> TypeError because n > 2**32-1

(1..Infinity)  // -> valid iterator
# Tab Atkins Jr. (2 months ago)

On Tue, Dec 13, 2016 at 3:07 AM, Hikaru Nakashima <oao.hikaru.oao at gmail.com> wrote:

My idia is as follows:

[1..5]  //-> [1,2,3,4,5]

(1..5)   //-> iterate 1, 2, 3, 4, 5


[1..Infinity]  // -> TypeError because n > 2**32-1

(1..Infinity)  // -> valid iterator

As Andy just explained in the previous message, that doesn't work. In particular, [1..end] is equivalent to [(1).end], which is a perfectly valid expression that creates a length-1 array containing the value of the "end" property from a Number wrapper auto-constructed around 1. (Which happens to be undefined, unless you do shenanigans.)

# Hikaru Nakashima (2 months ago)

Oh, I understood it. It looks like serious problem, but it is may not actually. If this spec change doesn't break web, we can introduce this idea?

# Andy Earnshaw (2 months ago)

I think you'd be lucky to even get to that stage. Vendors aren't keen on any kind of backwards incompatibility in new specs and trying to get this to stage 4 with such a glaring one would be practically impossible.

It's not just the incompatibility either. You also introduce an inconsistencies where things like [1..toFixed(2)] doesn't mean the same as [ 1..toFixed(2) ]. That kind of thing is just confusing to developers.

When you consider these things, it becomes clear that it's not practical to change the language this way for such a small benefit.

# Hikaru Nakashima (2 months ago)

I understand. I hope to find a good form of literals.

Is there a fact that literals are easier to optimize in the following cases?

for (let i of [1 to 5]) { ...... }
vs
for (let i of Array.range(1, 5)) { ...... }

If so, it seems that we can attract vendors' interests.

2016-12-14 17:29 GMT+09:00 Andy Earnshaw <andyearnshaw at gmail.com>:

# Jeremy Martin (2 months ago)

While slightly more verbose, the previously suggested ... syntax does have a superficial consistency with the spread operator. Both perform an expansion of sorts, which has a subtle elegance to it, IMO.

# Alexander Jones (2 months ago)

IMO this is quite unnecessary syntax sugar. Python has everything you could need here without special syntax.