Newbie: sorting an array of custom objects

E

Einar Høst

Hi,

As a project for learning Ruby, I'm writing a simple game. I have some
simple objects that I would like to be able to sort 'automatically' in
an array. In Java, I can implement the Comparable interface to make the
Array.sort method do this for me. I'm sure I can do something similar or
simpler i Ruby, I just don't know how. Can anyone help? An even more
elegant solution would be to able to say to the array 'just put this
object where it belongs', without really having to sort the whole array.

Thanks a lot!

Kind regards,
Einar
 
M

Marcin Mielżyński

Einar said:
Hi,

As a project for learning Ruby, I'm writing a simple game. I have some
simple objects that I would like to be able to sort 'automatically' in
an array. In Java, I can implement the Comparable interface to make the
Array.sort method do this for me. I'm sure I can do something similar or
simpler i Ruby, I just don't know how. Can anyone help? An even more
elegant solution would be to able to say to the array 'just put this
object where it belongs', without really having to sort the whole array.

Thanks a lot!

Kind regards,
Einar

arr.sort_by{|obj| obj.some_field}

lopex
 
M

Marcin Mielżyński

Marcin said:
arr.sort_by{|obj| obj.some_field}

there is also another method for array sorting (actually it was the
first in standard library)

arr.sort{|a,b| a.some_field <=> b.some_field}

if you define <=> operator for your class

def <=> arg
@some_field <=> arg.some_field
end

ther you will be able to

arr.sort

lopex
 
E

Einar Høst

Marcin Mielżyński skrev:
arr.sort_by{|obj| obj.some_field}

lopex

That solution requires some_field to be 'naturally' ordered, though,
doesn't it? (I'm very new to Ruby...) What if some_field contains a
string, and I want 'Oranges' to be sorted before 'Apples'? Actually, I'm
writing a card game, so I want 'Spades' < 'Hearts' < 'Clubs' < 'Diamonds'.

- Einar
 
D

Daniel Harple

As a project for learning Ruby, I'm writing a simple game. I have =20
some simple objects that I would like to be able to sort =20
'automatically' in an array. In Java, I can implement the =20
Comparable interface to make the Array.sort method do this for me. =20
I'm sure I can do something similar or simpler i Ruby, I just don't =20=
know how. Can anyone help? An even more elegant solution would be =20
to able to say to the array 'just put this object where it =20
belongs', without really having to sort the whole array.

You could use a SortedSet, however this has a downside. A Set =20
contains no duplicate objects.

require 'set'

set =3D SortedSet.new
set << 3
set << 1
set << 10
set << 3
p set # -> #<SortedSet: {1, 3, 10}>

-- Daniel=
 
X

Xavier Noria

As a project for learning Ruby, I'm writing a simple game. I have =20
some simple objects that I would like to be able to sort =20
'automatically' in an array. In Java, I can implement the =20
Comparable interface to make the Array.sort method do this for me. =20
I'm sure I can do something similar or simpler i Ruby, I just don't =20=
know how. Can anyone help? An even more elegant solution would be =20
to able to say to the array 'just put this object where it =20
belongs', without really having to sort the whole array.

The analogous approach in Ruby uses the Comparable mixin:

class Foo
include Comparable

def <=3D>(other)
# custom order logic
end
end

which, based on the custom <=3D>, in addition provides for free the =20
operators <, <=3D, =3D=3D, >=3D, and >.

-- fxn
 
B

Brian Mattern

That solution requires some_field to be 'naturally' ordered, though,
doesn't it? (I'm very new to Ruby...) What if some_field contains a
string, and I want 'Oranges' to be sorted before 'Apples'? Actually, I'm
writing a card game, so I want 'Spades' < 'Hearts' < 'Clubs' < 'Diamonds'.

- Einar

Ideally you would have a Card class that has value and suit fields. Then yo=
u=20
could implement <=3D> and just use Array#sort.

Alternatively, if you want to sort a specific way just the one time, you ca=
n=20
pass a block to sort which takes two parameters |a,b| and returns -1 if a <=
=20
b, 0 if a =3D=3D b and 1 if a >b.

So, you could do something like:

#an array of suit strings
SUITS =3D %w{Spades Hearts Clubs Diamonds}

# use a simple array for each card for this example
cards =3D [[3, 'Clubs'], [2, 'Spades'], [10, 'Diamonds'], [5, 'Clubs'], [7,=
=20
'Hearts']]

sorted =3D cards.sort { |a, b|=20
ord =3D SUITS.index(a.last) <=3D> SUITS.index(b.last) #sort by suit
ord =3D a.first <=3D> b.first if ord =3D 0 #suit matched, so sort value
ord
}
=20
#print out the cards in sorted order
puts sorted.map{ |card| "#{card.first} of #{card.last}" }.join(', ')

=2D-
Brian Mattern
 
D

Daniel Harple

That solution requires some_field to be 'naturally' ordered, =20
though, doesn't it? (I'm very new to Ruby...) What if some_field =20
contains a string, and I want 'Oranges' to be sorted before =20
'Apples'? Actually, I'm writing a card game, so I want 'Spades' < =20
'Hearts' < 'Clubs' < 'Diamonds'.

- Einar

As mentioned, you should use the Comparable mix-in.

