Search code examples
rubysortingoverridingmixins

Is it possible to override a built-in Ruby method outside of a class or module?


I was researching how to fiddle with special sorting mechanisms in Ruby. I ended up rewriting this neat JavaScript solution in Ruby:

class SpecialStr
  include Comparable
  attr_accessor :str
  def initialize (str)
    @str = str
  end

  def <=> (other)
    self_num, self_string = @str.split(' ')
    other_num, other_string = other.str.split(' ')
    self_num > other_num ? 1 : other_num > self_num ? -1 :
      self_string > other_string ? -1 : 1
  end
end

arr = ['2 xxx', '20 axxx', '2 m', '38 xxxx', '20 bx', '8540 xxxxxx', '2 z']
arr_object = []
arr.each { |str| arr_object << SpecialStr.new(str) }
arr_object.sort! { |x, y| y <=> x }
output_arr = []
arr_object.each { |obj| output_arr << obj.str}
puts output_arr

This has the desired output (numbers descending, then strings ascending):

8540 xxxxxx
38 xxxx
20 axxx
20 bx
2 m
2 xxx
2 z

But the code seemed unnecessarily complicated. (Ruby's supposed to be more concise than JS!) So I asked myself (and now I ask you), why can't I just do this?

def <=> (other)
  self_num, self_string = self.split(' ')
  other_num, other_string = other.split(' ')
  self_num > other_num ? 1 : other_num > self_num ? -1 :
    self_string > other_string ? -1 : 1
end
arr = ['2 xxx', '20 axxx', '2 m', '38 xxxx', '20 bx', '8540 xxxxxx', '2 z']
arr.sort! { |x, y| y <=> x }
puts arr

This outputs incorrectly, based on sort as if I had not redefined <=>:

8540 xxxxxx
38 xxxx
20 bx
20 axxx
2 z
2 xxx
2 m

The code here is shorter, but doesn't work. It uses the version of <=> built into Ruby's Comparable module, rather than my attempt to override it. Why wasn't I able to override it? Can methods be overridden only inside of classes or modules? Is there a shorter way to write that first script in Ruby? (Sorry if this is a noob question, I'm a beginner.)


Solution

  • The easiest way would be to split the string in a number and word, and sort by an Array of minus number (to get decreasing numbers) and word :

    arr = ['2 xxx', '20 axxx', '2 m', '38 xxxx', '20 bx', '8540 xxxxxx', '2 z']
    
    arr.sort_by! do |number_word|
      number, word = number_word.split
      [ -number.to_i, word ]
    end
    
    puts arr
    # =>
    # 8540 xxxxxx
    # 38 xxxx
    # 20 axxx
    # 20 bx
    # 2 m
    # 2 xxx
    # 2 z
    

    When sorting arrays, the first element (-number) has priority. If both first elements are the same, the sort uses the second element (word).