friends 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 594e797cfad3741eac04b32f65fad9bdaa79e6d6
4
- data.tar.gz: d4db59517521f8d5110b85426533d8a199db2d75
3
+ metadata.gz: 014d82e8189c2cf8442af3e9058d56dbfdfb0fd3
4
+ data.tar.gz: a4a10982b5139cbe3b800aea6f984e32763beea9
5
5
  SHA512:
6
- metadata.gz: 2dc7bb05e42a152f8fc5e5d532069295525bceb3c5f30b8a35cbf9b32dcd8561644d947f205a5a0cb964b4d1a2ae084bc745ee14bfa1e48daeb1c880130cf4fe
7
- data.tar.gz: e675e071c9be2c956c7d138c327ef15a5c20823643480b754304d7ef9b219159d8de3c853351d054fd9648a0bdd519691be8bafea31d87cb9cbf1dcef6d160e6
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
- List your friends:
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
- Or specify a date for the activity:
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
- List the activities you've recorded:
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 often are implemented very
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 "minitest/pride"
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: \e[1m#{suggestions[:distant].sample}\e[0m"
131
- puts "Moderate friend: \e[1m#{suggestions[:moderate].sample}\e[0m"
132
- puts "Close friend: \e[1m#{suggestions[:close].sample}\e[0m"
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 = %q{Spend time with the people you care about.}
12
- spec.description = %q{Spend time with the people you care about. Introvert-tested. Extrovert-approved.}
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"
@@ -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 = "\e[1m#{date}\e[0m"
38
+ date_s = Paint[date, :bold]
38
39
  description_s = description.to_s
39
40
  while match = description_s.match(/(\*\*)([^\*]+)(\*\*)/)
40
- description_s =
41
- "#{match.pre_match}\e[1m#{match[2]}\e[0m#{match.post_match}"
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] for use in aggregate computations
55
- # @param friends [Array] list of friends to highlight in the description
56
- # @raise [FriendsError] if more than one friend matches a part of the
57
- # description
58
- def highlight_friends(introvert:, friends:)
59
- # Map each friend to a list of all possible regexes for that friend.
60
- friend_regexes = {}
61
- friends.each { |f| friend_regexes[f] = regexes_for_name(f.name) }
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
- # Go through the description and substitute full, asterisk'd names for
78
- # anything that matches a friend's name.
79
- new_description = description.clone
80
- regex_map.each do |regex, friends|
81
- if friends.size > 1 # If there are multiple matches, find best friend.
82
- introvert.set_n_activities!
83
- friends.sort_by! { |friend| -friend.n_activities }
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
- new_description.gsub!(regex, "**#{friends.first.name}**")
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
- @description = new_description
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
@@ -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.
@@ -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(filename: @filename)
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, friends: friends)
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 do
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
- private
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
- # Gets the list of friends as read from the file.
228
- # @return [Array] a list of all friends
229
- def friends
230
- @friends
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
- # @param filename [String] the name of the friends file
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.
@@ -1,3 +1,3 @@
1
1
  module Friends
2
- VERSION = "0.2"
2
+ VERSION = "0.3"
3
3
  end
@@ -56,8 +56,9 @@ describe Friends::Activity do
56
56
 
57
57
  it do
58
58
  subject.
59
- must_equal "\e[1m#{date_s}\e[0m: "\
60
- "Lunch with \e[1m#{friend1.name}\e[0m and \e[1m#{friend2.name}\e[0m"
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
- let(:friend1) { Friends::Friend.new(name: "Elizabeth Cady Stanton") }
76
- let(:friend2) { Friends::Friend.new(name: "John Cage") }
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(:description) { "Lunch with #{friend1.name} and #{friend2.name}." }
79
- let(:introvert) { Minitest::Mock.new }
88
+ let(:introvert) { Friends::Introvert.new }
80
89
  subject do