class Card
include Comparable

SUITES =3D %w{Spade Heart Club Diamond}
VALUES =3D %w{Ace King Queen Jack} + ("1".."10").to_a.reverse

def initialize(suite, value)
@suite, @value =3D suite, value
end
attr_reader :suite, :value

def <=3D>(card)
if @suite =3D=3D card.suite
VALUES.index(@value) <=3D> VALUES.index(card.value)
else
SUITES.index(@suite) <=3D> SUITES.index(card.suite)
end
end
end

and full example of a Card game skeleton:

require 'pp'

module CardGame
class Deck
def initialize
@cards =3D []
Card::SUITES.each do |suite|
Card::VALUES.each { |v| @cards << Card.new(suite, v) }
end
# shuffle the deck
@cards =3D @cards.sort_by { rand }
end

def draw_card
@cards.pop
end
end

class Card
include Comparable

SUITES =3D %w{Spade Heart Club Diamond}
VALUES =3D %w{Ace King Queen Jack} + ("1".."10").to_a.reverse

def initialize(suite, value)
@suite, @value =3D suite, value
end
attr_reader :suite, :value

def <=3D>(card)
if @suite =3D=3D card.suite
VALUES.index(@value) <=3D> VALUES.index(card.value)
else
SUITES.index(@suite) <=3D> SUITES.index(card.suite)
end
end
end

class Hand
def initialize
@cards =3D []
end

def <<(card)
@cards << card
@cards.sort!
@cards
end
end
end

deck =3D CardGame::Deck.new
hand1 =3D CardGame::Hand.new
hand2 =3D CardGame::Hand.new

# Draw some cards
3.times do
hand1 << deck.draw_card
hand2 << deck.draw_card
end

pp hand1, hand2

__END__

-- Daniel=
 
D

Daniel Harple

and full example of a Card game skeleton:

However, the SUITES and VALUES constants should be in the Deck class.
And be sure to raise an error from Deck#draw_card it is empty ;)

-- Daniel
 
G

George Ogata

=?UTF-8?B?RWluYXIgSMO4c3Q=?= said:
That solution requires some_field to be 'naturally' ordered, though,
doesn't it? (I'm very new to Ruby...) What if some_field contains a
string, and I want 'Oranges' to be sorted before 'Apples'? Actually,
I'm writing a card game, so I want 'Spades' < 'Hearts' < 'Clubs' <
'Diamonds'.

arr.sort_by{|card| ~card.suit[0] & 0x1a}

;-)
 
E

Einar

Daniel said:
That solution requires some_field to be 'naturally' ordered, though,
doesn't it? (I'm very new to Ruby...) What if some_field contains a
string, and I want 'Oranges' to be sorted before 'Apples'? Actually,
I'm writing a card game, so I want 'Spades' < 'Hearts' < 'Clubs' <
'Diamonds'.

- Einar


As mentioned, you should use the Comparable mix-in.

class Card
include Comparable

SUITES = %w{Spade Heart Club Diamond}
VALUES = %w{Ace King Queen Jack} + ("1".."10").to_a.reverse

def initialize(suite, value)
@suite, @value = suite, value
end
attr_reader :suite, :value

def <=>(card)
if @suite == card.suite
VALUES.index(@value) <=> VALUES.index(card.value)
else
SUITES.index(@suite) <=> SUITES.index(card.suite)
end
end
end

and full example of a Card game skeleton:

require 'pp'

module CardGame
class Deck
def initialize
@cards = []
Card::SUITES.each do |suite|
Card::VALUES.each { |v| @cards << Card.new(suite, v) }
end
# shuffle the deck
@cards = @cards.sort_by { rand }
end

def draw_card
@cards.pop
end
end

class Card
include Comparable

SUITES = %w{Spade Heart Club Diamond}
VALUES = %w{Ace King Queen Jack} + ("1".."10").to_a.reverse

def initialize(suite, value)
@suite, @value = suite, value
end
attr_reader :suite, :value

def <=>(card)
if @suite == card.suite
VALUES.index(@value) <=> VALUES.index(card.value)
else
SUITES.index(@suite) <=> SUITES.index(card.suite)
end
end
end

class Hand
def initialize
@cards = []
end

def <<(card)
@cards << card
@cards.sort!
@cards
end
end
end

deck = CardGame::Deck.new
hand1 = CardGame::Hand.new
hand2 = CardGame::Hand.new

# Draw some cards
3.times do
hand1 << deck.draw_card
hand2 << deck.draw_card
end

pp hand1, hand2

__END__

-- Daniel

Thanks a lot! This sort of resembles the code I've written, apart from
all the interesting bits! :) In particular the sorting bit, but also
the shuffling - so much more elegant than my "manual" approach.

In general I'm interested "idiomatic programming", so I'm very much
looking for "the Ruby way" of doing these things. In fact, I'm also
writing the game in C#, which is the language I know best. In a way, the
more different the implementations become, the happier I'll be.

- Einar
 
E

Einar

Also, let me add that I've heard lots of nice things about the Ruby
community - now I know that it's not just talk. Thanks to everyone for
their replies :)

- Einar
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,766
Messages
2,569,569
Members
45,045
Latest member
DRCM

Latest Threads

Top