basketball 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eade2dd71d031c05eaf7e5fa792056d9ee6621f6168c6cbd288e5106f0fe4c3a
4
- data.tar.gz: 66b92eb67dd64d84288dc5869b2076753d043311e22d903d4d8e5468ea90f7f6
3
+ metadata.gz: 71ab0cd4aa4e2bb10d0a9640e18a756938fbe749842abdbbda9be9f2023be9cf
4
+ data.tar.gz: 5d216901cadb3c92d41a1e445956aef4a11fc1d3e418430003d050693c43d3be
5
5
  SHA512:
6
- metadata.gz: 72a89afec17d978e79dd979a50188c62c19532a77143312a79f387a182cab5988a7e90399be60f019ce3a5d6639f5eb0ddc3f14ca896e5f3c3496488be3427de
7
- data.tar.gz: 4f04e99ddd1e64ad893395264c5753b975f4c46b741324d53bd125c1f2a8daa2079c44e2a85fd52bf6dd24d7b09fcf29c5f9266db4c67da19e8f952884de8a72
6
+ metadata.gz: a3812684e58e97711745e31a8696c12c32c79e542661302db86eb7b0fecc3784ede1650ec2d0079b66a641f5e09da657f22f596583630d932e03e55ba7ed3514
7
+ data.tar.gz: f1edd69f31a2ff8ca4d5e53bab9bd59c74a091ab276db1a53901aa1e7de17202898ba01a917304f1e7018da1b3ad129fb012a3ac12516e832f4b8fa6cb8cd232
data/CHANGELOG.md CHANGED
@@ -1,4 +1,15 @@
1
- #### 0.0.1 - May 4th, 2023
1
+ #### 0.0.4 - May 5th, 2023
2
+
3
+ * Add ability to skip draft picks using `Basketball::Drafting::Engine#skip!`
4
+ * Add ability to output event full drafting event log using CLI: `exe/basketball-draft -i tmp/draft-wip.json -l`
5
+ * Add ability to skip draft picks using CLI: `exe/basketball-draft -i tmp/draft-wip.json -x 1`
6
+
7
+ #### 0.0.3 - May 5th, 2023
8
+
9
+ * `Drafting::Engine#sim!` should return events
10
+ * Added `Drafting::Engine#undrafted_player_search`
11
+
12
+ #### 0.0.2 - May 4th, 2023
2
13
 
3
14
  * Remove autoloading in favor of require statements.
4
15
 
@@ -9,12 +9,16 @@ module Basketball
9
9
  module Drafting
10
10
  # Example:
11
11
  # exe/basketball-draft -o tmp/draft.json
12
- # exe/basketball-draft -i tmp/draft.json -o tmp/draft-wip.json -s 28 -p ONEALSH01,ONEALJE01 -t 10 -q PG
12
+ # exe/basketball-draft -i tmp/draft.json -o tmp/draft-wip.json -s 26 -p P-5,P-10 -t 10 -q PG
13
+ # exe/basketball-draft -i tmp/draft-wip.json -x 2
13
14
  # exe/basketball-draft -i tmp/draft-wip.json -r -t 10
14
15
  # exe/basketball-draft -i tmp/draft-wip.json -t 10 -q SG
15
16
  # exe/basketball-draft -i tmp/draft-wip.json -s 30 -t 10
16
17
  # exe/basketball-draft -i tmp/draft-wip.json -a -r
18
+ # exe/basketball-draft -i tmp/draft-wip.json -l
17
19
  class CLI
20
+ class PlayerNotFound < StandardError; end
21
+
18
22
  attr_reader :opts, :serializer, :io
19
23
 
20
24
  def initialize(args:, io: $stdout)
@@ -43,11 +47,13 @@ module Basketball
43
47
  io.puts('Draft is complete!')
44
48
  else
45
49
  io.puts("#{engine.remaining_picks} Remaining pick(s)")
