Sorting and comparison

The spaceship operator

Ruby compares with <=>, which answers -1, 0 or 1. Everything that sorts uses it, and a class that defines it can be sorted, compared and put in a range.

sort calls it on pairs; sort_by runs the block once per item and sorts the keys, which is faster and easier to read when the key is not the value itself:

ruby -e 'p ["b", "A", "c"].sort_by(&:downcase)'

["A", "b", "c"]

Sort and sort_by

A plain sort on strings puts capitals before lowercase, because it compares character codes. A sort_by(&:downcase) gives the order a reader expects and keeps the original casing in the result.

ruby -e 'p ["b", "A", "c"].sort; p ["b", "A", "c"].sort_by(&:downcase)'

["A", "b", "c"]
["A", "b", "c"]

Here both lines agree; with an accented or a longer list they do not. Sorting by a key from a hash is the common case: rows.sort_by { |row| row[:name].downcase }.

Sorting text and numbers

A number and its text form sort differently. A list of strings that look like numbers sorts as text, which puts ten before two:

ruby -e 'p [10, 9, 2].sort; p %w[10 9 2].sort'

[2, 9, 10]
["10", "2", "9"]

This is not a Ruby quirk; any language that sorts strings does the same. Convert before sorting when the values are numbers: list.sort_by(&:to_i).

Comparable on your own class

Include Comparable and define <=>, and the class gains <, >, between? and clamp:

Box = Struct.new(:size) do include Comparable def <=>(other) size <=> other.size end end puts [Box.new(2), Box.new(1)].min.size

ruby -e 'Box = Struct.new(:size) { include Comparable; def <=>(o) = size <=> o.size }; puts [Box.new(2), Box.new(1)].min.size'

1

The comparison delegates to the value that decides the order, which keeps the rule in one place. A class with several attributes compares an array of them: [size, name] <=> [other.size, other.name].

What to watch for

sort is stable in Ruby only as a side effect of the algorithm, not as a promise. When ties must keep their input order, include the position in the key: sort_by.with_index { |row, i| [row[:v], i] }.

sort without a block raises ArgumentError as soon as two items cannot be compared, for example a string and a number in one list. The message names the pair, which is usually enough to find the bad row.

Next: Sets and set operations

Article author: Arthur Isaev