Build Ranges out of custom class instances
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.
Ranges of custom objects example
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.
class Identifier
attr_reader :index
def initialize(index)
@index = index
end
def to_s
"#{index}-#{index.chr}"
end
endThe three steps are to:
- include the
Comparablemodule - define the
<=>operator. - define the method
succ
class Identifier
include Comparable
def <=>(other_identifier)
# TODO
end
def succ
# TODO
end
endHere is a brief description of what happens here:
The Comparable module
The module Comparable adds the instance methods: <, >, <=, >=, ==, between?, clamp.
Comparable.instance_methods # => [:<, :>, :<=, :>=, :==, :between?, :clamp]The three-way comparison operator
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.
The succ method
The 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.
Example
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
endNow we can do fancy stuff, for example:
(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) # => falseThis is what happens when we invoke to_s on every instance from the (Identifier.new(65)..Identifier.new(90)) range:
(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"]Conclusion
Mixing the Comparable module and defining the succ and <=> gives us the ability to create ranges out of custom class instances.