46
- io.puts("Round #{engine.current_round} pick #{engine.current_round_pick} for #{engine.current_team}")
50
+ io.puts("Up Next: Round #{engine.current_round} pick #{engine.current_round_pick} for #{engine.current_team}")
47
51
  end
48
52
 
49
53
  write(engine)
50
54
 
55
+ log(engine)
56
+
51
57
  rosters(engine)
52
58
 
53
59
  query(engine)
@@ -70,6 +76,8 @@ module Basketball
70
76
  o.integer '-t', '--top', 'Output the top rated available players (default is 0).', default: 0
71
77
  o.string '-q', '--query', "Filter TOP by position: #{Position::ALL_VALUES.join(', ')}."
72
78
  o.bool '-r', '--rosters', 'Output all team rosters.', default: false
79
+ o.integer '-x', '--skip', 'Number of picks to skip (default is 0).', default: 0
80
+ o.bool '-l', '--log', 'Output event log.', default: false
73
81
 
74
82
  o.on '-h', '--help', 'Print out help, like this is doing right now.' do
75
83
  io.puts(o)
@@ -121,6 +129,15 @@ module Basketball
121
129
  end
122
130
  end
123
131
 
132
+ def log(engine)
133
+ return unless opts[:log]
134
+
135
+ io.puts
136
+ io.puts('Event Log')
137
+
138
+ puts engine.events
139
+ end
140
+
124
141
  # rubocop:disable Metrics/AbcSize
125
142
  def query(engine)
126
143
  top = opts[:top]
@@ -161,6 +178,8 @@ module Basketball
161
178
 
162
179
  player = engine.players.find { |p| p.id == id.to_s.upcase }
163
180
 
181
+ raise PlayerNotFound, "player not found by id: #{id}" unless player
182
+
164
183
  event = engine.pick!(player)
165
184
 
166
185
  io.puts(event)
@@ -168,6 +187,14 @@ module Basketball
168
187
  event_count += 1
169
188
  end
170
189
 
190
+ opts[:skip].times do
191
+ event = engine.skip!
192
+
193
+ io.puts(event)
194
+
195
+ event_count += 1
196
+ end
197
+
171
198
  engine.sim!(opts[:simulate]) do |event|
172
199
  io.puts(event)
173
200
 
@@ -96,14 +96,31 @@ module Basketball
96
96
  !done?
97
97
  end
98
98
 
99
+ def skip!
100
+ return if done?
101
+
102
+ event = SkipEvent.new(
103
+ id: SecureRandom.uuid,
104
+ team: current_team,
105
+ pick: current_pick,
106
+ round: current_round,
107
+ round_pick: current_round_pick
108
+ )
109
+
110
+ play!(event)
111
+
112
+ event
113
+ end
114
+
99
115
  def sim!(times = nil)
100
116
  counter = 0
117
+ events = []
101
118
 
102
119
  until done? || (times && counter >= times)
103
120
  team = current_team
104
121
 
105
122
  player = team.pick(
106
- undrafted_players:,
123
+ undrafted_player_search:,
107
124
  drafted_players: drafted_players(team),
108
125
  round: current_round
109
126
  )
@@ -122,9 +139,10 @@ module Basketball
122
139
  yield(event) if block_given?
123
140
 
124
141
  counter += 1
142
+ events << event
125
143
  end
126
144
 
127
- self
145
+ events
128
146
  end
129
147
 
130
148
  def pick!(player)
@@ -146,16 +164,24 @@ module Basketball
146
164
  players - drafted_players
147
165
  end
148
166
 
167
+ def undrafted_player_search
168
+ PlayerSearch.new(undrafted_players)
169
+ end
170
+
149
171
  private
150
172
 
151
173
  attr_reader :players_by_id, :teams_by_id
152
174
 
175
+ def player_events
176
+ events.select { |e| e.respond_to?(:player) }
177
+ end
178
+
153
179
  def internal_current_pick
154
180
  events.length + 1
155
181
  end
156
182
 
157
183
  def drafted_players(team = nil)
158
- events.each_with_object([]) do |e, memo|
184
+ player_events.each_with_object([]) do |e, memo|
159
185
  next unless team.nil? || e.team == team
