stable_match 0.1.0 → 0.1.1
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/README.md +28 -7
- data/Rakefile +29 -2
- data/examples/example_1.rb +2 -0
- data/lib/stable_match/candidate.rb +6 -6
- data/lib/stable_match/runner.rb +19 -5
- data/lib/stable_match/version.rb +1 -1
- data/test/functional/nrmp_test.rb +59 -0
- data/test/test_helper.rb +3 -3
- data/test/unit/{stable_match → lib/stable_match}/candidate_test.rb +1 -1
- data/test/unit/{stable_match → lib/stable_match}/runner_test.rb +24 -1
- data/test/unit/lib/stable_match_test.rb +7 -0
- metadata +21 -17
data/README.md
CHANGED
@@ -1,24 +1,45 @@
|
|
1
1
|
# StableMatch
|
2
2
|
|
3
|
-
|
3
|
+
A generic implementation of the Stable Match class of algorithms.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
|
+
### Without Bundler
|
8
|
+
|
9
|
+
Install it yourself:
|
10
|
+
|
11
|
+
```
|
12
|
+
$ gem install stable_match
|
13
|
+
```
|
14
|
+
|
15
|
+
### With Bundler
|
16
|
+
|
7
17
|
Add this line to your application's Gemfile:
|
8
18
|
|
9
|
-
|
19
|
+
```ruby
|
20
|
+
gem 'stable_match'
|
21
|
+
```
|
10
22
|
|
11
23
|
And then execute:
|
12
24
|
|
13
|
-
|
25
|
+
```
|
26
|
+
$ bundle
|
27
|
+
```
|
14
28
|
|
15
|
-
|
29
|
+
## Usage
|
16
30
|
|
17
|
-
|
31
|
+
* See: `examples/example_1.rb`
|
32
|
+
* Run: `rake example`
|
18
33
|
|
19
|
-
|
34
|
+
TODO: Write more usage instructions here
|
35
|
+
|
36
|
+
## References
|
20
37
|
|
21
|
-
|
38
|
+
* [http://halfamind.aghion.com/the-national-resident-matching-programs-nrmp](http://halfamind.aghion.com/the-national-resident-matching-programs-nrmp)
|
39
|
+
* [http://rosettacode.org/wiki/Stable_marriage_problem#Ruby](http://rosettacode.org/wiki/Stable_marriage_problem#Ruby)
|
40
|
+
* [http://en.wikipedia.org/wiki/Stable_marriage_problem#Algorithm](http://en.wikipedia.org/wiki/Stable_marriage_problem#Algorithm)
|
41
|
+
* [http://en.wikipedia.org/wiki/National_Resident_Matching_Program](http://en.wikipedia.org/wiki/National_Resident_Matching_Program)
|
42
|
+
* [http://www.nrmp.org/res_match/about_res/algorithms.html](http://www.nrmp.org/res_match/about_res/algorithms.html)
|
22
43
|
|
23
44
|
## Contributing
|
24
45
|
|
data/Rakefile
CHANGED
@@ -13,11 +13,38 @@ task :example => "examples:any"
|
|
13
13
|
|
14
14
|
namespace :test do
|
15
15
|
desc "Run All The Tests"
|
16
|
-
task :all
|
16
|
+
task :all do
|
17
|
+
suites = %w(
|
18
|
+
test:functional
|
19
|
+
test:unit
|
20
|
+
)
|
21
|
+
|
22
|
+
suites.each do | suite |
|
23
|
+
puts "=========================================="
|
24
|
+
puts "Running: #{ suite }"
|
25
|
+
puts "=========================================="
|
26
|
+
begin
|
27
|
+
Rake::Task[ suite ].invoke
|
28
|
+
rescue
|
29
|
+
puts "=========================================="
|
30
|
+
puts "FAILED: #{ suite }"
|
31
|
+
ensure
|
32
|
+
puts "=========================================="
|
33
|
+
puts "\n"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "Run The Functional Tests"
|
39
|
+
Rake::TestTask.new( :functional ) do | t |
|
40
|
+
t.libs << [ "test" ]
|
41
|
+
t.pattern = "test/functional/**/*_test.rb"
|
42
|
+
t.verbose = true
|
43
|
+
end
|
17
44
|
|
18
45
|
desc "Run The Unit Tests"
|
19
46
|
Rake::TestTask.new( :unit ) do | t |
|
20
|
-
t.libs
|
47
|
+
t.libs << [ "test" ]
|
21
48
|
t.pattern = "test/unit/**/*_test.rb"
|
22
49
|
t.verbose = true
|
23
50
|
end
|
data/examples/example_1.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require "stable_match"
|
2
2
|
|
3
3
|
class Test
|
4
|
+
## This is a modified version of the example NRMP case
|
5
|
+
#
|
4
6
|
PROGRAMS = {
|
5
7
|
'city' => { :match_positions => 1, :preferences => ['garcia', 'hassan', 'eastman', 'brown', 'chen', 'davis', 'ford']},
|
6
8
|
'general' => { :match_positions => 3, :preferences => ['brown', 'eastman', 'hassan', 'anderson', 'chen', 'davis', 'garcia']},
|
@@ -91,12 +91,12 @@ module StableMatch
|
|
91
91
|
require "yaml"
|
92
92
|
|
93
93
|
{
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
100
|
}.to_yaml
|
101
101
|
end
|
102
102
|
|
data/lib/stable_match/runner.rb
CHANGED
@@ -24,23 +24,29 @@ module StableMatch
|
|
24
24
|
fattr( :candidate_set2 ){ {} } # for Candidates
|
25
25
|
fattr :set2 # raw data
|
26
26
|
|
27
|
+
## The way in which the run loop executes
|
28
|
+
#
|
29
|
+
# Should be either: symmetric OR asymmetric
|
30
|
+
#
|
31
|
+
fattr( :strategy ){ :symmetric }
|
32
|
+
|
27
33
|
## Runner::run
|
28
34
|
#
|
29
35
|
# Class-level factory method to construct, check, build and run a Runner instance
|
30
36
|
#
|
31
|
-
def self.run( *args
|
32
|
-
runner = new *args
|
37
|
+
def self.run( *args )
|
38
|
+
runner = new *args
|
33
39
|
runner.check!
|
34
40
|
runner.build!
|
35
41
|
runner.run
|
36
42
|
end
|
37
43
|
|
38
|
-
def initialize( *args
|
44
|
+
def initialize( *args )
|
39
45
|
options = Map.opts args
|
40
46
|
@set1 = options.set1 rescue args.shift or raise ArgumentError.new( "No `set1` provided!" )
|
41
47
|
@set2 = options.set2 rescue args.shift or raise ArgumentError.new( "No `set2` provided!" )
|
42
48
|
|
43
|
-
|
49
|
+
@strategy = options.strategy if options.get( :strategy )
|
44
50
|
end
|
45
51
|
|
46
52
|
## Runner#build!
|
@@ -130,6 +136,7 @@ module StableMatch
|
|
130
136
|
end
|
131
137
|
|
132
138
|
{
|
139
|
+
'strategy' => strategy.to_s,
|
133
140
|
'candidate_set1' => inspection[ candidate_set1 ],
|
134
141
|
'candidate_set2' => inspection[ candidate_set2 ]
|
135
142
|
}.to_yaml
|
@@ -137,12 +144,19 @@ module StableMatch
|
|
137
144
|
|
138
145
|
## Runner#remaining_candidates
|
139
146
|
#
|
147
|
+
# This method respects the runner's strategy!
|
148
|
+
#
|
140
149
|
# List the remaining candidates that:
|
141
150
|
# -> have remaining slots available for matches AND
|
142
151
|
# -> have not already proposed to all of their preferences
|
143
152
|
#
|
144
153
|
def remaining_candidates
|
145
|
-
|
154
|
+
case strategy.to_sym
|
155
|
+
when :symmetric
|
156
|
+
candidates.reject { | candidate | candidate.full? || candidate.exhausted_preferences? }
|
157
|
+
when :asymmetric
|
158
|
+
candidate_set1.values.reject { | candidate | candidate.full? || candidate.exhausted_preferences? }
|
159
|
+
end
|
146
160
|
end
|
147
161
|
|
148
162
|
## Runner#run
|
data/lib/stable_match/version.rb
CHANGED
@@ -0,0 +1,59 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class NationalResidentMatchingProgramFunctionalTest < MiniTest::Unit::TestCase
|
4
|
+
def test_case
|
5
|
+
programs = {
|
6
|
+
'mercy' => { :match_positions => 2, :preferences => ['chen', 'garcia']},
|
7
|
+
'city' => { :match_positions => 2, :preferences => ['garcia', 'hassan', 'eastman', 'anderson', 'brown', 'chen', 'davis', 'ford']},
|
8
|
+
'general' => { :match_positions => 2, :preferences => ['brown', 'eastman', 'hassan', 'anderson', 'chen', 'davis', 'garcia']},
|
9
|
+
'state' => { :match_positions => 2, :preferences => ['brown', 'eastman', 'anderson', 'chen', 'hassan', 'ford', 'davis', 'garcia']}
|
10
|
+
}
|
11
|
+
|
12
|
+
applicants = {
|
13
|
+
'anderson' => { :match_positions => 1 , :preferences => ['city'] },
|
14
|
+
'brown' => { :match_positions => 1 , :preferences => ['city', 'mercy'] },
|
15
|
+
'chen' => { :match_positions => 1 , :preferences => ['city', 'mercy'] },
|
16
|
+
'davis' => { :match_positions => 1 , :preferences => ['mercy', 'city', 'general', 'state'] },
|
17
|
+
'eastman' => { :match_positions => 1 , :preferences => ['city', 'mercy', 'state', 'general'] },
|
18
|
+
'ford' => { :match_positions => 1 , :preferences => ['city', 'general', 'mercy', 'state'] },
|
19
|
+
'garcia' => { :match_positions => 1 , :preferences => ['city', 'mercy', 'state', 'general'] },
|
20
|
+
'hassan' => { :match_positions => 1 , :preferences => ['state', 'city', 'mercy', 'general' ] }
|
21
|
+
}
|
22
|
+
|
23
|
+
programs_expectations = {
|
24
|
+
'mercy' => [ 'chen' ],
|
25
|
+
'city' => [ 'garcia' , 'eastman' ],
|
26
|
+
'general' => [ 'davis' ],
|
27
|
+
'state' => [ 'ford' , 'hassan' ]
|
28
|
+
}
|
29
|
+
|
30
|
+
applicants_expectations = {
|
31
|
+
'anderson' => [],
|
32
|
+
'brown' => [],
|
33
|
+
'chen' => [ 'mercy' ],
|
34
|
+
'davis' => [ 'general' ],
|
35
|
+
'eastman' => [ 'city' ],
|
36
|
+
'ford' => [ 'state' ],
|
37
|
+
'garcia' => [ 'city' ],
|
38
|
+
'hassan' => [ 'state' ]
|
39
|
+
}
|
40
|
+
|
41
|
+
runner = StableMatch.run applicants , programs , :strategy => :asymmetric
|
42
|
+
|
43
|
+
## applicants
|
44
|
+
#
|
45
|
+
applicants_expectations.each do | applicant , expected_matches |
|
46
|
+
candidate = runner.candidate_set1[ applicant ]
|
47
|
+
actual_matches = candidate.matches.map &:target
|
48
|
+
assert{ expected_matches.sort == actual_matches.sort }
|
49
|
+
end
|
50
|
+
|
51
|
+
## programs
|
52
|
+
#
|
53
|
+
programs_expectations.each do | program , expected_matches |
|
54
|
+
candidate = runner.candidate_set2[ program ]
|
55
|
+
actual_matches = candidate.matches.map &:target
|
56
|
+
assert{ expected_matches.sort == actual_matches.sort }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -4,7 +4,7 @@ gem "minitest"
|
|
4
4
|
require "minitest/autorun"
|
5
5
|
require "minitest/pride"
|
6
6
|
|
7
|
-
support_dir = File.expand_path File.dirname( __FILE__ ) , "support"
|
8
|
-
Dir[ "#{ support_dir }/**/*.rb" ].each { | f | require f }
|
9
|
-
|
10
7
|
require "stable_match"
|
8
|
+
|
9
|
+
support_dir = File.join File.expand_path( File.dirname __FILE__ ) , "support"
|
10
|
+
Dir[ "#{ support_dir }/**/*.rb" ].each { | f | require f }
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
|
-
class StableMatch::
|
3
|
+
class StableMatch::CandidateUnitTest < MiniTest::Unit::TestCase
|
4
4
|
def setup
|
5
5
|
@candidate1 = StableMatch::Candidate.new 1 , [ 2 , 3 ]
|
6
6
|
@candidate2 = StableMatch::Candidate.new 2 , [ 1 , 3 ]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require "test_helper"
|
2
2
|
|
3
|
-
class StableMatch::
|
3
|
+
class StableMatch::RunnerUnitTest < MiniTest::Unit::TestCase
|
4
4
|
def setup
|
5
5
|
@set1 = { 1 => { :preferences => [ 2 ] } }
|
6
6
|
@set2 = { 2 => { :preferences => [ 1 ] } }
|
@@ -27,6 +27,15 @@ class StableMatch::RunnerTest < MiniTest::Unit::TestCase
|
|
27
27
|
assert{ runner.set2 == @set2 }
|
28
28
|
end
|
29
29
|
|
30
|
+
def test_accepts_a_strategy_option
|
31
|
+
runner = assert{ StableMatch::Runner.new @set1 , @set2 , :strategy => :asymmetric }
|
32
|
+
assert{ :asymmetric == runner.strategy }
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_default_strategy_is_symmetric
|
36
|
+
assert{ StableMatch::Runner.new( @set1 , @set2 ).strategy == :symmetric }
|
37
|
+
end
|
38
|
+
|
30
39
|
## Runner#build!
|
31
40
|
#
|
32
41
|
def test_build_creates_candidate_sets_from_each_raw_set
|
@@ -100,6 +109,20 @@ class StableMatch::RunnerTest < MiniTest::Unit::TestCase
|
|
100
109
|
assert{ original_size > runner.remaining_candidates.size }
|
101
110
|
end
|
102
111
|
|
112
|
+
def test_remaining_candidates_respects_the_run_strategy
|
113
|
+
runner = build_prepared_runner
|
114
|
+
assert{ runner.strategy == :symmetric }
|
115
|
+
|
116
|
+
all = runner.candidates.map &:target
|
117
|
+
remaining = runner.remaining_candidates.map &:target
|
118
|
+
assert{ all.sort == remaining.sort }
|
119
|
+
|
120
|
+
runner.strategy :asymmetric
|
121
|
+
set1 = @set1.keys
|
122
|
+
remaining = runner.remaining_candidates.map &:target
|
123
|
+
assert{ set1.sort == remaining.sort }
|
124
|
+
end
|
125
|
+
|
103
126
|
## Runner#run
|
104
127
|
#
|
105
128
|
def test_has_a_run_loop
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stable_match
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -14,7 +14,7 @@ date: 2012-05-01 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: fattr
|
17
|
-
requirement: &
|
17
|
+
requirement: &70346750299680 !ruby/object:Gem::Requirement
|
18
18
|
none: false
|
19
19
|
requirements:
|
20
20
|
- - ~>
|
@@ -22,10 +22,10 @@ dependencies:
|
|
22
22
|
version: 2.2.1
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
|
-
version_requirements: *
|
25
|
+
version_requirements: *70346750299680
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: map
|
28
|
-
requirement: &
|
28
|
+
requirement: &70346750298320 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
31
31
|
- - ~>
|
@@ -33,10 +33,10 @@ dependencies:
|
|
33
33
|
version: 5.5.0
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
|
-
version_requirements: *
|
36
|
+
version_requirements: *70346750298320
|
37
37
|
- !ruby/object:Gem::Dependency
|
38
38
|
name: minitest
|
39
|
-
requirement: &
|
39
|
+
requirement: &70346750281820 !ruby/object:Gem::Requirement
|
40
40
|
none: false
|
41
41
|
requirements:
|
42
42
|
- - ~>
|
@@ -44,10 +44,10 @@ dependencies:
|
|
44
44
|
version: 2.12.1
|
45
45
|
type: :development
|
46
46
|
prerelease: false
|
47
|
-
version_requirements: *
|
47
|
+
version_requirements: *70346750281820
|
48
48
|
- !ruby/object:Gem::Dependency
|
49
49
|
name: rake
|
50
|
-
requirement: &
|
50
|
+
requirement: &70346750280460 !ruby/object:Gem::Requirement
|
51
51
|
none: false
|
52
52
|
requirements:
|
53
53
|
- - ~>
|
@@ -55,10 +55,10 @@ dependencies:
|
|
55
55
|
version: 0.9.2.2
|
56
56
|
type: :development
|
57
57
|
prerelease: false
|
58
|
-
version_requirements: *
|
58
|
+
version_requirements: *70346750280460
|
59
59
|
- !ruby/object:Gem::Dependency
|
60
60
|
name: rego
|
61
|
-
requirement: &
|
61
|
+
requirement: &70346750279080 !ruby/object:Gem::Requirement
|
62
62
|
none: false
|
63
63
|
requirements:
|
64
64
|
- - ~>
|
@@ -66,7 +66,7 @@ dependencies:
|
|
66
66
|
version: 1.0.0
|
67
67
|
type: :development
|
68
68
|
prerelease: false
|
69
|
-
version_requirements: *
|
69
|
+
version_requirements: *70346750279080
|
70
70
|
description: A generic implementation of the stable match algorightm.
|
71
71
|
email:
|
72
72
|
- cookrn@gmail.com
|
@@ -86,10 +86,12 @@ files:
|
|
86
86
|
- lib/stable_match/runner.rb
|
87
87
|
- lib/stable_match/version.rb
|
88
88
|
- stable_match.gemspec
|
89
|
+
- test/functional/nrmp_test.rb
|
89
90
|
- test/support/minitest.rb
|
90
91
|
- test/test_helper.rb
|
91
|
-
- test/unit/stable_match/candidate_test.rb
|
92
|
-
- test/unit/stable_match/runner_test.rb
|
92
|
+
- test/unit/lib/stable_match/candidate_test.rb
|
93
|
+
- test/unit/lib/stable_match/runner_test.rb
|
94
|
+
- test/unit/lib/stable_match_test.rb
|
93
95
|
homepage: https://github.com/cookrn/stable_match
|
94
96
|
licenses: []
|
95
97
|
post_install_message:
|
@@ -104,7 +106,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
104
106
|
version: '0'
|
105
107
|
segments:
|
106
108
|
- 0
|
107
|
-
hash:
|
109
|
+
hash: 1686263240201859954
|
108
110
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
111
|
none: false
|
110
112
|
requirements:
|
@@ -113,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
115
|
version: '0'
|
114
116
|
segments:
|
115
117
|
- 0
|
116
|
-
hash:
|
118
|
+
hash: 1686263240201859954
|
117
119
|
requirements: []
|
118
120
|
rubyforge_project:
|
119
121
|
rubygems_version: 1.8.16
|
@@ -121,7 +123,9 @@ signing_key:
|
|
121
123
|
specification_version: 3
|
122
124
|
summary: stable_match v0.1.0
|
123
125
|
test_files:
|
126
|
+
- test/functional/nrmp_test.rb
|
124
127
|
- test/support/minitest.rb
|
125
128
|
- test/test_helper.rb
|
126
|
-
- test/unit/stable_match/candidate_test.rb
|
127
|
-
- test/unit/stable_match/runner_test.rb
|
129
|
+
- test/unit/lib/stable_match/candidate_test.rb
|
130
|
+
- test/unit/lib/stable_match/runner_test.rb
|
131
|
+
- test/unit/lib/stable_match_test.rb
|