viking-pairity 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|