12

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.

4 Answers 4

22

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.

6
  • 1
    Looks great! But I am having problems with it with my specific code, maybe you can help me with it. If I use the one-liner I get the following error comparison of FalseClass with true failed, and if I use the first code you mentioned, I get comparison of [MyModule]::Product with [MyModule]::Product failed. Not sure why this would fail.
    – Ecnalyr
    Commented Feb 11, 2013 at 15:26
  • Some notes: 1) One should use always sort_by over sort whenever possible (more declarative, more concise, more efficient). It's possible to use it in all snippets shown above. 2) It may me preferable to link to the non-destructive Enumerable#sort_by, in-place updates should be avoided whenever possible. 3) @product.sort_by(&:on_sale?).
    – tokland
    Commented Feb 11, 2013 at 15:28
  • Looks like <=> doesn't work right for booleans (in ruby-1.9.3-p327). Looking into it now... Yeah, grosser.it/2010/07/30/ruby-true-false-comparison-with confirms it for an earlier version Commented Feb 11, 2013 at 15:39
  • Updated answer, and incorporated tokland's sort_by advice Commented Feb 11, 2013 at 15:56
  • I worked out a solution using your answer in a different area of my app - a little less elegant, but this post is what got me there. Marked as the answer.
    – Ecnalyr
    Commented Feb 11, 2013 at 16:01
5

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

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

5

No need to sort:

products_grouped = @products.partition(&:on_sale?).flatten(1)
0

Ascending and descending can be done by inter changing "false" and "true"

Products.sort_by {|product| product.on_sale? ==  true ? "false" : "true" }

Not the answer you're looking for? Browse other questions tagged or ask your own question.