Lieven Cardoen
Lieven Cardoen

Reputation: 25979

Sorting an array of hashes in ruby

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

Upvotes: 1

Views: 204

Answers (3)

Niklas B.
Niklas B.

Reputation: 95358

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"}]

Upvotes: 2

PinnyM
PinnyM

Reputation: 35541

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]] }

Upvotes: 1

Patrick Oscity
Patrick Oscity

Reputation: 54734

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

Upvotes: 0

Related Questions