tcollier-sommelier 0.1.0 → 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.
- checksums.yaml +4 -4
- data/.travis.yml +6 -0
- data/CHANGELOG.txt +6 -2
- data/README.md +48 -12
- data/Rakefile +1 -0
- data/lib/sommelier.rb +5 -2
- data/lib/sommelier/match_maker/decider.rb +8 -4
- data/lib/sommelier/match_maker/generator.rb +5 -6
- data/lib/sommelier/version.rb +1 -1
- data/lib/tasks/sommelier.rake +20 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b069aee84c44d4bde4c6482d81c47b8bfcf7ae65
|
4
|
+
data.tar.gz: 4f8b1044d8973656f19dd921a1e6051edd87869d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 459dc2eef3cddd6194f42ded392a32ed3e166759fb30b0edb4216215de9c47aef588fbcc1d51aed19ea5d6454cde51b2f6b995fce1a509977add818e73e609c7
|
7
|
+
data.tar.gz: 40fca90df92292e7f5e29810ae039094f155661f7e45a5ad04662b8a446515dedeefe98a11e282a82cb0630dfac2565f8415674449e5b48d04e5127e213dc20c
|
data/.travis.yml
ADDED
data/CHANGELOG.txt
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
-
Version 0.
|
1
|
+
Version 0.2.0, 2018-07-21
|
2
2
|
-------------------------
|
3
|
-
|
3
|
+
fab29e5 Add rake task to apply Sommelier to a CSV
|
4
|
+
|
5
|
+
Version 0.1.0, 2018-07-21
|
6
|
+
-------------------------
|
7
|
+
b5c4925 Initial release with Gale-Shapely variant
|
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
[](https://travis-ci.org/tcollier/sommelier)
|
2
|
+
|
1
3
|
# Sommelier
|
2
4
|
|
3
5
|
Sommelier provides a solution to a variant of the Stable Marriage problem in
|
@@ -11,7 +13,7 @@ restaurant menu and the brides-to-be are wines.
|
|
11
13
|
The [Stable Marriage Problem](https://en.wikipedia.org/wiki/Stable_marriage_problem)
|
12
14
|
is a mathematical problem that attempts to uniquely match a set of _N_ items
|
13
15
|
(classically male suitors) with another set of _N_ items (classically females the
|
14
|
-
suitors wish to marry).
|
16
|
+
suitors wish to marry).
|
15
17
|
|
16
18
|
## Gale-Shapely Algorithm
|
17
19
|
|
@@ -23,17 +25,18 @@ this is not always practical.
|
|
23
25
|
|
24
26
|
## Variation
|
25
27
|
|
26
|
-
The algorithm applied here has
|
28
|
+
The algorithm applied here has a few key differences from Gale-Shapely
|
27
29
|
|
28
|
-
1.
|
29
|
-
2.
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
1. The number of dishes and wines do not need to be equal.
|
31
|
+
2. Neither dishes nor wines need to have a complete ranking of the other set.
|
32
|
+
3. The rankings are determined by a symmetrical match score. For example, the match
|
33
|
+
score for Eggplant and Cabernet is the same as for Cabernet and Eggplant. Though scores are
|
34
|
+
relative, so Eggplant's match score for Cabernet may be its highest scoring match, but
|
35
|
+
that same score may only be the fifth highest scoring match for Cabernet.
|
33
36
|
|
34
|
-
Because of these differences, not every
|
35
|
-
|
36
|
-
|
37
|
+
Because of these differences, not every dish or wine is guaranteed to be in the
|
38
|
+
pairings map. Swapping the dish and wine sets in the match catalog alter the final
|
39
|
+
set of pairings.
|
37
40
|
|
38
41
|
## Usage
|
39
42
|
|
@@ -51,16 +54,49 @@ sommelier = Sommelier.new
|
|
51
54
|
sommelier.add_match('Asparagus', 'Pinot Noir', 0.366)
|
52
55
|
sommelier.add_match('Asparagus', 'Sauvignon Blanc', 0.453)
|
53
56
|
sommelier.add_match('Asparagus', 'Chardonnay', 0.245)
|
54
|
-
sommelier.add_match('Tofu', '
|
57
|
+
sommelier.add_match('Tofu', 'Rosé', 0.486)
|
55
58
|
sommelier.add_match('Tofu', 'Sauvignon Blanc', 0.304)
|
56
59
|
sommelier.add_match('Eggplant', 'Sauvignon Blanc', 0.299)
|
57
60
|
sommelier.add_match('Salmon', 'Sauvignon Blanc', 0.602)
|
58
61
|
puts sommelier.pairings
|
59
62
|
# {
|
60
63
|
# "Salmon" => "Sauvignon Blanc",
|
61
|
-
# "Tofu" => "
|
64
|
+
# "Tofu" => "Rosé",
|
62
65
|
# "Asparagus" => "Pinot Noir"
|
63
66
|
# }
|
64
67
|
|
65
68
|
# Note: neither "Eggplant" nor "Chardonnay" were matched in the pairings map
|
66
69
|
```
|
70
|
+
|
71
|
+
### CSV
|
72
|
+
|
73
|
+
This gem provides a rake task to apply the Sommelier algorithm to a CSV file.
|
74
|
+
The file must have a header row and the columns are expected to be in the following
|
75
|
+
order:
|
76
|
+
|
77
|
+
1. `dish`
|
78
|
+
2. `wine`
|
79
|
+
3. `score`
|
80
|
+
|
81
|
+
Note: the header row is simply ignored, so the columns can be named anything.
|
82
|
+
|
83
|
+
Any additional columns will be ignored.
|
84
|
+
|
85
|
+
```csv
|
86
|
+
# matches.csv
|
87
|
+
dish,wine,score
|
88
|
+
Asparagus,Pinot Noir,0.366
|
89
|
+
Asparagus,Sauvignon Blanc,0.453
|
90
|
+
Asparagus,Chardonnay,0.245
|
91
|
+
Tofu,Rosé,0.486
|
92
|
+
Tofu,Sauvignon Blanc,0.304
|
93
|
+
Eggplant,Sauvignon Blanc,0.299
|
94
|
+
Salmon,Sauvignon Blanc,0.602
|
95
|
+
```
|
96
|
+
|
97
|
+
```bash
|
98
|
+
rake sommelier:from_csv matches.csv
|
99
|
+
# Salmon => Sauvignon Blanc
|
100
|
+
# Tofu => Rosé
|
101
|
+
# Asparagus => Pinot Noir
|
102
|
+
```
|
data/Rakefile
CHANGED
data/lib/sommelier.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'rake'
|
2
|
+
load 'tasks/sommelier.rake'
|
3
|
+
|
1
4
|
require_relative 'sommelier/match_maker'
|
2
5
|
require_relative 'sommelier/match_catalog'
|
3
6
|
|
@@ -11,14 +14,14 @@ require_relative 'sommelier/match_catalog'
|
|
11
14
|
# sommelier.add_match('Asparagus', 'Pinot Noir', 0.366)
|
12
15
|
# sommelier.add_match('Asparagus', 'Sauvignon Blanc', 0.453)
|
13
16
|
# sommelier.add_match('Asparagus', 'Chardonnay', 0.245)
|
14
|
-
# sommelier.add_match('Tofu', '
|
17
|
+
# sommelier.add_match('Tofu', 'Rosé', 0.486)
|
15
18
|
# sommelier.add_match('Tofu', 'Sauvignon Blanc', 0.304)
|
16
19
|
# sommelier.add_match('Eggplant', 'Sauvignon Blanc', 0.299)
|
17
20
|
# sommelier.add_match('Salmon', 'Sauvignon Blanc', 0.602)
|
18
21
|
# puts sommelier.pairings
|
19
22
|
# # {
|
20
23
|
# # "Salmon" => "Sauvignon Blanc",
|
21
|
-
# # "Tofu" => "
|
24
|
+
# # "Tofu" => "Rosé",
|
22
25
|
# # "Asparagus" => "Pinot Noir"
|
23
26
|
# # }
|
24
27
|
#
|
@@ -1,18 +1,22 @@
|
|
1
1
|
class Sommelier
|
2
2
|
class MatchMaker
|
3
|
+
# Decide a single dish's pairing request to accept for each wine
|
3
4
|
class Decider
|
4
5
|
def initialize(match_catalog)
|
5
6
|
@match_catalog = match_catalog
|
6
7
|
end
|
7
8
|
|
8
|
-
# Decide
|
9
|
+
# Decide on which requested pairings to accepts (up to one per wine) and
|
10
|
+
# update the `accepted` and `reversed` input maps.
|
9
11
|
#
|
10
12
|
# @param requests [Hash<Object, Array<Object>>] mapping of wines to
|
11
|
-
# the list of dishes that have requested pairing in the current round
|
13
|
+
# the list of dishes that have requested pairing in the current round.
|
12
14
|
# @param accepted [Hash<Object, Object>] mapping of dishes to the wine
|
13
|
-
# that has accepted its pairing request
|
15
|
+
# that has accepted its pairing request. Note: invoking this method may
|
16
|
+
# cause modifications to this object.
|
14
17
|
# @param reversed [Hash<Object, Object>] mapping of wines to the dish
|
15
|
-
# that has its pairing request accepted by wine
|
18
|
+
# that has its pairing request accepted by wine. Note: invoking this
|
19
|
+
# method may cause modifications to this object.
|
16
20
|
def decide!(requests, accepted, reversed)
|
17
21
|
requests.each do |wine, current_dishes|
|
18
22
|
# Be sure to consider the full set of current dishes and potentially
|
@@ -1,16 +1,15 @@
|
|
1
1
|
class Sommelier
|
2
2
|
class MatchMaker
|
3
|
+
# Generate all of the requested pairings for a single round of the
|
4
|
+
# algorithm. Each dish that isn't currently locked in a pairing from a
|
5
|
+
# prior round and hasn't exhausted its list of preferred wines will
|
6
|
+
# request a pairing with the highest matching wine it has not yet
|
7
|
+
# requested.
|
3
8
|
class Generator
|
4
9
|
def initialize(match_catalog)
|
5
10
|
@match_catalog = match_catalog
|
6
11
|
end
|
7
12
|
|
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
13
|
# @param round [Integer] the round number
|
15
14
|
# @param accepted [Hash<Object, Object>] mapping of dishes to the wine
|
16
15
|
# that has accepted his pairing
|
data/lib/sommelier/version.rb
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
namespace :sommelier do
|
2
|
+
desc 'Apply Sommelier to a CSV with a headers row ["dish", "wine", "score"]'
|
3
|
+
task :from_csv do
|
4
|
+
unless ARGV.length == 2
|
5
|
+
raise ArgumentError, 'Expecting a file as the first argument'
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'csv'
|
9
|
+
require 'sommelier'
|
10
|
+
|
11
|
+
csv = CSV.open(ARGV[1], headers: true)
|
12
|
+
sommelier = Sommelier.new
|
13
|
+
csv.each do |row|
|
14
|
+
sommelier.add_match(row[0], row[1], row[2].to_f)
|
15
|
+
end
|
16
|
+
sommelier.pairings.each do |dish, wine|
|
17
|
+
puts "#{dish} => #{wine}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tcollier-sommelier
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tom Collier
|
@@ -89,6 +89,7 @@ extra_rdoc_files: []
|
|
89
89
|
files:
|
90
90
|
- ".gitignore"
|
91
91
|
- ".rspec"
|
92
|
+
- ".travis.yml"
|
92
93
|
- CHANGELOG.txt
|
93
94
|
- Gemfile
|
94
95
|
- Gemfile.lock
|
@@ -103,6 +104,7 @@ files:
|
|
103
104
|
- lib/sommelier/match_maker/generator.rb
|
104
105
|
- lib/sommelier/preference.rb
|
105
106
|
- lib/sommelier/version.rb
|
107
|
+
- lib/tasks/sommelier.rake
|
106
108
|
- sommelier.gemspec
|
107
109
|
homepage: https://github.com/tcollier/sommelier
|
108
110
|
licenses:
|