friends 0.2 → 0.3
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 +4 -4
- data/README.md +25 -18
- data/bin/friends +23 -9
- data/friends.gemspec +3 -2
- data/lib/friends/activity.rb +55 -55
- data/lib/friends/friend.rb +45 -0
- data/lib/friends/introvert.rb +67 -32
- data/lib/friends/version.rb +1 -1
- data/test/activity_spec.rb +120 -83
- data/test/friend_spec.rb +37 -0
- data/test/introvert_spec.rb +44 -28
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 014d82e8189c2cf8442af3e9058d56dbfdfb0fd3
|
4
|
+
data.tar.gz: a4a10982b5139cbe3b800aea6f984e32763beea9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ac424ae74a5b3640c9374d4c7a9691d8e5305be1afba2a97f5714dfb662ffa2ea50238c6e30beb54ce48132dc610d8371229d9250a92e349772a5840ea1c9cf1
|
7
|
+
data.tar.gz: 8df418e8482b5d27eeb6823d870cabe1b818931628283e64c74cdb6d8ade3a78ed0371a2bf16415d08cc3e45492cd92ba88db78e0ec227ea138e69ff42c51cd7
|
data/README.md
CHANGED
@@ -31,34 +31,36 @@ care about.
|
|
31
31
|
$ gem install friends
|
32
32
|
```
|
33
33
|
|
34
|
-
## Usage
|
34
|
+
## Usage*
|
35
|
+
|
36
|
+
*Note that the command-line output is colored, which this README cannot show.
|
35
37
|
|
36
38
|
### Basic commands:
|
37
39
|
|
38
|
-
Add a friend:
|
40
|
+
##### Add a friend:
|
39
41
|
|
40
42
|
```
|
41
43
|
$ friends add friend "Grace Hopper"
|
42
44
|
Friend added: "Grace Hopper"
|
43
45
|
```
|
44
|
-
|
45
|
-
```
|
46
|
-
$ friends list friends
|
47
|
-
George Washington Carver
|
48
|
-
Grace Hopper
|
49
|
-
Marie Curie
|
50
|
-
```
|
51
|
-
Record an activity with a friend:
|
46
|
+
##### Record an activity with a friend:
|
52
47
|
```
|
53
48
|
$ friends add activity "Got lunch with Grace and George."
|
54
49
|
Activity added: "2015-01-04: Got lunch with Grace Hopper and George Washington Carver."
|
55
50
|
```
|
56
|
-
|
51
|
+
`friends` will **automatically** figure out which "Grace" and "George" you're referring to, *even if you're friends with lots of different Graces and Georges*.
|
52
|
+
|
53
|
+
You can of course specify a date for the activity:
|
57
54
|
```
|
58
55
|
$ friends add activity "2014-12-31: Celebrated the new year with Marie."
|
59
56
|
Activity added: "2014-12-31: Celebrated the new year with Marie Curie."
|
60
57
|
```
|
61
|
-
|
58
|
+
Or get an **interactive prompt** by just typing `friends add activity`, with or without a date specified:
|
59
|
+
```
|
60
|
+
$ friends add activity 2015-11-01
|
61
|
+
2015-11-01: <type description here>
|
62
|
+
```
|
63
|
+
##### List the activities you've recorded:
|
62
64
|
```
|
63
65
|
$ friends list activities
|
64
66
|
2015-01-04: Got lunch with Grace Hopper and George Washington Carver.
|
@@ -72,7 +74,7 @@ $ friends list activities --with "George"
|
|
72
74
|
2014-11-15: Talked to George Washington Carver on the phone for an hour.
|
73
75
|
|
74
76
|
```
|
75
|
-
Find your favorite friends:
|
77
|
+
##### Find your favorite friends:
|
76
78
|
```
|
77
79
|
$ friends list favorites
|
78
80
|
Your favorite friends:
|
@@ -87,7 +89,7 @@ Your favorite friends:
|
|
87
89
|
1. George Washington Carver (2 activities)
|
88
90
|
2. Grace Hopper (1)
|
89
91
|
```
|
90
|
-
Graph your relationship with a friend over time:
|
92
|
+
##### Graph (in color!) your relationship with a friend over time:
|
91
93
|
```
|
92
94
|
$ friends graph "George"
|
93
95
|
Nov 2014 |█
|
@@ -95,16 +97,21 @@ Dec 2014 |
|
|
95
97
|
Jan 2015 |█████
|
96
98
|
Feb 2015 |███
|
97
99
|
```
|
98
|
-
|
100
|
+
##### List all of your friends:
|
101
|
+
```
|
102
|
+
$ friends list friends
|
103
|
+
George Washington Carver
|
104
|
+
Grace Hopper
|
105
|
+
Marie Curie
|
106
|
+
```
|
99
107
|
### Global options:
|
100
108
|
|
101
109
|
##### --quiet
|
102
110
|
|
103
111
|
Quiet output messages:
|
104
112
|
```
|
105
|
-
$ friends add activity "Went rollerskating with George."
|
113
|
+
$ friends --quiet add activity "Went rollerskating with George."
|
106
114
|
$ # No output!
|
107
|
-
|
108
115
|
```
|
109
116
|
|
110
117
|
##### --filename
|
@@ -160,7 +167,7 @@ In case you're *really* interested, we have
|
|
160
167
|
|
161
168
|
If you have an idea,
|
162
169
|
[make a GitHub Issue](https://github.com/JacobEvelyn/friends/issues/new)!
|
163
|
-
Suggestions are very very welcome, and
|
170
|
+
Suggestions are very very welcome, and usually are implemented very
|
164
171
|
quickly. And if you'd like to do the implementing yourself:
|
165
172
|
|
166
173
|
1. Fork it (https://github.com/JacobEvelyn/friends/fork)
|
data/bin/friends
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
# Todo:
|
4
|
+
# - Create file if none exists.
|
5
|
+
# - Split out serialization into separate repository.
|
6
|
+
# - Add auto check for updates.
|
7
|
+
# - Allow friends to have nicknames.
|
4
8
|
# - Allow easy editing of most recent entry.
|
5
|
-
# - Make automatching use context better. (David & Meryl vs. David & Patricia)
|
6
9
|
# - Allow escape char to prevent automatching. ("\Zane wasn't there.")
|
7
10
|
|
8
11
|
require "gli"
|
9
|
-
require "
|
12
|
+
require "paint"
|
10
13
|
|
11
14
|
require "friends/introvert"
|
12
15
|
require "friends/version"
|
@@ -110,14 +113,23 @@ desc "Graph"
|
|
110
113
|
arg_name "NAME"
|
111
114
|
command :graph do |graph|
|
112
115
|
graph.action do |_, _, args|
|
116
|
+
# This math is taken from Minitest's Pride plugin (the PrideLOL class).
|
117
|
+
PI_3 = Math::PI / 3
|
118
|
+
|
119
|
+
colors = (0...(6 * 7)).map do |n|
|
120
|
+
n *= 1.0 / 6
|
121
|
+
r = (3 * Math.sin(n ) + 3).to_i
|
122
|
+
g = (3 * Math.sin(n + 2 * PI_3) + 3).to_i
|
123
|
+
b = (3 * Math.sin(n + 4 * PI_3) + 3).to_i
|
124
|
+
|
125
|
+
[r, g, b].map { |c| c * 51 }
|
126
|
+
end
|
127
|
+
|
113
128
|
data = @introvert.graph(name: args.first)
|
114
129
|
|
115
130
|
data.each do |month, count|
|
116
131
|
print "#{month} |"
|
117
|
-
|
118
|
-
colorer = Minitest::PrideLOL.new($stdout)
|
119
|
-
count.times { print colorer.pride "█" }
|
120
|
-
puts
|
132
|
+
puts colors.take(count).map { |rgb| Paint["█", rgb] }.join
|
121
133
|
end
|
122
134
|
end
|
123
135
|
end
|
@@ -127,9 +139,11 @@ command :suggest do |suggest|
|
|
127
139
|
suggest.action do
|
128
140
|
suggestions = @introvert.suggest
|
129
141
|
|
130
|
-
puts "Distant friend: \
|
131
|
-
|
132
|
-
puts "
|
142
|
+
puts "Distant friend: "\
|
143
|
+
"#{Paint[suggestions[:distant].sample, :bold, :magenta]}"
|
144
|
+
puts "Moderate friend: "\
|
145
|
+
"#{Paint[suggestions[:moderate].sample, :bold, :magenta]}"
|
146
|
+
puts "Close friend: #{Paint[suggestions[:close].sample, :bold, :magenta]}"
|
133
147
|
end
|
134
148
|
end
|
135
149
|
|
data/friends.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = Friends::VERSION
|
9
9
|
spec.authors = ["Jacob Evelyn"]
|
10
10
|
spec.email = ["jacobevelyn@gmail.com"]
|
11
|
-
spec.summary =
|
12
|
-
spec.description =
|
11
|
+
spec.summary = "Spend time with the people you care about."
|
12
|
+
spec.description = "Spend time with the people you care about. Introvert-tested. Extrovert-approved."
|
13
13
|
spec.homepage = "https://github.com/JacobEvelyn/friends"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
@@ -20,6 +20,7 @@ Gem::Specification.new do |spec|
|
|
20
20
|
|
21
21
|
spec.add_dependency "gli", "~> 2.12"
|
22
22
|
spec.add_dependency "memoist", "~> 0.11"
|
23
|
+
spec.add_dependency "paint", "~> 1.0"
|
23
24
|
|
24
25
|
spec.add_development_dependency "bundler", "~> 1.7"
|
25
26
|
spec.add_development_dependency "codeclimate-test-reporter", "~> 0.4"
|
data/lib/friends/activity.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# Activity represents an activity you've done with one or more Friends.
|
2
2
|
|
3
3
|
require "memoist"
|
4
|
+
require "paint"
|
4
5
|
|
5
6
|
require "friends/serializable"
|
6
7
|
|
@@ -34,11 +35,12 @@ module Friends
|
|
34
35
|
|
35
36
|
# @return [String] the command-line display text for the activity
|
36
37
|
def display_text
|
37
|
-
date_s =
|
38
|
+
date_s = Paint[date, :bold]
|
38
39
|
description_s = description.to_s
|
39
40
|
while match = description_s.match(/(\*\*)([^\*]+)(\*\*)/)
|
40
|
-
description_s =
|
41
|
-
|
41
|
+
description_s = "#{match.pre_match}"\
|
42
|
+
"#{Paint[match[2], :bold, :magenta]}"\
|
43
|
+
"#{match.post_match}"
|
42
44
|
end
|
43
45
|
"#{date_s}: #{description_s}"
|
44
46
|
end
|
@@ -51,14 +53,15 @@ module Friends
|
|
51
53
|
# Modify the description to turn inputted friend names
|
52
54
|
# (e.g. "Jacob" or "Jacob Evelyn") into full asterisk'd names
|
53
55
|
# (e.g. "**Jacob Evelyn**")
|
54
|
-
# @param introvert [Introvert]
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
56
|
+
# @param introvert [Introvert] used to access the list of friends and the
|
57
|
+
# connections between the
|
58
|
+
# NOTE: When a friend name matches more than one friend, this method chooses
|
59
|
+
# a friend based on a best-guess algorithm that looks at which friends do
|
60
|
+
# activities together and which friends are stronger than others. For
|
61
|
+
# more information see the comments below and the
|
62
|
+
# introvert#set_likelihood_score! method.
|
63
|
+
def highlight_friends(introvert:)
|
64
|
+
friend_regexes = introvert.friend_regex_map
|
62
65
|
|
63
66
|
# Create hash mapping regex to friend. Note that because two friends may
|
64
67
|
# have the same regex (e.g. /John/), we need to store the names in an
|
@@ -74,19 +77,51 @@ module Friends
|
|
74
77
|
end
|
75
78
|
end
|
76
79
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
80
|
+
matched_friends = []
|
81
|
+
|
82
|
+
# First, we find all of the regex matches with only one possibility, and
|
83
|
+
# make those substitutions.
|
84
|
+
regex_map.
|
85
|
+
select { |_, arr| arr.size == 1 }.each do |regex, friend_list|
|
86
|
+
if match = @description.match(regex)
|
87
|
+
friend = friend_list.first # There's only one friend in the list.
|
88
|
+
matched_friends << friend
|
89
|
+
@description = "#{match.pre_match}"\
|
90
|
+
"**#{friend.name}**"\
|
91
|
+
"#{match.post_match}"
|
84
92
|
end
|
93
|
+
end
|
94
|
+
|
95
|
+
possible_matched_friends = []
|
85
96
|
|
86
|
-
|
97
|
+
# Now, we look at regex matches that are ambiguous.
|
98
|
+
regex_map.
|
99
|
+
reject { |_, arr| arr.size == 1 }.each do |regex, friend_list|
|
100
|
+
if @description.match(regex)
|
101
|
+
possible_matched_friends << friend_list
|
102
|
+
end
|
87
103
|
end
|
88
104
|
|
89
|
-
|
105
|
+
# Now, we compute the likelihood of each friend in the possible-match set.
|
106
|
+
introvert.set_n_activities!
|
107
|
+
introvert.set_likelihood_score!(
|
108
|
+
matches: matched_friends,
|
109
|
+
possible_matches: possible_matched_friends
|
110
|
+
)
|
111
|
+
|
112
|
+
# Now we go through and replace all of the ambiguous matches with our best
|
113
|
+
# guess.
|
114
|
+
regex_map.
|
115
|
+
reject { |_, arr| arr.size == 1 }.each do |regex, friend_list|
|
116
|
+
if match = @description.match(regex)
|
117
|
+
guessed_friend = friend_list.sort_by do |friend|
|
118
|
+
[-friend.likelihood_score, -friend.n_activities]
|
119
|
+
end.first
|
120
|
+
@description = "#{match.pre_match}"\
|
121
|
+
"**#{guessed_friend.name}**"\
|
122
|
+
"#{match.post_match}"
|
123
|
+
end
|
124
|
+
end
|
90
125
|
end
|
91
126
|
|
92
127
|
# @param friend [Friend] the friend to test
|
@@ -108,40 +143,5 @@ module Friends
|
|
108
143
|
def <=>(other)
|
109
144
|
other.date <=> date
|
110
145
|
end
|
111
|
-
|
112
|
-
# @return [Array] a list of all regexes to match the name in a string, with
|
113
|
-
# longer regexes first
|
114
|
-
# Note: for now we only match on full names or first names
|
115
|
-
# Example: [
|
116
|
-
# /Jacob\s+Morris\s+Evelyn/,
|
117
|
-
# /Jacob/
|
118
|
-
# ]
|
119
|
-
def regexes_for_name(name)
|
120
|
-
# We generously allow any amount of whitespace between parts of a name.
|
121
|
-
splitter = "\\s+"
|
122
|
-
|
123
|
-
# We don't want to match names that are directly touching double asterisks
|
124
|
-
# as these are treated as sacred by our program.
|
125
|
-
no_leading_asterisks = "(?<!\\*\\*)"
|
126
|
-
no_ending_asterisks = "(?!\\*\\*)"
|
127
|
-
|
128
|
-
# We don't want to match names that are part of other words.
|
129
|
-
no_leading_alphabeticals = "(?<![A-z])"
|
130
|
-
no_ending_alphabeticals = "(?![A-z])"
|
131
|
-
|
132
|
-
# Create the list of regexes and return it.
|
133
|
-
chunks = name.split(Regexp.new(splitter))
|
134
|
-
|
135
|
-
[chunks, [chunks.first]].map do |words|
|
136
|
-
Regexp.new(
|
137
|
-
no_leading_asterisks +
|
138
|
-
no_leading_alphabeticals +
|
139
|
-
words.join(splitter) +
|
140
|
-
no_ending_alphabeticals +
|
141
|
-
no_ending_asterisks,
|
142
|
-
Regexp::IGNORECASE
|
143
|
-
)
|
144
|
-
end
|
145
|
-
end
|
146
146
|
end
|
147
147
|
end
|
data/lib/friends/friend.rb
CHANGED
@@ -37,6 +37,51 @@ module Friends
|
|
37
37
|
@n_activities || 0
|
38
38
|
end
|
39
39
|
|
40
|
+
# The likelihood_score that an activity description that matches part of
|
41
|
+
# this friend's name does in fact refer to this friend. A higher
|
42
|
+
# likelihood_score means it is more likely to be this friend. For more
|
43
|
+
# information see the activity#highlight_friends and
|
44
|
+
# introvert#set_likelihood_score! methods.
|
45
|
+
attr_writer :likelihood_score
|
46
|
+
def likelihood_score
|
47
|
+
@likelihood_score || 0
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Array] a list of all regexes to match the name in a string, with
|
51
|
+
# longer regexes first
|
52
|
+
# Note: for now we only match on full names or first names
|
53
|
+
# Example: [
|
54
|
+
# /Jacob\s+Morris\s+Evelyn/,
|
55
|
+
# /Jacob/
|
56
|
+
# ]
|
57
|
+
def regexes_for_name
|
58
|
+
# We generously allow any amount of whitespace between parts of a name.
|
59
|
+
splitter = "\\s+"
|
60
|
+
|
61
|
+
# We don't want to match names that are directly touching double asterisks
|
62
|
+
# as these are treated as sacred by our program.
|
63
|
+
no_leading_asterisks = "(?<!\\*\\*)"
|
64
|
+
no_ending_asterisks = "(?!\\*\\*)"
|
65
|
+
|
66
|
+
# We don't want to match names that are part of other words.
|
67
|
+
no_leading_alphabeticals = "(?<![A-z])"
|
68
|
+
no_ending_alphabeticals = "(?![A-z])"
|
69
|
+
|
70
|
+
# Create the list of regexes and return it.
|
71
|
+
chunks = name.split(Regexp.new(splitter))
|
72
|
+
|
73
|
+
[chunks, [chunks.first]].map do |words|
|
74
|
+
Regexp.new(
|
75
|
+
no_leading_asterisks +
|
76
|
+
no_leading_alphabeticals +
|
77
|
+
words.join(splitter) +
|
78
|
+
no_ending_alphabeticals +
|
79
|
+
no_ending_asterisks,
|
80
|
+
Regexp::IGNORECASE
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
40
85
|
private
|
41
86
|
|
42
87
|
# Default sorting for an array of friends is alphabetical.
|
data/lib/friends/introvert.rb
CHANGED
@@ -22,23 +22,20 @@ module Friends
|
|
22
22
|
|
23
23
|
# Read in the input file. It's easier to do this now and optimize later
|
24
24
|
# than try to overly be clever about what we read and write.
|
25
|
-
read_file
|
25
|
+
read_file
|
26
26
|
end
|
27
27
|
|
28
|
-
attr_reader :filename
|
29
|
-
attr_reader :activities
|
30
|
-
|
31
28
|
# Write out the friends file with cleaned/sorted data.
|
32
29
|
def clean
|
33
30
|
# Short-circuit if we've already cleaned the file so we don't write it
|
34
31
|
# twice.
|
35
|
-
return filename if @cleaned_file
|
32
|
+
return @filename if @cleaned_file
|
36
33
|
|
37
|
-
descriptions = activities.sort.map(&:serialize)
|
38
|
-
names = friends.sort.map(&:serialize)
|
34
|
+
descriptions = @activities.sort.map(&:serialize)
|
35
|
+
names = @friends.sort.map(&:serialize)
|
39
36
|
|
40
37
|
# Write out the cleaned file.
|
41
|
-
File.open(filename, "w") do |file|
|
38
|
+
File.open(@filename, "w") do |file|
|
42
39
|
file.puts(ACTIVITIES_HEADER)
|
43
40
|
descriptions.each { |desc| file.puts(desc) }
|
44
41
|
file.puts # Blank line separating friends from activities.
|
@@ -48,7 +45,7 @@ module Friends
|
|
48
45
|
|
49
46
|
@cleaned_file = true
|
50
47
|
|
51
|
-
filename
|
48
|
+
@filename
|
52
49
|
end
|
53
50
|
|
54
51
|
# Add a friend and write out the new friends file.
|
@@ -66,7 +63,7 @@ module Friends
|
|
66
63
|
raise FriendsError, e
|
67
64
|
end
|
68
65
|
|
69
|
-
friends << friend
|
66
|
+
@friends << friend
|
70
67
|
clean # Write a cleaned file.
|
71
68
|
|
72
69
|
friend # Return the added friend.
|
@@ -85,8 +82,8 @@ module Friends
|
|
85
82
|
# If there's no description, prompt the user for one.
|
86
83
|
activity.description ||= Readline.readline(activity.display_text)
|
87
84
|
|
88
|
-
activity.highlight_friends(introvert: self
|
89
|
-
activities << activity
|
85
|
+
activity.highlight_friends(introvert: self)
|
86
|
+
@activities << activity
|
90
87
|
clean # Write a cleaned file.
|
91
88
|
|
92
89
|
activity # Return the added activity.
|
@@ -95,7 +92,7 @@ module Friends
|
|
95
92
|
# List all friend names in the friends file.
|
96
93
|
# @return [Array] a list of all friend names
|
97
94
|
def list_friends
|
98
|
-
friends.map(&:name)
|
95
|
+
@friends.map(&:name)
|
99
96
|
end
|
100
97
|
|
101
98
|
# List your favorite friends.
|
@@ -111,13 +108,11 @@ module Friends
|
|
111
108
|
set_n_activities! # Set n_activities for all friends.
|
112
109
|
|
113
110
|
# Sort the results, with the most favorite friend first.
|
114
|
-
results = friends.sort_by { |friend| -friend.n_activities }
|
111
|
+
results = @friends.sort_by { |friend| -friend.n_activities }
|
115
112
|
|
116
113
|
# If we need to, trim the list.
|
117
114
|
results = results.take(limit) unless limit.nil?
|
118
115
|
|
119
|
-
# max_str_size = results.first.n_activities.to_s.size
|
120
|
-
# results.map { |friend| "#{friend.n_activities.to_s.rjust(max_str_size)} #{friend.name}" }
|
121
116
|
max_str_size = results.map(&:name).map(&:size).max
|
122
117
|
results.map.with_index(0) do |friend, index|
|
123
118
|
name = friend.name.ljust(max_str_size)
|
@@ -133,7 +128,7 @@ module Friends
|
|
133
128
|
# unfiltered
|
134
129
|
# @return [Array] a list of all activity text values
|
135
130
|
def list_activities(limit:, with:)
|
136
|
-
acts = activities
|
131
|
+
acts = @activities
|
137
132
|
|
138
133
|
# Filter by friend name if argument is passed.
|
139
134
|
unless with.nil?
|
@@ -162,7 +157,7 @@ module Friends
|
|
162
157
|
friend = friend_with_name_in(name) # Find the friend by name.
|
163
158
|
|
164
159
|
# Filter out activities that don't include the given friend.
|
165
|
-
acts = activities.select { |act| act.includes_friend?(friend: friend) }
|
160
|
+
acts = @activities.select { |act| act.includes_friend?(friend: friend) }
|
166
161
|
|
167
162
|
# Initialize the table of activities to have all of the months of that
|
168
163
|
# friend's activity range (including months in the middle of the range
|
@@ -189,12 +184,12 @@ module Friends
|
|
189
184
|
set_n_activities! # Set n_activities for all friends.
|
190
185
|
|
191
186
|
# Sort our friends, with the least favorite friend first.
|
192
|
-
sorted_friends = friends.sort_by(&:n_activities)
|
187
|
+
sorted_friends = @friends.sort_by(&:n_activities)
|
193
188
|
|
194
189
|
output = Hash.new { |h, k| h[k] = [] }
|
195
190
|
|
196
191
|
# First, get not-so-good friends.
|
197
|
-
while sorted_friends.first.n_activities < 2
|
192
|
+
while sorted_friends.first.n_activities < 2
|
198
193
|
output[:distant] << sorted_friends.shift.name
|
199
194
|
end
|
200
195
|
|
@@ -205,11 +200,15 @@ module Friends
|
|
205
200
|
output
|
206
201
|
end
|
207
202
|
|
203
|
+
###################################################################
|
204
|
+
# Methods below this are only used internally and are not tested. #
|
205
|
+
###################################################################
|
206
|
+
|
208
207
|
# Sets the n_activities field on each friend.
|
209
208
|
def set_n_activities!
|
210
209
|
# Construct a hash of friend name to frequency of appearance.
|
211
210
|
freq_table = Hash.new { |h, k| h[k] = 0 }
|
212
|
-
activities.each do |activity|
|
211
|
+
@activities.each do |activity|
|
213
212
|
activity.friend_names.each do |friend_name|
|
214
213
|
freq_table[friend_name] += 1
|
215
214
|
end
|
@@ -222,28 +221,64 @@ module Friends
|
|
222
221
|
end
|
223
222
|
end
|
224
223
|
|
225
|
-
|
224
|
+
# @return [Hash] mapping each friend to a list of all possible regexes for
|
225
|
+
# that friend's name
|
226
|
+
def friend_regex_map
|
227
|
+
@friends.each_with_object({}) do |friend, hash|
|
228
|
+
hash[friend] = friend.regexes_for_name
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Sets the likelihood_score field on each friend in `possible_matches`. This
|
233
|
+
# score represents how likely it is that an activity containing the friends
|
234
|
+
# in `matches` and containing a friend from each group in `possible_matches`
|
235
|
+
# contains that given friend.
|
236
|
+
# @param matches [Array<Friend>] the friends in a specific activity
|
237
|
+
# @param possible_matches [Array<Array<Friend>>] an array of groups of
|
238
|
+
# possible matches, for example:
|
239
|
+
# [
|
240
|
+
# [Friend.new(name: "John Doe"), Friend.new(name: "John Deere")],
|
241
|
+
# [Friend.new(name: "Aunt Mae"), Friend.new(name: "Aunt Sue")]
|
242
|
+
# ]
|
243
|
+
# These groups will all contain friends with similar names; the purpose of
|
244
|
+
# this method is to give us a likelihood that a "John" in an activity
|
245
|
+
# description, for instance, is "John Deere" vs. "John Doe"
|
246
|
+
def set_likelihood_score!(matches:, possible_matches:)
|
247
|
+
combinations = (matches + possible_matches.flatten).
|
248
|
+
combination(2).
|
249
|
+
reject do |friend1, friend2|
|
250
|
+
(matches & [friend1, friend2]).size == 2 ||
|
251
|
+
possible_matches.any? do |group|
|
252
|
+
(group & [friend1, friend2]).size == 2
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
@activities.each do |activity|
|
257
|
+
names = activity.friend_names
|
226
258
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
259
|
+
combinations.each do |group|
|
260
|
+
if (names & group.map(&:name)).size == 2
|
261
|
+
group.each { |friend| friend.likelihood_score += 1 }
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
231
265
|
end
|
232
266
|
|
267
|
+
private
|
268
|
+
|
233
269
|
# Process the friends.md file and store its contents in internal data
|
234
270
|
# structures.
|
235
|
-
|
236
|
-
def read_file(filename:)
|
271
|
+
def read_file
|
237
272
|
@friends = []
|
238
273
|
@activities = []
|
239
274
|
|
240
|
-
return unless File.exist?(filename)
|
275
|
+
return unless File.exist?(@filename)
|
241
276
|
|
242
277
|
state = :initial
|
243
278
|
line_num = 0
|
244
279
|
|
245
280
|
# Loop through all lines in the file and process them.
|
246
|
-
File.foreach(filename) do |line|
|
281
|
+
File.foreach(@filename) do |line|
|
247
282
|
line_num += 1
|
248
283
|
line.chomp! # Remove trailing newline from each line.
|
249
284
|
|
@@ -278,7 +313,7 @@ module Friends
|
|
278
313
|
# @return [Friend] the friend whose name exactly matches the argument
|
279
314
|
# @raise [FriendsError] if more than one friend has the given name
|
280
315
|
def friend_with_exact_name(name)
|
281
|
-
results = friends.select { |friend| friend.name == name }
|
316
|
+
results = @friends.select { |friend| friend.name == name }
|
282
317
|
|
283
318
|
case results.size
|
284
319
|
when 0 then nil
|
@@ -309,7 +344,7 @@ module Friends
|
|
309
344
|
# @return [Array] a list of all friends that match the given text
|
310
345
|
def friends_with_name_in(text)
|
311
346
|
regex = Regexp.new(text, Regexp::IGNORECASE)
|
312
|
-
friends.select { |friend| friend.name.match(regex) }
|
347
|
+
@friends.select { |friend| friend.name.match(regex) }
|
313
348
|
end
|
314
349
|
|
315
350
|
# Raise an error that a line in the friends file is malformed.
|
data/lib/friends/version.rb
CHANGED
data/test/activity_spec.rb
CHANGED
@@ -56,8 +56,9 @@ describe Friends::Activity do
|
|
56
56
|
|
57
57
|
it do
|
58
58
|
subject.
|
59
|
-
must_equal "
|
60
|
-
"Lunch with
|
59
|
+
must_equal "#{Paint[date_s, :bold]}: "\
|
60
|
+
"Lunch with #{Paint[friend1.name, :bold, :magenta]} and "\
|
61
|
+
"#{Paint[friend2.name, :bold, :magenta]}"
|
61
62
|
end
|
62
63
|
end
|
63
64
|
|
@@ -72,117 +73,153 @@ describe Friends::Activity do
|
|
72
73
|
end
|
73
74
|
|
74
75
|
describe "#highlight_friends" do
|
75
|
-
|
76
|
-
|
76
|
+
# Add helpers to set internal states for friends and activities.
|
77
|
+
def stub_friends(val)
|
78
|
+
introvert.instance_variable_set(:@friends, val)
|
79
|
+
yield
|
80
|
+
end
|
81
|
+
|
82
|
+
def stub_activities(val)
|
83
|
+
introvert.instance_variable_set(:@activities, val)
|
84
|
+
yield
|
85
|
+
end
|
86
|
+
|
77
87
|
let(:friends) { [friend1, friend2] }
|
78
|
-
let(:
|
79
|
-
let(:introvert) { Minitest::Mock.new }
|
88
|
+
let(:introvert) { Friends::Introvert.new }
|
80
89
|
subject do
|
81
|
-
activity.highlight_friends(introvert: introvert
|
90
|
+
stub_friends(friends) { activity.highlight_friends(introvert: introvert) }
|
82
91
|
end
|
83
92
|
|
84
93
|
it "finds all friends" do
|
85
94
|
subject
|
86
95
|
activity.description.
|
87
|
-
must_equal "Lunch with **#{friend1.name}** and **#{friend2.name}
|
96
|
+
must_equal "Lunch with **#{friend1.name}** and **#{friend2.name}**"
|
88
97
|
end
|
89
98
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
must_equal "Lunch with **#{friend1.name}** and **#{friend2.name}**."
|
99
|
+
describe "when description has first names" do
|
100
|
+
let(:description) { "Lunch with Elizabeth and John." }
|
101
|
+
it "matches friends" do
|
102
|
+
subject
|
103
|
+
activity.description.
|
104
|
+
must_equal "Lunch with **#{friend1.name}** and **#{friend2.name}**."
|
105
|
+
end
|
98
106
|
end
|
99
107
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
activity.description.
|
107
|
-
must_equal "Lunch with **Elizabeth Cady Stanton**."
|
108
|
+
describe "when names are not entered case-sensitively" do
|
109
|
+
let(:description) { "Lunch with elizabeth cady stanton." }
|
110
|
+
it "matches friends" do
|
111
|
+
subject
|
112
|
+
activity.description.must_equal "Lunch with **Elizabeth Cady Stanton**."
|
113
|
+
end
|
108
114
|
end
|
109
115
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
# No match found.
|
118
|
-
activity.description.must_equal "Field trip to the Johnson Co."
|
116
|
+
describe "when name is at beginning of word" do
|
117
|
+
let(:description) { "Field trip to the Johnson Co." }
|
118
|
+
it "does not match a friend" do
|
119
|
+
subject
|
120
|
+
# No match found.
|
121
|
+
activity.description.must_equal "Field trip to the Johnson Co."
|
122
|
+
end
|
119
123
|
end
|
120
124
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
# No match found.
|
129
|
-
activity.description.must_equal "Field trip to the JimJohnJames Co."
|
125
|
+
describe "when name is in middle of word" do
|
126
|
+
let(:description) { "Field trip to the JimJohnJames Co." }
|
127
|
+
it "does not match a friend" do
|
128
|
+
subject
|
129
|
+
# No match found.
|
130
|
+
activity.description.must_equal "Field trip to the JimJohnJames Co."
|
131
|
+
end
|
130
132
|
end
|
131
133
|
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
# No match found.
|
140
|
-
activity.description.must_equal "Field trip to the JimJohn Co."
|
134
|
+
describe "when name is at end of word" do
|
135
|
+
let(:description) { "Field trip to the JimJohn Co." }
|
136
|
+
it "does not match a friend" do
|
137
|
+
subject
|
138
|
+
# No match found.
|
139
|
+
activity.description.must_equal "Field trip to the JimJohn Co."
|
140
|
+
end
|
141
141
|
end
|
142
142
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
# No match found.
|
151
|
-
activity.description.must_equal "Dinner with **Elizabeth Cady Stanton."
|
143
|
+
describe "when name has leading asterisks" do
|
144
|
+
let(:description) { "Dinner with **Elizabeth Cady Stanton." }
|
145
|
+
it "does not match a friend" do
|
146
|
+
subject
|
147
|
+
# No match found.
|
148
|
+
activity.description.must_equal "Dinner with **Elizabeth Cady Stanton."
|
149
|
+
end
|
152
150
|
end
|
153
151
|
|
154
|
-
|
155
|
-
|
156
|
-
|
152
|
+
describe "when name has ending asterisks" do
|
153
|
+
let(:description) { "Dinner with Elizabeth**." }
|
154
|
+
it "does not match a friend" do
|
155
|
+
subject
|
157
156
|
|
158
157
|
# Note: for now we can't guarantee that "Elizabeth Cady Stanton**" won't
|
159
158
|
# match, because the Elizabeth isn't surrounded by asterisks.
|
160
|
-
description
|
161
|
-
|
162
|
-
activity.highlight_friends(introvert: introvert, friends: friends)
|
163
|
-
|
164
|
-
# No match found.
|
165
|
-
activity.description.must_equal "Dinner with Elizabeth**."
|
159
|
+
activity.description.must_equal "Dinner with Elizabeth**."
|
160
|
+
end
|
166
161
|
end
|
167
162
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
163
|
+
describe "when there are multiple matches" do
|
164
|
+
describe "when there is context from past activities" do
|
165
|
+
let(:description) { "Dinner with Elizabeth and John." }
|
166
|
+
let(:friends) do
|
167
|
+
[
|
168
|
+
friend1,
|
169
|
+
friend2,
|
170
|
+
Friends::Friend.new(name: "Elizabeth II")
|
171
|
+
]
|
172
|
+
end
|
173
|
+
|
174
|
+
it "chooses a match based on the context" do
|
175
|
+
# Create a past activity in which Elizabeth Cady Stanton did something
|
176
|
+
# with John Cage. Then, create past activities to make Elizabeth II a
|
177
|
+
# better friend than Elizabeth Cady Stanton.
|
178
|
+
old_activities = [
|
179
|
+
Friends::Activity.new(
|
180
|
+
date_s: date_s,
|
181
|
+
description: "Picnic with **Elizabeth Cady Stanton** and "\
|
182
|
+
"**John Cage**."
|
183
|
+
),
|
184
|
+
Friends::Activity.new(
|
185
|
+
date_s: date_s,
|
186
|
+
description: "Got lunch with with **Elizabeth II**."
|
187
|
+
),
|
188
|
+
Friends::Activity.new(
|
189
|
+
date_s: date_s,
|
190
|
+
description: "Ice skated with **Elizabeth II**."
|
191
|
+
)
|
192
|
+
]
|
193
|
+
|
194
|
+
# Elizabeth II is the better friend, but historical activities have
|
195
|
+
# had Elizabeth Cady Stanton and John Cage together. Thus, we should
|
196
|
+
# interpret "Elizabeth" as Elizabeth Cady Stanton.
|
197
|
+
stub_activities(old_activities) { subject }
|
198
|
+
|
199
|
+
activity.description.
|
200
|
+
must_equal "Dinner with **Elizabeth Cady Stanton** and "\
|
201
|
+
"**John Cage**."
|
202
|
+
end
|
203
|
+
end
|
174
204
|
|
175
|
-
|
176
|
-
|
177
|
-
friend1.n_activities = 5
|
178
|
-
friend2.n_activities = 7
|
205
|
+
describe "when there is no context from past activities" do
|
206
|
+
let(:description) { "Dinner with Elizabeth." }
|
179
207
|
|
180
|
-
|
208
|
+
it "falls back to choosing the better friend" do
|
209
|
+
friend2.name = "Elizabeth II"
|
181
210
|
|
182
|
-
|
183
|
-
|
211
|
+
# Give a past activity to Elizabeth II.
|
212
|
+
old_activity = Friends::Activity.new(
|
213
|
+
date_s: date_s,
|
214
|
+
description: "Do something with **Elizabeth II**."
|
215
|
+
)
|
184
216
|
|
185
|
-
|
217
|
+
stub_activities([old_activity]) { subject }
|
218
|
+
|
219
|
+
# Pick the friend with more activities.
|
220
|
+
activity.description.must_equal "Dinner with **Elizabeth II**."
|
221
|
+
end
|
222
|
+
end
|
186
223
|
end
|
187
224
|
end
|
188
225
|
|
data/test/friend_spec.rb
CHANGED
@@ -40,6 +40,43 @@ describe Friends::Friend do
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
+
describe "#n_activities" do
|
44
|
+
subject { friend.n_activities }
|
45
|
+
|
46
|
+
it "defaults to zero" do
|
47
|
+
subject.must_equal 0
|
48
|
+
end
|
49
|
+
|
50
|
+
it "is writable" do
|
51
|
+
friend.n_activities += 1
|
52
|
+
subject.must_equal 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#likelihood_score" do
|
57
|
+
subject { friend.likelihood_score }
|
58
|
+
|
59
|
+
it "defaults to zero" do
|
60
|
+
subject.must_equal 0
|
61
|
+
end
|
62
|
+
|
63
|
+
it "is writable" do
|
64
|
+
friend.likelihood_score += 1
|
65
|
+
subject.must_equal 1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "#regexes_for_name" do
|
70
|
+
subject { friend.regexes_for_name }
|
71
|
+
|
72
|
+
it "generates appropriate regexes" do
|
73
|
+
subject.must_equal [
|
74
|
+
/(?<!\*\*)(?<![A-z])Jacob\s+Evelyn(?![A-z])(?!\*\*)/i,
|
75
|
+
/(?<!\*\*)(?<![A-z])Jacob(?![A-z])(?!\*\*)/i
|
76
|
+
]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
43
80
|
describe "#<=>" do
|
44
81
|
it "sorts alphabetically" do
|
45
82
|
aaron = Friends::Friend.new(name: "Aaron")
|
data/test/introvert_spec.rb
CHANGED
@@ -1,6 +1,22 @@
|
|
1
1
|
require_relative "helper"
|
2
2
|
|
3
3
|
describe Friends::Introvert do
|
4
|
+
# Add readers to make internal state easier to test.
|
5
|
+
class Friends::Introvert
|
6
|
+
attr_reader :filename, :activities, :friends
|
7
|
+
end
|
8
|
+
|
9
|
+
# Add helpers to set internal states for friends and activities.
|
10
|
+
def stub_friends(val)
|
11
|
+
introvert.instance_variable_set(:@friends, val)
|
12
|
+
yield
|
13
|
+
end
|
14
|
+
|
15
|
+
def stub_activities(val)
|
16
|
+
introvert.instance_variable_set(:@activities, val)
|
17
|
+
yield
|
18
|
+
end
|
19
|
+
|
4
20
|
let(:filename) { "test/tmp/friends.md" }
|
5
21
|
let(:args) { { filename: filename } }
|
6
22
|
let(:introvert) { Friends::Introvert.new(args) }
|
@@ -60,8 +76,8 @@ describe Friends::Introvert do
|
|
60
76
|
"#{Friends::Introvert::FRIENDS_HEADER}\n#{name_output}\n"
|
61
77
|
|
62
78
|
# Read the input as unsorted, and make sure we get sorted output.
|
63
|
-
|
64
|
-
|
79
|
+
stub_friends(unsorted_friends) do
|
80
|
+
stub_activities(unsorted_activities) do
|
65
81
|
subject
|
66
82
|
File.read(filename).must_equal expected_output
|
67
83
|
end
|
@@ -75,7 +91,7 @@ describe Friends::Introvert do
|
|
75
91
|
subject { introvert.list_friends }
|
76
92
|
|
77
93
|
it "lists the names of friends" do
|
78
|
-
|
94
|
+
stub_friends(friends) do
|
79
95
|
subject.must_equal friend_names
|
80
96
|
end
|
81
97
|
end
|
@@ -90,14 +106,14 @@ describe Friends::Introvert do
|
|
90
106
|
|
91
107
|
describe "when there is no existing friend with that name" do
|
92
108
|
it "adds the given friend" do
|
93
|
-
|
109
|
+
stub_friends(friends) do
|
94
110
|
subject
|
95
111
|
introvert.list_friends.must_include new_friend_name
|
96
112
|
end
|
97
113
|
end
|
98
114
|
|
99
115
|
it "returns the friend added" do
|
100
|
-
|
116
|
+
stub_friends(friends) do
|
101
117
|
subject.name.must_equal new_friend_name
|
102
118
|
end
|
103
119
|
end
|
@@ -107,7 +123,7 @@ describe Friends::Introvert do
|
|
107
123
|
let(:new_friend_name) { friend_names.first }
|
108
124
|
|
109
125
|
it "raises an error" do
|
110
|
-
|
126
|
+
stub_friends(friends) do
|
111
127
|
proc { subject }.must_raise Friends::FriendsError
|
112
128
|
end
|
113
129
|
end
|
@@ -123,7 +139,7 @@ describe Friends::Introvert do
|
|
123
139
|
let(:limit) { 1 }
|
124
140
|
|
125
141
|
it "lists that number of activities" do
|
126
|
-
|
142
|
+
stub_activities(activities) do
|
127
143
|
subject.size.must_equal limit
|
128
144
|
end
|
129
145
|
end
|
@@ -133,7 +149,7 @@ describe Friends::Introvert do
|
|
133
149
|
let(:limit) { activities.size }
|
134
150
|
|
135
151
|
it "lists all activities" do
|
136
|
-
|
152
|
+
stub_activities(activities) do
|
137
153
|
subject.size.must_equal activities.size
|
138
154
|
end
|
139
155
|
end
|
@@ -143,7 +159,7 @@ describe Friends::Introvert do
|
|
143
159
|
let(:limit) { activities.size + 5 }
|
144
160
|
|
145
161
|
it "lists all activities" do
|
146
|
-
|
162
|
+
stub_activities(activities) do
|
147
163
|
subject.size.must_equal activities.size
|
148
164
|
end
|
149
165
|
end
|
@@ -153,7 +169,7 @@ describe Friends::Introvert do
|
|
153
169
|
let(:limit) { nil }
|
154
170
|
|
155
171
|
it "lists all activities" do
|
156
|
-
|
172
|
+
stub_activities(activities) do
|
157
173
|
subject.size.must_equal activities.size
|
158
174
|
end
|
159
175
|
end
|
@@ -163,7 +179,7 @@ describe Friends::Introvert do
|
|
163
179
|
let(:with) { nil }
|
164
180
|
|
165
181
|
it "lists the activities" do
|
166
|
-
|
182
|
+
stub_activities(activities) do
|
167
183
|
subject.must_equal activities.map(&:display_text)
|
168
184
|
end
|
169
185
|
end
|
@@ -176,8 +192,8 @@ describe Friends::Introvert do
|
|
176
192
|
let(:friend_names) { ["George Washington Carver", "Boy George"] }
|
177
193
|
|
178
194
|
it "raises an error" do
|
179
|
-
|
180
|
-
|
195
|
+
stub_friends(friends) do
|
196
|
+
stub_activities(activities) do
|
181
197
|
proc { subject }.must_raise Friends::FriendsError
|
182
198
|
end
|
183
199
|
end
|
@@ -188,8 +204,8 @@ describe Friends::Introvert do
|
|
188
204
|
let(:friend_names) { ["Joe"] }
|
189
205
|
|
190
206
|
it "raises an error" do
|
191
|
-
|
192
|
-
|
207
|
+
stub_friends(friends) do
|
208
|
+
stub_activities(activities) do
|
193
209
|
proc { subject }.must_raise Friends::FriendsError
|
194
210
|
end
|
195
211
|
end
|
@@ -198,8 +214,8 @@ describe Friends::Introvert do
|
|
198
214
|
|
199
215
|
describe "when there is exactly one friend match" do
|
200
216
|
it "filters the activities by that friend" do
|
201
|
-
|
202
|
-
|
217
|
+
stub_friends(friends) do
|
218
|
+
stub_activities(activities) do
|
203
219
|
# Only one activity has that friend.
|
204
220
|
subject.must_equal activities[0..0].map(&:display_text)
|
205
221
|
end
|
@@ -218,14 +234,14 @@ describe Friends::Introvert do
|
|
218
234
|
after { File.delete(filename) if File.exists?(filename) }
|
219
235
|
|
220
236
|
it "adds the given activity" do
|
221
|
-
|
237
|
+
stub_friends(friends) do
|
222
238
|
subject
|
223
239
|
introvert.activities.last.description.must_equal activity_description
|
224
240
|
end
|
225
241
|
end
|
226
242
|
|
227
243
|
it "returns the activity added" do
|
228
|
-
|
244
|
+
stub_friends(friends) do
|
229
245
|
subject.description.must_equal activity_description
|
230
246
|
end
|
231
247
|
end
|
@@ -238,8 +254,8 @@ describe Friends::Introvert do
|
|
238
254
|
let(:limit) { nil }
|
239
255
|
|
240
256
|
it "returns all friends in order of favoritism with activity counts" do
|
241
|
-
|
242
|
-
|
257
|
+
stub_friends(friends) do
|
258
|
+
stub_activities(activities) do
|
243
259
|
subject.must_equal [
|
244
260
|
"Betsy Ross (2 activities)",
|
245
261
|
"George Washington Carver (1)"
|
@@ -253,8 +269,8 @@ describe Friends::Introvert do
|
|
253
269
|
let(:limit) { 1 }
|
254
270
|
|
255
271
|
it "returns the number of favorites requested" do
|
256
|
-
|
257
|
-
|
272
|
+
stub_friends(friends) do
|
273
|
+
stub_activities(activities) do
|
258
274
|
subject.must_equal ["Betsy Ross (2 activities)"]
|
259
275
|
end
|
260
276
|
end
|
@@ -266,8 +282,8 @@ describe Friends::Introvert do
|
|
266
282
|
subject { introvert.suggest }
|
267
283
|
|
268
284
|
it "returns distant, moderate, and close friends" do
|
269
|
-
|
270
|
-
|
285
|
+
stub_friends(friends) do
|
286
|
+
stub_activities(activities) do
|
271
287
|
subject.must_equal(
|
272
288
|
distant: ["George Washington Carver"],
|
273
289
|
moderate: [],
|
@@ -293,7 +309,7 @@ describe Friends::Introvert do
|
|
293
309
|
let(:friend_name) { "e" }
|
294
310
|
|
295
311
|
it "raises an error" do
|
296
|
-
|
312
|
+
stub_friends(friends) do
|
297
313
|
proc { subject }.must_raise Friends::FriendsError
|
298
314
|
end
|
299
315
|
end
|
@@ -325,8 +341,8 @@ describe Friends::Introvert do
|
|
325
341
|
end
|
326
342
|
|
327
343
|
it "returns a hash of months and frequencies" do
|
328
|
-
|
329
|
-
|
344
|
+
stub_friends(friends) do
|
345
|
+
stub_activities(activities) do
|
330
346
|
strftime_format = Friends::Introvert::GRAPH_DATE_FORMAT
|
331
347
|
|
332
348
|
first = activities[0]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: friends
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.3'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jacob Evelyn
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-11-
|
11
|
+
date: 2015-11-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: gli
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0.11'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: paint
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: bundler
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|