The objects of the class Range
implement sequential intervals of values.
Ever wondered how to build ranges out of custom class objects?
Yes, this is possible and it can be achieved by following a few simple steps.
Let's imagine that we need to have the ability to create ranges of this sort (Identifier.new(3)..Identifier.new(10))
.
Our class Identifier
creates strings like this one: "65-A"
where the integers at the start are the ASCII index for the character shown at the end of the string.
1
2
3
4
5
6
7
8
9
10
11
class Identifier
attr_reader :index
def initialize(index)
@index = index
end
def to_s
"#{index}-#{index.chr}"
end
end
The three steps are to:
Comparable
module<=>
operator.succ
1
2
3
4
5
6
7
8
9
10
11
class Identifier
include Comparable
def <=>(other_identifier)
# TODO
end
def succ
# TODO
end
end
Here is a brief description of what happens here:
Comparable
moduleThe module Comparable
adds the instance methods: <
, >
, <=
, >=
, ==
, between?
, clamp
.
1
Comparable.instance_methods # => [:<, :>, :<=, :>=, :==, :between?, :clamp]
The <=>
three-way comparison operator, (a.k.a. the spaceship operator) gives us the ability to compare two instances of the same class in the sense of <
, >
, <=
, >=
, ==
operators.
The expression a <=> b
will output either -1
, 0
, or 1
depending which of the elements has a lesser value, compared to the other.
The method Comparable#clamp
relies on the <=>
operator.
succ
methodThe succ
method evaluates the "next" instance from the sequence. Think of it as an increment operator.
For example: 1.succ
, will evaluate to 2
in the context of integers.
There are some data types such as Float
, where there is no way to determine the "next" element due to the nature of the elements.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Identifier
include Comparable
attr_reader :index
def initialize(index)
@index = index
end
def <=>(other_identifier)
index <=> other_identifier.index
end
def succ
Identifier.new(index.succ)
end
def to_s
"#{index}-#{index.chr}"
end
end
Now we can do fancy stuff, for example:
1
2
3
4
5
(Identifier.new(65)..Identifier.new(70)).to_a
(Identifier.new(65)..Identifier.new(70)).member?(Identifier.new(69)) # => true
Identifier.new(65) > Identifier.new(64) # => true
Identifier.new(65) > Identifier.new(66) # => false
This is what happens when we invoke to_s
on every instance from the (Identifier.new(65)..Identifier.new(90))
range:
1
2
(Identifier.new(65)..Identifier.new(90)).to_a.map(&:to_s)
# => ["65-A", "66-B", "67-C", "68-D", "69-E", "70-F", "71-G", "72-H", "73-I", "74-J", "75-K", "76-L", "77-M", "78-N", "79-O", "80-P", "81-Q", "82-R", "83-S", "84-T", "85-U", "86-V", "87-W", "88-X", "89-Y", "90-Z"]
Mixing the Comparable
module and defining the succ
method and the <=>
operator gives us the ability to create ranges out of custom class instances.