160
186
 
161
187
  memo << e.player
@@ -166,15 +192,21 @@ module Basketball
166
192
  # rubocop:disable Metrics/CyclomaticComplexity
167
193
  # rubocop:disable Metrics/PerceivedComplexity
168
194
  def play!(event)
169
- raise AlreadyPickedError, "#{player} was already picked" if drafted_players.include?(event.player)
170
- raise DupeEventError, "#{event} is a dupe" if events.include?(event)
171
- raise EventOutOfOrder, "#{event} team cant pick right now" if event.team != current_team
172
- raise EventOutOfOrder, "#{event} has wrong pick" if event.pick != current_pick
173
- raise EventOutOfOrder, "#{event} has wrong round" if event.round != current_round
174
- raise EventOutOfOrder, "#{event} has wrong round_pick" if event.round_pick != current_round_pick
175
- raise UnknownTeamError, "#{team} doesnt exist" unless teams.include?(event.team)
176
- raise UnknownPlayerError, "#{player} doesnt exist" unless players.include?(event.player)
177
- raise EndOfDraftError, "#{total_picks} pick limit reached" if events.length > total_picks + 1
195
+ if event.respond_to?(:player) && drafted_players.include?(event.player)
196
+ raise AlreadyPickedError, "#{player} was already picked"
197
+ end
198
+
199
+ if event.respond_to?(:player) && !players.include?(event.player)
200
+ raise UnknownPlayerError, "#{event.player} doesnt exist"
201
+ end
202
+
203
+ raise DupeEventError, "#{event} is a dupe" if events.include?(event)
204
+ raise EventOutOfOrder, "#{event} team cant pick right now" if event.team != current_team
205
+ raise EventOutOfOrder, "#{event} has wrong pick" if event.pick != current_pick
206
+ raise EventOutOfOrder, "#{event} has wrong round" if event.round != current_round
207
+ raise EventOutOfOrder, "#{event} has wrong round_pick" if event.round_pick != current_round_pick
208
+ raise UnknownTeamError, "#{team} doesnt exist" unless teams.include?(event.team)
209
+ raise EndOfDraftError, "#{total_picks} pick limit reached" if events.length > total_picks + 1
178
210
 
179
211
  events << event
180
212
 
@@ -5,13 +5,15 @@ require_relative 'player'
5
5
  require_relative 'team'
6
6
  require_relative 'pick_event'
7
7
  require_relative 'sim_event'
8
+ require_relative 'skip_event'
8
9
 
9
10
  module Basketball
10
11
  module Drafting
11
12
  class EngineSerializer
12
13
  EVENT_CLASSES = {
13
14
  'PickEvent' => PickEvent,
14
- 'SimEvent' => SimEvent
15
+ 'SimEvent' => SimEvent,
16
+ 'SkipEvent' => SkipEvent
15
17
  }.freeze
16
18
 
17
19
  private_constant :EVENT_CLASSES
@@ -71,7 +73,7 @@ module Basketball
71
73
  roster.id,
72
74
  {
73
75
  events: roster.events.map(&:id),
74
- players: roster.events.map { |event| event.player.id }
76
+ players: roster.players.map(&:id)
75
77
  }
76
78
  ]
77
79
  end
@@ -101,7 +103,7 @@ module Basketball
101
103
  first_name: player.first_name,
102
104
  last_name: player.last_name,
103
105
  overall: player.overall,
104
- position: player.position.value
106
+ position: player.position.code
105
107
  }
106
108
  ]
107
109
  end
@@ -112,12 +114,13 @@ module Basketball
112
114
  {
113
115
  type: event.class.name.split('::').last,
114
116
  id: event.id,
115
- player: event.player.id,
116
117
  team: event.team.id,
117
118
  pick: event.pick,
118
119
  round: event.round,
119
120
  round_pick: event.round_pick
120
- }
121
+ }.tap do |hash|
122
+ hash[:player] = event.player.id if event.respond_to?(:player)
123
+ end
121
124
  end
