Ecnalyr Ecnalyr - 4 months ago 5x
Ruby Question

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

where I include products that are both for sale and not for sale. Only I want the
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
sold_products << product

. . . 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
model's method
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.


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

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

or the snazzier


but sadly, <=> doesn't work for booleans (see Why doesn't sort or the 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

or this:

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

(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 <=>

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.