stable_match 0.1.1 → 0.2.0

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.
@@ -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