Module: Enumerable

Defined in:
lib/sort_by_str.rb

Instance Method Summary collapse

Instance Method Details

#sort_by_str(str) ⇒ Object

Similar to sort_by, sort_by_str accepts a string containing a SQL-style sort expression.

The simplest sort expression is a comma separated list of fields: 'month,day,year'

But can also include optional ASC (ascending) or DESC (descending) order modifiers. For example: 'year DESC, month ASC, day ASC' If an order modifier is omitted for a field, an ASC sort is assumed.

Field values are checked for comparision using a send, so any method name can be used.

Raises:

  • (ArgumentError)


12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/sort_by_str.rb', line 12

def sort_by_str str
  sort_parts = str.split(',').collect { |p| p.split(' ') }
  
  # double-check structure of parsed data
  raise ArgumentError,  "'#{str}' doesn't appear correctly formatted." if sort_parts.empty?
  sort_parts.each { |p| raise ArgumentError, "'#{str}' doesn't appear correctly formatted." if p.length > 2 }
  
  # split into list of fields and sort directions for those fields
  fields          = sort_parts.collect { |p| p.first }
  sort_directions = sort_parts.collect { |p| p.last.upcase == 'DESC' ? :desc : :asc }  # if unspecified assumed to be an ASC sort

  # From here we follow pattern of Schwartzian Transform used in sort_by (http://bit.ly/aPEsNO).
  # This means that rather than doing a sort! directly on self, we cache values to be sorted into an array.
  # So for example, given:
  #  a = Date.today
  #  b = Date.today + 1
  #  [a,b].sort_by_str('year ASC, day DESC')
  #
  # we transform to
  #   [[a, 2010, 22],[b,2010,23]]
  # and then proceed with sort.
  # Following sort, we collect list of first elements to produce final, sorted array.
  # This prevents sort attrs from being called multiple times during sort
  # which could be problematic if sort attr is an expensive calcuation.

  # 1. collect data into intermediate arrays, data element first
  sort_data = collect do |element|
    data = [element]
    fields.each { |f| data << element.send(f) }
    data
  end

  # 2. actually sort the data -- a,b are limited to a[1..-1] so as to exclude first [data] element
  sort_data.sort! do |a,b|
    cmp = 0
    sort_directions.zip(a[1..-1], b[1..-1]) do |field_data|
      direction, a_val, b_val = field_data
      if direction == :desc
        cmp = b_val <=> a_val
      else
        cmp = a_val <=> b_val
      end
      break if cmp != 0
    end

    cmp
  end
  
  # 3. now that data is sorted, reduce back to list of data elements
  sort_data.collect { |element| element.first }
end