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.
@@ -1,39 +1,37 @@
1
+ require 'yaml'
2
+ require 'stable_match/util/initialize_with_defaults'
3
+
1
4
  module StableMatch
2
5
  class Runner
3
- ## Whether the sets have been built into candidate sets
4
- #
5
- fattr( :built ){ false }
6
+ include Util::InitializeWithDefaults
7
+
8
+ # Whether the sets have been built into candidate sets
9
+ attr_accessor :built
6
10
  alias_method :built? , :built
7
11
 
8
- ## Container for all the candidates
9
- #
10
- fattr( :candidates ){ [] }
12
+ # Container for all the candidates
13
+ attr_accessor :candidates
11
14
 
12
- ## Whether the sets have been checked for consistency
13
- #
14
- fattr( :checked ){ false }
15
+ # Whether the sets have been checked for consistency
16
+ attr_accessor :checked
15
17
  alias_method :checked? , :checked
16
18
 
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
- ## The way in which the run loop executes
28
- #
29
- # Should be either: symmetric OR asymmetric
30
- #
31
- fattr( :strategy ){ :symmetric }
32
-
33
- ## Runner::run
34
- #
35
- # Class-level factory method to construct, check, build and run a Runner instance
36
- #
19
+ # The first set to use in the matching
20
+ attr_accessor \
21
+ :candidate_set1, # for Candidates
22
+ :set1 # raw data
23
+
24
+ # The second set to use in the matching
25
+ attr_accessor \
26
+ :candidate_set2, # for Candidates
27
+ :set2 # raw data
28
+
29
+ # The way in which the run loop executes
30
+ #
31
+ # Should be either: symmetric OR asymmetric
32
+ attr_accessor :strategy
33
+
34
+ # Class-level factory method to construct, check, build and run a Runner instance
37
35
  def self.run( *args )
38
36
  runner = new *args
39
37
  runner.check!
@@ -41,52 +39,61 @@ module StableMatch
41
39
  runner.run
42
40
  end
43
41
 
44
- def initialize( *args )
45
- options = Map.opts args
46
- @set1 = options.set1 rescue args.shift or raise ArgumentError.new( "No `set1` provided!" )
47
- @set2 = options.set2 rescue args.shift or raise ArgumentError.new( "No `set2` provided!" )
42
+ def initialize( set1 , set2 , strategy = :symmetric )
43
+ @set1 = set1
44
+ @set2 = set2
45
+ @strategy = strategy
46
+ end
48
47
 
49
- @strategy = options.strategy if options.get( :strategy )
48
+ def initialize_defaults!
49
+ @built ||= false
50
+ @candidates ||= []
51
+ @checked ||= false
52
+ @candidate_set1 ||= {}
53
+ @candidate_set2 ||= {}
50
54
  end
51
55
 
52
- ## Runner#build!
53
- #
54
- # Convert `set1` and `set2` into `candidate_set1` and `candidate_set2`
55
- # Also, track a master array of `candidates`
56
- # Mark itself as `built`
57
- #
56
+ # Convert `set1` and `set2` into `candidate_set1` and `candidate_set2`
57
+ # Also, track a master array of `candidates`
58
+ # Mark itself as `built`
58
59
  def build!
59
60
  set1.each do | target , options |
60
- candidate = Candidate.new target , *[ options ]
61
+ candidate =
62
+ Candidate.new \
63
+ target,
64
+ *( options.first.is_a?( Array ) ? options : [ options ] )
65
+
61
66
  candidates.push candidate
62
67
  candidate_set1[ target ] = candidate
63
68
  end
64
69
 
65
70
  set2.each do | target , options |
66
- candidate = Candidate.new target , *[ options ]
71
+ candidate =
72
+ Candidate.new \
73
+ target,
74
+ *( options.first.is_a?( Array ) ? options : [ options ] )
75
+
67
76
  candidates.push candidate
68
77
  candidate_set2[ target ] = candidate
69
78
  end
70
79
 
71
80
  candidate_set1.each do | target , candidate |
72
- candidate.preferences.map! { | preference_target | candidate_set2[ preference_target ] }
81
+ candidate.preferences =
82
+ candidate.raw_preferences.map { | preference_target | candidate_set2[ preference_target ] }
73
83
  end
74
84
 
75
85
  candidate_set2.each do | target , candidate |
76
- candidate.preferences.map! { | preference_target | candidate_set1[ preference_target ] }
86
+ candidate.preferences =
87
+ candidate.raw_preferences.map { | preference_target | candidate_set1[ preference_target ] }
77
88
  end
78
89
 
79
- ## We've built the candidates
80
- #
90
+ # We've built the candidates
81
91
  self.built = true
82
92
  end
83
93
 
