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