stable_match 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MWI3M2IwN2ZiZGJiYzcwZGIyMDcxOWY1MTViYmZiYzJhYTliMDJlZg==
5
+ data.tar.gz: !binary |-
6
+ NmMyZjk0MDI0NjA0NDZhOWE1M2Q2M2Y4NThmMGVmM2QyMzc4NTdiZg==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ NTAyNjhjYjgxYzYyN2E2ODZlOWE3NzFkODJhMjk2MmRkNWQ5MTE5Y2I4ZTlh
10
+ OWJmZmMwZTEyNmY3MjFlYTcxMWMzMmNjZGQ5MDc4ODZhZWFjMzI3YTRmYWUw
11
+ NTdiZjNiNWVmZGE1MGY4Y2JmNzY1NGUzNTM4YjIzNzQ5NGM5NDA=
12
+ data.tar.gz: !binary |-
13
+ ZTQ1MGIxZjQyZTZmOGY3ODBlOGFhNWEzNzVlMzkzMGExN2Y2ODkyOTU1M2Ix
14
+ Y2FkYTNlZTUyOWM5ZmNkNjUwODQwM2E1MGJmNWUzY2M1MmIxMzJiZTM0YzAy
15
+ ZDRjMzMyYmY1Yjc5N2MwNGY3YzgzZDE5ZmVjZDczMGMxNzJkOTU=
data/Gemfile CHANGED
@@ -1,6 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
-
4
- group :development do
5
- gem "pry" , "~> 0.9.9.4"
6
- end
3
+ gem 'pry'
data/README.md CHANGED
@@ -2,6 +2,52 @@
2
2
 
3
3
  A generic implementation of the Stable Match class of algorithms.
4
4
 
5
+ ## Usage
6
+
7
+ * See: `examples/example_1.rb`
8
+ * See: `test/functional/nrmp_test.rb`
9
+ * See: `test/functional/stable_marriage_test.rb`
10
+ * Run: `rake example`
11
+
12
+ The idea behind stable matching is the following: You have two sets of
13
+ data that you would like to find the most ideal matching between. Stable
14
+ matching means that candidates from either set might remain unmatched.
15
+ Unstable matching means that matches are forced even if they are not
16
+ ideal.
17
+
18
+ Your inputs are two hashes where the keys are known as 'target's to
19
+ StableMatch. Targets ideally are domain specific objects to your
20
+ application. The value in the hash for each target is an array of
21
+ preferences for the target. (See the note below about when preferences
22
+ are themselves each an Array object.)
23
+
24
+ Preferences are an ordered array of 'target's belonging to the other set
25
+ such that, the lower the index, the higher the preference. This is required
26
+ and checked at runtime. It can be an empty array, but all preferences must
27
+ belong to the other set.
28
+
29
+ The final argument that a `Candidate` object can be instantiated with is
30
+ called `match_positions` and equates to the number of matches that the
31
+ given target can acquire. This is optional and defaults to 1.
32
+
33
+ ### In-Depth Example (IN PROGRESS)
34
+
35
+ Let's talk about a dog-walker example. We have two domain classes: Dog
36
+ and Walker. Let's say that the preferences will be determined by the
37
+ weight and geographic location of the dog.
38
+
39
+ ```
40
+ TODO !!
41
+ FIXME !!
42
+ this needs finished
43
+ ```
44
+
45
+ ### Gotchas
46
+
47
+ * To match against objects that _are_ arrays, they'll need to be
48
+ preemptively wrapped in another array when passing them a runner. See
49
+ the NRMP test in `test/functional` for example.
50
+
5
51
  ## Installation
6
52
 
7
53
  ### Without Bundler
@@ -26,13 +72,6 @@ And then execute:
26
72
  $ bundle