84
- ## Runner#check!
85
- #
86
- # Run basic checks against each raw set
87
- # Meant to be run before being built into candidate sets
88
- # Mark itself as `checked`
89
- #
94
+ # Run basic checks against each raw set
95
+ # Meant to be run before being built into candidate sets
96
+ # Mark itself as `checked`
90
97
  def check!
91
98
  error = proc { | message | raise ArgumentError.new( message ) }
92
99
  set1_keys = set1.keys
@@ -94,62 +101,41 @@ module StableMatch
94
101
  set1_size = set1.size
95
102
  set2_size = set2.size
96
103
 
97
- ## Check set1
98
- #
104
+ # Check set1
99
105
  set1.each do | target , options |
100
106
  message = "Preferences for #{ target.inspect } in `set1` do not match availabilities in `set2`!"
107
+ options = options.first if options.first.is_a?( Array )
101
108
  error[ message ] unless \
102
- ## Anything there is a preference for is in the other set
103
- #
104
- ( options[ :preferences ].inject( true ){ | memo , preference | memo && set2_keys.include?( preference ) } )
109
+ # Anything there is a preference for is in the other set
110
+ ( options.all? { | preference | set2_keys.include?( preference ) } )
105
111
  end
106
112
 
107
- ## Check set2 the same way
108
- #
113
+ # Check set2 the same way
109
114
  set2.each do | target , options |
110
115
  message = "Preferences for #{ target.inspect } in `set2` do not match availabilities in `set1`!"
116
+ options = options.first if options.first.is_a?( Array )
111
117
  error[ message ] unless \
112
- ( options[ :preferences ].inject( true ){ | memo , preference | memo && set1_keys.include?( preference ) } )
118
+ # Anything there is a preference for is in the other set
119
+ ( options.all? { | preference | set1_keys.include?( preference ) } )
113
120
  end
114
121
 
115
- ## We've run the check
116
- #
122
+ # We've run the check
117
123
  self.checked = true
118
124
  end
119
125
 
120
126
  def inspect
121
- require "yaml"
122
-
123
- inspection = proc do | set |
124
- set.keys.inject( Hash.new ) do | hash , key |
125
- candidate = set[ key ]
126
- preferences = candidate.preferences
127
-
128
- hash.update(
129
- key => {
130
- 'matches' => candidate.matches.map( &:target ),
131
- 'preferences' => candidate.preferences.map( &:target ),
132
- 'proposals' => candidate.proposals.map( &:target )
133
- }
134
- )
135
- end
136
- end
137
-
138
- {
139
- 'strategy' => strategy.to_s,
140
- 'candidate_set1' => inspection[ candidate_set1 ],
141
- 'candidate_set2' => inspection[ candidate_set2 ]
142
- }.to_yaml
127
+ candidates.map( &:inspect ).join "\n\n"
143
128
  end
144
129
 
145
- ## Runner#remaining_candidates
146
- #
147
- # This method respects the runner's strategy!
148
- #
149
- # List the remaining candidates that:
150
- # -> have remaining slots available for matches AND
151
- # -> have not already proposed to all of their preferences
152
- #
130
+ alias_method \
131
+ :pretty_inspect,
132
+ :inspect
133
+
134
+ # This method respects the runner's strategy!
135
+ #
136
+ # List the remaining candidates that:
137
+ # -> have remaining slots available for matches AND
138
+ # -> have not already proposed to all of their preferences
153
139
  def remaining_candidates
154
140
  case strategy.to_sym
155
141
  when :symmetric
@@ -159,12 +145,9 @@ module StableMatch
159
145
  end
160
146
  end
161
147
 
162
- ## Runner#run
163
- #
164
- # While there are remaining candidates, ask each one to propose to all of their preferences until:
165
- # -> a candidate has proposed to all of their preferences
166
- # -> a candidate has no more `matching_positions` to be filled
167
- #
148
+ # While there are remaining candidates, ask each one to propose to all of their preferences until:
149
+ # -> a candidate has proposed to all of their preferences
150
+ # -> a candidate has no more `matching_positions` to be filled
168
151
  def run
169
152
  while ( rcs = remaining_candidates ).any?
170
153
  rcs.each do | candidate |
