tcollier-sommelier 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7aec6ab221e7b53607609c6a5f7b3f84945461bb
4
+ data.tar.gz: a759cd1e83e4933366ae5b28d2fffb806d5c8ace
5
+ SHA512:
6
+ metadata.gz: 43f4525771fec89eb6fc7703eb0503d15439c8c78c4ae5c7867eadcaae80c28ad208cc04818074b51908231de9a06068d58b1a83f0df9c21d06b8823d54c072e
7
+ data.tar.gz: c39b125857e32674f126970486399b3d9d98d47b44ab2960aade9c7cbcc920db8d6ab71a438094d1881a5ff29149c3ecfed166ae7c65d0b6b9ad306f9828b7af
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ coverage/
2
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require 'spec_helper'
data/CHANGELOG.txt ADDED
@@ -0,0 +1,3 @@
1
+ Version 0.1.0, 2018-07-20
2
+ -------------------------
3
+ e66dc58 Initial release with Gale-Shapely variant
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in amsi.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,53 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ tcollier-sommelier (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ byebug (10.0.2)
10
+ coderay (1.1.2)
11
+ diff-lcs (1.3)
12
+ docile (1.3.1)
13
+ json (2.1.0)
14
+ method_source (0.9.0)
15
+ pry (0.11.3)
16
+ coderay (~> 1.1.0)
17
+ method_source (~> 0.9.0)
18
+ pry-byebug (3.6.0)
19
+ byebug (~> 10.0)
20
+ pry (~> 0.10)
21
+ rake (10.5.0)
22
+ rspec (3.7.0)
23
+ rspec-core (~> 3.7.0)
24
+ rspec-expectations (~> 3.7.0)
25
+ rspec-mocks (~> 3.7.0)
26
+ rspec-core (3.7.1)
27
+ rspec-support (~> 3.7.0)
28
+ rspec-expectations (3.7.0)
29
+ diff-lcs (>= 1.2.0, < 2.0)
30
+ rspec-support (~> 3.7.0)
31
+ rspec-mocks (3.7.0)
32
+ diff-lcs (>= 1.2.0, < 2.0)
33
+ rspec-support (~> 3.7.0)
34
+ rspec-support (3.7.1)
35
+ simplecov (0.16.1)
36
+ docile (~> 1.1)
37
+ json (>= 1.8, < 3)
38
+ simplecov-html (~> 0.10.0)
39
+ simplecov-html (0.10.2)
40
+
41
+ PLATFORMS
42
+ ruby
43
+
44
+ DEPENDENCIES
45
+ bundler (~> 1.12)
46
+ pry-byebug (~> 3.3)
47
+ rake (~> 10.0)
48
+ rspec (~> 3.4)
49
+ simplecov (~> 0.16)
50
+ tcollier-sommelier!
51
+
52
+ BUNDLED WITH
53
+ 1.16.1
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Tom Collier
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # Sommelier
2
+
3
+ Sommelier provides a solution to a variant of the Stable Marriage problem in
4
+ mathematics. In this gem, the classical suitors are replaced with dishes from a
5
+ restaurant menu and the brides-to-be are wines.
6
+
7
+ ## Stable Marriage Problem
8
+
9
+ [![Numberphile: Stable Marriage Problem](https://img.youtube.com/vi/Qcv1IqHWAzg/0.jpg)](https://www.youtube.com/watch?v=Qcv1IqHWAzg)
10
+
11
+ The [Stable Marriage Problem](https://en.wikipedia.org/wiki/Stable_marriage_problem)
12
+ is a mathematical problem that attempts to uniquely match a set of _N_ items
13
+ (classically male suitors) with another set of _N_ items (classically females the
14
+ suitors wish to marry). The final matches are referred to as proposals.
15
+
16
+ ## Gale-Shapely Algorithm
17
+
18
+ This gem provides a variant of the [Gale-Shapely algorithm](https://en.wikipedia.org/wiki/Stable_marriage_problem#Solution).
19
+ Gale-Shapely guarantees a complete matching (i.e. every suitor is paired with
20
+ exactly one female and vice versa). Though this algorithm assumes every member
21
+ of either group has a complete ranking of the other group. For large populations,
22
+ this is not always practical.
23
+
24
+ ## Variation
25
+
26
+ The algorithm applied here has 2 primary differences from Gale-Shapely
27
+
28
+ 1. Neither suitors nor suitees need to have a complete ranking of the other set.
29
+ 2. The rankings are determined by a symmetrical match score. For example, the match
30
+ score for Alice and Steve is the same as for Steve and Alice. Though scores are
31
+ relative, so Alice's match score for Steve may be her highest scoring match, but
32
+ that same score may only be the fifth highest scoring match for Steve.
33
+
34
+ Because of these differences, not every suitor or suitee is guaranteed to have
35
+ a proposal. Swapping the suitor and suitee sets can have dramatic effects on the
36
+ final set of proposals.
37
+
38
+ ## Usage
39
+
40
+ ### Installation
41
+
42
+ ```bash
43
+ gem install tcollier-sommelier
44
+ ```
45
+
46
+ ### Example
47
+
48
+ ```ruby
49
+ require 'sommelier'
50
+ sommelier = Sommelier.new
51
+ sommelier.add_match('Asparagus', 'Pinot Noir', 0.366)
52
+ sommelier.add_match('Asparagus', 'Sauvignon Blanc', 0.453)
53
+ sommelier.add_match('Asparagus', 'Chardonnay', 0.245)
54
+ sommelier.add_match('Tofu', 'Rose', 0.486)
55
+ sommelier.add_match('Tofu', 'Sauvignon Blanc', 0.304)
56
+ sommelier.add_match('Eggplant', 'Sauvignon Blanc', 0.299)
57
+ sommelier.add_match('Salmon', 'Sauvignon Blanc', 0.602)
58
+ puts sommelier.pairings
59
+ # {
60
+ # "Salmon" => "Sauvignon Blanc",
61
+ # "Tofu" => "Rose",
62
+ # "Asparagus" => "Pinot Noir"
63
+ # }
64
+
65
+ # Note: neither "Eggplant" nor "Chardonnay" were matched in the pairings map
66
+ ```
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/lib/sommelier.rb ADDED
@@ -0,0 +1,61 @@
1
+ require_relative 'sommelier/match_maker'
2
+ require_relative 'sommelier/match_catalog'
3
+
4
+ # Pair off members of one set with members of another, optimizing for the total
5
+ # match score of all pairings. This is similar to how a sommelier may provide
6
+ # a complete set of wine pairings for each dish on a menu.
7
+ #
8
+ # Example:
9
+ #
10
+ # sommelier = Sommelier.new
11
+ # sommelier.add_match('Asparagus', 'Pinot Noir', 0.366)
12
+ # sommelier.add_match('Asparagus', 'Sauvignon Blanc', 0.453)
13
+ # sommelier.add_match('Asparagus', 'Chardonnay', 0.245)
14
+ # sommelier.add_match('Tofu', 'Rose', 0.486)
15
+ # sommelier.add_match('Tofu', 'Sauvignon Blanc', 0.304)
16
+ # sommelier.add_match('Eggplant', 'Sauvignon Blanc', 0.299)
17
+ # sommelier.add_match('Salmon', 'Sauvignon Blanc', 0.602)
18
+ # puts sommelier.pairings
19
+ # # {
20
+ # # "Salmon" => "Sauvignon Blanc",
21
+ # # "Tofu" => "Rose",
22
+ # # "Asparagus" => "Pinot Noir"
23
+ # # }
24
+ #
25
+ # # Note: neither "Eggplant" nor "Chardonnay" were matched in the pairings map
26
+ #
27
+ class Sommelier
28
+ def initialize
29
+ @match_catalog = MatchCatalog.new
30
+ end
31
+
32
+ # Include a potential pairing between the dish and wine. The pairing has a
33
+ # score (a higher score means a better pairing) that is used the generate
34
+ # preferences of dish => wines and wine => dishes. These preferences are used
35
+ # in a variant of the Gale-Shapely algorithm.
36
+ #
37
+ # Since the dishes are acting as the suitors (in Gale-Shapely terminology),
38
+ # they tend to get better preferred wines than vice versa. However, since a
39
+ # pairing is symmetrical, passing the wines in as the dish and the dishes in
40
+ # as the wine will work, but yield potentially different pairings.
41
+ #
42
+ # @param dish [Object] the proposal maker in the Gale-Shapely algorithm.
43
+ # @param wine [Object] the proposal acceptor/rejector in the Gale-Shapely
44
+ # algorithm.
45
+ # @param score [Numeric] a numeric representation of the strength of the
46
+ # pairing. A higher score means a better paring.
47
+ def add_match(dish, wine, score)
48
+ match_catalog.add(dish, wine, score)
49
+ end
50
+
51
+ # A map with dishes as keys and wines as objects
52
+ #
53
+ # @return [Hash<Object, Object>] the final matching between dishes and wines.
54
+ def pairings
55
+ MatchMaker.new(match_catalog).pairings
56
+ end
57
+
58
+ private
59
+
60
+ attr_reader :match_catalog
61
+ end
@@ -0,0 +1,22 @@
1
+ class Sommelier
2
+ # An extension of the ruby built-in Array class with an optimization for
3
+ # insertion sort. This class assumes the array elements are sorted in
4
+ # descending order.
5
+ class DescendingInsertionSortArray < Array
6
+ # Insert `item` just before the element with the highest value that is lower
7
+ # than the value of `item`
8
+ #
9
+ # @param item [Object] any object that responds to `>`
10
+ def sorted_insert(item)
11
+ insertion_index = (0...size).bsearch(&search_proc(item))
12
+ insert(insertion_index || length, item)
13
+ end
14
+
15
+ private
16
+
17
+ def search_proc(item)
18
+ ->(index) { item > self[index] }
19
+ end
20
+ end
21
+ private_constant :DescendingInsertionSortArray
22
+ end
@@ -0,0 +1,75 @@
1
+ require_relative 'descending_insertion_sort_array'
2
+ require_relative 'preference'
3
+
4
+ class Sommelier
5
+ # A collection of all matches to consider for pairing up dishes and wines.
6
+ class MatchCatalog
7
+ attr_reader :max_dish_preferences
8
+
9
+ def initialize
10
+ @dishes = {}
11
+ @wines = {}
12
+ @max_dish_preferences = 0
13
+ end
14
+
15
+ # Add a symmetrical match for the dish and wine
16
+ #
17
+ # @param dish [Object] the dish
18
+ # @param wine [Object] the wine
19
+ # @param score [Number] a number dictating the strength of the match between
20
+ # the dish and wine (a higher number indicate a stronger match)
21
+ def add(dish, wine, score)
22
+ dish_prefs = add_dish_prefs(dish)
23
+ wine_prefs = add_wine_prefs(wine)
24
+
25
+ dish_prefs.sorted_insert(Preference.new(wine, score))
26
+ if dish_prefs.count > max_dish_preferences
27
+ @max_dish_preferences = dish_prefs.count
28
+ end
29
+
30
+ wine_prefs.sorted_insert(Preference.new(dish, score))
31
+ end
32
+
33
+ # @yield dish, preferences_count [Object, Integer] yield each dish and the
34
+ # number of wines in it's preference list.
35
+ def each_dish(&block)
36
+ dishes.each do |dish, prefs|
37
+ yield dish, prefs.count
38
+ end
39
+ end
40
+
41
+ # @param dish [Object] the dish to find the wine for
42
+ # @param rank [Integer] the 0-based rank in the dish's preferences
43
+ # @return [Object] return the Nth ranked wine based on the given dishes
44
+ # preferences
45
+ def wine_preferred_at(dish, rank)
46
+ dishes[dish][rank].object
47
+ end
48
+
49
+ # Return the highest ranked dish (for the wine) in the list of dishes
50
+ #
51
+ # @param wine [Object]
52
+ # @param dishes [Array<Object>]
53
+ # @return [Object] the highest ranked dish in the list
54
+ def most_preferred_dish(wine, dishes)
55
+ wines[wine].detect do |preference|
56
+ dishes.include?(preference.object)
57
+ end.object
58
+ end
59
+
60
+ private
61
+
62
+ def add_dish_prefs(object)
63
+ dishes[object] ||= DescendingInsertionSortArray.new
64
+ dishes[object]
65
+ end
66
+
67
+ def add_wine_prefs(object)
68
+ wines[object] ||= DescendingInsertionSortArray.new
69
+ wines[object]
70
+ end
71
+
72
+ attr_reader :dishes, :wines
73
+ end
74
+ private_constant :MatchCatalog
75
+ end
@@ -0,0 +1,43 @@
1
+ require_relative 'match_maker/generator'
2
+ require_relative 'match_maker/decider'
3
+
4
+ class Sommelier
5
+ # Implementation of a variation of the Gale-Shapely algorithm. In this
6
+ # variation, every dish that isn't currently in a pairing and hasn't exhausted
7
+ # its list of preferred wines will attempt a pairing with its highest
8
+ # matching wine that it has not yet attempt to pair with.
9
+ #
10
+ # Once all of the pairings for a round are made, then wines will accept
11
+ # only the highest matching dish that has attempted to pair with it. It will
12
+ # reject all others, even if it had previously accepted the pairing in a prior
13
+ # round.
14
+ class MatchMaker
15
+ def initialize(match_catalog)
16
+ @match_catalog = match_catalog
17
+ end
18
+
19
+ # Return a set of pairings. Given that not all of the Gale-Shapely
20
+ # constraints must've been met, this is not guaranteed to include every
21
+ # dish or every wine.
22
+ #
23
+ # @return [Hash<Object, Object>] a mapping of dish to wine pairings
24
+ def pairings
25
+ accepted = {}
26
+ reversed = {}
27
+
28
+ generator = Generator.new(match_catalog)
29
+ decider = Decider.new(match_catalog)
30
+ (0...match_catalog.max_dish_preferences).each do |round|
31
+ requests = generator.requests(round, accepted)
32
+ decider.decide!(requests, accepted, reversed)
33
+ end
34
+
35
+ accepted
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :match_catalog
41
+ end
42
+ private_constant :MatchMaker
43
+ end
@@ -0,0 +1,39 @@
1
+ class Sommelier
2
+ class MatchMaker
3
+ class Decider
4
+ def initialize(match_catalog)
5
+ @match_catalog = match_catalog
6
+ end
7
+
8
+ # Decide a single dish's pairing request to accept for each wine
9
+ #
10
+ # @param requests [Hash<Object, Array<Object>>] mapping of wines to
11
+ # the list of dishes that have requested pairing in the current round
12
+ # @param accepted [Hash<Object, Object>] mapping of dishes to the wine
13
+ # that has accepted its pairing request
14
+ # @param reversed [Hash<Object, Object>] mapping of wines to the dish
15
+ # that has its pairing request accepted by wine
16
+ def decide!(requests, accepted, reversed)
17
+ requests.each do |wine, current_dishes|
18
+ # Be sure to consider the full set of current dishes and potentially
19
+ # a dish from a prior round who had its pairing request accepted by
20
+ # the wine.
21
+ prior_accepted_dish = reversed[wine]
22
+ dishes = current_dishes + [*prior_accepted_dish]
23
+ winning_dish = match_catalog.most_preferred_dish(wine, dishes)
24
+
25
+ if prior_accepted_dish != winning_dish
26
+ accepted.delete(prior_accepted_dish)
27
+ accepted[winning_dish] = wine
28
+ reversed[wine] = winning_dish
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :match_catalog
36
+ end
37
+ private_constant :Decider
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ class Sommelier
2
+ class MatchMaker
3
+ class Generator
4
+ def initialize(match_catalog)
5
+ @match_catalog = match_catalog
6
+ end
7
+
8
+ # Generate all of the requested pairings for a single round of the
9
+ # algorithm. Each dish that isn't currently locked in a pairing from a
10
+ # prior round and hasn't exhausted its list of preferred wines will
11
+ # request a pairing with the highest matching wine it has not yet
12
+ # requested.
13
+ #
14
+ # @param round [Integer] the round number
15
+ # @param accepted [Hash<Object, Object>] mapping of dishes to the wine
16
+ # that has accepted his pairing
17
+ # @return [Hash<Object, Array<Object>>] mapping of wines to the list of
18
+ # dishes that have requested pairings in the current round
19
+ def requests(round, accepted)
20
+ requests = {}
21
+ match_catalog.each_dish do |dish, preferences_count|
22
+ if round < preferences_count && !accepted.key?(dish)
23
+ wine = match_catalog.wine_preferred_at(dish, round)
24
+ requests[wine] ||= []
25
+ requests[wine] << dish
26
+ end
27
+ end
28
+ requests
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :match_catalog
34
+ end
35
+ private_constant :Generator
36
+ end
37
+ end
@@ -0,0 +1,18 @@
1
+ class Sommelier
2
+ # Simple class to bind a score (useable for ranking/sorting) to a generic
3
+ # object.
4
+ class Preference
5
+ attr_reader :object, :score
6
+
7
+ def initialize(object, score)
8
+ @object = object
9
+ @score = score
10
+ end
11
+
12
+ # Items used in DescendingInsertionSortArray must implement `>`
13
+ def >(other)
14
+ score > other.score
15
+ end
16
+ end
17
+ private_constant :Preference
18
+ end
@@ -0,0 +1,3 @@
1
+ class Sommelier
2
+ VERSION = '0.1.0'
3
+ end
data/sommelier.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'sommelier/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'tcollier-sommelier'
8
+ spec.version = Sommelier::VERSION
9
+ spec.authors = ['Tom Collier']
10
+ spec.email = ['tcollier@gmail.com']
11
+
12
+ spec.summary = 'Ruby implementation of a Stable Marriage solver'
13
+ spec.homepage = 'https://github.com/tcollier/sommelier'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_development_dependency 'bundler', '~> 1.12'
21
+ spec.add_development_dependency 'pry-byebug', '~> 3.3'
22
+ spec.add_development_dependency 'rake', '~> 10.0'
23
+ spec.add_development_dependency 'rspec', '~> 3.4'
24
+ spec.add_development_dependency 'simplecov', '~> 0.16'
25
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tcollier-sommelier
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tom Collier
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-07-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry-byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.16'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.16'
83
+ description:
84
+ email:
85
+ - tcollier@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - CHANGELOG.txt
93
+ - Gemfile
94
+ - Gemfile.lock
95
+ - LICENSE
96
+ - README.md
97
+ - Rakefile
98
+ - lib/sommelier.rb
99
+ - lib/sommelier/descending_insertion_sort_array.rb
100
+ - lib/sommelier/match_catalog.rb
101
+ - lib/sommelier/match_maker.rb
102
+ - lib/sommelier/match_maker/decider.rb
103
+ - lib/sommelier/match_maker/generator.rb
104
+ - lib/sommelier/preference.rb
105
+ - lib/sommelier/version.rb
106
+ - sommelier.gemspec
107
+ homepage: https://github.com/tcollier/sommelier
108
+ licenses:
109
+ - MIT
110
+ metadata: {}
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubyforge_project:
127
+ rubygems_version: 2.6.11
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Ruby implementation of a Stable Marriage solver
131
+ test_files: []