views:

376

answers:

6

Does ruby or rails provide a method to order strings in a specified order? Say that I have the following priorities "Severe, High, Medium, Low".

These priorities are not going to change frequently (if at all). I have a Task model with a priority column:

tasks
  - id (integer)
  - name (string)
  - priority (string)

I'd like to get an array of all the tasks ordered by priority. Since the logical order does not follow alphabetical order, it's not possible to simply order by the priority column:

Task.all(:order => :priority)

What I've done is create a Priority model and defined the associations: Task belongs_to Priority. In the priorities table, I then assigned each priority name a value and ordered by that value. Is there a better way to do this? I'd rather not have a priorities table at all and declare a PRIORITY constant (as a hash), or simply specify the priority as a string in the tasks table.

+1  A: 

I would use integers in db. It's easier to sort and index, then override the attribute getter and setter to use symbols to interface externally.

Aaron Qian
A: 

I would suggest using acts_as_list (http://github.com/rails/acts%5Fas%5Flist/tree/master), which will add a position field to your object but will also give you all the methods for moving things up and down the list while preserving the order correctly.

If the priorities are never going to change it might be better to make priority a field on the task table. The problem is that you then need to be able to sort by priority. For that you have a few options, but whatever you choose it should be done by the db.

jonnii
+1  A: 

Switch to a integer like Aaron said, then you will probably want to use a default_scope.

For example:

class Task < ActiveRecord::Base
  default_scope :order => 'tasks.position' # assuming the column name is position
  ...

With a default scope you won't have to specify the sort order in any of your find method calls - the tasks will automatically be sorted.

Andy Gaskell
Great, thanks! Exactly what I was looking for.
Homar
Even better would be to use acts_as_list with this which gives some sorting methods and so on for it, really neat.
Ryan Bigg
A: 

Just to solve your specific question: You can order it by the last letter.

Sever(e), Hig(h), Mediu(m), Lo(w)

you can either do it in rails or sql:

order by RIGHT(priority, 1)

I choose this approach because

  1. it is the easiest possible way.
  2. you mentioned it is not going to change.

the approach might not be flexible enough, but too me, I try not to generalize the problem unless it is necessary

ez
That's the least obvious thing ive ever seen. I would totally recommend against this, since in 6 months time you will come back and think "wtf is this?".
Omar Qureshi
I disagree. The solution will get it done and move on. Refact it if more requirements arise and the solution is no longer fit. I am not too worry about what is going to happen after 6 month. that's tomorrow's problem. here is a good article about it: http://www.purpleworkshops.com/articles/simplest-thing
ez
A nice simple solution. However, if I do end up changing the priorities, it will need to be rewritten. Thanks though!
Homar
+1  A: 

If changing your schema to use an integer priority isn't an option, you can also define a custom <=> method on Task to define the sort order.

class Task

  PRIORITIES = {
    :high => 3,
    :med  => 2,
    :low  => 1,
  }

  def <=> (other)
    PRIORITIES[self.priority] <=> PRIORITIES[other.priority]
  end

end

The downside of doing it this way is that you'll need to do the sorting on the Ruby side, rather than let your database do it for you, which will preclude the use of default_scope. The upside is that any time you call sort on an Array of Tasks the order will come out correctly (or, if you only want a custom ordering in one particular place, you can use sort_by).

John Hyland
Thanks John! I think the simpler option would be to use default_scope. However, you have solved one of my other problems. Many thanks.
Homar
+1  A: 

You could use a case statement in your where clause. It's a little ugly but should work:

class Task
  PRIORITIES_ORDERED = ['high', 'medium', 'low']

  # Returns a case statement for ordering by a particular set of strings
  # Note that the SQL is built by hand and therefore injection is possible,
  # however since we're declaring the priorities in a constant above it's
  # safe.
  def self.order_by_case
    ret = "CASE"
    PRIORITIES_ORDERED.each_with_index do |p, i|
      ret << " WHEN priority = '#{p}' THEN #{i}"
    end
    ret << " END"
  end

  named_scope :by_priority, :order => order_by_case

end

And then when you want them ordered by priority, you can do something like this:

Task.all.by_priority

Of course as other people have mentioned, it's cleaner to make a foreign key to a Priority table instead of a text string column. In the Priorty table, you could add a position column as an integer that you could sort by, and plug-ins exist to make this easier.

Evil Trout