class Range

Practical examples and pitfalls for Range

Range examples: express boundaries without building a list

Practical notes by Ruby-Doc.org

Use an exclusive end for adjacent windows

You want to use a display to show items in windows of three. If you write the windows as half-open intervals, then there is no ambiguity about the separation between adjacent windows. The excluded endpoint of one window is the starting position of the next. Furthermore, this format corresponds to how most offsets and lengths of intervals are explained.

Example 1
items = %w[A B C D E F G]
p items[0...3]
p items[3...6]
p items[6...9]
Expected output
["A", "B", "C"]
["D", "E", "F"]
["G"]

Check the last window separately. In some cases, the number of items contained in the collection may be less than the number of items that you have asked for in your window, or you may already be past the last item by the time that you start. A range defines limits; how those limits should be interpreted is determined by the method that receives the range. Therefore, both an array slice and a membership test require separate expected results.

Ask a boundary question directly

If you know that the input may exist somewhere between two bounds (and thus can be expressed as a range), you can express that relationship without creating an array using cover?. This is especially helpful if either the size of the range is very large or the values within the range do not represent a sequence that would benefit from being enumerated.

Example 2
letters = "a".."d"
p letters.cover?("bb")
p letters.include?("bb")
p (10...20).cover?(20)
Expected output
true
false
false

Coverage and inclusion differ for strings. There is another difference to keep in mind when working with strings versus other types of data. While coverage relates to boundaries, inclusion involves whether each string exists in the given sequence. The example uses a string that compares between the endpoint strings but is not a member of the sequence generated from those endpoints. Choose the correct operator based on your specific question rather than simply choosing between methods whose names seem similar.

Limit the traversal before consuming it

While an infinite range can serve as a convenient source for a lazy stream, it is not something that waits to be converted into a finite collection with to_a. Determine exactly how much output you desire before retrieving all of the items from your range.

When dealing with boundary conditions for applications, test for an exact match at both the initial position and final position of your traversal as well as the closest position that was explicitly excluded. Document whether each endpoint is included or excluded beside the code that defines the boundary. A comment explaining the last acceptable position gives a future maintainer a reason for the two-dot or three-dot choice.

API reference: Range API reference

Related: Array · Enumerable

A Range object represents a collection of values that are between given begin and end values.

You can create an Range object explicitly with:

Beginless Ranges

A beginless range has a definite end value, but a nil begin value. Such a range includes all values up to the end value.

r = (..4)               # => nil..4
r.begin                 # => nil
r.include?(-50)         # => true
r.include?(4)           # => true

r = (...4)              # => nil...4
r.include?(4)           # => false

Range.new(nil, 4)       # => nil..4
Range.new(nil, 4, true) # => nil...4

A beginless range may be used to slice an array:

a = [1, 2, 3, 4]
# Include the third array element in the slice
r = (..2)  # => nil..2
a[r]       # => [1, 2, 3]
# Exclude the third array element from the slice
r = (...2) # => nil...2
a[r]       # => [1, 2]

Method each for a beginless range raises an exception.

Endless Ranges

An endless range has a definite begin value, but a nil end value. Such a range includes all values from the begin value.

r = (1..)         # => 1..
r.end             # => nil
r.include?(50)    # => true

Range.new(1, nil) # => 1..

The literal for an endless range may be written with either two dots or three. The range has the same elements, either way. But note that the two are not equal:

r0 = (1..)           # => 1..
r1 = (1...)          # => 1...
r0.begin == r1.begin # => true
r0.end == r1.end     # => true
r0 == r1             # => false

An endless range may be used to slice an array:

a = [1, 2, 3, 4]
r = (2..) # => 2..
a[r]      # => [3, 4]

Method each for an endless range calls the given block indefinitely:

a = []
r = (1..)
r.each do |i|
  a.push(i) if i.even?
  break if i > 10
end
a # => [2, 4, 6, 8, 10]

A range can be both beginless and endless. For literal beginless, endless ranges, at least the beginning or end of the range must be given as an explicit nil value. It is recommended to use an explicit nil beginning and end, since that is what Ruby uses for Range#inspect:

(nil..)    # => (nil..nil)
(..nil)    # => (nil..nil)
(nil..nil) # => (nil..nil)

Ranges and Other Classes

An object may be put into a range if its class implements instance method <=>. Ruby core classes that do so include Array, Complex, File::Stat, Float, Integer, Kernel, Module, Numeric, Rational, String, Symbol, and Time.

Example:

t0 = Time.now         # => 2021-09-19 09:22:48.4854986 -0500
t1 = Time.now         # => 2021-09-19 09:22:56.0365079 -0500
t2 = Time.now         # => 2021-09-19 09:23:08.5263283 -0500
(t0..t2).include?(t1) # => true
(t0..t1).include?(t2) # => false

A range can be iterated over only if its elements implement instance method succ. Ruby core classes that do so include Integer, String, and Symbol (but not the other classes mentioned above).

Iterator methods include:

Example:

a = []
(1..4).each {|i| a.push(i) }
a # => [1, 2, 3, 4]

Ranges and User-Defined Classes

A user-defined class that is to be used in a range must implement instance method <=>; see Integer#<=>. To make iteration available, it must also implement instance method succ; see Integer#succ.

The class below implements both <=> and succ, and so can be used both to construct ranges and to iterate over them. Note that the Comparable module is included so the == method is defined in terms of <=>.

# Represent a string of 'X' characters.
class Xs
  include Comparable
  attr_accessor :length
  def initialize(n)
    @length = n
  end
  def succ
    Xs.new(@length + 1)
  end
  def <=>(other)
    @length <=> other.length
  end
  def to_s
    sprintf "%2d #{inspect}", @length
  end
  def inspect
    'X' * @length
  end
end

r = Xs.new(3)..Xs.new(6) #=> XXX..XXXXXX
r.to_a                   #=> [XXX, XXXX, XXXXX, XXXXXX]
r.include?(Xs.new(5))    #=> true
r.include?(Xs.new(7))    #=> false

What’s Here

First, what’s elsewhere. Class Range:

Here, class Range provides methods that are useful for:

Methods for Creating a Range

Methods for Querying

Methods for Comparing

Methods for Iterating

Methods for Converting

Methods for Working with JSON

To make these methods available:

require 'json/add/range'