stable-matching 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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.gitlab-ci.yml +28 -0
- data/.rspec +4 -0
- data/.rubocop.yml +31 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +154 -0
- data/Rakefile +6 -0
- data/circle.yml +3 -0
- data/lib/stable-matching/logging_helper.rb +16 -0
- data/lib/stable-matching/marriage.rb +135 -0
- data/lib/stable-matching/marriage/phase_i_runner.rb +122 -0
- data/lib/stable-matching/marriage/preference_table.rb +64 -0
- data/lib/stable-matching/marriage/validator.rb +51 -0
- data/lib/stable-matching/member.rb +82 -0
- data/lib/stable-matching/phase_runner.rb +44 -0
- data/lib/stable-matching/preference_list.rb +18 -0
- data/lib/stable-matching/preference_table.rb +80 -0
- data/lib/stable-matching/roommate.rb +124 -0
- data/lib/stable-matching/roommate/phase_i_runner.rb +111 -0
- data/lib/stable-matching/roommate/phase_ii_runner.rb +105 -0
- data/lib/stable-matching/roommate/phase_iii_runner.rb +159 -0
- data/lib/stable-matching/roommate/preference_table.rb +20 -0
- data/lib/stable-matching/roommate/validator.rb +26 -0
- data/lib/stable-matching/stable_matching.rb +11 -0
- data/lib/stable-matching/validator.rb +93 -0
- data/lib/version.rb +3 -0
- data/stable-matching.gemspec +29 -0
- metadata +134 -0
@@ -0,0 +1,111 @@
|
|
1
|
+
# Provides a ruby implementation of several common matching algorithms
|
2
|
+
#
|
3
|
+
# Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
|
4
|
+
# License:: MIT
|
5
|
+
|
6
|
+
#
|
7
|
+
# Implements Phase I of Irving's (1985) Stable Roommates algorithm.
|
8
|
+
#
|
9
|
+
# See:
|
10
|
+
# - https://en.wikipedia.org/wiki/Stable_roommates_problem
|
11
|
+
# - https://www.youtube.com/watch?v=9Lo7TFAkohEaccepted_proposal?
|
12
|
+
#
|
13
|
+
# In this round each member who has not had their proposal accepted "proposes"
|
14
|
+
# to their top remaining preference
|
15
|
+
#
|
16
|
+
# Each recipient of a proposal can take one of 3 actions -
|
17
|
+
#
|
18
|
+
# 1. The recipient has not received a previous proposal and immediately accepts
|
19
|
+
#
|
20
|
+
# 2. The recipient prefers this new proposal over an existing one.
|
21
|
+
# The recipient "rejects" it's initial proposl and accepts this new one
|
22
|
+
#
|
23
|
+
# 3. The recipient prefers the existing proposal over the new one.
|
24
|
+
# The recipient "rejects" the new proposal
|
25
|
+
#
|
26
|
+
# In the above situations, every rejection is mutual - if `i` removes `j` from
|
27
|
+
# its preference list, then `j` must also remove `i` from its list
|
28
|
+
#
|
29
|
+
# This cycle continues until one of two things happens
|
30
|
+
#
|
31
|
+
# A. Every member has had their proposal accepted
|
32
|
+
# -> Move on to Phase II
|
33
|
+
# B. At least one member has exhausted their preference list
|
34
|
+
# -> No solution exists
|
35
|
+
#
|
36
|
+
#
|
37
|
+
# === EXAMPLE
|
38
|
+
#
|
39
|
+
# Take the following preference lists
|
40
|
+
#
|
41
|
+
# A => [B, D, F, C, E],
|
42
|
+
# B => [D, E, F, A, C],
|
43
|
+
# C => [D, E, F, A, B],
|
44
|
+
# D => [F, C, A, E, B],
|
45
|
+
# E => [F, C, D, B, A],
|
46
|
+
# F => [A, B, D, C, E]
|
47
|
+
#
|
48
|
+
# We always start with the first unmatched user. Initially this is "A".
|
49
|
+
# The sequence of events are -
|
50
|
+
#
|
51
|
+
# 'A' proposes to 'B'
|
52
|
+
# 'B' accepts 'A'
|
53
|
+
# 'B' proposes to 'D'
|
54
|
+
# 'D' accepts 'B'
|
55
|
+
# 'C' proposes to 'D'
|
56
|
+
# 'D' accepts 'C', rejects 'B'
|
57
|
+
# 'B' proposes to 'E'
|
58
|
+
# 'E' accepts 'B'
|
59
|
+
# 'D' proposes to 'F'
|
60
|
+
# 'F' accepts 'D'
|
61
|
+
# 'E' proposes to 'F'
|
62
|
+
# 'F' rejects
|
63
|
+
# 'E' proposes to 'C'
|
64
|
+
# 'C' accepts 'E'
|
65
|
+
# 'F' proposes to 'A'
|
66
|
+
# 'A' accepts 'F'
|
67
|
+
#
|
68
|
+
# The result of this phase is shown below.
|
69
|
+
# A "-" indicates a proposal made and a "+" indicates a proposal accepted.
|
70
|
+
# Rejected members are removed.
|
71
|
+
#
|
72
|
+
# A => [-B, D, +F, C, E],
|
73
|
+
# B => [-E, F, +A, C],
|
74
|
+
# C => [-D, +E, F, A, B],
|
75
|
+
# D => [-F, +C, A, E],
|
76
|
+
# E => [-C, D, +B, A],
|
77
|
+
# F => [-A, B, +D, C]
|
78
|
+
|
79
|
+
require_relative "../phase_runner"
|
80
|
+
|
81
|
+
class StableMatching
|
82
|
+
class Roommate
|
83
|
+
class PhaseIRunner < StableMatching::PhaseRunner
|
84
|
+
def initialize(preference_table, opts = {})
|
85
|
+
@preference_table = preference_table
|
86
|
+
@logger = opts.fetch(:logger)
|
87
|
+
end
|
88
|
+
|
89
|
+
def run
|
90
|
+
while @preference_table.unmatched.any?
|
91
|
+
ensure_table_is_stable!
|
92
|
+
|
93
|
+
member = @preference_table.unmatched.first
|
94
|
+
top_choice = member.first_preference
|
95
|
+
|
96
|
+
simulate_proposal(member, top_choice)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Check once more since final iteration may have left the table unstable
|
100
|
+
ensure_table_is_stable!
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def ensure_table_is_stable!
|
106
|
+
return true if @preference_table.stable?
|
107
|
+
raise StableMatching::NoStableSolutionError, "No stable match found!"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# Provides a ruby implementation of several common matching algorithms
|
2
|
+
#
|
3
|
+
# Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
|
4
|
+
# License:: MIT
|
5
|
+
|
6
|
+
# Implements Phase II of Irving's (1985) Stable Roommates algorithm.
|
7
|
+
#
|
8
|
+
# See:
|
9
|
+
# - https://en.wikipedia.org/wiki/Stable_roommates_problem
|
10
|
+
# - https://www.youtube.com/watch?v=9Lo7TFAkohEaccepted_proposal?
|
11
|
+
#
|
12
|
+
# In this phase, each member that has accepted a proposal will remove those they
|
13
|
+
# prefer less than their current proposer.
|
14
|
+
#
|
15
|
+
# At the end of one iteration, one of two states are possible -
|
16
|
+
#
|
17
|
+
# A. At least one member has exhausted their preference list
|
18
|
+
# -> No solution exists
|
19
|
+
#
|
20
|
+
# B. Some members have multiple preferences remaining
|
21
|
+
# -> Proceed to Phase III
|
22
|
+
#
|
23
|
+
# C. All members have one preference remaining
|
24
|
+
# -> Solution has been found, no need to run Phase III
|
25
|
+
#
|
26
|
+
#
|
27
|
+
# NOTE: For the ease of implementation, step (C) is implemented as the first
|
28
|
+
# check in Phase III, although it is logically part of this step.
|
29
|
+
#
|
30
|
+
#
|
31
|
+
# === EXAMPLE
|
32
|
+
#
|
33
|
+
# Assume the output of Phase I is
|
34
|
+
#
|
35
|
+
# A => [-B, D, +F, C, E],
|
36
|
+
# B => [-E, F, +A, C],
|
37
|
+
# C => [-D, +E, F, A, B],
|
38
|
+
# D => [-F, +C, A, E],
|
39
|
+
# E => [-C, D, +B, A],
|
40
|
+
# F => [-A, B, +D, C]
|
41
|
+
#
|
42
|
+
# Where a "-" indicates a proposal made and a "+" indicates a proposal accepted.
|
43
|
+
#
|
44
|
+
# Rejections for Phase II would occur as follows. Note that all rejections are
|
45
|
+
# mutual - if `i` removes `j` from its preference list, then `j` must also
|
46
|
+
# remove `i` from its list
|
47
|
+
#
|
48
|
+
# A accepted by B. B rejecting members less preferred than A: ["C"]
|
49
|
+
# B accepted by E. E rejecting members less preferred than B: ["A"]
|
50
|
+
# C accepted by D. D rejecting members less preferred than C: ["A", "E"]
|
51
|
+
# D accepted by F. F rejecting members less preferred than D: ["C"]
|
52
|
+
# E accepted by C. C rejecting members less preferred than E: ["A"]
|
53
|
+
# F accepted by A. A rejecting members less preferred than F: []
|
54
|
+
#
|
55
|
+
# The output of this phase is a further reduced table is as follows
|
56
|
+
#
|
57
|
+
# A => [B, F],
|
58
|
+
# B => [E, F, A],
|
59
|
+
# C => [D, E],
|
60
|
+
# D => [F, C],
|
61
|
+
# E => [C, B],
|
62
|
+
# F => [A, B, D]
|
63
|
+
#
|
64
|
+
# Since at least one member has multiple preferences remaining, we proceed to
|
65
|
+
# Phase III
|
66
|
+
#
|
67
|
+
|
68
|
+
require_relative "../phase_runner"
|
69
|
+
|
70
|
+
class StableMatching
|
71
|
+
class Roommate
|
72
|
+
class PhaseIIRunner < StableMatching::PhaseRunner
|
73
|
+
def initialize(preference_table, opts = {})
|
74
|
+
@preference_table = preference_table
|
75
|
+
@logger = opts.fetch(:logger)
|
76
|
+
end
|
77
|
+
|
78
|
+
def run
|
79
|
+
@preference_table.members.each do |proposer|
|
80
|
+
acceptor, rejections =
|
81
|
+
determine_acceptor_and_their_rejections(proposer)
|
82
|
+
|
83
|
+
@logger.debug(
|
84
|
+
"#{proposer.name} accepted by #{acceptor.name}. "\
|
85
|
+
"#{acceptor.name} rejecting members less preferred than "\
|
86
|
+
"#{proposer.name}: #{rejections.map(&:name)}"
|
87
|
+
)
|
88
|
+
|
89
|
+
rejections.each { |rejected| acceptor.reject!(rejected) }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def determine_acceptor_and_their_rejections(proposer)
|
96
|
+
acceptor = proposer.current_acceptor
|
97
|
+
current_proposer_index = acceptor.preference_list.index(proposer)
|
98
|
+
|
99
|
+
rejections = acceptor.preference_list[current_proposer_index + 1..-1]
|
100
|
+
|
101
|
+
[acceptor, rejections]
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# Provides a ruby implementation of several common matching algorithms
|
2
|
+
#
|
3
|
+
# Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
|
4
|
+
# License:: MIT
|
5
|
+
|
6
|
+
# Implements Phase II of Irving's (1985) Stable Roommates algorithm.
|
7
|
+
#
|
8
|
+
# See:
|
9
|
+
# - https://en.wikipedia.org/wiki/Stable_roommates_problem
|
10
|
+
# - https://www.youtube.com/watch?v=9Lo7TFAkohEaccepted_proposal?
|
11
|
+
#
|
12
|
+
# In this last phase we attempt to find any preference cycles and reject them.
|
13
|
+
#
|
14
|
+
# This is done by building a pair of members (Xi, Yi) as follows
|
15
|
+
#
|
16
|
+
# - Xi is the first member with at least 2 preferences
|
17
|
+
# - Yi is null
|
18
|
+
# - Yi+1 is the 2nd preference of Xi
|
19
|
+
# - Xi+1 is the last preference of Yi+1
|
20
|
+
#
|
21
|
+
# Continue calculating pairs (Xi, Yi) until Xi repeats values. At this point a
|
22
|
+
# cycle has been found.
|
23
|
+
#
|
24
|
+
# Mutually reject every pair (Xi, Yi)
|
25
|
+
#
|
26
|
+
# After this one of 3 possiblities exists -
|
27
|
+
#
|
28
|
+
# A. At least one member has exhausted their preference list
|
29
|
+
# -> No solution exists
|
30
|
+
#
|
31
|
+
# B. Some members have multiple preferences remaining
|
32
|
+
# -> Repeat the above process to eliminate further cycles
|
33
|
+
#
|
34
|
+
# C. All members have one preference remaining
|
35
|
+
# -> Solution has been found
|
36
|
+
#
|
37
|
+
# === EXAMPLE
|
38
|
+
#
|
39
|
+
# Assume the reduced output of Phase II is
|
40
|
+
#
|
41
|
+
# A => [B, F],
|
42
|
+
# B => [E, F, A],
|
43
|
+
# C => [D, E],
|
44
|
+
# D => [F, C],
|
45
|
+
# E => [C, B],
|
46
|
+
# F => [A, B, D]
|
47
|
+
#
|
48
|
+
# Start with "A" since it is the first member with at least two preferences.
|
49
|
+
#
|
50
|
+
# Build (Xi, Yi) pairs as follows
|
51
|
+
#
|
52
|
+
# i 1 2 3 4
|
53
|
+
# -----+---+---+---+----
|
54
|
+
# x: | A | D | E | A
|
55
|
+
# y: | - | F | C | B
|
56
|
+
#
|
57
|
+
# Where -
|
58
|
+
#
|
59
|
+
# 'F' is the 2nd preference of 'A'
|
60
|
+
# 'D' is the last preference of 'F'
|
61
|
+
# 'C' is the 2nd preference of 'D'
|
62
|
+
# etc...
|
63
|
+
#
|
64
|
+
# As soon as we see "A" again, we stop since we have found a cycle.
|
65
|
+
#
|
66
|
+
# Now we will mutually reject the following pairs, as definied by the inner
|
67
|
+
# pairings
|
68
|
+
#
|
69
|
+
# - (D, F)
|
70
|
+
# - (E, C)
|
71
|
+
# - (A, B)
|
72
|
+
#
|
73
|
+
# At this point, no preference list is exhausted and some have more than one
|
74
|
+
# preference remaining. We need to find and eliminate more preference cycles.
|
75
|
+
#
|
76
|
+
# i 1 2
|
77
|
+
# -----+---+---
|
78
|
+
# x: | B | B
|
79
|
+
# y: | - | F
|
80
|
+
#
|
81
|
+
# Now we will mutually reject
|
82
|
+
#
|
83
|
+
# - (F, B)
|
84
|
+
#
|
85
|
+
# This gives us the stable solution below, since each roommate has exactly one
|
86
|
+
# preference remaining
|
87
|
+
#
|
88
|
+
# A => [F],
|
89
|
+
# B => [E],
|
90
|
+
# C => [D],
|
91
|
+
# D => [C],
|
92
|
+
# E => [B],
|
93
|
+
# F => [A]
|
94
|
+
|
95
|
+
require_relative "../phase_runner"
|
96
|
+
|
97
|
+
class StableMatching
|
98
|
+
class Roommate
|
99
|
+
class PhaseIIIRunner < StableMatching::PhaseRunner
|
100
|
+
def initialize(preference_table, opts = {})
|
101
|
+
@preference_table = preference_table
|
102
|
+
@logger = opts.fetch(:logger)
|
103
|
+
end
|
104
|
+
|
105
|
+
def run
|
106
|
+
# The output of previous phase may have resulted in a complete
|
107
|
+
# table, in which case this step doesn't need to be run
|
108
|
+
return @preference_table if @preference_table.complete?
|
109
|
+
|
110
|
+
while table_is_stable? && !@preference_table.complete?
|
111
|
+
x = [@preference_table.members_with_multiple_preferences.first]
|
112
|
+
y = [nil]
|
113
|
+
|
114
|
+
until any_repeats?(x)
|
115
|
+
# require 'pry'; binding.pry if x.last.second_preference.nil?
|
116
|
+
y << x.last.second_preference
|
117
|
+
x << y.last.last_preference
|
118
|
+
end
|
119
|
+
|
120
|
+
detect_and_reject_cycles(x, y)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def any_repeats?(arr)
|
127
|
+
arr.uniq.count != arr.count
|
128
|
+
end
|
129
|
+
|
130
|
+
def detect_and_reject_cycles(x, y)
|
131
|
+
x, y = retrieve_cycle(x, y)
|
132
|
+
|
133
|
+
msg = "Found cycle: "
|
134
|
+
y.each_with_index { |_, i| msg << "(#{x[i].name}, #{y[i].name})" }
|
135
|
+
@logger.debug(msg)
|
136
|
+
|
137
|
+
x.each_with_index do |r1, i|
|
138
|
+
r2 = y[i]
|
139
|
+
@logger.debug("Mutually rejecting '#{r1.name}', '#{r2.name}'")
|
140
|
+
r2.reject!(r1)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def retrieve_cycle(x, y)
|
145
|
+
repeated_member = x.detect { |i| x.count(i) > 1 }
|
146
|
+
|
147
|
+
first_index = 1
|
148
|
+
last_index = x.count - x.reverse.index(repeated_member) - 1
|
149
|
+
|
150
|
+
[x[first_index..last_index], y[first_index..last_index]]
|
151
|
+
end
|
152
|
+
|
153
|
+
def table_is_stable?
|
154
|
+
return true if @preference_table.stable?
|
155
|
+
raise StableMatching::NoStableSolutionError, "No stable match found!"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Provides a ruby implementation of several common matching algorithms
|
2
|
+
#
|
3
|
+
# Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
|
4
|
+
# License:: MIT
|
5
|
+
|
6
|
+
require_relative "../preference_table"
|
7
|
+
|
8
|
+
class StableMatching
|
9
|
+
class Roommate
|
10
|
+
class PreferenceTable < StableMatching::PreferenceTable
|
11
|
+
def stable?
|
12
|
+
members.all? { |member| !member.preference_list.empty? }
|
13
|
+
end
|
14
|
+
|
15
|
+
def members_with_multiple_preferences
|
16
|
+
members.select { |member| member.preference_list.count > 1 }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Provides a ruby implementation of several common matching algorithms
|
2
|
+
#
|
3
|
+
# Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
|
4
|
+
# License:: MIT
|
5
|
+
|
6
|
+
require_relative "../validator"
|
7
|
+
|
8
|
+
class StableMatching
|
9
|
+
class Roommate
|
10
|
+
class Validator < StableMatching::Validator
|
11
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
12
|
+
def validate!
|
13
|
+
case
|
14
|
+
when !hash_of_arrays? then handle_not_hash_of_arrays
|
15
|
+
when empty? then handle_empty
|
16
|
+
when !strings_or_integers? then handle_not_strings_or_integers
|
17
|
+
when !even_sized? then handle_not_even_sized
|
18
|
+
when !symmetrical? then handle_not_symmetrical
|
19
|
+
end
|
20
|
+
|
21
|
+
raise ::StableMatching::InvalidPreferences, @error if @error
|
22
|
+
end
|
23
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# Provides a ruby implementation of several common matching algorithms
|
2
|
+
#
|
3
|
+
# Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
|
4
|
+
# License:: MIT
|
5
|
+
|
6
|
+
class StableMatching
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
class NoStableSolutionError < Error; end
|
10
|
+
class InvalidPreferences < Error; end
|
11
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# Provides a ruby implementation of several common matching algorithms
|
2
|
+
#
|
3
|
+
# Author:: Abhishek Chandrasekhar (mailto:me@abhchand.me)
|
4
|
+
# License:: MIT
|
5
|
+
|
6
|
+
class StableMatching
|
7
|
+
class Validator
|
8
|
+
def self.validate!(preference_table)
|
9
|
+
new(preference_table).validate!
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(preference_table)
|
13
|
+
@preference_table = preference_table
|
14
|
+
end
|
15
|
+
|
16
|
+
# @abstract Subclass is expected to implement #validate!
|
17
|
+
# @!method validate!
|
18
|
+
# Validate the structure and content of the `preference_table`
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def hash_of_arrays?
|
23
|
+
return false unless @preference_table.is_a?(Hash)
|
24
|
+
@preference_table.values.each { |p| return false unless p.is_a?(Array) }
|
25
|
+
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
def handle_not_hash_of_arrays
|
30
|
+
@error = "Expecting a preference table hash of arrays"
|
31
|
+
end
|
32
|
+
|
33
|
+
def empty?
|
34
|
+
return true if @preference_table.empty?
|
35
|
+
|
36
|
+
@preference_table.each_value.any?(&:empty?)
|
37
|
+
end
|
38
|
+
|
39
|
+
def handle_empty
|
40
|
+
@error = "Preferences table can not empty"
|
41
|
+
end
|
42
|
+
|
43
|
+
def strings_or_integers?
|
44
|
+
@preference_table.each do |key, array|
|
45
|
+
@member_klass ||= key.class
|
46
|
+
|
47
|
+
return false unless valid_member?(key)
|
48
|
+
array.each { |value| return false unless valid_member?(value) }
|
49
|
+
end
|
50
|
+
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def handle_not_strings_or_integers
|
55
|
+
@error = "All keys must be String or Integer"
|
56
|
+
end
|
57
|
+
|
58
|
+
def even_sized?
|
59
|
+
@preference_table.keys.size.even?
|
60
|
+
end
|
61
|
+
|
62
|
+
def handle_not_even_sized
|
63
|
+
@error = "Preference table must have an even number of keys"
|
64
|
+
end
|
65
|
+
|
66
|
+
def symmetrical?
|
67
|
+
@preference_table.each do |name, preference_list|
|
68
|
+
expected_members = @preference_table.keys - [name]
|
69
|
+
actual_members = preference_list
|
70
|
+
|
71
|
+
next if expected_members.sort == actual_members.sort
|
72
|
+
|
73
|
+
@name = name
|
74
|
+
@extra = actual_members - expected_members
|
75
|
+
@missing = expected_members - actual_members
|
76
|
+
return false
|
77
|
+
end
|
78
|
+
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
82
|
+
def handle_not_symmetrical
|
83
|
+
@error = "Entry #{@name} has invalid preferences. "\
|
84
|
+
"The extra members are: #{@extra}. "\
|
85
|
+
"The missing members are: #{@missing}"
|
86
|
+
end
|
87
|
+
|
88
|
+
def valid_member?(member)
|
89
|
+
(member.is_a?(String) || member.is_a?(Integer)) &&
|
90
|
+
member.class == @member_klass
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|