Two dots include the end, three dots exclude it¶
Level: 101 · a Python programmer who knows range and slices
One line: 1..5 is five numbers and 1...5 is four; a Range can be endless (1..), beginless (..5), made of Strings ("a".."e") or of Floats; and cover? asks "between the ends?" where include? asks "one of the elements?" — Python's range is always half-open, always integers, and a sequence you can index.
A Range is an object with a begin, an end and an exclude_end? flag — it is not a list of its elements, and it stays a Range until something asks it to iterate. That is why 1.. can exist (there is no end to store), why to_a on it raises, and why (1..10**12).sum returns at once: for Integer ends, sum uses the arithmetic-series formula instead of looping (Not machine-checked here.). The same object serves four jobs: slicing (a[1..2], s[..3]), case/when (whose === is cover?), iteration (each, step, map), and membership. Membership has two spellings that agree on numbers and disagree on Strings: cover? compares against the two ends with <=>, so ("a".."z").cover?("bb") is true because "a" <= "bb" <= "z"; include? walks the elements for a String range, so ("a".."z").include?("bb") is false — but for a numeric range include? is cover?, which is how (1..10).include?(5.5) is true. A Range is frozen, cannot be indexed, and compares by its ends rather than its elements.
Verified output of ranges_two_dots_and_three_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. (1..5).to_a, (1...5).to_a [[1, 2, 3, 4, 5], [1, 2, 3, 4]]
2. (1..5).size, (1...5).size, (1..5).class [5, 4, Range]
3. ("a".."e").to_a, ("a".."e").to_a.join [["a", "b", "c", "d", "e"], "abcde"]
4. (1..).first(3), (1..).end (endless) [[1, 2, 3], nil]
5. (..5).include?(3), (..5).cover?(9) (beginless) [true, false]
6. (1..).to_a RangeError: cannot convert endless range to an array
7. (1..10).step(3).to_a, ((1..10) % 3).to_a [[1, 4, 7, 10], [1, 4, 7, 10]]
8. (1.0..2.0).step(0.5).to_a (a Float range) [1.0, 1.5, 2.0]
9. ("a".."z"): include?("bb"), cover?("bb"), === "bb" [false, true, true]
10. (1..10).include?(5.5), (1..10).to_a.include?(5.5) [true, false]
11. case 42 when 1..10 / when 11..100 / else "medium"
12. a[1..2], a[1...-1], a[..1], a[2..], "hello"[1..3] [[20, 30], [20, 30, 40], [10, 20], [30, 40, 50], "ell"]
13. (1..100).sum, (1..10**12).sum (a formula, not a loop) [5050, 500000000000500000000000]
14. (1...5).last, (1...5).last(1), (1...5).max [5, [4], 4]
15. (5..1).to_a, 5.downto(1).to_a [[], [5, 4, 3, 2, 1]]
16. (1..5) == (1...6), (1..5).to_a == (1...6).to_a [false, true]
17. (1..5)[1] (a Range is not indexable) NoMethodError: undefined method '[]' for an instance of Range
18. 7.clamp(1..5), (1..5).cover?(2..3), (1..5).frozen? [5, true, true]
Reading the output¶
Rows 1–3: .. keeps the end and ... drops it, so the sizes are 5 and 4; a String range runs from "a" to "e" — it steps with succ (Not machine-checked here.).
Rows 4–6: an endless range has end nil and can still answer first(3); a beginless one answers include? and cover? by comparing against its one end; asking the endless one for to_a is a RangeError.
Rows 7–8: step(3) and its alias % produce every third element (an Enumerator::ArithmeticSequence), and a Float range steps by 0.5.
Rows 9–10 are the two membership questions. On "a".."z", cover?("bb") and === "bb" are true and include?("bb") is false; on 1..10, include?(5.5) is true because a numeric range answers by comparison, while the same question of the array (1..10).to_a is false.
Rows 11–13: when 11..100 matches 42 through ===; ranges slice arrays and strings, with ..1 and 2.. for the open ends; the two sums come from the formula — the second, (1..10**12).sum, would not return in any reasonable time as a loop.
Row 14 is a small trap: (1...5).last is 5, the stored end, even though the end is excluded — last(1) and max iterate and give 4.
Rows 15–18: 5..1 is empty rather than descending (downto counts down); 1..5 and 1...6 are different Ranges with the same elements; (1..5)[1] is a NoMethodError; clamp takes a range, cover? accepts a whole range, and every Range is frozen.
Compared with Python¶
The twin asks range, itertools.count and slices the same eighteen questions. range(1, 6) is the only spelling — half-open, integer-only (range(1.0, 2.0) is a TypeError, row 8), with the step as a third argument (row 7). There are no character ranges (row 3 builds the letters from code points or takes them from string.ascii_lowercase), no endless range (itertools.count is the endless iterator, row 4, and list(count(1)) would never return, so row 6 does not run it (Not machine-checked here.)), and no beginless range (row 5 is a comparison). Membership in a range is integer membership: 5.5 is not in range(1, 11) but 5.0 is (row 10), and the two-spellings question of row 9 becomes in on a list against a chained comparison. match has no range pattern, so row 11 uses guards. sum(range(...)) iterates, so the twin prints the formula for 10**12 instead of waiting (row 13). Where Ruby's Range refuses indexing, Python's range is a full sequence: it has [-1], slices to another range (rows 14 and 17), and compares as a sequence, so range(0) == range(1, 1) is true (row 16) where (1..5) == (1...6) was false. clamp is min(max(x, lo), hi) (row 18).
Verified output of ranges_two_dots_and_three_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. list(range(1, 6)), list(range(1, 5)) (half-open only) [[1, 2, 3, 4, 5], [1, 2, 3, 4]]
2. len(range(1, 6)), len(range(1, 5)), type(range(1, 6)) [5, 4, range]
3. [chr(c) for c in range(97, 102)], ascii_lowercase[:5] [['a', 'b', 'c', 'd', 'e'], 'abcde']
4. list(islice(count(1), 3)); count(1) has no end [[1, 2, 3], None]
5. 3 <= 5, 9 <= 5 (no beginless range: a comparison) [True, False]
6. list(count(1)) (not run: it would never return) -
7. list(range(1, 11, 3)) (the step is the third argument) [1, 4, 7, 10]
8. range(1.0, 2.0); [1 + i * 0.5 for i in range(3)] TypeError, [1.0, 1.5, 2.0]
9. "bb" in letters (a list), "a" <= "bb" <= "z" [False, True]
10. 5.5 in range(1, 11), 5.0 in range(1, 11) [False, True]
11. match 42: case n if n in range(1, 11) / range(11, 101) 'medium'
12. a[1:3], a[1:-1], a[:2], a[2:], "hello"[1:4] [[20, 30], [20, 30, 40], [10, 20], [30, 40, 50], 'ell']
13. sum(range(1, 101)); n * (n + 1) // 2 for n = 10**12 [5050, 500000000000500000000000]
14. range(1, 5)[-1], range(1, 5)[-1:], max(range(1, 5)) [4, range(4, 5), 4]
15. list(range(5, 1)), list(range(5, 0, -1)) [[], [5, 4, 3, 2, 1]]
16. range(1, 6) == range(1, 6, 1), range(0) == range(1, 1) [True, True]
17. range(1, 6)[1], range(1, 6)[1:3] (range is a sequence) [2, range(2, 4)]
18. min(max(7, 1), 5), set(range(2, 4)) <= set(range(1, 6)) [5, True]
The Python library's Slicing is not indexing ↗ is the page on s[:n], the slice that never raises.
Try it¶
- Write
to_ranges(ints)that compresses a sorted array of integers into inclusive ranges —[1, 2, 3, 5, 7, 8, 9]becomes[1..3, 5..5, 7..9]— andexpand(ranges)that turns them back, then check the round trip on a few inputs including[]and a single element.slice_whenis the shortcut. - Write
page_ranges(total, per_page)that returns[1..10, 11..20, 21..25]for(25, 10), twice: once from1.step(total, per_page)and once from(1..total).each_slice(per_page). Then explain why the last range must end attotaland not at21 + per_page - 1.
Solution to 1
ranges_two_dots_and_three_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: compress a sorted list of integers into inclusive ranges, then expand
# the ranges back and check the round trip.
def to_ranges(ints)
ints.slice_when { |a, b| b != a + 1 }.map { |run| run.first..run.last }
end
def expand(ranges) = ranges.flat_map(&:to_a)
[[1, 2, 3, 5, 7, 8, 9], [4], [], [1, 3, 5], [10, 11, 12, 13]].each do |ints|
ranges = to_ranges(ints)
verdict = expand(ranges) == ints ? "round trip ok" : "ROUND TRIP FAILED"
printf("%-24s -> %-28s %s\n", ints.inspect, ranges.inspect, verdict)
end
Verified output of ranges_two_dots_and_three_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- Past the end, an array gives nil, not an error —
a[1..2]againsta[1, 2], and what a slice past the end returns. - Define
<=>and Comparable gives you the rest —cover?,clampandbetween?all run on<=>. case/whenuses===— whywhen 11..100matches 42.- Lazy enumerators —
(1..).lazyis where an endless range becomes useful. - Writing an iterator — an infinite
Enumeratoris the other way to say "no end". - Integers are unbounded —
(1..10**12).sumis an Integer with no size limit. succcounts in letters ↗ — the Ruby text library on thesuccthat String ranges step with.- Slicing is not indexing ↗ — the Python library on the slice contract.
- Twenty-three Python traps for a Ruby programmer — The twenty-three places where a Ruby reflex gives the wrong answer in Python