@@ -0,0 +1,49 @@
1
+ require 'stable_match'
2
+ require 'minitest/test'
3
+
4
+ module StableMatch
5
+ class Test < Minitest::Test
6
+ alias_method '__assert__' , '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
+ def assert( *args , &block )
16
+ if args.size == 1 and args.first.is_a?(Hash)
17
+ options = args.first
18
+ expected = getopt(:expected, options){ missing }
19
+ actual = getopt(:actual, options){ missing }
20
+ if expected == missing and actual == missing
21
+ actual , expected , *_ = options.to_a.flatten
22
+ end
23
+ expected = expected.call() if expected.respond_to?(:call)
24
+ actual = actual.call() if actual.respond_to?(:call)
25
+ assert_equal expected , actual
26
+ end
27
+
28
+ if block
29
+ label = "assert(#{ args.join(' ') })"
30
+ result = nil
31
+ raised = false
32
+ result = begin
33
+ block.call
34
+ rescue Object => e
35
+ raised = e
36
+ false
37
+ end
38
+ __assert__ !raised , ( raised.message rescue label )
39
+ __assert__ result , label
40
+ result
41
+ else
42
+ result = args.shift
43
+ label = "assert(#{ args.join(' ') })"
44
+ __assert__ result , label
45
+ result
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1 @@
1
+ require 'stable_match/util/initialize_with_defaults'
@@ -0,0 +1,27 @@
1
+ module StableMatch
2
+ module Util
3
+ module InitializeWithDefaults
4
+ def self.included( klass )
5
+ klass.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def new( *args , &block )
10
+ object = allocate
11
+
12
+ object.initialize_defaults!
13
+
14
+ object.send \
15
+ :initialize,
16
+ *args,
17
+ &block
18
+
19
+ object
20
+ end
21
+ end
22
+
23
+ def initialize_defaults!
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,3 +1,3 @@
1
1
  module StableMatch
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -2,27 +2,19 @@
2
2
  require File.expand_path('../lib/stable_match/version', __FILE__)
3
3
 
4
4
  Gem::Specification.new do | gem |
5
- gem.authors = [ "Ryan Cook" , "Ara Howard" ]
6
- gem.email = [ "cookrn@gmail.com" , "ara.t.howard@gmail.com" ]
5
+ gem.authors = [ 'Ryan Cook' , 'Ara Howard' ]
6
+ gem.email = [ 'cookrn@gmail.com' , 'ara.t.howard@gmail.com' ]
7
7
  gem.description = %q{A generic implementation of the stable match algorightm.}
8
8
  gem.summary = %q{stable_match v0.1.0}
9
- gem.homepage = "https://github.com/cookrn/stable_match"
9
+ gem.homepage = 'https://github.com/cookrn/stable_match'
10
10
 
11
11
  gem.files = `git ls-files`.split($\)
12
12
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
13
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
- gem.name = "stable_match"
15
- gem.require_paths = [ "lib" ]
14
+ gem.name = 'stable_match'
15
+ gem.require_paths = [ 'lib' ]
16
16
  gem.version = StableMatch::VERSION
17
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"
18
+ gem.add_development_dependency 'minitest'
19
+ gem.add_development_dependency 'rake'
28
20
  end
@@ -1,23 +1,23 @@
1
- require "test_helper"
1
+ require 'test_helper'
2
2
 
3
- class NationalResidentMatchingProgramFunctionalTest < MiniTest::Unit::TestCase
3
+ class NrmpTest < StableMatch::Test
4
4
  def test_case
5
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']}
6
+ 'mercy' => [ ['chen', 'garcia'] , 2 ],
7
+ 'city' => [ ['garcia', 'hassan', 'eastman', 'anderson', 'brown', 'chen', 'davis', 'ford'] , 2 ],
8
+ 'general' => [ ['brown', 'eastman', 'hassan', 'anderson', 'chen', 'davis', 'garcia'] , 2 ],
9
+ 'state' => [ ['brown', 'eastman', 'anderson', 'chen', 'hassan', 'ford', 'davis', 'garcia'] , 2 ]
10
10
  }
11
11
 
12
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' ] }
13
+ 'anderson' => [ 'city' ],
14
+ 'brown' => [ 'city' , 'mercy' ],
15
+ 'chen' => [ 'city' , 'mercy' ],
16
+ 'davis' => [ 'mercy' , 'city' , 'general' , 'state' ],
17
+ 'eastman' => [ 'city' , 'mercy' , 'state' , 'general' ],
18
+ 'ford' => [ 'city' , 'general' , 'mercy' , 'state' ],
19
+ 'garcia' => [ 'city' , 'mercy' , 'state' , 'general' ],
20
+ 'hassan' => [ 'state' , 'city' , 'mercy' , 'general' ]
21
21
  }
22
22
 
23
23
  programs_expectations = {
@@ -38,14 +38,15 @@ class NationalResidentMatchingProgramFunctionalTest < MiniTest::Unit::TestCase
38
38
  'hassan' => [ 'state' ]
39
39
  }
40
40
 
41
- runner = StableMatch.run applicants , programs , :strategy => :asymmetric
41
+ runner = StableMatch.run applicants , programs , :asymmetric
42
42
 
43
43
  ## applicants
44
44
  #
45
45
  applicants_expectations.each do | applicant , expected_matches |
