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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Severity
  attr_accessor :level

  def initialize(level)
    @level = level
  end
end

module SeverityComparator
  LEVELS = ["info", "low", "medium", "high"]

  module_function
  def compare(a, b)
    LEVELS.index(a.level) <=> LEVELS.index(b.level)
  end
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
2
3
4
5
6
7
8
9
arr = ["info", "low", "medium", "high"].each_with_object([]){|a, memo|
  memo << Severity.new(a)
}

arr.sort{|a,b| SeverityComparator.compare(a,b)}.map(&:level)
# => ["info", "low", "medium", "high"]

arr.sort{|a,b| -SeverityComparator.compare(a,b)}.amp(&:level)
#=> ["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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module SeverityComparator
  # adding new levels
  LEVELS = ["1.0", "2.0", "3.0", "4.0", "5.0", "info", "low", "medium", "high"]

  # .....
end

arr2 = ["1.0", "2.0", "3.0", "4.0", "5.0"].each_with_object([]){|a, memo|
  memo << Severity.new(a)
}

arr2.sort{|a,b| SeverityComparator.compare(a,b)}.map(&:level)
# => ["1.0", "2.0", "3.0", "4.0", "5.0"]

arr2.sort{|a,b| -SeverityComparator.compare(a,b)}.map(&:level)
# => ["5.0", "4.0", "3.0", "2.0", "1.0"]

# sorting still works as before
arr.sort{|a,b| SeverityComparator.compare(a,b)}.map(&:level)
# => ["info", "low", "medium", "high"]

Keep Hacking!!!

Further References