tcollier-sommelier 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7aec6ab221e7b53607609c6a5f7b3f84945461bb
4
- data.tar.gz: a759cd1e83e4933366ae5b28d2fffb806d5c8ace
3
+ metadata.gz: b069aee84c44d4bde4c6482d81c47b8bfcf7ae65
4
+ data.tar.gz: 4f8b1044d8973656f19dd921a1e6051edd87869d
5
5
  SHA512:
6
- metadata.gz: 43f4525771fec89eb6fc7703eb0503d15439c8c78c4ae5c7867eadcaae80c28ad208cc04818074b51908231de9a06068d58b1a83f0df9c21d06b8823d54c072e
7
- data.tar.gz: c39b125857e32674f126970486399b3d9d98d47b44ab2960aade9c7cbcc920db8d6ab71a438094d1881a5ff29149c3ecfed166ae7c65d0b6b9ad306f9828b7af
6
+ metadata.gz: 459dc2eef3cddd6194f42ded392a32ed3e166759fb30b0edb4216215de9c47aef588fbcc1d51aed19ea5d6454cde51b2f6b995fce1a509977add818e73e609c7
7
+ data.tar.gz: 40fca90df92292e7f5e29810ae039094f155661f7e45a5ad04662b8a446515dedeefe98a11e282a82cb0630dfac2565f8415674449e5b48d04e5127e213dc20c
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.4.4
4
+ - 2.5.1
5
+ branches:
6
+ only: master
@@ -1,3 +1,7 @@
1
- Version 0.1.0, 2018-07-20
1
+ Version 0.2.0, 2018-07-21
2
2
  -------------------------
3
- e66dc58 Initial release with Gale-Shapely variant
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
+ [![Build Status](https://travis-ci.org/tcollier/sommelier.svg?branch=master)](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). The final matches are referred to as proposals.
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 2 primary differences from Gale-Shapely
28
+ The algorithm applied here has a few key differences from Gale-Shapely
27
29
 
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.
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 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
+ 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', 'Rose', 0.486)
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" => "Rose",
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
@@ -1,5 +1,6 @@
1
1
  require 'bundler/gem_tasks'
2
2
  require 'rspec/core/rake_task'
3
+ require 'sommelier'
3
4
 
4
5
  RSpec::Core::RakeTask.new(:spec)
5
6
 
@@ -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', 'Rose', 0.486)
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" => "Rose",
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 a single dish's pairing request to accept for each wine
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
@@ -1,3 +1,3 @@
1
1
  class Sommelier
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -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.1.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: