Christopher Ronning
Christopher Ronning

Reputation: 1890

Ruby - Spaceship operator won't work in a block for .sort

I'm getting an error when attempting to use the spaceship operator with non alpha-numeric characters in the sort function.

word = "out-classed"
letters = word.downcase.split('')
letters.sort! do |x, y|
  if y < 'a'
    next
  else
   value = x <=> y
  end
end

I'm getting ArgumentError: comparison of String with String failed, and I'm almost positive this happens with the spaceship operator and not the < comparison.

The interesting part is that when I do this same comparison in irb outside of the context of a sort block, the comparison works. It also works when the word variable only consists of letters.

Can anybody help me to understand why this doesn't work in this specific context alone?

Upvotes: 3

Views: 282

Answers (3)

Cary Swoveland
Cary Swoveland

Reputation: 110695

If you attempt to sort a collection, x<=>y must return 0, 1 or -1 for every pair of elements of the collection. If <=> is defined artificially for some pairs (e.g., 'a'<=>'-' #=> 0 and '-'<=>'a' #=> 0), your sort may return erroneous results.

This is because sort algorithms do not necessarily evaluate all pairs of elements in the collection. If, for example, it finds that:

'a' <=> 'b' #=> 0

and

'b' <=> 'c' #=> 0

it will conclude that:

`a` <=> `c` #=> 0

because the collection being sorted must satisfy transitivity: x <== z if x <= y and y <= z.

For example, if the collection is the array ['z', '-', 'a'] and it finds that 'z' <= '-' and '-' <= 'a', it will conclude that 'z' <= 'a' (and not evaluate 'z' <=> 'a').

That's why:

['z', '-', 'a'].sort { |x,y| p [x,y]; (y < 'a') ? 0 : x<=>y }
  #-> ["z", "-"]
  #-> ["-", "a"]
  #=> ["z", "-", "a"]

doesn't work. You have two choices:

Remove the offending elements before sorting:

['z', '-', 'a'].select { |c| ('a'..'z').cover?(c) }.
                sort { |x,y| (y < 'a') ? 0 : x<=>y }
  #=> ["a", "z"]

or sort all elements of the collection:

['z', '-', 'a'].sort
  #=> ["-", "a", "z"] 

If the collection contains non-comparable elements (e.g., [1,2,'cat']), you only choice is to remove elements from the array until all remaining elements are comparable.

Upvotes: 3

Kris
Kris

Reputation: 19948

Instead of next you need to return 0, 1 or -1. Try this:

word = "out-classed"
letters = word.downcase.split('')
letters.sort! do |x, y|
  if y < 'a'
    0
  else
   value = x <=> y
  end
end

Upvotes: 2

Piotr Kruczek
Piotr Kruczek

Reputation: 2390

Your problem lies here

if y < 'a'
  next
else
  ...

sort method expects you to return a comparison value between every pair, so when you call next without returning anything, it says that comparison failed.

Try e.g. this:

if y < 'a'
  1
else
  value = x <=> y
end

Upvotes: 1

Related Questions