122
125
  end
123
126
 
@@ -162,12 +165,15 @@ module Basketball
162
165
  def deserialize_events(json, players, teams)
163
166
  (json.dig(:engine, :events) || []).map do |event_hash|
164
167
  event_opts = event_hash.slice(:id, :pick, :round, :round_pick).merge(
165
- player: players.find { |p| p.id == event_hash[:player] },
166
168
  team: teams.find { |t| t.id == event_hash[:team] }
167
169
  )
168
170
 
169
171
  class_constant = EVENT_CLASSES.fetch(event_hash[:type])
170
172
 
173
+ if [PickEvent, SimEvent].include?(class_constant)
174
+ event_opts[:player] = players.find { |p| p.id == event_hash[:player] }
175
+ end
176
+
171
177
  class_constant.new(**event_opts)
172
178
  end
173
179
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'entity'
4
-
5
3
  module Basketball
6
4
  module Drafting
7
5
  class Event < Entity
@@ -2,16 +2,18 @@
2
2
 
3
3
  module Basketball
4
4
  module Drafting
5
- class FrontOffice
5
+ class FrontOffice < ValueObject
6
6
  MAX_DEPTH = 3
7
7
  MAX_FUZZ = 2
8
8
  MAX_POSITIONS = 12
9
9
 
10
10
  private_constant :MAX_DEPTH, :MAX_FUZZ, :MAX_POSITIONS
11
11
 
12
- attr_reader :prioritized_positions, :fuzz, :depth
12
+ attr_reader_value :prioritized_positions, :fuzz, :depth
13
13
 
14
14
  def initialize(prioritized_positions: [], fuzz: rand(0..MAX_FUZZ), depth: rand(0..MAX_DEPTH))
15
+ super()
16
+
15
17
  @fuzz = fuzz.to_i
16
18
  @depth = depth.to_i
17
19
  @prioritized_positions = prioritized_positions
@@ -24,44 +26,36 @@ module Basketball
24
26
  freeze
25
27
  end
26
28
 
27
- def to_s
28
- "#{prioritized_positions.map(&:to_s).join(',')} (F:#{fuzz} D:#{depth})"
29
- end
30
-
31
- def pick(undrafted_players:, drafted_players:, round:)
29
+ def pick(undrafted_player_search:, drafted_players:, round:)
32
30
  players = []
33
31
 
34
- players = adaptive_search(undrafted_players:, drafted_players:) if depth >= round
35
- players = balanced_search(undrafted_players:, drafted_players:) if players.empty?
36
- players = top_players(undrafted_players:) if players.empty?
32
+ players = adaptive_search(undrafted_player_search:, drafted_players:) if depth >= round
33
+ players = balanced_search(undrafted_player_search:, drafted_players:) if players.empty?
34
+ players = top_players(undrafted_player_search:) if players.empty?
37
35
 
38
36
  players[0..fuzz].sample
39
37
  end
40
38
 
41
39
  private
42
40
 
43
- def adaptive_search(undrafted_players:, drafted_players:)
44
- search = PlayerSearch.new(undrafted_players)
45
-
41
+ def adaptive_search(undrafted_player_search:, drafted_players:)
46
42
  drafted_positions = drafted_players.map(&:position)
47
43
 
48
- search.query(exclude_positions: drafted_positions)
44
+ undrafted_player_search.query(exclude_positions: drafted_positions)
49
45
  end
50
46
 
51
- def balanced_search(undrafted_players:, drafted_players:)
52
- search = PlayerSearch.new(undrafted_players)
53
-
47
+ def balanced_search(undrafted_player_search:, drafted_players:)
54
48
  players = []
55
49
 
56
50
  # Try to find best pick for exact desired position.
57
51
  # If you cant find one, then move to the next desired position until the end of the queue
58
52
  available_prioritized_positions(drafted_players:).each do |position|
59
- players = search.query(position:)
53
+ players = undrafted_player_search.query(position:)
60
54
 
