Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to sort a list of objects depending on the individual object's response to a method?

I am wanting to display a gallery of products where I include products that are both for sale and not for sale. Only I want the products that are for sale to appear at the front of the list and the objects that are not for sale will appear at the end of the list.

An easy way for me to accomplish this is to make two lists, then merge them (one list of on_sale? objects and one list of not on_sale? objects):

available_products = []
sold_products = []
@products.each do |product|
  if product.on_sale?
    available_products << product
  else
    sold_products << product
  end
end

. . . But do to the structure of my existing app, this would require an excessive amount of refactoring due to an oddity in my code (I lose my pagination, and I would rather not refactor). It would be easier if there were a way to sort the existing list of objects by my product model's method on_sale? which returns a boolean value.

Is it possible to more elegantly iterate through an existing list and sort it by this true or false value in rails? I only ask because there is so much I'm not aware of hidden within this framework / language (ruby) and I'd like to know if they work has been done before me.

like image 252
Ecnalyr Avatar asked Feb 11 '13 15:02

Ecnalyr


3 Answers

Sure. Ideally we'd do something like this using sort_by!:

@products.sort_by! {|product| product.on_sale?}

or the snazzier

@products.sort_by!(&:on_sale?)

but sadly, <=> doesn't work for booleans (see Why doesn't sort or the spaceship (flying saucer) operator (<=>) work on booleans in Ruby?) and sort_by doesn't work for boolean values, so we need to use this trick (thanks rohit89!)

@products.sort_by! {|product| product.on_sale? ? 0 : 1}

If you want to get fancier, the sort method takes a block, and inside that block you can use whatever logic you like, including type conversion and multiple keys. Try something like this:

@products.sort! do |a,b|
  a_value = a.on_sale? ? 0 : 1
  b_value = b.on_sale? ? 0 : 1
  a_value <=> b_value
end

or this:

@products.sort! do |a,b|
  b.on_sale?.to_s <=> a.on_sale?.to_s
end

(putting b before a because you want "true" values to come before "false")

or if you have a secondary sort:

@products.sort! do |a,b|
  if a.on_sale? != b.on_sale?
    b.on_sale?.to_s <=> a.on_sale?.to_s
  else
    a.name <=> b.name
  end
end

Note that sort returns a new collection, which is usually a cleaner, less error-prone solution, but sort! modifies the contents of the original collection, which you said was a requirement.

like image 73
AlexChaffee Avatar answered Sep 26 '22 20:09

AlexChaffee


No need to sort:

products_grouped = @products.partition(&:on_sale?).flatten(1)
like image 29
tokland Avatar answered Sep 25 '22 20:09

tokland


@products.sort_by {|product| product.on_sale? ? 0 : 1}

This is what I did when I had to sort based on booleans.

like image 22
rohit89 Avatar answered Sep 22 '22 20:09

rohit89