-1
class Foo
  attr_reader :size, :color

  def <=>
    ...


foo1 = Foo.new( size: 'large', color: 'blue'  )
foo2 = Foo.new( size: 'small'                 )
foo3 = Foo.new( color: 'green'                )
foo4 = Foo.new( size: 'small', color: 'red'   )
  ...

Size is ordered small, nil, medium, large, super-sized. Color is ordered green, nil, blue, red.

How to efficiently sort first by size, then by color?

B Seven
  • 44,484
  • 66
  • 240
  • 385

3 Answers3

2

First I'd explicitly declare the order:

@@size_order = {
    'small' => 1, 
    'medium' => 2, 
    'large' => 3, 
    'super-sized' => 4
}

@@color_order = {
    'green' => 1,
    'blue' => 2,
    'red' => 3
}

then you can just definite the <=> method along the lines of:

def <=> ( o )
    if (@size == o.size)
        if (@color == o.color) 
            return 0
        else
            return (@@color_order[@color] > @@color_order[o.color]) ? 1 : -1
        end
    else
        return (@@size_order[@size] > @@size_order[o.size]) ? 1 : -1
    end
end

Here's a test example.

But it would be probably better (more OOP) to subclass Foo with two classes: Color and Size and define a <=> for each of them.

Shoe
  • 74,840
  • 36
  • 166
  • 272
2
class Foo
  attr_reader :size, :color
  VALID_COLORS = ["small",nil,"medium","large","super-sized"]
  VALID_SIZES  = ["green", nil, "blue", "red" ]

  def size_pos
    VALID_COLORS.index(size) || -1
  end

  def color_pos
    VALID_SIZES.index(color) || -1
  end

  def initialize(opts={})
    @size=opts[:size]
    @color=opts[:color]
  end

  def <=>(other)
    [size_pos,color_pos] <=> [other.size_pos, other.color_pos]
  end
end

foo1 = Foo.new( size: 'large', color: 'blue'  )
foo2 = Foo.new( size: 'small'                 )
foo3 = Foo.new( color: 'green'                )
foo4 = Foo.new( size: 'small', color: 'red'   )

[foo1,foo2,foo3,foo4].sort

#[#<Foo:0x000000020848d0 @size="small", @color=nil>, 
 #<Foo:0x00000002065700 @size="small", @color="red">, 
 #<Foo:0x00000002074868 @size=nil, @color="green">, 
 #<Foo:0x0000000208da98 @size="large", @color="blue"> ]

You could improve performance by extracting out the positions into a class variable hash or constant instead of calling the index each time.

Shawn Balestracci
  • 7,380
  • 1
  • 34
  • 52
0

Assuming 'foos' is a collection of foo objects:

foos.sort_by {|f| [ f.size || '', f.color || '' ]}

This might be more efficient than using sort or defining <=>. You could use something other than the empty string when the attribute is nil if you want to adjust the sort order. For increased flexibility, you could do something like this:

def sort_by_size
  foos.sort_by {|f| [ f.size || '', f.color || '' ]}
end

def sort_by_color
  foos.sort_by {|f| [ f.color|| '', f.size|| '' ]}
end
Whit Kemmey
  • 2,230
  • 22
  • 18