27
73
  ```
28
74
 
29
- ## Usage
30
-
31
- * See: `examples/example_1.rb`
32
- * Run: `rake example`
33
-
34
- TODO: Write more usage instructions here
35
-
36
75
  ## References
37
76
 
38
77
  * [http://halfamind.aghion.com/the-national-resident-matching-programs-nrmp](http://halfamind.aghion.com/the-national-resident-matching-programs-nrmp)
data/Rakefile CHANGED
@@ -1,18 +1,36 @@
1
1
  #!/usr/bin/env rake
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rake/testtask"
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
5
 
6
6
  namespace :examples do
7
- desc "Run any example"
7
+ desc 'Run any example'
8
+ task :all do
9
+ Dir[ "#{ File.dirname File.expand_path( __FILE__ ) }/examples/**/*.rb" ].each do | file |
10
+ puts
11
+ puts '============================================='
12
+ puts "Start Example: #{ file }"
13
+ puts '============================================='
14
+ puts
15
+ system "ruby #{ file }"
16
+ puts
17
+ puts '============================================='
18
+ puts "End Example: #{ file }"
19
+ puts '============================================='
20
+ puts
21
+ end
22
+ end
23
+
24
+ desc 'Run any example'
8
25
  task :any do
9
26
  exec "ruby #{ Dir[ "#{ File.dirname File.expand_path( __FILE__ ) }/examples/**/*.rb" ].first }"
10
27
  end
11
28
  end
12
- task :example => "examples:any"
29
+ task :example => 'examples:any'
30
+ task :examples => 'examples:all'
13
31
 
14
32
  namespace :test do
15
- desc "Run All The Tests"
33
+ desc 'Run All The Tests'
16
34
  task :all do
17
35
  suites = %w(
18
36
  test:functional
@@ -20,59 +38,36 @@ namespace :test do
20
38
  )
21
39
 
22
40
  suites.each do | suite |
23
- puts "=========================================="
41
+ puts '=========================================='
24
42
  puts "Running: #{ suite }"
25
- puts "=========================================="
43
+ puts '=========================================='
26
44
  begin
27
45
  Rake::Task[ suite ].invoke
28
46
  rescue
29
- puts "=========================================="
47
+ puts '=========================================='
30
48
  puts "FAILED: #{ suite }"
31
49
  ensure
32
- puts "=========================================="
50
+ puts '=========================================='
33
51
  puts "\n"
34
52
  end
35
53
  end
36
54
  end
37
55
 
38
- desc "Run The Functional Tests"
56
+ desc 'Run The Functional Tests'
39
57
  Rake::TestTask.new( :functional ) do | t |
40
- t.libs << [ "test" ]
41
- t.pattern = "test/functional/**/*_test.rb"
58
+ t.libs << [ 'test' ]
59
+ t.pattern = 'test/functional/**/*_test.rb'
42
60
  t.verbose = true
43
61
  end
44
62
 
45
- desc "Run The Unit Tests"
63
+ desc 'Run The Unit Tests'
46
64
  Rake::TestTask.new( :unit ) do | t |
47
- t.libs << [ "test" ]
48
- t.pattern = "test/unit/**/*_test.rb"
65
+ t.libs << [ 'test' ]
66
+ t.pattern = 'test/unit/**/*_test.rb'
49
67
  t.verbose = true
50
68
  end
51
-
52
- desc "Start a watcher process that runs the tests on file changes in `lib` or `test` dirs"
53
- task :watch do
54
- exec "rego {lib,test} -- rake"
55
- end
56
69
  end
57
70
 
58
- desc "Run All The Tests"
59
- task :test => "test:all"
71
+ desc 'Run All The Tests'
72
+ task :test => 'test:all'
60
73
  task :default => :test
61
-
62
- BEGIN {
63
- def load_bundle_env!
64
- is_blank = lambda { |o| o.nil? or o.size < 1 }
65
- ENV[ "BUNDLE_GEMFILE" ] = File.expand_path( "Gemfile" ) unless !is_blank[ ENV[ "BUNDLE_GEMFILE" ] ]
66
- begin
67
- require "bundler"
68
- rescue
69
- require "rubygems"
70
- require "bundler"
71
- ensure
72
- raise LoadError.new( "Bundler not found!" ) unless defined?( Bundler )
73
- require "bundler/setup"
74
- end
75
- end
76
-
77
- load_bundle_env! unless defined?( Bundler )
78
- }
@@ -4,21 +4,21 @@ class Test
4
4
  ## This is a modified version of the example NRMP case
5
5
  #
6
6
  PROGRAMS = {
7
- 'city' => { :match_positions => 1, :preferences => ['garcia', 'hassan', 'eastman', 'brown', 'chen', 'davis', 'ford']},
8
- 'general' => { :match_positions => 3, :preferences => ['brown', 'eastman', 'hassan', 'anderson', 'chen', 'davis', 'garcia']},
9
- 'mercy' => { :match_positions => 1, :preferences => ['chen', 'garcia', 'brown']},
10
- 'state' => { :match_positions => 1, :preferences => ['anderson', 'brown', 'eastman', 'chen', 'hassan', 'ford', 'davis', 'garcia']}
7
+ 'city' => ['garcia', 'hassan', 'eastman', 'brown', 'chen', 'davis', 'ford'],
8
+ 'general' => [ ['brown', 'eastman', 'hassan', 'anderson', 'chen', 'davis', 'garcia'] , 3 ],
9
+ 'mercy' => [ ['chen', 'garcia', 'brown'] , 2 ],
10
+ 'state' => ['anderson', 'brown', 'eastman', 'chen', 'hassan', 'ford', 'davis', 'garcia']
11
11
  }
12
12
 
13
13
  APPLICANTS = {
14
- 'anderson' => { :match_positions => 1 , :preferences => ['state', 'city'] },
15
- 'brown' => { :match_positions => 2 , :preferences => ['city', 'mercy', 'state'] },
16
- 'chen' => { :match_positions => 1 , :preferences => ['city', 'mercy'] },
17
- 'davis' => { :match_positions => 1 , :preferences => ['mercy', 'city', 'general', 'state'] },
18
- 'eastman' => { :match_positions => 1 , :preferences => ['city', 'mercy', 'state', 'general'] },
19
- 'ford' => { :match_positions => 1 , :preferences => ['city', 'general', 'mercy', 'state'] },
20
- 'garcia' => { :match_positions => 1 , :preferences => ['city', 'mercy', 'state', 'general'] },
21
- 'hassan' => { :match_positions => 1 , :preferences => ['state', 'city', 'mercy', 'general' ] }
14
+ 'anderson' => ['state', 'city'],
15
+ 'brown' => ['city', 'mercy', 'state'],
16
+ 'chen' => ['city', 'mercy'],
17
+ 'davis' => ['mercy', 'city', 'general', 'state'],
18
+ 'eastman' => ['city', 'mercy', 'state', 'general'],
19
+ 'ford' => ['city', 'general', 'mercy', 'state'],
20
+ 'garcia' => ['city', 'mercy', 'state', 'general'],
21
+ 'hassan' => ['state', 'city', 'mercy', 'general' ]
22
22
  }
23
23
 
24
24
  def self.main
@@ -1,12 +1,9 @@
1
- require "fattr"
2
- require "map"
3
-
4
1
  module StableMatch
5
2
  def self.run( *args , &block )
6
3
  Runner.run *args , &block
7
4
  end
8
5
  end
9
6
 
10
- require "stable_match/candidate"
11
- require "stable_match/runner"
12
- require "stable_match/version"
7
+ require 'stable_match/candidate'
8
+ require 'stable_match/runner'
9
+ require 'stable_match/version'
@@ -1,45 +1,50 @@
1
+ require 'yaml'
2
+ require 'stable_match/util/initialize_with_defaults'
3
+
1
4
  module StableMatch
2
5
  class Candidate
3
- ## The matches this candidate has attained
4
- #
5
- fattr( :matches ){ [] }
6
-
7
- ## The number of matches the candidate is able to make
8
- #
9
- fattr( :match_positions ){ 1 }
10
-
11
- ## The tracked position for preferences that have been attempted for matches
12
- #
13
- fattr( :preference_position ){ -1 }
14
-
15
- ## An ordered array of candidates where, the lower the index, the higher the preference
16
- #
17
- # WARNING -- this may be instantiated with targets at first that get converted to Candidates
18
- #
19
- fattr( :preferences ){ [] }
20
-
21
- ## The array to track proposals that have already been made
22
- #
23
- fattr( :proposals ){ [] }
24
-
25
- ## The object that the candidate represents
26
- #
27
- fattr :target
28
-
29
- def initialize(*args)
30
- options = Map.opts(args)
31
- @target = options.target rescue args.shift or raise ArgumentError.new( "No `target` provided!" )
32
- @preferences = options.preferences rescue args.shift or raise ArgumentError.new( "No `preferences` provided!" )
33
-
34
- @match_positions = options.match_positions if options.get( :match_positions )
6
+ include Util::InitializeWithDefaults
7
+
8
+ # The matches this candidate has attained
9
+ attr_accessor :matches
10
+
11
+ # The number of matches the candidate is able to make
12
+ attr_accessor :match_positions
13
+
14
+ # The tracked position for preferences that have been attempted for matches
15
+ attr_accessor :preference_position
16
+
17
+ # An ordered array of candidates where, the lower the index, the higher the preference
18
+ #
19
+ # WARNING -- this may be instantiated with targets at first that get converted to Candidates
20
+ attr_accessor :preferences
21
+
22
+ # The array to track proposals that have already been made
23
+ attr_accessor :proposals
24
+
25
+ # An ordered array of raw preference values
26
+ attr_accessor :raw_preferences
27
+
28
+ # The object that the candidate represents
29
+ attr_accessor :target
30
+
31
+ def initialize( target , raw_preferences , match_positions = 1 )
32
+ @target = target
33
+ @raw_preferences = raw_preferences
34
+ @match_positions = match_positions
35
35
  end
36
36
 
37
- ## Candidate#better_match?
38
- #
39
- # Is the passed candidate a better match than any of the current matches?
40
- #
41
- # ARG: `other` -- another Candidate instance to check against the current set
42
- #
37
+ def initialize_defaults!
38
+ @matches ||= []
39
+ @preference_position ||= -1
40
+ @preferences ||= []
41
+ @proposals ||= []
42
+ @raw_preferences ||= []
43
+ end
44
+
45
+ # Is the passed candidate a better match than any of the current matches?
46
+ #
47
+ # ARG: `other` -- another Candidate instance to check against the current set
43
48
  def better_match?( other )
44
49
  return true if prefers?( other ) && free?
45
50
  preference_index = preferences.index other
@@ -47,141 +52,106 @@ module StableMatch
47
52
  preference_index and match_preference_indexes.any? { |i| i > preference_index }
48
53
  end
49
54
 
50
- ## Candidate#exhausted_preferences?
51
- #
52
- # Have all possible preferences been cycled through?
53
- #
55
+ # Have all possible preferences been cycled through?
54
56
  def exhausted_preferences?
55
57
  preference_position >= preferences.size - 1
56
58
  end
57
59
 
58
- ## Candidate#free?
59
- #
60
- # Is there available positions for more matches based on the defined `match_positions`?
61
- #
60
+ # Is there available positions for more matches based on the defined `match_positions`?
62
61
  def free?
63
62
  matches.length < match_positions
64
63
  end
65
64
 
66
- ## Candidate#free!
67
- #
68
- # Delete the least-preferred candidate from the matches array
69
- #
65
+ # Delete the least-preferred candidate from the matches array
70
66
  def free!
71
67
  return false if matches.empty?
72
68
  match_preference_indexes = matches.map { | match | preferences.index match }
73
69
  max = match_preference_indexes.max # The index of the match with the lowest preference
74
70
  candidate_to_reject = preferences[ max ]
75
71
 
76
- ## Delete from both sides
77
- #
72
+ # Delete from both sides
78
73
  candidate_to_reject.matches.delete self
79
74
  self.matches.delete candidate_to_reject
80
75
  end
81
76
 
82
- ## Candidate#full?
83
- #
84
- # Are there no remaining positions available for matches?
85
- #
77
+ # Are there no remaining positions available for matches?
86
78
  def full?
87
79
  !free?
88
80
  end
89
81
 
90
82
  def inspect
91
- require "yaml"
92
-
93
83
  {
94
84
  'target' => target,
95
85
  'match_positions' => match_positions,
96
86
  'matches' => matches.map( &:target ),
97
87
  'preference_position' => preference_position,
98
88
  'preferences' => preferences.map( &:target ),
99
- 'proposals' => proposals.map( &:target )
89
+ 'proposals' => proposals.map( &:target ),
90
+ 'raw_preferences' => raw_preferences
100
91
  }.to_yaml
101
92
  end
102
93
 
103
- ## Candidate#match!
104
- #
105
- # Match with another Candidate
106
- #
107
- # ARG: `other` -- another Candidate instance to match with
108
- #
94
+ alias_method \
95
+ :pretty_inspect,
96
+ :inspect
97
+
98
+ # Match with another Candidate
99
+ #
100
+ # ARG: `other` -- another Candidate instance to match with
109
101
  def match!( other )
110
102
  return false unless prefers?( other ) && !matched?( other )
111
103
  matches << other
112
104
  other.matches << self
113
105
  end
114
106
 
115
- ## Candidate#matched?
116
- #
117
- # If no argument is passed: Do we have at least as many matches as available `match_positions`?
118
- # If another Candidate is passed: Is that candidate included in the matches?
119
- #
120
- # ARG: `other` [optional] -- another Candidate instance
121
- #
107
+ # If no argument is passed: Do we have at least as many matches as available `match_positions`?
108
+ # If another Candidate is passed: Is that candidate included in the matches?
109
+ #
110
+ # ARG: `other` [optional] -- another Candidate instance
122
111
  def matched?( other = nil )
123
112
  return full? if other.nil?
124
113
  matches.include? other
125
114
  end
126
115
 
127
- ## Candidate#next_preference!
128
- #
129
- # Increment `preference_position` and return the preference at that position
130
- #
116
+ # Increment `preference_position` and return the preference at that position
131
117
  def next_preference!
132
118
  self.preference_position += 1
133
119
  preferences.fetch preference_position
134
120
  end
135
121
 
136
- ## Candidate#prefers?
137
- #
138
- # Is there a preference for the passed Candidate?
139
- #
140
- # ARG: `other` -- another Candidate instance
141
- #
122
+ # Is there a preference for the passed Candidate?
123
+ #
124
+ # ARG: `other` -- another Candidate instance
142
125
  def prefers?( other )
143
126
  preferences.include? other
144
127
  end
145
128
 
146
- ## Candidate#propose_to
147
- #
148
- # Track that a proposal was made then ask the other Candidate to respond to a proposal
149
- #
150
- # ARG: `other` -- another Candidate instance
151
- #
129
+ # Track that a proposal was made then ask the other Candidate to respond to a proposal
130
+ #
131
+ # ARG: `other` -- another Candidate instance
152
132
  def propose_to( other )
153
133
  proposals << other
154
134
  other.respond_to_proposal_from self
155
135
  end
156
136
 
157
- ## Candidate#propose_to_next_preference
158
- #
159
- # Send a proposal to the next tracked preference
160
- #
161
137
  def propose_to_next_preference
162
138
  propose_to next_preference!
163
139
  end
164
140
 
165
- ## Candidate#respond_to_proposal_from
166
- #
167
- # Given another candidate, respond properly based on current state
168
- #
169
- # ARG: `other` -- another Candidate instance
170
- #
141
+ # Given another candidate, respond properly based on current state
142
+ #
143
+ # ARG: `other` -- another Candidate instance
171
144
  def respond_to_proposal_from( other )
172
145
  case
173
- ## Is there a preference for the candidate?
174
- #
146
+ # Is there a preference for the candidate?
175
147
  when !prefers?( other )
176
148
  false
177
149
 
178
- ## Are there available positions for more matches?
179
- #
150
+ # Are there available positions for more matches?
180
151
  when free?
181
152
  match! other
182
153
 
183
- ## Is the passed Candidate a better match than any other match?
184
- #
154
+ # Is the passed Candidate a better match than any other match?
185
155
  when better_match?( other )
186
156
  free!
187
157
  match! other