stable_match 0.1.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.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .rvmrc
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ vendor/bundle
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ group :development do
5
+ gem "pry" , "~> 0.9.9.4"
6
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Ryan Cook
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # StableMatch
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'stable_match'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install stable_match
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env rake
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ namespace :examples do
7
+ desc "Run any example"
8
+ task :any do
9
+ exec "ruby #{ Dir[ "#{ File.dirname File.expand_path( __FILE__ ) }/examples/**/*.rb" ].first }"
10
+ end
11
+ end
12
+ task :example => "examples:any"
13
+
14
+ namespace :test do
15
+ desc "Run All The Tests"
16
+ task :all => [ "test:unit" ]
17
+
18
+ desc "Run The Unit Tests"
19
+ Rake::TestTask.new( :unit ) do | t |
20
+ t.libs = [ "test" ]
21
+ t.pattern = "test/unit/**/*_test.rb"
22
+ t.verbose = true
23
+ end
24
+
25
+ desc "Start a watcher process that runs the tests on file changes in `lib` or `test` dirs"
26
+ task :watch do
27
+ exec "rego {lib,test} -- rake"
28
+ end
29
+ end
30
+
31
+ desc "Run All The Tests"
32
+ task :test => "test:all"
33
+ task :default => :test
34
+
35
+ BEGIN {
36
+ def load_bundle_env!
37
+ is_blank = lambda { |o| o.nil? or o.size < 1 }
38
+ ENV[ "BUNDLE_GEMFILE" ] = File.expand_path( "Gemfile" ) unless !is_blank[ ENV[ "BUNDLE_GEMFILE" ] ]
39
+ begin
40
+ require "bundler"
41
+ rescue
42
+ require "rubygems"
43
+ require "bundler"
44
+ ensure
45
+ raise LoadError.new( "Bundler not found!" ) unless defined?( Bundler )
46
+ require "bundler/setup"
47
+ end
48
+ end
49
+
50
+ load_bundle_env! unless defined?( Bundler )
51
+ }
@@ -0,0 +1,27 @@
1
+ require "stable_match"
2
+
3
+ class Test
4
+ PROGRAMS = {
5
+ 'city' => { :match_positions => 1, :preferences => ['garcia', 'hassan', 'eastman', 'brown', 'chen', 'davis', 'ford']},
6
+ 'general' => { :match_positions => 3, :preferences => ['brown', 'eastman', 'hassan', 'anderson', 'chen', 'davis', 'garcia']},
7
+ 'mercy' => { :match_positions => 1, :preferences => ['chen', 'garcia', 'brown']},
8
+ 'state' => { :match_positions => 1, :preferences => ['anderson', 'brown', 'eastman', 'chen', 'hassan', 'ford', 'davis', 'garcia']}
9
+ }
10
+
11
+ APPLICANTS = {
12
+ 'anderson' => { :match_positions => 1 , :preferences => ['state', 'city'] },
13
+ 'brown' => { :match_positions => 2 , :preferences => ['city', 'mercy', 'state'] },
14
+ 'chen' => { :match_positions => 1 , :preferences => ['city', 'mercy'] },
15
+ 'davis' => { :match_positions => 1 , :preferences => ['mercy', 'city', 'general', 'state'] },
16
+ 'eastman' => { :match_positions => 1 , :preferences => ['city', 'mercy', 'state', 'general'] },
17
+ 'ford' => { :match_positions => 1 , :preferences => ['city', 'general', 'mercy', 'state'] },
18
+ 'garcia' => { :match_positions => 1 , :preferences => ['city', 'mercy', 'state', 'general'] },
19
+ 'hassan' => { :match_positions => 1 , :preferences => ['state', 'city', 'mercy', 'general' ] }
20
+ }
21
+
22
+ def self.main
23
+ p StableMatch.run(APPLICANTS, PROGRAMS)
24
+ end
25
+ end
26
+
27
+ Test.main
@@ -0,0 +1,12 @@
1
+ require "fattr"
2
+ require "map"
3
+
4
+ module StableMatch
5
+ def self.run( *args , &block )
6
+ Runner.run *args , &block
7
+ end
8
+ end
9
+
10
+ require "stable_match/candidate"
11
+ require "stable_match/runner"
12
+ require "stable_match/version"
@@ -0,0 +1,194 @@
1
+ module StableMatch
2
+ 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 )
35
+ end
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
+ #
43
+ def better_match?( other )
44
+ return true if prefers?( other ) && free?
45
+ preference_index = preferences.index other
46
+ match_preference_indexes = matches.map { | match | preferences.index match }
47
+ preference_index and match_preference_indexes.any? { |i| i > preference_index }
48
+ end
49
+
50
+ ## Candidate#exhausted_preferences?
51
+ #
52
+ # Have all possible preferences been cycled through?
53
+ #
54
+ def exhausted_preferences?
55
+ preference_position >= preferences.size - 1
56
+ end
57
+
58
+ ## Candidate#free?
59
+ #
60
+ # Is there available positions for more matches based on the defined `match_positions`?
61
+ #
62
+ def free?
63
+ matches.length < match_positions
64
+ end
65
+
66
+ ## Candidate#free!
67
+ #
68
+ # Delete the least-preferred candidate from the matches array
69
+ #
70
+ def free!
71
+ return false if matches.empty?
72
+ match_preference_indexes = matches.map { | match | preferences.index match }
73
+ max = match_preference_indexes.max # The index of the match with the lowest preference
74
+ candidate_to_reject = preferences[ max ]
75
+
76
+ ## Delete from both sides
77
+ #
78
+ candidate_to_reject.matches.delete self
79
+ self.matches.delete candidate_to_reject
80
+ end
81
+
82
+ ## Candidate#full?
83
+ #
84
+ # Are there no remaining positions available for matches?
85
+ #
86
+ def full?
87
+ !free?
88
+ end
89
+
90
+ def inspect
91
+ require "yaml"
92
+
93
+ {
94
+ :target => target,
95
+ :match_positions => match_positions,
96
+ :matches => matches.map( &:target ),
97
+ :preference_position => preference_position,
98
+ :preferences => preferences.map( &:target ),
99
+ :proposals => proposals.map( &:target )
100
+ }.to_yaml
101
+ end
102
+
103
+ ## Candidate#match!
104
+ #
105
+ # Match with another Candidate
106
+ #
107
+ # ARG: `other` -- another Candidate instance to match with
108
+ #
109
+ def match!( other )
110
+ return false unless prefers?( other ) && !matched?( other )
111
+ matches << other
112
+ other.matches << self
113
+ end
114
+
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
+ #
122
+ def matched?( other = nil )
123
+ return full? if other.nil?
124
+ matches.include? other
125
+ end
126
+
127
+ ## Candidate#next_preference!
128
+ #
129
+ # Increment `preference_position` and return the preference at that position
130
+ #
131
+ def next_preference!
132
+ self.preference_position += 1
133
+ preferences.fetch preference_position
134
+ end
135
+
136
+ ## Candidate#prefers?
137
+ #
138
+ # Is there a preference for the passed Candidate?
139
+ #
140
+ # ARG: `other` -- another Candidate instance
141
+ #
142
+ def prefers?( other )
143
+ preferences.include? other
144
+ end
145
+
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
+ #
152
+ def propose_to( other )
153
+ proposals << other
154
+ other.respond_to_proposal_from self
155
+ end
156
+
157
+ ## Candidate#propose_to_next_preference
158
+ #
159
+ # Send a proposal to the next tracked preference
160
+ #
161
+ def propose_to_next_preference
162
+ propose_to next_preference!
163
+ end
164
+
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
+ #
171
+ def respond_to_proposal_from( other )
172
+ case
173
+ ## Is there a preference for the candidate?
174
+ #
175
+ when !prefers?( other )
176
+ false
177
+
178
+ ## Are there available positions for more matches?
179
+ #
180
+ when free?
181
+ match! other
182
+
183
+ ## Is the passed Candidate a better match than any other match?
184
+ #
185
+ when better_match?( other )
186
+ free!
187
+ match! other
188
+
189
+ else
190
+ false
191
+ end
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,165 @@
1
+ module StableMatch
2
+ class Runner
3
+ ## Whether the sets have been built into candidate sets
4
+ #
5
+ fattr( :built ){ false }
6
+ alias_method :built? , :built
7
+
8
+ ## Container for all the candidates
9
+ #
10
+ fattr( :candidates ){ [] }
11
+
12
+ ## Whether the sets have been checked for consistency
13
+ #
14
+ fattr( :checked ){ false }
15
+ alias_method :checked? , :checked
16
+
17
+ ## The first set to use in the matching
18
+ #
19
+ fattr( :candidate_set1 ){ {} } # for Candidates
20
+ fattr :set1 # raw data
21
+
22
+ ## The second set to use in the matching
23
+ #
24
+ fattr( :candidate_set2 ){ {} } # for Candidates
25
+ fattr :set2 # raw data
26
+
27
+ ## Runner::run
28
+ #
29
+ # Class-level factory method to construct, check, build and run a Runner instance
30
+ #
31
+ def self.run( *args , &block )
32
+ runner = new *args , &block
33
+ runner.check!
34
+ runner.build!
35
+ runner.run
36
+ end
37
+
38
+ def initialize( *args , &block )
39
+ options = Map.opts args
40
+ @set1 = options.set1 rescue args.shift or raise ArgumentError.new( "No `set1` provided!" )
41
+ @set2 = options.set2 rescue args.shift or raise ArgumentError.new( "No `set2` provided!" )
42
+
43
+ yield self if block_given?
44
+ end
45
+
46
+ ## Runner#build!
47
+ #
48
+ # Convert `set1` and `set2` into `candidate_set1` and `candidate_set2`
49
+ # Also, track a master array of `candidates`
50
+ # Mark itself as `built`
51
+ #
52
+ def build!
53
+ set1.each do | target , options |
54
+ candidate = Candidate.new target , *[ options ]
55
+ candidates.push candidate
56
+ candidate_set1[ target ] = candidate
57
+ end
58
+
59
+ set2.each do | target , options |
60
+ candidate = Candidate.new target , *[ options ]
61
+ candidates.push candidate
62
+ candidate_set2[ target ] = candidate
63
+ end
64
+
65
+ candidate_set1.each do | target , candidate |
66
+ candidate.preferences.map! { | preference_target | candidate_set2[ preference_target ] }
67
+ end
68
+
69
+ candidate_set2.each do | target , candidate |
70
+ candidate.preferences.map! { | preference_target | candidate_set1[ preference_target ] }
71
+ end
72
+
73
+ ## We've built the candidates
74
+ #
75
+ self.built = true
76
+ end
77
+
78
+ ## Runner#check!
79
+ #
80
+ # Run basic checks against each raw set
81
+ # Meant to be run before being built into candidate sets
82
+ # Mark itself as `checked`
83
+ #
84
+ def check!
85
+ error = proc { | message | raise ArgumentError.new( message ) }
86
+ set1_keys = set1.keys
87
+ set2_keys = set2.keys
88
+ set1_size = set1.size
89
+ set2_size = set2.size
90
+
91
+ ## Check set1
92
+ #
93
+ set1.each do | target , options |
94
+ message = "Preferences for #{ target.inspect } in `set1` do not match availabilities in `set2`!"
95
+ error[ message ] unless \
96
+ ## Anything there is a preference for is in the other set
97
+ #
98
+ ( options[ :preferences ].inject( true ){ | memo , preference | memo && set2_keys.include?( preference ) } )
99
+ end
100
+
101
+ ## Check set2 the same way
102
+ #
103
+ set2.each do | target , options |
104
+ message = "Preferences for #{ target.inspect } in `set2` do not match availabilities in `set1`!"
105
+ error[ message ] unless \
106
+ ( options[ :preferences ].inject( true ){ | memo , preference | memo && set1_keys.include?( preference ) } )
107
+ end
108
+
109
+ ## We've run the check
110
+ #
111
+ self.checked = true
112
+ end
113
+
114
+ def inspect
115
+ require "yaml"
116
+
117
+ inspection = proc do | set |
118
+ set.keys.inject( Hash.new ) do | hash , key |
119
+ candidate = set[ key ]
120
+ preferences = candidate.preferences
121
+
122
+ hash.update(
123
+ key => {
124
+ 'matches' => candidate.matches.map( &:target ),
125
+ 'preferences' => candidate.preferences.map( &:target ),
126
+ 'proposals' => candidate.proposals.map( &:target )
127
+ }
128
+ )
129
+ end
130
+ end
131
+
132
+ {
133
+ 'candidate_set1' => inspection[ candidate_set1 ],
134
+ 'candidate_set2' => inspection[ candidate_set2 ]
135
+ }.to_yaml
136
+ end
137
+
138
+ ## Runner#remaining_candidates
139
+ #
140
+ # List the remaining candidates that:
141
+ # -> have remaining slots available for matches AND
142
+ # -> have not already proposed to all of their preferences
143
+ #
144
+ def remaining_candidates
145
+ candidates.reject{ | candidate | candidate.full? || candidate.exhausted_preferences? }
146
+ end
147
+
148
+ ## Runner#run
149
+ #
150
+ # While there are remaining candidates, ask each one to propose to all of their preferences until:
151
+ # -> a candidate has proposed to all of their preferences
152
+ # -> a candidate has no more `matching_positions` to be filled
153
+ #
154
+ def run
155
+ while ( rcs = remaining_candidates ).any?
156
+ rcs.each do | candidate |
157
+ while !candidate.exhausted_preferences? && candidate.free?
158
+ candidate.propose_to_next_preference
159
+ end
160
+ end
161
+ end
162
+ self
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,3 @@
1
+ module StableMatch
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/stable_match/version', __FILE__)
3
+
4
+ Gem::Specification.new do | gem |
5
+ gem.authors = [ "Ryan Cook" , "Ara Howard" ]
6
+ gem.email = [ "cookrn@gmail.com" , "ara.t.howard@gmail.com" ]
7
+ gem.description = %q{A generic implementation of the stable match algorightm.}
8
+ gem.summary = %q{stable_match v0.1.0}
9
+ gem.homepage = "https://github.com/cookrn/stable_match"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "stable_match"
15
+ gem.require_paths = [ "lib" ]
16
+ gem.version = StableMatch::VERSION
17
+
18
+ ## Runtime Dependencies
19
+ #
20
+ gem.add_dependency "fattr" , "~> 2.2.1"
21
+ gem.add_dependency "map" , "~> 5.5.0"
22
+
23
+ ## Development Dependencies
24
+ #
25
+ gem.add_development_dependency "minitest" , "~> 2.12.1"
26
+ gem.add_development_dependency "rake" , "~> 0.9.2.2"
27
+ gem.add_development_dependency "rego" , "~> 1.0.0"
28
+ end
@@ -0,0 +1,51 @@
1
+ module MiniTest
2
+ class Unit
3
+ class TestCase
4
+ alias_method '__assert__' , 'assert'
5
+
6
+ ## TestCase#assert
7
+ #
8
+ # See: https://github.com/ahoward/testing.rb/blob/08dd643239a23543409ecb5fee100181f1621794/lib/testing.rb#L82-107
9
+ #
10
+ # Override assert to take a few different kinds of options
11
+ # Most notable argument type is a block that:
12
+ # -> asserts no exceptions were raised
13
+ # -> asserts the result of the block is truthy
14
+ # -> returns the result of the block
15
+ #
16
+ def assert( *args , &block )
17
+ if args.size == 1 and args.first.is_a?(Hash)
18
+ options = args.first
19
+ expected = getopt(:expected, options){ missing }
20
+ actual = getopt(:actual, options){ missing }
21
+ if expected == missing and actual == missing
22
+ actual , expected , *_ = options.to_a.flatten
23
+ end
24
+ expected = expected.call() if expected.respond_to?(:call)
25
+ actual = actual.call() if actual.respond_to?(:call)
26
+ assert_equal expected , actual
27
+ end
28
+
29
+ if block
30
+ label = "assert(#{ args.join(' ') })"
31
+ result = nil
32
+ raised = false
33
+ result = begin
34
+ block.call
35
+ rescue Object => e
36
+ raised = e
37
+ false
38
+ end
39
+ __assert__ !raised , ( raised.message rescue label )
40
+ __assert__ result , label
41
+ result
42
+ else
43
+ result = args.shift
44
+ label = "assert(#{ args.join(' ') })"
45
+ __assert__ result , label
46
+ result
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,10 @@
1
+ require "rubygems"
2
+ gem "minitest"
3
+
4
+ require "minitest/autorun"
5
+ require "minitest/pride"
6
+
7
+ support_dir = File.expand_path File.dirname( __FILE__ ) , "support"
8
+ Dir[ "#{ support_dir }/**/*.rb" ].each { | f | require f }
9
+
10
+ require "stable_match"
@@ -0,0 +1,189 @@
1
+ require "test_helper"
2
+
3
+ class StableMatch::CandidateTest < MiniTest::Unit::TestCase
4
+ def setup
5
+ @candidate1 = StableMatch::Candidate.new 1 , [ 2 , 3 ]
6
+ @candidate2 = StableMatch::Candidate.new 2 , [ 1 , 3 ]
7
+ @candidate3 = StableMatch::Candidate.new 3 , [ 2 , 1 ]
8
+
9
+ @candidate1.preferences = [ @candidate2 , @candidate3 ]
10
+ @candidate2.preferences = [ @candidate1 , @candidate3 ]
11
+ @candidate3.preferences = [ @candidate2 , @candidate1 ]
12
+ end
13
+
14
+ ## Candidate#initialize
15
+ #
16
+ def test_accepts_args_as_named_options_or_positionally
17
+ target = 1
18
+ preferences = 2
19
+
20
+ assert{ StableMatch::Candidate.new( target , preferences ) }
21
+ assert{ StableMatch::Candidate.new( :target => target , :preferences => preferences ) }
22
+
23
+ candidate = assert{ StableMatch::Candidate.new( target , :preferences => preferences ) }
24
+ assert{ candidate.target == target }
25
+
26
+ candidate = assert{ StableMatch::Candidate.new( preferences , :target => target ) }
27
+ assert{ candidate.preferences == preferences }
28
+ end
29
+
30
+ def test_match_positions_option_overrides_default
31
+ assert{ @candidate1.match_positions }
32
+
33
+ match_positions = 3
34
+ candidate = StableMatch::Candidate.new( 1 , [ 2 , 3 ] , :match_positions => match_positions )
35
+ assert{ candidate.match_positions == match_positions }
36
+ end
37
+
38
+ ## Candidate#better_match?
39
+ #
40
+ def test_better_match_returns_false_if_not_preferred
41
+ candidate4 = StableMatch::Candidate.new 4 , [ @candidate3 , @candidate2 ]
42
+ assert{ !@candidate1.better_match?( candidate4 ) }
43
+ end
44
+
45
+ def test_better_match_returns_false_if_already_matched
46
+ @candidate1.match! @candidate2
47
+ assert{ !@candidate1.better_match?( @candidate2 ) }
48
+ end
49
+
50
+ def test_better_match_returns_true_if_free
51
+ assert{ @candidate1.better_match? @candidate2 }
52
+ end
53
+
54
+ def test_better_match_returns_true_if_other_has_higher_preference
55
+ @candidate1.match! @candidate3
56
+ assert{ @candidate1.better_match? @candidate2 }
57
+ end
58
+
59
+ ## Candidate#exhausted_preferences?
60
+ #
61
+ def test_exhausted_preferences_returns_false_if_not_all_preferences_have_been_checked
62
+ assert{ !@candidate1.exhausted_preferences? }
63
+ end
64
+
65
+ def test_exhausted_preferences_returns_true_if_all_preferences_have_been_checked
66
+ @candidate1.preference_position = @candidate1.preferences.size
67
+ assert{ @candidate1.exhausted_preferences? }
68
+ end
69
+
70
+ ## Candidate#free?
71
+ #
72
+ def test_free_returns_false_if_all_match_positions_are_filled
73
+ @candidate1.match! @candidate2
74
+ assert{ !@candidate1.free? }
75
+ end
76
+
77
+ def test_free_returns_true_if_not_all_match_positions_are_filled
78
+ assert{ @candidate1.free? }
79
+ end
80
+
81
+ ## Candidate#free!
82
+ #
83
+ def test_free_returns_false_if_there_are_no_matches
84
+ assert{ !@candidate1.free! }
85
+ end
86
+
87
+ def test_free_deletes_the_least_preferred_match
88
+ @candidate1.match! @candidate2
89
+ assert{ @candidate1.free! }
90
+ assert{ !@candidate1.matches.include?( @candidate2 ) }
91
+ end
92
+
93
+ def test_free_deletes_the_least_preferred_match_from_both_sides
94
+ @candidate1.match! @candidate2
95
+ assert{ @candidate1.free! }
96
+ assert{ !@candidate2.matches.include?( @candidate1 ) }
97
+ end
98
+
99
+ ## Candidate#full?
100
+ #
101
+ def test_free_returns_false_if_all_match_positions_are_filled
102
+ @candidate1.match! @candidate2
103
+ assert{ @candidate1.full? }
104
+ end
105
+
106
+ def test_free_returns_true_if_not_all_match_positions_are_filled
107
+ assert{ !@candidate1.full? }
108
+ end
109
+
110
+ ## Candidate#match!
111
+ #
112
+ def test_match_returns_false_if_other_is_not_preferred
113
+ candidate4 = StableMatch::Candidate.new 4 , [ @candidate3 , @candidate2 ]
114
+ assert{ !@candidate1.match!( candidate4 ) }
115
+ end
116
+
117
+ def test_match_returns_false_if_already_matched
118
+ assert{ @candidate1.match! @candidate2 }
119
+ assert{ !@candidate1.match!( @candidate2 ) }
120
+ end
121
+
122
+ def test_match_adds_other_to_matches_array
123
+ assert{ @candidate1.match! @candidate2 }
124
+ assert{ @candidate1.matches.include? @candidate2 }
125
+ end
126
+
127
+ def test_match_adds_self_to_others_matches_array
128
+ assert{ @candidate1.match! @candidate2 }
129
+ assert{ @candidate2.matches.include? @candidate1 }
130
+ end
131
+
132
+ ## Candidate#matched?
133
+ #
134
+ def test_matched_returns_full_with_no_args
135
+ @candidate1.match! @candidate2
136
+ assert{ @candidate1.matched? == @candidate1.full? }
137
+ end
138
+
139
+ def test_matched_returns_whether_passed_candidate_is_a_match
140
+ @candidate1.match! @candidate2
141
+ assert{ @candidate1.matched? @candidate2 }
142
+ end
143
+
144
+ ## Candidate#next_preference!
145
+ #
146
+ def test_next_preference_increments_the_preference_position
147
+ preference_position = @candidate1.preference_position
148
+ assert{ @candidate1.next_preference! }
149
+ assert{ preference_position != @candidate1.preference_position }
150
+ end
151
+
152
+ def test_next_preference_returns_a_candidate
153
+ expected_candidate = @candidate1.preferences.first
154
+ assert{ expected_candidate == @candidate1.next_preference! }
155
+ end
156
+
157
+ ## Candidate#prefers?
158
+ #
159
+ def test_prefers_checks_for_existence_of_candidate_in_preferences
160
+ assert{ @candidate1.prefers? @candidate2 }
161
+ assert{ !@candidate1.prefers?( "bogus" ) }
162
+ end
163
+
164
+ ## Candidate#propose_to
165
+ #
166
+ def test_propose_to_tracks_proposals
167
+ assert{ @candidate1.propose_to @candidate2 }
168
+ assert{ @candidate1.proposals.include? @candidate2 }
169
+ end
170
+
171
+ ## Candidate#respond_to_proposal_from
172
+ #
173
+ def test_respond_to_proposal_from_returns_false_if_other_is_not_preferred
174
+ candidate4 = StableMatch::Candidate.new 4 , [ @candidate3 , @candidate2 ]
175
+ assert{ !@candidate1.respond_to_proposal_from( candidate4 ) }
176
+ end
177
+
178
+ def test_respond_to_proposal_from_matches_if_preferred_and_free
179
+ assert{ @candidate1.respond_to_proposal_from @candidate2 }
180
+ end
181
+
182
+ def test_respond_to_proposal_from_frees_and_matches_if_full
183
+ assert{ @candidate1.respond_to_proposal_from @candidate3 }
184
+ assert{ @candidate3.matched? @candidate1 }
185
+ assert{ @candidate1.respond_to_proposal_from @candidate2 }
186
+ assert{ @candidate2.matched? @candidate1 }
187
+ assert{ !@candidate3.matched?( @candidate1 ) }
188
+ end
189
+ end
@@ -0,0 +1,121 @@
1
+ require "test_helper"
2
+
3
+ class StableMatch::RunnerTest < MiniTest::Unit::TestCase
4
+ def setup
5
+ @set1 = { 1 => { :preferences => [ 2 ] } }
6
+ @set2 = { 2 => { :preferences => [ 1 ] } }
7
+ end
8
+
9
+ ## Runner::run
10
+ #
11
+ def test_has_a_factory_class_method_that_automatically_runs_match
12
+ runner = assert{ StableMatch::Runner.run @set1 , @set2 }
13
+ assert{ runner.checked? }
14
+ assert{ runner.built? }
15
+ end
16
+
17
+ ## Runner#initialize
18
+ #
19
+ def test_accepts_args_as_named_options_or_positionally
20
+ assert{ StableMatch::Runner.new( @set1 , @set2 ) }
21
+ assert{ StableMatch::Runner.new( :set1 => @set1 , :set2 => @set2 ) }
22
+
23
+ runner = assert{ StableMatch::Runner.new( @set1 , :set2 => @set2 ) }
24
+ assert{ runner.set1 == @set1 }
25
+
26
+ runner = assert{ StableMatch::Runner.new( @set2 , :set1 => @set1 ) }
27
+ assert{ runner.set2 == @set2 }
28
+ end
29
+
30
+ ## Runner#build!
31
+ #
32
+ def test_build_creates_candidate_sets_from_each_raw_set
33
+ runner = build_runner
34
+ assert{ runner.build! }
35
+
36
+ assert{ !runner.candidate_set1.empty? }
37
+ assert{ runner.candidate_set1[ @set1.keys.first ] }
38
+
39
+ assert{ !runner.candidate_set2.empty? }
40
+ assert{ runner.candidate_set2[ @set2.keys.first ] }
41
+ end
42
+
43
+ def test_build_adds_to_list_of_candidates
44
+ runner = build_runner
45
+ assert{ runner.candidates.empty? }
46
+ assert{ runner.build! }
47
+ assert{ !runner.candidates.empty? }
48
+ end
49
+
50
+ def test_build_replaces_candidates_preferences_with_candidates
51
+ runner = build_runner.tap { | r | r.build! }
52
+ assert{ runner.candidate_set1.values.first.preferences.all? { | c | c.is_a? StableMatch::Candidate } }
53
+ end
54
+
55
+ def test_build_marks_runner_as_built
56
+ runner = build_runner
57
+ assert{ runner.build! }
58
+ assert{ runner.built }
59
+ assert{ runner.built? }
60
+ end
61
+
62
+ ## Runner#check!
63
+ #
64
+ def test_checks_that_sets_meet_criteria
65
+ runner = build_runner
66
+ assert{ runner.check! }
67
+ assert{ runner.checked }
68
+ assert{ runner.checked? }
69
+ end
70
+
71
+ def test_checks_that_a_sets_preferences_are_included_in_other_set
72
+ set1 = { 1 => { :preferences => [ 2 ] } }
73
+ set2 = { 2 => { :preferences => [ 1 , 3 ] } }
74
+ runner = StableMatch::Runner.new set1 , set2
75
+
76
+ assert "set2 contains a preference for 3 that does not exist" do
77
+ raised = false
78
+ begin
79
+ runner.check!
80
+ rescue Object => e
81
+ raised = e.is_a? ArgumentError
82
+ end
83
+ raised
84
+ end
85
+ end
86
+
87
+ ## Runner#remaining_candidates
88
+ #
89
+ def test_remaining_candidates_rejects_candidates_that_have_filled_match_positions
90
+ runner = build_prepared_runner
91
+ original_size = runner.remaining_candidates.size
92
+ runner.candidates.first.tap { | c | c.propose_to_next_preference }
93
+ assert{ original_size > runner.remaining_candidates.size }
94
+ end
95
+
96
+ def test_remaining_candidates_rejects_candidates_that_have_made_all_possible_proposals
97
+ runner = build_prepared_runner
98
+ original_size = runner.remaining_candidates.size
99
+ runner.candidates.first.tap { | c | c.preference_position = c.preferences.size - 1 }
100
+ assert{ original_size > runner.remaining_candidates.size }
101
+ end
102
+
103
+ ## Runner#run
104
+ #
105
+ def test_has_a_run_loop
106
+ assert{ build_runner.respond_to? :run }
107
+ end
108
+
109
+ private
110
+
111
+ def build_runner
112
+ assert{ StableMatch::Runner.new( @set1 , @set2 ) }
113
+ end
114
+
115
+ def build_prepared_runner
116
+ runner = build_runner
117
+ assert{ runner.check! }
118
+ assert{ runner.build! }
119
+ runner
120
+ end
121
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: stable_match
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ryan Cook
9
+ - Ara Howard
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-05-01 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: fattr
17
+ requirement: &70273026247540 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: 2.2.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *70273026247540
26
+ - !ruby/object:Gem::Dependency
27
+ name: map
28
+ requirement: &70273026246260 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: 5.5.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *70273026246260
37
+ - !ruby/object:Gem::Dependency
38
+ name: minitest
39
+ requirement: &70273026229780 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ version: 2.12.1
45
+ type: :development
46
+ prerelease: false
47
+ version_requirements: *70273026229780
48
+ - !ruby/object:Gem::Dependency
49
+ name: rake
50
+ requirement: &70273026228300 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: 0.9.2.2
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: *70273026228300
59
+ - !ruby/object:Gem::Dependency
60
+ name: rego
61
+ requirement: &70273026227040 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ~>
65
+ - !ruby/object:Gem::Version
66
+ version: 1.0.0
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: *70273026227040
70
+ description: A generic implementation of the stable match algorightm.
71
+ email:
72
+ - cookrn@gmail.com
73
+ - ara.t.howard@gmail.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - .gitignore
79
+ - Gemfile
80
+ - LICENSE
81
+ - README.md
82
+ - Rakefile
83
+ - examples/example_1.rb
84
+ - lib/stable_match.rb
85
+ - lib/stable_match/candidate.rb
86
+ - lib/stable_match/runner.rb
87
+ - lib/stable_match/version.rb
88
+ - stable_match.gemspec
89
+ - test/support/minitest.rb
90
+ - test/test_helper.rb
91
+ - test/unit/stable_match/candidate_test.rb
92
+ - test/unit/stable_match/runner_test.rb
93
+ homepage: https://github.com/cookrn/stable_match
94
+ licenses: []
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ! '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ segments:
106
+ - 0
107
+ hash: 4207718686449127189
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ! '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ segments:
115
+ - 0
116
+ hash: 4207718686449127189
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 1.8.16
120
+ signing_key:
121
+ specification_version: 3
122
+ summary: stable_match v0.1.0
123
+ test_files:
124
+ - test/support/minitest.rb
125
+ - test/test_helper.rb
126
+ - test/unit/stable_match/candidate_test.rb
127
+ - test/unit/stable_match/runner_test.rb