81
- activity.highlight_friends(introvert: introvert, friends: friends)
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
- it "matches friends' first names" do
91
- activity = Friends::Activity.new(
92
- date_s: Date.today.to_s,
93
- description: "Lunch with Elizabeth and John."
94
- )
95
- activity.highlight_friends(introvert: introvert, friends: friends)
96
- activity.description.
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
- it "matches without case sensitivity" do
101
- activity = Friends::Activity.new(
102
- date_s: Date.today.to_s,
103
- description: "Lunch with elizabeth cady stanton."
104
- )
105
- activity.highlight_friends(introvert: introvert, friends: friends)
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
- it "ignores when at beginning of word" do
111
- activity = Friends::Activity.new(
112
- date_s: Date.today.to_s,
113
- description: "Field trip to the Johnson Co."
114
- )
115
- activity.highlight_friends(introvert: introvert, friends: friends)
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
- it "ignores when in middle of word" do
122
- activity = Friends::Activity.new(
123
- date_s: Date.today.to_s,
124
- description: "Field trip to the JimJohnJames Co."
125
- )
126
- activity.highlight_friends(introvert: introvert, friends: friends)
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
- it "ignores when at end of word" do
133
- activity = Friends::Activity.new(
134
- date_s: Date.today.to_s,
135
- description: "Field trip to the JimJohn Co."
136
- )
137
- activity.highlight_friends(introvert: introvert, friends: friends)
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
- it "does not match with leading asterisks" do
144
- activity = Friends::Activity.new(
145
- date_s: Date.today.to_s,
146
- description: "Dinner with **Elizabeth Cady Stanton."
147
- )
148
- activity.highlight_friends(introvert: introvert, friends: friends)
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
- it "does not match with ending asterisks" do
155
- activity = Friends::Activity.new(
156
- date_s: Date.today.to_s,
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: "Dinner with Elizabeth**."
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
- it "chooses the better friend when there are multiple matches" do
169
- friend2.name = "Elizabeth II"
170
- activity = Friends::Activity.new(
171
- date_s: Date.today.to_s,
172
- description: "Dinner with Elizabeth."
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
- # Pretend the introvert sets the friends' n_activities values.
176
- introvert.expect(:set_n_activities!, nil)
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
- activity.highlight_friends(introvert: introvert, friends: friends)
208
+ it "falls back to choosing the better friend" do
209
+ friend2.name = "Elizabeth II"
181
210
 
182
- # Pick the friend with more activities.
183
- activity.description.must_equal "Dinner with **Elizabeth II**."
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
- # introvert.verify
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")
@@ -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
- introvert.stub(:friends, unsorted_friends) do
64
- introvert.stub(:activities, unsorted_activities) do
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
- introvert.stub(:friends, friends) do
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
- introvert.stub(:friends, friends) do
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
- introvert.stub(:friends, friends) do
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
- introvert.stub(:friends, friends) do
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
- introvert.stub(:activities, activities) do
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
- introvert.stub(:activities, activities) do
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
- introvert.stub(:activities, activities) do
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
- introvert.stub(:activities, activities) do
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
- introvert.stub(:activities, activities) do
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
- introvert.stub(:friends, friends) do
180
- introvert.stub(:activities, activities) do
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
- introvert.stub(:friends, friends) do
192
- introvert.stub(:activities, activities) do
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
- introvert.stub(:friends, friends) do
202
- introvert.stub(:activities, activities) do
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
- introvert.stub(:friends, friends) do
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
- introvert.stub(:friends, friends) do
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
- introvert.stub(:friends, friends) do
242
- introvert.stub(:activities, activities) do
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
- introvert.stub(:friends, friends) do
257
- introvert.stub(:activities, activities) do
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
- introvert.stub(:friends, friends) do
270
- introvert.stub(:activities, activities) do
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
- introvert.stub(:friends, friends) do
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
- introvert.stub(:friends, friends) do
329
- introvert.stub(:activities, activities) do
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.2'
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-08 00:00:00.000000000 Z
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