viking-pairity 0.1.1
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/.byebug_history +179 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/Guardfile +70 -0
- data/LICENSE.txt +21 -0
- data/README.md +52 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/pairity +11 -0
- data/lib/pairity.rb +15 -0
- data/lib/pairity/adjacency_matrix.rb +142 -0
- data/lib/pairity/cli.rb +336 -0
- data/lib/pairity/config.rb +69 -0
- data/lib/pairity/edge.rb +17 -0
- data/lib/pairity/google_sync.rb +174 -0
- data/lib/pairity/pair_generator.rb +79 -0
- data/lib/pairity/pair_saver.rb +20 -0
- data/lib/pairity/pair_stats.rb +12 -0
- data/lib/pairity/person.rb +22 -0
- data/lib/pairity/slackbot.rb +28 -0
- data/lib/pairity/version.rb +3 -0
- data/pairity.gemspec +45 -0
- data/tags +121 -0
- data/test.rb +11 -0
- data/todays_pairs.txt +7 -0
- metadata +271 -0
data/exe/pairity
ADDED
data/lib/pairity.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "pairity/version"
|
2
|
+
require "pairity/edge"
|
3
|
+
require "pairity/person"
|
4
|
+
require "pairity/pair_stats"
|
5
|
+
require "pairity/pair_saver"
|
6
|
+
require "pairity/pair_generator"
|
7
|
+
require "pairity/adjacency_matrix"
|
8
|
+
require "pairity/google_sync"
|
9
|
+
require "pairity/cli"
|
10
|
+
require "pairity/slackbot"
|
11
|
+
require "pairity/config"
|
12
|
+
|
13
|
+
module Pairity
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'graph_matching'
|
2
|
+
|
3
|
+
module Pairity
|
4
|
+
class AdjacencyMatrix
|
5
|
+
attr_accessor :matrix, :han_solo
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@han_solo = Person.new(name: "Han Solo")
|
9
|
+
@people = [@han_solo]
|
10
|
+
@matrix = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
output = []
|
15
|
+
matrix.each do |pair, weight|
|
16
|
+
# next if pair.include?(@han_solo)
|
17
|
+
output << "#{pair} -> #{weight}"
|
18
|
+
end
|
19
|
+
output.join("\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
def people(solo = false)
|
23
|
+
if @people.size.odd? || solo
|
24
|
+
@people.reject { |person| person.name == "Han Solo" }
|
25
|
+
else
|
26
|
+
@people
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def matrix(solo = false)
|
31
|
+
if @people.size.odd? || solo
|
32
|
+
without_solo
|
33
|
+
else
|
34
|
+
@matrix
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def set_ids(persons)
|
39
|
+
persons.each_with_index do |p, i|
|
40
|
+
p.id = i+1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def all_people
|
45
|
+
@people
|
46
|
+
end
|
47
|
+
|
48
|
+
def without_solo
|
49
|
+
@matrix.reject { |pair, edge| pair.include?(han_solo) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def [](*args)
|
53
|
+
@matrix[args.sort]
|
54
|
+
end
|
55
|
+
|
56
|
+
def []=(*args, edge)
|
57
|
+
@matrix[args.sort] = edge
|
58
|
+
end
|
59
|
+
|
60
|
+
def weight_for_pairs(pairs)
|
61
|
+
pairs.inject(0) do |sum, pair|
|
62
|
+
sum += weight_for_pair(pair)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def weight_for_pair(pair)
|
67
|
+
@matrix[pair.sort].weight
|
68
|
+
end
|
69
|
+
|
70
|
+
def add_person(new_person)
|
71
|
+
@people.each do |person|
|
72
|
+
pair = [person, new_person].sort
|
73
|
+
@matrix[pair] = Edge.new
|
74
|
+
end
|
75
|
+
@people << new_person
|
76
|
+
end
|
77
|
+
|
78
|
+
def average_weight
|
79
|
+
people.combination(2).to_a.inject(0) do |sum, pair|
|
80
|
+
sum += self[*pair].weight
|
81
|
+
end / people.combination(2).size
|
82
|
+
end
|
83
|
+
|
84
|
+
def average_days
|
85
|
+
people.combination(2).to_a.inject(0) do |sum, pair|
|
86
|
+
sum += self[*pair].days
|
87
|
+
end / people.combination(2).size
|
88
|
+
end
|
89
|
+
|
90
|
+
def get_pairs(pairs, first)
|
91
|
+
return [] if pairs.empty?
|
92
|
+
pairs.first(first).map do |pair|
|
93
|
+
[pair, get_pairs(pairs_without_pair(pairs, pair), 1000).flatten]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def find_person(id)
|
98
|
+
@people.find { |p| p.id == id }
|
99
|
+
end
|
100
|
+
|
101
|
+
def optimal_pairs(nopes)
|
102
|
+
set_ids(people)
|
103
|
+
pairing_array = matrix.map do |pair, edge|
|
104
|
+
next if nopes.include?(pair.sort)
|
105
|
+
p1, p2 = pair
|
106
|
+
ids = [p1.id, p2.id].sort
|
107
|
+
[ids[0], ids[1], edge.weight * -1]
|
108
|
+
end
|
109
|
+
|
110
|
+
pairing_array.compact!
|
111
|
+
|
112
|
+
g = GraphMatching::Graph::WeightedGraph[*pairing_array]
|
113
|
+
m = g.maximum_weighted_matching(true)
|
114
|
+
m.edges.map do |pair|
|
115
|
+
[find_person(pair[0]), find_person(pair[1])]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def remove_person(person)
|
120
|
+
@matrix.reject! do |pair, weight|
|
121
|
+
pair.include?(person)
|
122
|
+
end
|
123
|
+
@people.delete(person)
|
124
|
+
end
|
125
|
+
|
126
|
+
def resistance(weight, pair)
|
127
|
+
tier_compensation = 0
|
128
|
+
if pair.all?{ |p| p.tier == 1} || pair.all?{ |p| p.tier == 3}
|
129
|
+
tier_compensation = 1.5
|
130
|
+
end
|
131
|
+
@matrix[pair.sort].resistance * weight + tier_compensation
|
132
|
+
end
|
133
|
+
|
134
|
+
def add_weight_to_pair(weight, pair)
|
135
|
+
@matrix[pair.sort].weight += resistance(weight, pair)
|
136
|
+
end
|
137
|
+
|
138
|
+
def add_day_to_pair(pair)
|
139
|
+
@matrix[pair.sort].days += 1
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
data/lib/pairity/cli.rb
ADDED
@@ -0,0 +1,336 @@
|
|
1
|
+
require 'highline/import'
|
2
|
+
require 'terminal-table'
|
3
|
+
require 'rainbow'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
module Pairity
|
7
|
+
class CLI
|
8
|
+
def initialize
|
9
|
+
@matrix = AdjacencyMatrix.new
|
10
|
+
@sync = GoogleSync.new(@matrix)
|
11
|
+
@generator = PairGenerator.new(@matrix)
|
12
|
+
@renderer = PairRenderer.new(@matrix)
|
13
|
+
load_google
|
14
|
+
end
|
15
|
+
|
16
|
+
def start
|
17
|
+
unless Config.configured?
|
18
|
+
slack_config
|
19
|
+
end
|
20
|
+
|
21
|
+
action_menu
|
22
|
+
end
|
23
|
+
|
24
|
+
def action_menu
|
25
|
+
puts
|
26
|
+
puts Rainbow("==== PAIRITY ====").white
|
27
|
+
choose do |menu|
|
28
|
+
menu.prompt = "What would you like to do?"
|
29
|
+
menu.choice("Generate Pairs") { generate_pairs }
|
30
|
+
menu.choice("Edit People") { edit_people }
|
31
|
+
# menu.choice("Edit Pair") { edit_pair }
|
32
|
+
menu.choice("Save Changes") { save_changes }
|
33
|
+
menu.choice("Open Google Sheet") { open_google_sheet }
|
34
|
+
menu.choice("Change Slack Channel") { change_channel }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def open_google_sheet
|
39
|
+
`open "#{@sync.sheet_url}"`
|
40
|
+
|
41
|
+
action_menu
|
42
|
+
end
|
43
|
+
|
44
|
+
def slack_config
|
45
|
+
puts "Let's set up Slack Integration."
|
46
|
+
Config.load
|
47
|
+
slack_webhook = ask("Please enter your Slack Webhook URL (more information: https://vikingcodeschool.slack.com/apps/new/A0F7XDUAZ-incoming-webhooks)") do |q|
|
48
|
+
q.validate = /hooks\.slack\.com\/services\//
|
49
|
+
end
|
50
|
+
|
51
|
+
channel = ask("What channel would you like to post to? (Don't write the #')")
|
52
|
+
|
53
|
+
Config.add(url: slack_webhook, channel: channel)
|
54
|
+
Config.save
|
55
|
+
end
|
56
|
+
|
57
|
+
def change_channel
|
58
|
+
puts "Let's set up Slack Integration."
|
59
|
+
Config.load
|
60
|
+
|
61
|
+
channel = ask("What channel would you like to post to? (Don't write the #')")
|
62
|
+
|
63
|
+
Config.add(channel: channel)
|
64
|
+
Config.save
|
65
|
+
puts "Channel changed to ##{channel}"
|
66
|
+
action_menu
|
67
|
+
end
|
68
|
+
|
69
|
+
def edit_people
|
70
|
+
choose do |menu|
|
71
|
+
menu.prompt = "Add or remove?"
|
72
|
+
menu.choice("Add") { add_people }
|
73
|
+
menu.choice("Remove") { remove_people }
|
74
|
+
menu.choice("Rename") { rename }
|
75
|
+
menu.choice("Change Tier") { change_tier }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def save_changes
|
80
|
+
@sync.save
|
81
|
+
action_menu
|
82
|
+
end
|
83
|
+
|
84
|
+
def rename
|
85
|
+
choice = choose_person
|
86
|
+
|
87
|
+
new_name = ask "What name would you like to give #{Rainbow(choice.name).white}?"
|
88
|
+
|
89
|
+
old_name = choice.name
|
90
|
+
choice.name = new_name
|
91
|
+
|
92
|
+
puts "#{Rainbow(old_name).white} shall henceforth be known as #{Rainbow(choice.name).white}!"
|
93
|
+
|
94
|
+
action_menu
|
95
|
+
end
|
96
|
+
|
97
|
+
def change_tier
|
98
|
+
choice = choose_person(tier: true)
|
99
|
+
|
100
|
+
tier = 2
|
101
|
+
choose do |menu|
|
102
|
+
menu.prompt = "Set #{Rainbow(choice.name).white} to what tier?"
|
103
|
+
menu.choice("Tier 1") { tier = 1 }
|
104
|
+
menu.choice("Tier 2") { tier = 2 }
|
105
|
+
menu.choice("Tier 3") { tier = 3 }
|
106
|
+
end
|
107
|
+
|
108
|
+
choice.tier = tier
|
109
|
+
|
110
|
+
puts "#{Rainbow(choice.name).white} is now tier #{tier}."
|
111
|
+
|
112
|
+
action_menu
|
113
|
+
end
|
114
|
+
|
115
|
+
def remove_people
|
116
|
+
choice = choose_person
|
117
|
+
|
118
|
+
answer = ask "Are you sure you would like to remove #{Rainbow(choice).white}?" do |q|
|
119
|
+
q.validate = /y|n/
|
120
|
+
end
|
121
|
+
|
122
|
+
if answer =~ /y/
|
123
|
+
@matrix.remove_person(choice)
|
124
|
+
end
|
125
|
+
|
126
|
+
puts "#{Rainbow(choice).white} has been removed."
|
127
|
+
|
128
|
+
action_menu
|
129
|
+
end
|
130
|
+
|
131
|
+
def choose_person(tier: false)
|
132
|
+
people = @matrix.people(true)
|
133
|
+
choice = nil
|
134
|
+
choose do |menu|
|
135
|
+
menu.prompt = "Select a person."
|
136
|
+
people.each_with_index do |person, index|
|
137
|
+
menu.choice(display_person(person, tier: tier)) { choice = people[index] }
|
138
|
+
end
|
139
|
+
end
|
140
|
+
choice
|
141
|
+
end
|
142
|
+
|
143
|
+
def display_person(person, tier: false)
|
144
|
+
output = []
|
145
|
+
output << "#{Rainbow(person.name).white}"
|
146
|
+
output << "-- Tier #{person.tier}" if tier
|
147
|
+
output.join(" ")
|
148
|
+
end
|
149
|
+
|
150
|
+
def add_people
|
151
|
+
names = ask "What are their names? (enter names separated by commas)"
|
152
|
+
names = names.split(",")
|
153
|
+
names.each do |name|
|
154
|
+
person = Person.new(name: name.strip)
|
155
|
+
@matrix.add_person(person)
|
156
|
+
puts "Added #{name.strip}!"
|
157
|
+
end
|
158
|
+
action_menu
|
159
|
+
end
|
160
|
+
|
161
|
+
def nope_pair
|
162
|
+
pair = choose_from_generated_pair
|
163
|
+
|
164
|
+
answer = ask "Are you sure you would like to nope today's #{display_pair_names(pair)} pairing?" do |q|
|
165
|
+
q.validate = /y|n/
|
166
|
+
end
|
167
|
+
|
168
|
+
if answer =~ /y/
|
169
|
+
p1, p2 = pair
|
170
|
+
@generator.nope(p1, p2)
|
171
|
+
puts "#{display_pair_names(pair)} have been 'Noped'!"
|
172
|
+
end
|
173
|
+
|
174
|
+
@generator.generate_pairs
|
175
|
+
pairs_menu
|
176
|
+
end
|
177
|
+
|
178
|
+
def choose_from_generated_pair
|
179
|
+
choice = nil
|
180
|
+
|
181
|
+
choose do |menu|
|
182
|
+
menu.prompt = "Who would you like to edit?"
|
183
|
+
@generator.pairs.each_with_index do |pair, index|
|
184
|
+
menu.choice(display_pair_names(pair)) { choice = index }
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
choice = @generator.pairs[choice]
|
189
|
+
end
|
190
|
+
|
191
|
+
def choose_pair
|
192
|
+
choice = nil
|
193
|
+
|
194
|
+
choose do |menu|
|
195
|
+
menu.prompt = "Who would you like to edit?"
|
196
|
+
all_combos.with_index do |pair, index|
|
197
|
+
menu.choice(display_pair_names(pair)) { choice = index }
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
choice = all_combos.to_a[choice]
|
202
|
+
end
|
203
|
+
|
204
|
+
|
205
|
+
def edit_pair
|
206
|
+
choice = choose_pair
|
207
|
+
|
208
|
+
choose do |menu|
|
209
|
+
menu.prompt = "How would you like to edit #{display_pair_names(choice)}"
|
210
|
+
menu.choice("Increase Pair Chance") { increase_chance(choice) }
|
211
|
+
menu.choice("Decrease Pair Chance") { decrease_chance(choice) }
|
212
|
+
menu.choice("Abolish Pair Chance") { condemn_pair(choice) }
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
def increase_chance(pair)
|
218
|
+
answer = ask "Are you sure you would like to increase the odds of #{display_pair_names(pair)} pairing?" do |q|
|
219
|
+
q.validate = /y|n/
|
220
|
+
end
|
221
|
+
|
222
|
+
if answer =~ /y/
|
223
|
+
p1, p2 = pair
|
224
|
+
@generator.resistance(p1, p2, 0.5)
|
225
|
+
puts "#{display_pair_names(pair)} are more likely to be paired."
|
226
|
+
end
|
227
|
+
|
228
|
+
action_menu
|
229
|
+
end
|
230
|
+
|
231
|
+
def decrease_chance(pair)
|
232
|
+
answer = ask "Are you sure you would like to decrease the odds of #{display_pair_names(pair)} pairing?" do |q|
|
233
|
+
q.validate = /y|n/
|
234
|
+
end
|
235
|
+
|
236
|
+
if answer =~ /y/
|
237
|
+
p1, p2 = pair
|
238
|
+
@generator.resistance(p1, p2, 2)
|
239
|
+
puts "#{display_pair_names(pair)} are less likely to be paired."
|
240
|
+
end
|
241
|
+
|
242
|
+
action_menu
|
243
|
+
end
|
244
|
+
|
245
|
+
def condemn_pair(pair)
|
246
|
+
|
247
|
+
answer = ask "Are you sure you would like to condemn #{display_pair_names(pair)}?" do |q|
|
248
|
+
q.validate = /y|n/
|
249
|
+
end
|
250
|
+
|
251
|
+
if answer =~ /y/
|
252
|
+
@generator.abolish_pairing(*pair)
|
253
|
+
puts "#{display_pair_names(pair)} will no longer be paired."
|
254
|
+
end
|
255
|
+
|
256
|
+
action_menu
|
257
|
+
end
|
258
|
+
|
259
|
+
def generate_pairs
|
260
|
+
@generator.generate_pairs
|
261
|
+
puts
|
262
|
+
pairs_menu
|
263
|
+
end
|
264
|
+
|
265
|
+
def pairs_menu
|
266
|
+
display_pairs
|
267
|
+
puts
|
268
|
+
choose do |menu|
|
269
|
+
menu.prompt = "What would you like to do?"
|
270
|
+
menu.choice("Save & Slack") { save_pairs }
|
271
|
+
menu.choice("'Nope' a Pair") { nope_pair }
|
272
|
+
menu.choice("Main Menu") { action_menu }
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def save_pairs
|
277
|
+
@generator.save_pairs
|
278
|
+
puts
|
279
|
+
PairSaver.new(@generator.pairs).post_to_slack
|
280
|
+
say "Posted pairings to slack!"
|
281
|
+
@sync.save
|
282
|
+
action_menu
|
283
|
+
end
|
284
|
+
|
285
|
+
def simulate_pairs
|
286
|
+
times = ask("How many days should we to travel?")
|
287
|
+
|
288
|
+
times.to_i.times do
|
289
|
+
@generator.generate_pairs
|
290
|
+
@generator.save_pairs
|
291
|
+
end
|
292
|
+
|
293
|
+
display_pair_stats
|
294
|
+
|
295
|
+
action_menu
|
296
|
+
end
|
297
|
+
|
298
|
+
def all_combos
|
299
|
+
@matrix.people.combination(2)
|
300
|
+
end
|
301
|
+
|
302
|
+
def display_pair_stats
|
303
|
+
rows = []
|
304
|
+
all_combos.each do |person1, person2|
|
305
|
+
display_pair(rows, person1, person2)
|
306
|
+
end
|
307
|
+
table = Terminal::Table.new headings: ["Pair","Times"], rows: rows
|
308
|
+
puts table
|
309
|
+
end
|
310
|
+
|
311
|
+
def display_pairs
|
312
|
+
rows = []
|
313
|
+
@generator.pairs.each do |person1, person2|
|
314
|
+
display_pair(rows, person1, person2)
|
315
|
+
end
|
316
|
+
table = Terminal::Table.new headings: ["Pair","Times"], rows: rows
|
317
|
+
puts table
|
318
|
+
end
|
319
|
+
|
320
|
+
def display_pair_names(pair)
|
321
|
+
person1, person2 = pair
|
322
|
+
Rainbow(person1).white + " & " + Rainbow(person2).white
|
323
|
+
end
|
324
|
+
|
325
|
+
def display_pair(rows, person1, person2)
|
326
|
+
cols = []
|
327
|
+
cols << display_pair_names([person1, person2])
|
328
|
+
cols << @renderer.stats_for_pair(person1, person2)
|
329
|
+
rows << cols
|
330
|
+
end
|
331
|
+
|
332
|
+
def load_google
|
333
|
+
@sync.load
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|