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 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