stable_match 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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