61
55
  break if players.any?
62
56
  end
63
57
 
64
- players = players.any? ? players : search.query
58
+ players = players.any? ? players : undrafted_player_search.query
65
59
  end
66
60
 
67
61
  def all_random_positions
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'entity'
4
-
5
3
  module Basketball
6
4
  module Drafting
7
5
  class Player < Entity
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Basketball
4
4
  module Drafting
5
- class Position
5
+ class Position < ValueObject
6
6
  extend Forwardable
7
7
 
8
8
  class << self
@@ -17,25 +17,18 @@ module Basketball
17
17
  FRONT_COURT_VALUES = %w[PF C].to_set.freeze
18
18
  ALL_VALUES = (BACK_COURT_VALUES.to_a + FRONT_COURT_VALUES.to_a).to_set.freeze
19
19
 
20
- attr_reader :value
20
+ attr_reader_value :code
21
21
 
22
- def_delegators :value, :to_s
22
+ def_delegators :code, :to_s
23
23
 
24
- def initialize(value)
25
- @value = value.to_s.upcase
24
+ def initialize(code)
25
+ super()
26
26
 
27
- raise InvalidPositionError, "Unknown position value: #{@value}" unless ALL_VALUES.include?(@value)
27
+ @code = code.to_s.upcase
28
28
 
29
- freeze
30
- end
29
+ raise InvalidPositionError, "Unknown position code: #{@code}" unless ALL_VALUES.include?(@code)
31
30
 
32
- def ==(other)
33
- value == other.value
34
- end
35
- alias eql? ==
36
-
37
- def hash
38
- value.hash
31
+ freeze
39
32
  end
40
33
  end
41
34
  end
@@ -2,16 +2,18 @@
2
2
 
3
3
  module Basketball
4
4
  module Drafting
5
- class Roster
5
+ class Roster < ValueObject
6
6
  extend Forwardable
7
7
 
8
8
  class WrongTeamEventError < StandardError; end
9
9
 
10
- attr_reader :team, :events
10
+ attr_reader_value :team, :events
11
11
 
12
12
  def_delegators :team, :id
13
13
 
14
14
  def initialize(team:, events: [])
15
+ super()
16
+
15
17
  raise ArgumentError, 'team is required' unless team
16
18
 
17
19
  other_teams_pick_event_ids = events.reject { |e| e.team == team }.map(&:id)
@@ -25,8 +27,12 @@ module Basketball
25
27
  @events = events
26
28
  end
27
29
 
30
+ def player_events
31
+ events.select { |e| e.respond_to?(:player) }
32
+ end
33
+
28
34
  def players
29
- events.map(&:player)
35
+ player_events.map(&:player)
30
36
  end
31
37
 
32
38
  def to_s
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'event'
4
-
5
3
  module Basketball
6
4
  module Drafting
7
5
  class SimEvent < Event
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'event'
4
+
5
+ module Basketball
6
+ module Drafting
7
+ class SkipEvent < Event
8
+ def to_s
9
+ "skipped #{super}"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'entity'
4
-
5
3
  module Basketball
6
4
  module Drafting
7
5
  class Team < Entity
6
+ extend Forwardable
7
+
8
8
  attr_reader :name, :front_office
9
9
 
10
+ def_delegators :front_office, :pick
11
+
10
12
  def initialize(id:, name: '', front_office: FrontOffice.new)
11
13
  super(id)
12
14
 
@@ -21,10 +23,6 @@ module Basketball
21
23
  def to_s
22
24
  "[#{super}] #{name}"
23
25
  end
24
-
25
- def pick(undrafted_players:, drafted_players:, round:)
26
- front_office.pick(undrafted_players:, drafted_players:, round:)
27
- end
28
26
  end
29
27
  end