46
46
  candidate = runner.candidate_set1[ applicant ]
47
47
  actual_matches = candidate.matches.map &:target
48
- assert{ expected_matches.sort == actual_matches.sort }
48
+ assertion = "#{ applicant } matches #{ actual_matches }"
49
+ assert( assertion ){ expected_matches.sort == actual_matches.sort }
49
50
  end
50
51
 
51
52
  ## programs
@@ -53,6 +54,7 @@ class NationalResidentMatchingProgramFunctionalTest < MiniTest::Unit::TestCase
53
54
  programs_expectations.each do | program , expected_matches |
54
55
  candidate = runner.candidate_set2[ program ]
55
56
  actual_matches = candidate.matches.map &:target
57
+ assertion = "#{ program } matches #{ actual_matches }"
56
58
  assert{ expected_matches.sort == actual_matches.sort }
57
59
  end
58
60
  end
@@ -0,0 +1,75 @@
1
+ require 'test_helper'
2
+
3
+ class StableMarriageTest < StableMatch::Test
4
+ def test_case
5
+ men = {
6
+ 'abe' => %w(abi eve cath ivy jan dee fay bea hope gay),
7
+ 'bob' => %w(cath hope abi dee eve fay bea jan ivy gay),
8
+ 'col' => %w(hope eve abi dee bea fay ivy gay cath jan),
9
+ 'dan' => %w(ivy fay dee gay hope eve jan bea cath abi),
10
+ 'ed' => %w(jan dee bea cath fay eve abi ivy hope gay),
11
+ 'fred' => %w(bea abi dee gay eve ivy cath jan hope fay),
12
+ 'gav' => %w(gay eve ivy bea cath abi dee hope jan fay),
13
+ 'hal' => %w(abi eve hope fay ivy cath jan bea gay dee),
14
+ 'ian' => %w(hope cath dee gay bea abi fay ivy jan eve),
15
+ 'jon' => %w(abi fay jan gay eve bea dee cath ivy hope)
16
+ }
17
+
18
+ women = {
19
+ 'abi' => %w(bob fred jon gav ian abe dan ed col hal),
20
+ 'bea' => %w(bob abe col fred gav dan ian ed jon hal),
21
+ 'cath' => %w(fred bob ed gav hal col ian abe dan jon),
22
+ 'dee' => %w(fred jon col abe ian hal gav dan bob ed),
23
+ 'eve' => %w(jon hal fred dan abe gav col ed ian bob),
24
+ 'fay' => %w(bob abe ed ian jon dan fred gav col hal),
25
+ 'gay' => %w(jon gav hal fred bob abe col ed dan ian),
26
+ 'hope' => %w(gav jon bob abe ian dan hal ed col fred),
27
+ 'ivy' => %w(ian col hal gav fred bob abe ed jon dan),
28
+ 'jan' => %w(ed hal gav abe bob jon col ian fred dan)
29
+ }
30
+
31
+ mens_expectations = {
32
+ 'abe' => [ 'ivy' ],
33
+ 'bob' => [ 'cath' ],
34
+ 'col' => [ 'dee' ],
35
+ 'dan' => [ 'fay' ],
36
+ 'ed' => [ 'jan' ],
37
+ 'fred' => [ 'bea' ],
38
+ 'gav' => [ 'gay' ],
39
+ 'hal' => [ 'eve' ],
40
+ 'ian' => [ 'hope' ],
41
+ 'jon' => [ 'abi' ]
42
+ }
43
+
44
+ womens_expectations = {
45
+ 'abi' => [ 'jon' ],
46
+ 'bea' => [ 'fred' ],
47
+ 'cath' => [ 'bob' ],
48
+ 'dee' => [ 'col' ],
49
+ 'eve' => [ 'hal' ],
50
+ 'fay' => [ 'dan' ],
51
+ 'gay' => [ 'gav' ],
52
+ 'hope' => [ 'ian' ],
53
+ 'ivy' => [ 'abe' ],
54
+ 'jan' => [ 'ed' ]
55
+ }
56
+
57
+ runner = StableMatch.run men , women , :asymmetric
58
+
59
+ ## men
60
+ #
61
+ mens_expectations.each do | man , expected_matches |
62
+ candidate = runner.candidate_set1[ man ]
63
+ actual_matches = candidate.matches.map &:target
64
+ assert{ expected_matches.sort == actual_matches.sort }
65
+ end
66
+
67
+ ## women
68
+ #
69
+ womens_expectations.each do | woman , expected_matches |
70
+ candidate = runner.candidate_set2[ woman ]
71
+ actual_matches = candidate.matches.map &:target
72
+ assert{ expected_matches.sort == actual_matches.sort }
73
+ end
74
+ end
75
+ end