viking-pairity 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/exe/pairity ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'pairity'
4
+
5
+
6
+ begin
7
+ Pairity::CLI.new.start
8
+ rescue Interrupt
9
+ puts
10
+ puts "Quitting Pairity..."
11
+ end
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
@@ -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