1

I had the following code:

def sort_descending _objects, field
  _objects.sort { |b,a| a.send(field) <=> b.send(field) }
end

When a.send(field) returns a String and b.send(field) returns an Integer, then an ArgumentError is thrown. I tried to catch this exception by:

def sort_descending _objects, field
  _objects.sort { |b,a| safe_compare(a,b,field) }
end

def safe_compare a, b, field
   a.send(field) <=> b.send(field)
rescue
   a.send(field).to_s <=> b.send(field).to_s
end

but this also throws an ArgumentError. I have no idea why. Can anybody explain this behavior of exceptions thrown by sort?

Though this workaround works, it looks ugly

def sort_ascending _objects, field
  _objects.sort do |a,b|
    safe_compare(a,field,b) <=> safe_compare(b,field,a)
  end
end

def safe_compare a, field, b
  _a,_b = a.send(field), b.send(field)
  _a.class == _b.class ? _a : _a.to_s
end

Code to reproduce is here.

sawa
  • 165,429
  • 45
  • 277
  • 381
Nockenfell
  • 1,111
  • 1
  • 8
  • 14
  • The reason the `ArgumentError` isn't caught is explained here: http://stackoverflow.com/questions/10692860/ruby-ignores-rescue-argumenterror – lurker Aug 21 '13 at 00:20
  • The code to reproduce the problem should be included in the question, and be a short summary, not a link to somewhere else. You're asking us to chase down pages to help you. See http://sscce.org/ – the Tin Man Aug 21 '13 at 03:25
  • @theTinMan oh, sorry. I promise to get better. Though my account here is a bit older, I'm just starting to use it actively. – Nockenfell Aug 21 '13 at 06:44
  • It's something to do to encourage answers. Remember, everyone is a volunteer working on their own time so making people track down information discourages their help. – the Tin Man Aug 21 '13 at 13:04

1 Answers1

3

Can anybody explain this?

Yes, the method <=>() does not raise an exception. Take a look:

def sort_descending _objects, field
  _objects.sort {|b,a| safe_compare(a,b,field) }
end

def safe_compare a, b, field
  a.send(field) <=> b.send(field)
rescue 
  puts 'in rescue clause'   #Let's trace the flow of execution
  a.send(field).to_s <=> b.send(field).to_s
end

class Dog
  def greet
    "hello"
  end
end

class Cat
  def greet
    10
  end
end

d = Dog.new
c = Cat.new

p d.send("greet")
p c.send("greet")

p safe_compare(d, c, "greet")

--output:--
"hello"
10
nil

Note that there is no output from the puts statement inside the rescue clause.

From the ruby String docs:

string <=> other_string → -1, 0, +1 or nil

nil is returned if the two values are incomparable.

This line:

a.send(field) <=> b.send(field)

is equivalent to:

a.send(field).<=>( b.send(field) )

If a.send(field) returns a string, then a string is calling the <=>() method. The Numeric class also defines the <=>() method, so if a.send(field) returns a number, then a number is calling the <=>() method. Both String#<=> and Numeric#<=> return nil if the two objects are not comparable--they do not throw an exception. Other classes, have similar definitions of the <=>() method.

As a result, no ArgumentError is ever raised inside your safe_compare method. However, nil is not a valid return value from a sort block, so sort() raises an ArgumentError.

You need to do something like this:

def sort_descending _objects, field
  _objects.sort { |b,a| safe_compare a, b, field }
end

def safe_compare a, b, field
  result = a.send(field) <=> b.send(field)
  result ||= a.send(field).to_s <=> b.send(field).to_s
end
7stud
  • 46,922
  • 14
  • 101
  • 127
  • thanks a lot for your detailed explanation. It makes it very clear what my problem was. And your *safe_compare* is much nicer than my *workaround*-solution. – Nockenfell Aug 21 '13 at 06:32
  • For completeness, I've added you solution to the complete gist https://gist.github.com/iboard/6288415 - thanx again. – Nockenfell Aug 21 '13 at 06:41
  • @Nockenfell, Another, simpler way to do what you want is to call to_s regardless: `_objects.sort { |b,a| a.send(field).to_s <=> b.send(field).to_s }` – 7stud Aug 21 '13 at 08:53
  • 1
    no because "123" will then come before "2" if all fields are Integers ;-) _I use this in a module where I don't know (and don't care) about the field's data-types_. – Nockenfell Aug 21 '13 at 09:11