Custom sorting in Ruby

Assume we have a Severity class which represents the level of severity which may arise in a system.

This class has a single attribute: a severity string. The levels of severities can range from "info, low, medium, high". If we try to sort a collection of severities in ascending order by its level, we get the following result:

1 ["info", "low", "high", "medium"]

Likewise for a sort in DESC order based on its levels:

1 ["medium", "low", "high", "info"]

The issue here is that the sort order does not represent the inherent meaning of the levels themselves. We want the "high" level to be at the front of the list when sorting in descending order and "low" to be at the front in ascending order. Ruby is sorting lexically and does not capture the inherent meaning of the levels.

We can develop a custom comparator class whereby we implement the sort order ourselves.

 1 class Severity
 2   attr_accessor :level
 3 
 4   def initialize(level)
 5     @level = level
 6   end
 7 end
 8 
 9 module SeverityComparator
10   LEVELS = ["info", "low", "medium", "high"]
11 
12   module_function
13   def compare(a, b)
14     LEVELS.index(a.level) <=> LEVELS.index(b.level)
15   end
16 end

The SeverityComparator module holds an array LABELS whereby we predefine the severity levels in the order we want.

Within the compare method, we compare the position of the object’s index within the LEVELS array based on its level attribute. For example:

1 arr = ["info", "low", "medium", "high"].each_with_object([]){|a, memo|
2   memo << Severity.new(a)
3 }
4 
5 arr.sort{|a,b| SeverityComparator.compare(a,b)}.map(&:level)
6 # => ["info", "low", "medium", "high"]
7 
8 arr.sort{|a,b| -SeverityComparator.compare(a,b)}.amp(&:level)
9 #=> ["high", "medium", "low", "info"]

Supposing that we also discover that the severity levels can also range from "1.0" to "5.0". The flexibility of this approach means that we can just add new levels to the LEVELS array and still use the same comparator:

 1 module SeverityComparator
 2   # adding new levels
 3   LEVELS = ["1.0", "2.0", "3.0", "4.0", "5.0", "info", "low", "medium", "high"]
 4 
 5   # .....
 6 end
 7 
 8 arr2 = ["1.0", "2.0", "3.0", "4.0", "5.0"].each_with_object([]){|a, memo|
 9   memo << Severity.new(a)
10 }
11 
12 arr2.sort{|a,b| SeverityComparator.compare(a,b)}.map(&:level)
13 # => ["1.0", "2.0", "3.0", "4.0", "5.0"]
14 
15 arr2.sort{|a,b| -SeverityComparator.compare(a,b)}.map(&:level)
16 # => ["5.0", "4.0", "3.0", "2.0", "1.0"]
17 
18 # sorting still works as before
19 arr.sort{|a,b| SeverityComparator.compare(a,b)}.map(&:level)
20 # => ["info", "low", "medium", "high"]

Keep Hacking!!!

Further References