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
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Pairity
|
4
|
+
class Config
|
5
|
+
|
6
|
+
FILENAME = '.pairity'
|
7
|
+
PATH = "#{Dir.home}/#{FILENAME}"
|
8
|
+
|
9
|
+
@@config = {}
|
10
|
+
|
11
|
+
def self.config
|
12
|
+
@@config
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.configured?
|
16
|
+
self.load
|
17
|
+
!@@config.any? { |key, value| value.nil? || value.empty? }
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.add(options={})
|
21
|
+
add_option(:channel, options[:channel])
|
22
|
+
add_option(:url, options[:url])
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.save
|
26
|
+
File.open(PATH, 'w+') do |f|
|
27
|
+
f.write(YAML.dump(config))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.load
|
32
|
+
file_exists = File.exists?(PATH)
|
33
|
+
if File.exists?(PATH)
|
34
|
+
value = YAML.load_file(PATH)
|
35
|
+
else
|
36
|
+
value = defaults
|
37
|
+
end
|
38
|
+
@@config = value
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.get(key)
|
42
|
+
config[key]
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.display
|
46
|
+
if config.empty?
|
47
|
+
puts "No config values set"
|
48
|
+
else
|
49
|
+
config.each do |key, value|
|
50
|
+
puts "#{key.upcase}: #{value}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def self.defaults
|
58
|
+
{
|
59
|
+
:channel => 'general',
|
60
|
+
:url => '',
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.add_option(key, value)
|
65
|
+
config[key] = value unless value.nil?
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
data/lib/pairity/edge.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Pairity
|
2
|
+
|
3
|
+
class Edge
|
4
|
+
attr_accessor :weight, :days, :resistance
|
5
|
+
|
6
|
+
def initialize(weight: 0, days: 0, resistance: 1)
|
7
|
+
@weight = weight
|
8
|
+
@days = days
|
9
|
+
@resistance = resistance
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"Weight: #{weight} Days: #{days}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require "google/api_client"
|
2
|
+
require 'pp'
|
3
|
+
require "google_drive"
|
4
|
+
require "ruby-progressbar"
|
5
|
+
|
6
|
+
ENV['SSL_CERT_FILE'] = Gem.loaded_specs['google-api-client'].full_gem_path+'/lib/cacerts.pem'
|
7
|
+
|
8
|
+
module Pairity
|
9
|
+
class GoogleSync
|
10
|
+
|
11
|
+
CONFIG_FILE = Dir.home + "/.pairity_google.json"
|
12
|
+
|
13
|
+
attr_reader :sheet_url
|
14
|
+
|
15
|
+
def initialize(matrix)
|
16
|
+
@matrix = matrix
|
17
|
+
@people = {}
|
18
|
+
@sheet_url = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def load
|
22
|
+
unless File.exists?(CONFIG_FILE)
|
23
|
+
puts "Welcome, newcomer!"
|
24
|
+
puts "Please follow these instructions to allow #{Rainbow("Pairity").white} to use Google Sheets to sync its precious data."
|
25
|
+
end
|
26
|
+
|
27
|
+
session = GoogleDrive.saved_session(CONFIG_FILE)
|
28
|
+
|
29
|
+
puts "Loading Matrix from Google Sheets..."
|
30
|
+
progressbar = ProgressBar.create(total: 100)
|
31
|
+
|
32
|
+
progressbar.progress += 20
|
33
|
+
sheet = session.spreadsheet_by_title("Pairity")
|
34
|
+
unless sheet
|
35
|
+
puts "Creating a new spreadsheet called: Pairity"
|
36
|
+
sheet = session.create_spreadsheet("Pairity")
|
37
|
+
sheet.add_worksheet("Days")
|
38
|
+
sheet.add_worksheet("Resistance")
|
39
|
+
sheet.add_worksheet("Weights")
|
40
|
+
ws = sheet.worksheets[0]
|
41
|
+
ws.title = "People"
|
42
|
+
ws.save
|
43
|
+
else
|
44
|
+
end
|
45
|
+
@sheet_url = sheet.worksheets[0].human_url
|
46
|
+
|
47
|
+
ws = sheet.worksheets[0]
|
48
|
+
|
49
|
+
# Add People and Tiers to Matrix
|
50
|
+
ws.num_rows.times do |row|
|
51
|
+
next unless row > 0
|
52
|
+
name = ws[row + 1, 1]
|
53
|
+
next if name.strip.empty?
|
54
|
+
tier = ws[row + 1, 2]
|
55
|
+
if name == "Han Solo"
|
56
|
+
person = @matrix.han_solo
|
57
|
+
else
|
58
|
+
person = Person.new(name: name, tier: tier)
|
59
|
+
@matrix.add_person(person)
|
60
|
+
end
|
61
|
+
@people[person] = row + 1
|
62
|
+
end
|
63
|
+
|
64
|
+
# Add data to edges
|
65
|
+
(1..3).each do |i|
|
66
|
+
ws = sheet.worksheets[i]
|
67
|
+
@people.each do |p1, row|
|
68
|
+
@people.each do |p2, col|
|
69
|
+
next if p1 == p2
|
70
|
+
data = ws[row, col]
|
71
|
+
edit_edge(p1, p2, data, i)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
@matrix
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
def clear_worksheet(ws)
|
81
|
+
(1..ws.num_rows).each do |i|
|
82
|
+
(1..ws.num_rows).each do |j|
|
83
|
+
ws[i, j] = ""
|
84
|
+
ws[j, i] = ""
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def save
|
90
|
+
puts "Saving Matrix to Google Sheets..."
|
91
|
+
progressbar = ProgressBar.create(total: 100)
|
92
|
+
|
93
|
+
session = GoogleDrive.saved_session(CONFIG_FILE)
|
94
|
+
sheet = session.spreadsheet_by_title("Pairity")
|
95
|
+
@people = @matrix.all_people.sort
|
96
|
+
p @matrix.matrix
|
97
|
+
|
98
|
+
progressbar.progress += 20
|
99
|
+
|
100
|
+
ws = sheet.worksheets[0]
|
101
|
+
|
102
|
+
clear_worksheet(ws)
|
103
|
+
|
104
|
+
ws[1, 1] = "Name"
|
105
|
+
ws[1, 2] = "Tier (1-3)"
|
106
|
+
@people.each_with_index do |person, index|
|
107
|
+
ws[index + 2, 1] = person.name
|
108
|
+
ws[index + 2, 2] = person.tier
|
109
|
+
end
|
110
|
+
ws.save
|
111
|
+
|
112
|
+
(1..3).each do |i|
|
113
|
+
ws = sheet.worksheets[i]
|
114
|
+
|
115
|
+
clear_worksheet(ws)
|
116
|
+
|
117
|
+
@people.each_with_index do |person, index|
|
118
|
+
ws[1, index + 2] = person.name
|
119
|
+
ws[index + 2, 1] = person.name
|
120
|
+
end
|
121
|
+
|
122
|
+
progressbar.progress += 20
|
123
|
+
@people.combination(2) do |pair|
|
124
|
+
p1, p2 = pair
|
125
|
+
index1 = @people.index(p1)
|
126
|
+
index2 = @people.index(p2)
|
127
|
+
edge = @matrix[p1,p2]
|
128
|
+
case i
|
129
|
+
when 1
|
130
|
+
ws[1,1] = "Days"
|
131
|
+
data = edge.days
|
132
|
+
when 2
|
133
|
+
ws[1,1] = "Resistances"
|
134
|
+
data = edge.resistance
|
135
|
+
when 3
|
136
|
+
ws[1,1] = "Weights"
|
137
|
+
data = edge.weight
|
138
|
+
end
|
139
|
+
ws[index1 + 2, index2 + 2] = data
|
140
|
+
ws[index2 + 2, index1 + 2] = data
|
141
|
+
end
|
142
|
+
|
143
|
+
max = ws.max_rows
|
144
|
+
|
145
|
+
@people.each_with_index do |person, index|
|
146
|
+
ws[index + 2, index + 2] = ""
|
147
|
+
ws[index + 2, index + 2] = ""
|
148
|
+
max = index + 3
|
149
|
+
end
|
150
|
+
|
151
|
+
ws.save
|
152
|
+
end
|
153
|
+
progressbar.progress += 20
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
def edit_edge(p1, p2, data, i)
|
158
|
+
case i
|
159
|
+
when 1
|
160
|
+
@matrix[p1, p2].days = data.to_i
|
161
|
+
when 2
|
162
|
+
@matrix[p1, p2].resistance = (data ? 1 : data.to_i)
|
163
|
+
when 3
|
164
|
+
@matrix[p1, p2].weight = data.to_i
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def find_person(name)
|
169
|
+
|
170
|
+
person = @people.find { |person| person.name == name }
|
171
|
+
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'byebug'
|
2
|
+
require 'pry-byebug'
|
3
|
+
|
4
|
+
module Pairity
|
5
|
+
class PairGenerator
|
6
|
+
|
7
|
+
@@shuffle = false
|
8
|
+
|
9
|
+
attr_reader :pairs, :last_pairs
|
10
|
+
|
11
|
+
def self.shuffle=(toggle)
|
12
|
+
@@shuffle = toggle
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(matrix)
|
16
|
+
@matrix = matrix
|
17
|
+
@last_pairs = []
|
18
|
+
@pairs = []
|
19
|
+
@nopes = []
|
20
|
+
end
|
21
|
+
|
22
|
+
def generate_pairs
|
23
|
+
@pairs = []
|
24
|
+
@pairs = @matrix.optimal_pairs(@nopes)
|
25
|
+
move_solo_to_the_end
|
26
|
+
@pairs
|
27
|
+
end
|
28
|
+
|
29
|
+
def possible_pairs
|
30
|
+
@matrix.possible_pairs.reject { |pairing| pairing.any? { |pair| @nopes.include?(pair) }}
|
31
|
+
end
|
32
|
+
|
33
|
+
def move_solo_to_the_end
|
34
|
+
solo = @pairs.find { |pair| pair.any? { |person| person.name == "Han Solo" } }
|
35
|
+
return @pairs unless solo
|
36
|
+
index = @pairs.index(solo)
|
37
|
+
@pairs[index], @pairs[-1] = @pairs[-1], @pairs[index]
|
38
|
+
@pairs
|
39
|
+
end
|
40
|
+
|
41
|
+
def nope(p1,p2)
|
42
|
+
pair = [p1, p2].sort
|
43
|
+
@nopes << pair
|
44
|
+
end
|
45
|
+
|
46
|
+
def save_pairs
|
47
|
+
@pairs.each do |pair|
|
48
|
+
@matrix.add_weight_to_pair(1, pair)
|
49
|
+
@matrix.add_day_to_pair(pair)
|
50
|
+
end
|
51
|
+
@nopes = []
|
52
|
+
@last_pairs = @pairs
|
53
|
+
end
|
54
|
+
|
55
|
+
def revert
|
56
|
+
@pairs = @last_pairs
|
57
|
+
end
|
58
|
+
|
59
|
+
def days_for_pair(person1, person2)
|
60
|
+
@matrix[person1, person2].days
|
61
|
+
end
|
62
|
+
|
63
|
+
def abolish_pairing(person1, person2)
|
64
|
+
@matrix.add_weight_to_pair(1_000_000,[person1,person2].sort)
|
65
|
+
end
|
66
|
+
|
67
|
+
def resistance(person1, person2, resistance)
|
68
|
+
@matrix[person1, person2].resistance = resistance
|
69
|
+
end
|
70
|
+
|
71
|
+
def equilibrium
|
72
|
+
@matrix.people.combination(2).to_a.size / (@matrix.people.size / 2)
|
73
|
+
end
|
74
|
+
|
75
|
+
def include?(person1, person2)
|
76
|
+
@pairs.any? { |pair| pair == [person1, person2].sort }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Pairity
|
2
|
+
class PairSaver
|
3
|
+
def initialize(pairs)
|
4
|
+
@pairs = pairs
|
5
|
+
end
|
6
|
+
|
7
|
+
def post_to_slack
|
8
|
+
rows = []
|
9
|
+
@pairs.each_with_index do |pair, index|
|
10
|
+
col = []
|
11
|
+
col << "Room ##{index+1}"
|
12
|
+
col << pair[0]
|
13
|
+
col << pair[1]
|
14
|
+
rows << col
|
15
|
+
end
|
16
|
+
table = Terminal::Table.new headings: ["Room","Driver","Navigator"], rows: rows
|
17
|
+
Slackbot.new.post("```#{table.to_s}```")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Pairity
|
2
|
+
class Person
|
3
|
+
@@id = 0
|
4
|
+
|
5
|
+
attr_accessor :name, :tier, :id
|
6
|
+
|
7
|
+
def initialize(name:, tier: 2)
|
8
|
+
@name = name
|
9
|
+
@tier = !((1..3) === tier) ? 2 : tier
|
10
|
+
end
|
11
|
+
|
12
|
+
def <=>(other_person)
|
13
|
+
return 1 if name == "Han Solo"
|
14
|
+
return -1 if other_person.name == "Han Solo"
|
15
|
+
name <=> other_person.name
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"#{name}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# require 'slack-poster'
|
2
|
+
|
3
|
+
module Pairity
|
4
|
+
class Slackbot
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
Config.load
|
8
|
+
hook = Config.get(:url)
|
9
|
+
options = {
|
10
|
+
icon_url: 'https://static1.squarespace.com/static/54e22d6be4b00617871820ca/54e567bbe4b022f3194c63b5/55ddd86ae4b0f0c3127f8483/1440605037480/?format=1000w',
|
11
|
+
username: 'Pairity',
|
12
|
+
channel: slack_channel
|
13
|
+
}
|
14
|
+
# @poster = Slack::Poster.new(hook, options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def post(message)
|
18
|
+
# @poster.send_message(message)
|
19
|
+
end
|
20
|
+
|
21
|
+
def slack_channel
|
22
|
+
channel = Config.get(:channel)
|
23
|
+
return "#" + channel unless channel =~ /^#/
|
24
|
+
channel
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
data/pairity.gemspec
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pairity/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "viking-pairity"
|
8
|
+
spec.version = Pairity::VERSION
|
9
|
+
spec.authors = ["Kit Langton"]
|
10
|
+
spec.email = ["kitlangton@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{A fair and balanced gem for pair rotation.}
|
13
|
+
spec.description = %q{A fair and balanced gem for pair rotation.}
|
14
|
+
spec.homepage = "https://github.com/kitlangton/pairity"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
# Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
|
18
|
+
# delete this section to allow pushing this gem to any host.
|
19
|
+
if spec.respond_to?(:metadata)
|
20
|
+
spec.metadata['allowed_push_host'] = "https://rubygems.org"
|
21
|
+
else
|
22
|
+
raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
spec.bindir = "exe"
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ["lib"]
|
29
|
+
|
30
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
31
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
32
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
33
|
+
spec.add_development_dependency "guard-rspec"
|
34
|
+
spec.add_development_dependency "byebug"
|
35
|
+
spec.add_development_dependency "pry-byebug"
|
36
|
+
|
37
|
+
spec.add_runtime_dependency "highline"
|
38
|
+
spec.add_runtime_dependency "rainbow"
|
39
|
+
spec.add_runtime_dependency "google_drive"
|
40
|
+
spec.add_runtime_dependency "google-api-client", "~> 0.8.6"
|
41
|
+
spec.add_runtime_dependency "terminal-table"
|
42
|
+
spec.add_runtime_dependency "ruby-progressbar"
|
43
|
+
spec.add_runtime_dependency "slack-poster"
|
44
|
+
spec.add_runtime_dependency "graph_matching"
|
45
|
+
end
|