30
28
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ class Entity
5
+ extend Forwardable
6
+ include Comparable
7
+
8
+ attr_reader :id
9
+
10
+ def_delegators :id, :to_s
11
+
12
+ def initialize(id)
13
+ raise ArgumentError, 'id is required' if id.to_s.empty?
14
+
15
+ @id = id.to_s.upcase
16
+ end
17
+
18
+ def <=>(other)
19
+ id <=> other.id
20
+ end
21
+
22
+ def ==(other)
23
+ id == other.id
24
+ end
25
+ alias eql? ==
26
+
27
+ def hash
28
+ id.hash
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basketball
4
+ class ValueObject
5
+ include Comparable
6
+
7
+ # NOTE: This current implementation most likely does not work for deep inheritance trees.
8
+ class << self
9
+ def value_keys
10
+ @value_keys ||= []
11
+ end
12
+
13
+ def sorted_value_keys
14
+ value_keys.sort
15
+ end
16
+
17
+ def attr_reader_value(*keys)
18
+ keys.each { |k| value_keys << k.to_sym }
19
+
20
+ attr_reader(*keys)
21
+ end
22
+ end
23
+
24
+ def to_s
25
+ to_h.map { |k, v| "#{k}: #{v}" }.join(', ')
26
+ end
27
+
28
+ def to_h
29
+ self.class.sorted_value_keys.to_h { |k| [k, send(k)] }
30
+ end
31
+
32
+ def [](key)
33
+ to_h[key]
34
+ end
35
+
36
+ def <=>(other)
37
+ all_sorted_values <=> other.all_sorted_values
38
+ end
39
+
40
+ def ==(other)
41
+ to_h == other.to_h
42
+ end
43
+ alias eql? ==
44
+
45
+ def hash
46
+ all_sorted_values.map(&:hash).hash
47
+ end
48
+
49
+ def all_sorted_values
50
+ self.class.sorted_value_keys.map { |k| self[k] }
51
+ end
52
+ end
53
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basketball
4
- VERSION = '0.0.2'
4
+ VERSION = '0.0.4'
5
5
  end
data/lib/basketball.rb CHANGED
@@ -7,4 +7,9 @@ require 'json'
7
7
  require 'securerandom'
8
8
  require 'slop'
9
9
 
10
+ # Top-level
11
+ require_relative 'basketball/entity'
12
+ require_relative 'basketball/value_object'
13
+
14
+ # Submodules
10
15
  require_relative 'basketball/drafting'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: basketball
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Ruggio
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-04 00:00:00.000000000 Z
11
+ date: 2023-05-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faker
@@ -207,7 +207,6 @@ files:
207
207
  - lib/basketball/drafting/cli.rb
208
208
  - lib/basketball/drafting/engine.rb
209
209
  - lib/basketball/drafting/engine_serializer.rb
210
- - lib/basketball/drafting/entity.rb
211
210
  - lib/basketball/drafting/event.rb
212
211
  - lib/basketball/drafting/front_office.rb
213
212
  - lib/basketball/drafting/pick_event.rb
@@ -216,7 +215,10 @@ files:
216
215
  - lib/basketball/drafting/position.rb
217
216
  - lib/basketball/drafting/roster.rb
218
217
  - lib/basketball/drafting/sim_event.rb
218
+ - lib/basketball/drafting/skip_event.rb
219
219
  - lib/basketball/drafting/team.rb
220
+ - lib/basketball/entity.rb
221
+ - lib/basketball/value_object.rb
220
222
  - lib/basketball/version.rb
221
223
  homepage: https://github.com/mattruggio/basketball
222
224
  licenses:
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Basketball
4
- module Drafting
5
- class Entity
6
- extend Forwardable
7
- include Comparable
8
-
9
- attr_reader :id
10
-
11
- def_delegators :id, :to_s
12
-
13
- def initialize(id)
14
- raise ArgumentError, 'id is required' if id.to_s.empty?
15
-
16
- @id = id.to_s.upcase
17
- end
18
-
19
- def <=>(other)
20
- id <=> other.id
21
- end
22
-
23
- def ==(other)
24
- id == other.id
25
- end
26
- alias eql? ==
27
-
28
- def hash
29
- id.hash
30
- end
31
- end
32
- end
33
- end