Even with nearly a decade of experience writing Ruby, I still get surprised by the language’s versatility.
I recently stumbled upon a problem that required me to work with (annual) quarters. Mostly, comparing them and iterating over them.
Achieving that was a breeze thanks to Ruby’s Comparable and Enumerator modules.
Here is the simplified Quarter class we’ll work with for this example:
class Quarter
attr_reader :quarter, :year
# @param quarter [Integer] The quarter number (1-4).
# @param year [Integer] The year.
def initialize(quarter = nil, year = nil)
@quarter = quarter.to_i
@year = year.to_i
end
end
Comparable is a module you can include in your classes to provide comparison methods (<, <=, ==, >=, >, between?, clamp).
According to the documentation:
The Comparable mixin is used by classes whose objects may be ordered. The class must define the <=> operator, which compares the receiver against another object
https://ruby-doc.org/3.3.3/Comparable.html
Let’s implement <=> and include the Comparable module in our Quarter class:
class Quarter
include Comparable
attr_reader :quarter, :year
def initialize(quarter = nil, year = nil)
@quarter = quarter.to_i
@year = year.to_i
end
def <=>(other)
[year, quarter] <=> [other.year, other.quarter]
end
end
That’s all! Now, we can compare quarters:
>> q1 = Quarter.new(2024, 1)
>> q2 = Quarter.new(2024, 2)
>> q3 = Quarter.new(2024, 3)
>> q4 = Quarter.new(2024, 4)
>> q1 < q2
#> true
>> q2.between?(q1, q3)
#> true
>> q4.clamp(q1, q3)
#> q3
https://ruby-doc.org/3.3.3/Enumerable.html
Let’s say we want to iterate over quarters without initializing them one by one.
We can do this by implementing #succ, which returns the quarter following the current one. Or in other words: quarter + 1.
class Quarter
# ...
# Adds a specified number of quarters to the Quarter object.
# This method calculates the new quarter and year after adding the specified count of quarters.
# It handles year transition when the addition spans over multiple years.
#
# @param count [Integer] The number of quarters to add to the current Quarter object.
# @return [Quarter] A new Quarter object representing the time after adding the specified quarters.
def +(count)
total_quarters = quarter + count - 1
Quarter.new(year + total_quarters / 4, total_quarters % 4 + 1)
end
# Advances the Quarter object to the next quarter.
def succ
self + 1
end
end
Now, we can use a range of quarters:
>> first = Quarter.new(2024, 1)
>> last = Quarter.new(2024, 4)
>> range = (first..last)
>> range.find{ |q| q.quarter == 4 }
#> #<Quarter:0x000000011d137c98 @quarter=4, @year=2024>
>> range.group_by(&:year)
#> {
# 2024=>
# [#<Quarter:0x000000011d4d5030 @quarter=1, @year=2024>, #<Quarter:0x000000011d4fc9f0 @quarter=2, @year=2024>, ... ]
# 2025=>
# [#<Quarter:0x000000011d4fc888 @quarter=1, @year=2025>, #<Quarter:0x000000011d4fc7e8 @quarter=2, @year=2025>, ...]
# }
So, it turns out that Ruby’s Comparable and Enumerable modules are powerful tools that can help you build expressive and readable code with minimal effort.
Check out their documentation:
For reference, here is the whole Quarter class made compatible with Comparable and Enumerable:
class Quarter
include Comparable
attr_reader :quarter, :year
def initialize(year = nil, quarter = nil)
@quarter = quarter.to_i
@year = year.to_i
end
def <=>(other)
[@year, @quarter] <=> [other.year, other.quarter]
end
def +(count)
total_quarters = @quarter + count - 1
Quarter.new(@year + total_quarters / 4, total_quarters % 4 + 1)
end
def succ
self + 1
end
end
Please reach out if you have comments or questions: