1

I have an array of hashes which look like:

ward = {id: id, name: record["Externalization"], mnemonic: record["Mnemonic"],
   seqno: record["SeqNo"]}

All fields are strings.

Now I want to sort them first on seqno and then on name. seqno can be nil (if seqno is nil, then this ward must come after the ones having a seqno).

What I have so far is:

wardList.sort! do |a,b| 
  return (a[:name] <=> b[:name]) if (a[:seqno].nil? && b[:seqno].nil?) 
  return -1 if a[:seqno].nil?
  return 1 if b[:seqno].nil?
  (a[:seqno] <=> b[:seqno]).nonzero? ||
    (a[:name] <=> b[:name])
end

But this gives me the error: can't convert Symbol into Integer

3 Answers 3

2

First, normalize your data, you can't work with integers as strings here:

wardList = wardList.map { |x| x.merge({:id    => x[:id].to_i, 
                                       :seqno => x[:seqno].try(:to_i) }) }

Then you can use sort_by, which supports lexicographical sorting:

wardList.sort_by! { |x| [x[:seqno] || Float::INFINITY, x[:name]] }

Example:

irb(main):034:0> a = [{:seqno=>5, :name=>"xsd"}, 
                      {:seqno=>nil, :name=>"foo"}, 
                      {:seqno=>nil, :name=>"bar"}, 
                      {:seqno=>1, :name=>"meh"}]
irb(main):033:0> a.sort_by { |x| [x[:seqno] || Float::INFINITY, x[:name]] }
=> [{:seqno=>1, :name=>"meh"},
    {:seqno=>5, :name=>"xsd"},
    {:seqno=>nil, :name=>"bar"},
    {:seqno=>nil, :name=>"foo"}]
Sign up to request clarification or add additional context in comments.

23 Comments

Yes, but what if a[:seqno] is nil?
@Lieven, why nasty? how do you compare a number with nil? it's only natural to set a default value to cover this case.
@Lieven: So now you have two one-line solutions to solve this task perfectly simple and elegant. Congratulations.
@Lieven: You seem to be lacking some basic understanding of the tools you are working with. Check my answer for an example of how to transform the data properly.
@Lieven: Exactly the values false and nil are considered boolean false in Ruby. So with the first operand being nil, the second operand is not even evaluated. By the way, the expression doesn't return false, it returns the first falsy value (nil in this case).
|
1

This should work:

sorted = wardList.sort_by{|a| [a[:seqno] ? 0 : 1, a[:seqno], a[:name]] }

or for some rubies (e.g. 1.8.7):

sorted = wardList.sort_by{|a| [a[:seqno] ? 0 : 1, a[:seqno] || 0, a[:name]] }

16 Comments

Edited, that should include what you need
sorts first on nil? status of :seqno (as string - 'false' will always precede 'true'), then on :seqno, then on :name
Great solution. But how come it doesn't crash on a[:seqno] when it's nil?
sort_by uses tuple comparison, as opposed to standard sort comparison. Apparently tuples are allowed to contain nil (but not true or false), although I'm not sure myself how the comparison implementation actually does this. nil will normally precede non-nil, which is why we need to first sort on nil status
Oh, by the way, looking back at the question it should really be a[:seqno] ? 0 : 1
|
0

I don't think you should use return here, it causes the block to return to the iterator, the iterator to return to the enclosing method and the enclosing method to return to its caller. Use next instead which only causes the block to return to the iterator (sort! in this case) and do something like:

wardList.sort! do |x,y|
  next  1 if x[:seqno].nil?
  next -1 if y[:seqno].nil?
  comp = x[:seqno] <=> y[:seqno]
  comp.zero? ? x[:name] <=> y[:name] : comp
end

1 Comment

This gives the wrong result if x[:seqno].nil? && !y[:seqno].nil?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.