1

In Ruby, what would be the best way to sort an array of objects by an order property that may or may not exist, and if it doesn't, then fall back to sorting based on a property named title?

3
  • Jeff, I found your question interesting, but in need of clarification. A simple example, including the desired result, would be a big help. If you do an edit to clarify, there is a possibility the hold will be retracted. Commented Jan 2, 2015 at 0:17
  • probably a duplicate of stackoverflow.com/questions/8888030/… Commented Jan 2, 2015 at 16:03
  • Yes, it does seem like a pretty similar question. I actually asked it again because the other solutions I tried didn't work, but it turned out that was because of the data structure, not the solutions. Sorry! Commented Jan 5, 2015 at 7:34

6 Answers 6

3

Not sure if this is what you are after, but a quick solution could be:

arr = [{a:"never", b:"anna"}, {a:"always", b:"bob"}, {b:"colin"}, {b:"abe"}]
arr.sort_by! {|o| o[:a] ? o[:a] : o[:b] }
#=> [{:b=>"abe"}, {:a=>"always", :b=>"bob"}, {:b=>"colin"}, {:a=>"never", :b=>"anna"}]
Sign up to request clarification or add additional context in comments.

3 Comments

or { |o| o[a] || o[b] }.
The problem with this answer is that you will sometimes be comparing one record's :a with another record's :b. @Sacha - In your case, you would have an integer (order) being compared with a string (title), so I think you would see this error: ArgumentError: comparison of String with 1 failed
@nathan.f77 yes, this is what I understood from the question. That, indeed, when :a is not present sort by :b. So you would be sorting object1's :a with object2's :b, say. Of course, I also assumed that both :a and :b are of the same type.
2

Here's how to perform a sort with a fallback in Ruby:

Item = Struct.new(:order, :title)

items = [
  Item.new(nil, "d"),
  Item.new(nil, "b"),
  Item.new(1,   "a"),
  Item.new(3,   "c"),
  Item.new(2,   "e")
]

sorted_items = items.sort do |a, b|
  if a.order && b.order
    a.order <=> b.order
  elsif a.order || b.order
    # This prioritizes all items with an order
    a.order ? -1 : 1
  else
    a.title.to_s <=> b.title.to_s
  end
end

require 'awesome_print'
ap sorted_items

# [
#     [0] {
#         :order => 1,
#         :title => "a"
#     },
#     [1] {
#         :order => 2,
#         :title => "e"
#     },
#     [2] {
#         :order => 3,
#         :title => "c"
#     },
#     [3] {
#         :order => nil,
#         :title => "b"
#     },
#     [4] {
#         :order => nil,
#         :title => "d"
#     }
# ]

Let me also say that if you are fetching records from a database, then it would be better to do the sorting in your SQL query. If Item was an ActiveRecord model, you could do something like:

Item.order('order ASC NULLS LAST, title ASC')

(NULLS LAST can be used in Postgres, check out this answer for MySQL.)

Comments

1

If I understand you right here is how to do this:

arr1 = [{order: 1, title: 2},{title: 4},{order: 2, title: 1}]
arr2 = [{order: 1, title: 2},{order: 7, title: 4},{order: 2, title: 1}]

def sort_it prop1, prop2, ar
  ar.map{|el| el[prop1]}.include?(nil) ?
     ar.sort_by{|el| el[prop2]}
    :
     ar.sort_by{|el| el[prop1]}
end

p sort_it(:order, :title, arr1)
p sort_it(:order, :title, arr2)

Which gives:

#=> [{:order=>2, :title=>1}, {:order=>1, :title=>2}, {:title=>4}]
#=> [{:order=>1, :title=>2}, {:order=>2, :title=>1}, {:order=>7, :title=>4}]

So, the algorythm is simple: select all objects' properties (:order in our case) and if output temporary array contains at least one nil then sort by second given property, otherwise -- by first.

Comments

0

You could try

def sort_array(array)
  sort_by_property_name = sort_by_property_present?(array, :order, :title)
  array.sort_by { |ob| ob.public_send(sort_by_property_name) }
end

def sort_by_property_present?(array, primary_name, fallback_name)
  array.all? { |ob| ob.respond_to?(name) } || return fallback_name
  primary_name
end

1 Comment

I'm not good still in proper method naming as per their roles.. Please suggest a good name. I'm still learning.
0

Assuming you want to sort based on field/parameter that may or may not be present, I am assuming:

  1. When the parameter is present sort by it.
  2. When unavailable fall back to the next parameter.

Please review the following code that can sort an object array based on a number of fields, the system keeps falling back to the next field if the field is unavailable. Its developed with the assumption that the last field will definitely be present.

class CondtionalSort
  def run(array: a, keys_to_order_by: keys)
    array.sort do |e1, e2|
      keys_to_order_by.each do |key|
        break e1[key] <=> e2[key] if (e1.key?(key) && e2.key?(key))
      end
    end
  end
end


ArrayTest = [{order: 1, title: 2},{title: 4},{order: 2, title: 1}]
ArrayTest_SORTED = [{:order=>1, :title=>2}, {:order=>2, :title=>1}, {:title=>4}]

sorter = CondtionalSort.new
sorter.run array: ArrayTest, keys_to_order_by: [:order, :title]

Comments

0

I just use an array as the sort_by:

# sample data:
Item = Struct.new(:property1, :property2, :property3)
collection = [Item.new("thing1", 3, 6), Item.new("thing1", 3, 1), Item.new("aaa", 1,1) ]

# sort
collection.sort_by{|item| [item.property1, item.property2, item.property3] }
# => [#<struct Item property1="aaa", property2=1, property3=1>, 
      #<struct Item property1="thing1", property2=3, property3=1>, 
      #<struct Item property1="thing1", property2=3, property3=6>]

3 Comments

I wonder if that actually works...
I don't think this works. I get ArgumentError: comparison of Array with Array failed when I try this, both in Ruby IRb, and in the Rails console.
You have to actually line up your test conditions so it has comparable properties in the array, but I use it all the time. I'll add some sample test code.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.