friends 0.19 → 0.20

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: 4ba2ca1e570ce12bae59bd92e45eed450b15859d
4
- data.tar.gz: f29a32cae219ae83c7cbc925d1493894d76409cb
3
+ metadata.gz: 1429eb3d555f53ebb68816b253350252370d37bc
4
+ data.tar.gz: 42095e0608c73c17fd75f8f4bedab8313e19a4b1
5
5
  SHA512:
6
- metadata.gz: 264af4e8b7d6956401c9a7e6babecb6a8e8b6dc154de383394aef7890c2262685ea90c24c1d525d03802ecb1a028ffbc445cd4306fa91c5badbf044389667d1e
7
- data.tar.gz: 674aa75753fc7aedc6f50bbb54956ecd91f6df2d8930d886f5f018e08d9c89b0db2e4f5aa74c014b45b35c6d429809d6fbdb7982598b52b66b0636f377136d3a
6
+ metadata.gz: ab66c9042a1c23ec4e5c2a1685d03b91d5b66c1382ca3ed64b1822277bf08ed1bbe321a9144f59256d6c369b93f9b954d8d6307a9a50b6c9f6fc4374c9e7df6f
7
+ data.tar.gz: 87719311645cdff9c7af1a862c9533e8671c1e000c7221f7833226bfcad3f86935de92440acbad5a65aa87bf005485c2b31589cdb3d9ea094973093d13896add
data/.gitignore CHANGED
@@ -16,3 +16,4 @@ mkmf.log
16
16
  ideas.txt
17
17
  TODO.txt
18
18
  .DS_Store
19
+ .byebug_history
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Change Log
2
2
 
3
+ ## [v0.20](https://github.com/JacobEvelyn/friends/tree/v0.20) (2016-05-08)
4
+ [Full Changelog](https://github.com/JacobEvelyn/friends/compare/v0.19...v0.20)
5
+
6
+ **Implemented enhancements:**
7
+
8
+ - Add --tagged option to `list friends` [\#119](https://github.com/JacobEvelyn/friends/issues/119)
9
+ - Add --verbose option to `list friends` [\#117](https://github.com/JacobEvelyn/friends/issues/117)
10
+ - Add `list hashtags` command [\#116](https://github.com/JacobEvelyn/friends/issues/116)
11
+ - Add hashtag capabilities to friends [\#90](https://github.com/JacobEvelyn/friends/issues/90)
12
+ - Add hashtag capabilities to activities [\#89](https://github.com/JacobEvelyn/friends/issues/89)
13
+ - Add location data to friends [\#66](https://github.com/JacobEvelyn/friends/issues/66)
14
+
15
+ **Merged pull requests:**
16
+
17
+ - Implement hashtags [\#118](https://github.com/JacobEvelyn/friends/pull/118) ([JacobEvelyn](https://github.com/JacobEvelyn))
18
+
3
19
  ## [v0.19](https://github.com/JacobEvelyn/friends/tree/v0.19) (2016-05-02)
4
20
  [Full Changelog](https://github.com/JacobEvelyn/friends/compare/v0.18...v0.19)
5
21
 
data/README.md CHANGED
@@ -17,6 +17,7 @@ Extrovert-approved.
17
17
  - `add`
18
18
  - [`add activity`](#add-activity)
19
19
  - [`add friend`](#add-friend)
20
+ - [`add hashtag`](#add-hashtag)
20
21
  - [`add location`](#add-location)
21
22
  - [`add nickname`](#add-nickname)
22
23
  - [`clean`](#clean)
@@ -28,8 +29,11 @@ Extrovert-approved.
28
29
  - [`list favorite friends`](#list-favorite-friends)
29
30
  - [`list favorite locations`](#list-favorite-locations)
30
31
  - [`list friends`](#list-friends)
32
+ - [`list hashtags`](#list-hashtags)
31
33
  - [`list locations`](#list-locations)
32
- - [`remove nickname`](#remove-nickname)
34
+ - `remove`
35
+ - [`remove hashtag`](#remove-hashtag)
36
+ - [`remove nickname`](#remove-nickname)
33
37
  - `rename`
34
38
  - [`rename friend`](#rename-friend)
35
39
  - [`rename location`](#rename-location)
@@ -83,10 +87,14 @@ Easy, huh?
83
87
  `friends` is structured around several different types of things:
84
88
 
85
89
  - **Activities**: The things you do. Each activity has a date associated with
86
- it.
90
+ it. Activities may optionally contain any number of *friends*, *locations*,
91
+ and *hashtags*.
87
92
  - **Friends**: The people you do *activities* with. Each friend has a name and,
88
- optionally, one or several nicknames.
89
- - **Locations**: The places in which *activities* happen.
93
+ optionally, one or several nicknames. (Examples: `John`, `Grace Hopper`)
94
+ - **Locations**: The places in which *activities* happen. (Examples: `Paris`,
95
+ `Marie's Diner`)
96
+ - **Hashtags**: A way to categorize your *activities* with tags of your
97
+ choosing. (Examples: `#exercise`, `#concert`)
90
98
 
91
99
  The `friends.md` Markdown file that stores all of your data contains:
92
100
 
@@ -212,6 +220,14 @@ $ friends add activity "Went swimming near atlantis with George."
212
220
  Activity added: "2016-01-06: Went swimming near Atlantis with George Washington Carver."
213
221
  ```
214
222
 
223
+ Hashtags will be colored if they're provided (though this README can't display
224
+ color so you'll just have to have faith here):
225
+
226
+ ```bash
227
+ $ friends add activity "The office softball team wins a game! #work #exercise"
228
+ Activity added: "2016-05-05: The office softball team wins a game! #work #exercise"
229
+ ```
230
+
215
231
  You can of course specify a date for the activity:
216
232
 
217
233
  ```bash
@@ -247,6 +263,13 @@ $ friends add friend "Grace Hopper"
247
263
  Friend added: "Grace Hopper"
248
264
  ```
249
265
 
266
+ #### `add hashtag`
267
+
268
+ ```bash
269
+ $ friends add hashtag "Grace Hopper" science
270
+ Hashtag added to friend: "Grace Hopper #science
271
+ ```
272
+
250
273
  #### `add location`
251
274
 
252
275
  ```
@@ -350,8 +373,8 @@ Lists recent activities:
350
373
 
351
374
  ```bash
352
375
  $ friends list activities
353
- 2015-01-04: Got lunch with Grace Hopper and George Washington Carver.
354
- 2014-12-31: Celebrated the new year with Marie Curie in New York City.
376
+ 2015-01-04: Got lunch with Grace Hopper and George Washington Carver. #food
377
+ 2014-12-31: Celebrated the new year with Marie Curie in New York City. #partying
355
378
  2014-11-15: Talked to George Washington Carver on the phone for an hour.
356
379
  ```
357
380
 
@@ -359,15 +382,15 @@ You can adjust how many activities are shown:
359
382
 
360
383
  ```bash
361
384
  $ friends list activities --limit 2
362
- 2015-01-04: Got lunch with Grace Hopper and George Washington Carver.
363
- 2014-12-31: Celebrated the new year with Marie Curie in New York City.
385
+ 2015-01-04: Got lunch with Grace Hopper and George Washington Carver. #food
386
+ 2014-12-31: Celebrated the new year with Marie Curie in New York City. #partying
364
387
  ```
365
388
 
366
389
  Or only list the activities you did with a certain friend:
367
390
 
368
391
  ```bash
369
- $ friends list activities --with "George"
370
- 2015-01-04: Got lunch with Grace Hopper and George Washington Carver.
392
+ $ friends list activities --with George
393
+ 2015-01-04: Got lunch with Grace Hopper and George Washington Carver. #food
371
394
  2014-11-15: Talked to George Washington Carver on the phone for an hour.
372
395
  ```
373
396
 
@@ -375,7 +398,21 @@ Or filter your activities by location:
375
398
 
376
399
  ```bash
377
400
  $ friends list activities --in "New York"
378
- 2014-12-31: Celebrated the new year with Marie Curie in New York City.
401
+ 2014-12-31: Celebrated the new year with Marie Curie in New York City. #partying
402
+ ```
403
+
404
+ Or by hashtag:
405
+
406
+ ```bash
407
+ $ friends list activities --tagged food
408
+ 2015-01-04: Got lunch with Grace Hopper and George Washington Carver. #food
409
+ ```
410
+
411
+ And you can mix and match these options to your heart's content:
412
+
413
+ ```bash
414
+ $ friends list activities --tagged food --with Grace
415
+ 2015-01-04: Got lunch with Grace Hopper and George Washington Carver. #food
379
416
  ```
380
417
 
381
418
  #### `list favorite friends`
@@ -422,7 +459,7 @@ Your favorite locations:
422
459
 
423
460
  #### `list friends`
424
461
 
425
- Lists all of your friends:
462
+ Lists all of your friends in alphabetical order:
426
463
 
427
464
  ```bash
428
465
  $ friends list friends
@@ -431,6 +468,15 @@ Grace Hopper
431
468
  Marie Curie
432
469
  ```
433
470
 
471
+ You can also include friend nicknames, locations, and hashtags:
472
+
473
+ ```bash
474
+ $ friends list friends --verbose
475
+ George Washington Carver
476
+ Grace Hopper (a.k.a. The Admiral a.k.a. Amazing Grace) [Paris] #navy #science
477
+ Marie Curie [Atlantis] #science
478
+ ```
479
+
434
480
  You can filter your friends by location:
435
481
 
436
482
  ```bash
@@ -438,9 +484,46 @@ $ friends list friends --in Paris
438
484
  Marie Curie
439
485
  ```
440
486
 
487
+ And you can also filter your friends by hashtag:
488
+
489
+ ```bash
490
+ $ friends list friends --tagged science
491
+ Grace Hopper
492
+ Marie Curie
493
+ ```
494
+
495
+ #### `list hashtags`
496
+
497
+ Lists all hashtags you've used, in alphabetical order:
498
+
499
+ ```bash
500
+ $ friends list hashtags
501
+ #dancing
502
+ #food
503
+ #school
504
+ #swanky
505
+ ```
506
+
507
+ You can limit this to only hashtags from activities:
508
+
509
+ ```bash
510
+ $ friends list hashtags --from activities
511
+ #dancing
512
+ #food
513
+ #swanky
514
+ ```
515
+
516
+ Or only hashtags from friends:
517
+
518
+ ```bash
519
+ $ friends list hashtags --from friends
520
+ #school
521
+ #swanky
522
+ ```
523
+
441
524
  #### `list locations`
442
525
 
443
- Lists all of the locations you've added:
526
+ Lists all of the locations you've added, in alphabetical order::
444
527
 
445
528
  ```
446
529
  $ friends list locations
@@ -449,6 +532,15 @@ New York City
449
532
  Paris
450
533
  ```
451
534
 
535
+ #### `remove hashtag`
536
+
537
+ Removes a specific hashtag from a friend:
538
+
539
+ ```bash
540
+ $ friends remove hashtag "Grace Hopper" fun
541
+ Hashtag removed from friend: "Grace Hopper (a.k.a. Amazing Grace) #OtherHashtag"
542
+ ```
543
+
452
544
  #### `remove nickname`
453
545
 
454
546
  Removes a specific nickname from a friend:
data/bin/friends CHANGED
@@ -18,6 +18,28 @@ version Friends::VERSION
18
18
  subcommand_option_handling :normal
19
19
  arguments :strict
20
20
 
21
+ class Hashtag
22
+ # @param str [String] of the form "hashtag" or "#hashtag"
23
+ # @return [String] the string, with whitespace stripped and a hashtag
24
+ # prepended if there isn't one already
25
+ # NOTE: This logic could be only in the accept block if GLI allowed type
26
+ # conversions for arguments.
27
+ # See: https://github.com/davetron5000/gli/issues/241
28
+ def self.convert_to_hashtag(str)
29
+ str = str.strip
30
+ str.size > 0 && str[0] == "#" ? str : "##{str}"
31
+ end
32
+ end
33
+
34
+ accept(Hashtag) do |value|
35
+ Hashtag.convert_to_hashtag(value)
36
+ end
37
+
38
+ class Stripped ; end
39
+ accept(Stripped) do |value|
40
+ value.strip
41
+ end
42
+
21
43
  switch [:quiet],
22
44
  negatable: false,
23
45
  desc: "Quiet output messages"
@@ -72,10 +94,75 @@ command :list do |list|
72
94
  list.command :friends do |list_friends|
73
95
  list_friends.flag [:in],
74
96
  arg_name: "LOCATION",
75
- desc: "List only friends in the given location"
97
+ desc: "List only friends in the given location",
98
+ type: Stripped
99
+
100
+ list_friends.flag [:tagged],
101
+ arg_name: "HASHTAG",
102
+ desc: "List only friends with the given hashtag",
103
+ type: Hashtag
104
+
105
+ list_friends.switch [:verbose],
106
+ negatable: false,
107
+ desc: "Output friend nicknames, locations, and hashtags"
76
108
 
77
109
  list_friends.action do |_, options|
78
- puts @introvert.list_friends(location_name: options[:in])
110
+ puts @introvert.list_friends(
111
+ location_name: options[:in],
112
+ tagged: options[:tagged],
113
+ verbose: options[:verbose]
114
+ )
115
+ end
116
+ end
117
+
118
+ list.desc "Lists all activities"
119
+ list.command :activities do |list_activities|
120
+ list_activities.flag [:limit],
121
+ arg_name: "NUMBER",
122
+ desc: "The number of activities to return",
123
+ default_value: 10,
124
+ type: Integer
125
+
126
+ list_activities.flag [:with],
127
+ arg_name: "NAME",
128
+ desc: "List only activities with the given friend",
129
+ type: Stripped
130
+
131
+ list_activities.flag [:in],
132
+ arg_name: "LOCATION",
133
+ desc: "List only activities in the given location",
134
+ type: Stripped
135
+
136
+ list_activities.flag [:tagged],
137
+ arg_name: "HASHTAG",
138
+ desc: "List only activities with the given hashtag",
139
+ type: Hashtag
140
+
141
+ list_activities.action do |_, options|
142
+ puts @introvert.list_activities(
143
+ limit: options[:limit],
144
+ with: options[:with],
145
+ location_name: options[:in],
146
+ tagged: options[:tagged]
147
+ )
148
+ end
149
+ end
150
+
151
+ list.desc "List all locations"
152
+ list.command :locations do |list_locations|
153
+ list_locations.action do
154
+ puts @introvert.list_locations
155
+ end
156
+ end
157
+
158
+ list.desc "List all hashtags used"
159
+ list.command :hashtags do |list_hashtags|
160
+ list_hashtags.flag [:from],
161
+ arg_name: '"activities" or "friends" (default: both)',
162
+ desc: "List only hashtags from activities or friends "\
163
+ "instead of both"
164
+ list_hashtags.action do |_, options|
165
+ puts @introvert.list_hashtags(from: options[:from])
79
166
  end
80
167
  end
81
168
 
@@ -85,12 +172,12 @@ command :list do |list|
85
172
  list_favorite.command :friends do |list_favorite_friends|
86
173
  list_favorite_friends.flag [:limit],
87
174
  arg_name: "NUMBER",
175
+ desc: "The number of friends to return",
88
176
  default_value: 10,
89
- desc: "The number of friends to return"
177
+ type: Integer
90
178
 
91
179
  list_favorite_friends.action do |_, options|
92
- limit = options[:limit].to_i
93
- favorites = @introvert.list_favorite_friends(limit: limit)
180
+ favorites = @introvert.list_favorite_friends(limit: options[:limit])
94
181
 
95
182
  if limit == 1
96
183
  puts "Your best friend is #{favorites.first}"
@@ -109,12 +196,12 @@ command :list do |list|
109
196
  list_favorite.command :locations do |list_favorite_locations|
110
197
  list_favorite_locations.flag [:limit],
111
198
  arg_name: "NUMBER",
199
+ desc: "The number of locations to return",
112
200
  default_value: 10,
113
- desc: "The number of locations to return"
201
+ type: Integer
114
202
 
115
203
  list_favorite_locations.action do |_, options|
116
- limit = options[:limit].to_i
117
- favorites = @introvert.list_favorite_locations(limit: limit)
204
+ favorites = @introvert.list_favorite_locations(limit: options[:limit])
118
205
 
119
206
  if limit == 1
120
207
  puts "Your favorite location is #{favorites.first}"
@@ -129,38 +216,6 @@ command :list do |list|
129
216
  end
130
217
  end
131
218
  end
132
-
133
- list.desc "Lists all activities"
134
- list.command :activities do |list_activities|
135
- list_activities.flag [:limit],
136
- arg_name: "NUMBER",
137
- default_value: 10,
138
- desc: "The number of activities to return"
139
-
140
- list_activities.flag [:with],
141
- arg_name: "NAME",
142
- desc: "List only activities involving the given friend"
143
-
144
- list_activities.flag [:in],
145
- arg_name: "LOCATION",
146
- desc: "List only activities in the given location"
147
-
148
- list_activities.action do |_, options|
149
- limit = options[:limit].to_i
150
- puts @introvert.list_activities(
151
- limit: limit,
152
- with: options[:with],
153
- location_name: options[:in]
154
- )
155
- end
156
- end
157
-
158
- list.desc "List all locations"
159
- list.command :locations do |list_locations|
160
- list_locations.action do
161
- puts @introvert.list_locations
162
- end
163
- end
164
219
  end
165
220
 
166
221
  desc "Adds a friend (or nickname), activity, or location"
@@ -183,11 +238,11 @@ command :add do |add|
183
238
 
184
239
  # If there's no description, prompt the user for one.
185
240
  if activity.description.nil? || activity.description.empty?
186
- activity.description = Readline.readline(activity.display_text)
241
+ activity.description = Readline.readline(activity.to_s)
187
242
  activity.highlight_description(introvert: @introvert)
188
243
  end
189
244
 
190
- @message = "Activity added: \"#{activity.display_text}\""
245
+ @message = "Activity added: \"#{activity}\""
191
246
  @dirty = true # Mark the file for cleaning.
192
247
  end
193
248
  end
@@ -211,6 +266,19 @@ command :add do |add|
211
266
  @dirty = true # Mark the file for cleaning.
212
267
  end
213
268
  end
269
+
270
+ add.desc "Adds a hashtag to a friend"
271
+ add.arg_name "NAME HASHTAG"
272
+ add.command :hashtag do |add_hashtag|
273
+ add_hashtag.action do |_, _, args|
274
+ friend = @introvert.add_hashtag(
275
+ name: args.first,
276
+ hashtag: Hashtag.convert_to_hashtag(args[1])
277
+ )
278
+ @message = "Hashtag added to friend: \"#{friend}\""
279
+ @dirty = true # Mark the file for cleaning.
280
+ end
281
+ end
214
282
  end
215
283
 
216
284
  desc "Set data about friends"
@@ -237,6 +305,19 @@ command :remove do |remove|
237
305
  @dirty = true # Mark the file for cleaning.
238
306
  end
239
307
  end
308
+
309
+ remove.desc "Removes a hashtag to a friend"
310
+ remove.arg_name "NAME HASHTAG"
311
+ remove.command :hashtag do |remove_hashtag|
312
+ remove_hashtag.action do |_, _, args|
313
+ friend = @introvert.remove_hashtag(
314
+ name: args.first,
315
+ hashtag: Hashtag.convert_to_hashtag(args[1])
316
+ )
317
+ @message = "Hashtag removed from friend: \"#{friend}\""
318
+ @dirty = true # Mark the file for cleaning.
319
+ end
320
+ end
240
321
  end
241
322
 
242
323
  desc "Graph all activities or a friend's relationship over time"
@@ -268,7 +349,8 @@ desc "Suggest friends to do activities with"
268
349
  command :suggest do |suggest|
269
350
  suggest.flag [:in],
270
351
  arg_name: "LOCATION",
271
- desc: "Suggest only friends in the given location"
352
+ desc: "Suggest only friends in the given location",
353
+ type: Stripped
272
354
 
273
355
  suggest.action do |_, options|
274
356
  suggestions = @introvert.suggest(location_name: options[:in])
data/friends.md CHANGED
@@ -1,13 +1,13 @@
1
1
  ### Activities:
2
- - 2015-11-01: **Grace Hopper** and I went to _Marie's Diner_. George had to cancel at the last minute.
3
- - 2015-01-04: Got lunch with **Grace Hopper** and **George Washington Carver**.
4
- - 2014-12-31: Celebrated the new year in _Paris_ with **Marie Curie**.
2
+ - 2015-11-01: **Grace Hopper** and I went to _Marie's Diner_. George had to cancel at the last minute. #food
3
+ - 2015-01-04: Got lunch with **Grace Hopper** and **George Washington Carver**. #food
4
+ - 2014-12-31: Celebrated the new year in _Paris_ with **Marie Curie**. #partying
5
5
  - 2014-11-15: Talked to **George Washington Carver** on the phone for an hour.
6
6
 
7
7
  ### Friends:
8
8
  - George Washington Carver
9
- - Grace Hopper (a.k.a. The Admiral a.k.a. Amazing Grace) [Paris]
10
- - Marie Curie [Atlantis]
9
+ - Grace Hopper (a.k.a. The Admiral a.k.a. Amazing Grace) [Paris] #navy #science
10
+ - Marie Curie [Atlantis] #science
11
11
 
12
12
  ### Locations:
13
13
  - Atlantis
data/lib/friends.rb CHANGED
@@ -2,4 +2,5 @@ require "friends/introvert"
2
2
  require "friends/version"
3
3
 
4
4
  module Friends
5
+ HASHTAG_REGEX = /#\p{Alnum}+/
5
6
  end
@@ -50,7 +50,7 @@ module Friends
50
50
  attr_accessor :description
51
51
 
52
52
  # @return [String] the command-line display text for the activity
53
- def display_text
53
+ def to_s
54
54
  date_s = Paint[date, :bold]
55
55
  description_s = description.to_s
56
56
  # rubocop:disable Lint/AssignmentInCondition
@@ -68,6 +68,10 @@ module Friends
68
68
  "#{Paint[match[1], :bold, :yellow]}"\
69
69
  "#{match.post_match}"
70
70
  end
71
+
72
+ description_s = description_s.
73
+ gsub(HASHTAG_REGEX, Paint['\0', :bold, :cyan])
74
+
71
75
  "#{date_s}: #{description_s}"
72
76
  end
73
77
 
@@ -120,6 +124,17 @@ module Friends
120
124
  friend_names.include? friend.name
121
125
  end
122
126
 
127
+ # @param hashtag [String] the hashtag to test, of the form "#hashtag"
128
+ # @return [Boolean] true iff this activity includes the given hashtag
129
+ def includes_hashtag?(hashtag:)
130
+ hashtags.include? hashtag
131
+ end
132
+
133
+ # @return [Set] all hashtags in this activity (including the "#")
134
+ def hashtags
135
+ Set.new(@description.scan(HASHTAG_REGEX))
136
+ end
137
+
123
138
  # Find the names of all friends in this description.
124
139
  # @return [Array] list of all friend names in the description
125
140
  def friend_names
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  # Friend represents a friend. You know, a real-life friend!
3
3
 
4
+ require "friends"
4
5
  require "friends/regex_builder"
5
6
  require "friends/serializable"
6
7
 
@@ -15,7 +16,7 @@ module Friends
15
16
  def self.deserialization_regex
16
17
  # Note: this regex must be on one line because whitespace is important
17
18
  # rubocop:disable Metrics/LineLength
18
- /(#{SERIALIZATION_PREFIX})?(?<name>[^\(\[]+)(\((?<nickname_str>#{NICKNAME_PREFIX}.+)\))?\s?(\[(?<location_name>[^\]]+)\])?/
19
+ /(#{SERIALIZATION_PREFIX})?(?<name>[^\(\[#]*[^\(\[#\s])(\s+\(#{NICKNAME_PREFIX}(?<nickname_str>.+)\))?(\s+\[(?<location_name>[^\]]+)\])?(\s+(?<hashtags_str>(#{HASHTAG_REGEX}\s*)+))?/
19
20
  # rubocop:enable Metrics/LineLength
20
21
  end
21
22
 
@@ -25,16 +26,23 @@ module Friends
25
26
  end
26
27
 
27
28
  # @param name [String] the name of the friend
28
- def initialize(name:, nickname_str: nil, location_name: nil)
29
- @name = name.strip
29
+ def initialize(
30
+ name:,
31
+ nickname_str: nil,
32
+ location_name: nil,
33
+ hashtags_str: nil
34
+ )
35
+ @name = name
30
36
  @nicknames = nickname_str &&
31
- nickname_str.split(NICKNAME_PREFIX)[1..-1].map(&:strip) ||
37
+ nickname_str.split(" #{NICKNAME_PREFIX}") ||
32
38
  []
33
39
  @location_name = location_name
40
+ @hashtags = hashtags_str && hashtags_str.split(/\s+/) || []
34
41
  end
35
42
 
36
43
  attr_accessor :name
37
44
  attr_accessor :location_name
45
+ attr_reader :hashtags
38
46
 
39
47
  # @return [String] the file serialization text for the friend
40
48
  def serialize
@@ -52,10 +60,28 @@ module Friends
52
60
 
53
61
  location_str = " [#{@location_name}]" unless @location_name.nil?
54
62
 
55
- "#{@name}#{nickname_str}#{location_str}"
63
+ hashtag_str = " #{@hashtags.join(' ')}" unless @hashtags.empty?
64
+
65
+ "#{@name}#{nickname_str}#{location_str}#{hashtag_str}"
66
+ end
67
+
68
+ # Adds a hashtag, ignoring duplicates.
69
+ # @param hashtag [String] the hashtag to add
70
+ def add_hashtag(hashtag)
71
+ @hashtags << hashtag
72
+ @hashtags.uniq!
73
+ end
74
+
75
+ # @param hashtag [String] the hashtag to remove
76
+ def remove_hashtag(hashtag)
77
+ unless @hashtags.include? hashtag
78
+ raise FriendsError, "Hashtag \"#{hashtag}\" not found for \"#{name}\""
79
+ end
80
+
81
+ @hashtags.delete(hashtag)
56
82
  end
57
83
 
58
- # Adds a nickname, avoiding duplicates and stripping surrounding whitespace.
84
+ # Adds a nickname, ignoring duplicates.
59
85
  # @param nickname [String] the nickname to add
60
86
  def add_nickname(nickname)
61
87
  @nicknames << nickname
@@ -63,7 +89,7 @@ module Friends
63
89
  end
64
90
 
65
91
  # @param nickname [String] the nickname to remove
66
- # @return [Boolean] true if the nickname was present, false otherwise
92
+ # @raise [FriendsError] if the friend does not have the given nickname
67
93
  def remove_nickname(nickname)
68
94
  unless @nicknames.include? nickname
69
95
  raise FriendsError, "Nickname \"#{nickname}\" not found for \"#{name}\""
@@ -116,9 +116,6 @@ module Friends
116
116
  # @raise [FriendsError] if 0 or 2+ friends match the given name
117
117
  # @return [Friend] the existing friend
118
118
  def rename_friend(old_name:, new_name:)
119
- old_name.strip!
120
- new_name.strip!
121
-
122
119
  friend = friend_with_name_in(old_name)
123
120
  @activities.each do |activity|
124
121
  activity.update_friend_name(old_name: friend.name, new_name: new_name)
@@ -133,9 +130,6 @@ module Friends
133
130
  # @raise [FriendsError] if 0 or 2+ friends match the given name
134
131
  # @return [Location] the existing location
135
132
  def rename_location(old_name:, new_name:)
136
- old_name.strip!
137
- new_name.strip!
138
-
139
133
  loc = location_with_name_in(old_name)
140
134
 
141
135
  # Update locations in activities.
@@ -159,27 +153,54 @@ module Friends
159
153
  # @return [Friend] the existing friend
160
154
  def add_nickname(name:, nickname:)
161
155
  friend = friend_with_name_in(name)
162
- friend.add_nickname(nickname.strip)
156
+ friend.add_nickname(nickname)
157
+ friend
158
+ end
159
+
160
+ # Add a hashtag to an existing friend.
161
+ # @param name [String] the name of the friend
162
+ # @param hashtag [String] the hashtag to add to the friend
163
+ # @raise [FriendsError] if 0 or 2+ friends match the given name
164
+ # @return [Friend] the existing friend
165
+ def add_hashtag(name:, hashtag:)
166
+ friend = friend_with_name_in(name)
167
+ friend.add_hashtag(hashtag)
168
+ friend
169
+ end
170
+
171
+ # Remove a hashtag from an existing friend.
172
+ # @param name [String] the name of the friend
173
+ # @param hashtag [String] the hashtag to remove from the friend
174
+ # @raise [FriendsError] if 0 or 2+ friends match the given name
175
+ # @raise [FriendsError] if the friend does not have the given nickname
176
+ # @return [Friend] the existing friend
177
+ def remove_hashtag(name:, hashtag:)
178
+ friend = friend_with_name_in(name)
179
+ friend.remove_hashtag(hashtag)
163
180
  friend
164
181
  end
165
182
 
166
- # Remove a nickname from an existing friend and write out the new friends
167
- # file.
183
+ # Remove a nickname from an existing friend.
168
184
  # @param name [String] the name of the friend
169
185
  # @param nickname [String] the nickname to remove from the friend
170
186
  # @raise [FriendsError] if 0 or 2+ friends match the given name
187
+ # @raise [FriendsError] if the friend does not have the given nickname
171
188
  # @return [Friend] the existing friend
172
189
  def remove_nickname(name:, nickname:)
173
190
  friend = friend_with_name_in(name)
174
- friend.remove_nickname(nickname.strip)
191
+ friend.remove_nickname(nickname)
175
192
  friend
176
193
  end
177
194
 
178
195
  # List all friend names in the friends file.
179
196
  # @param location_name [String] the name of a location to filter by, or nil
180
197
  # for unfiltered
198
+ # @param tagged [String] the name of a hashtag to filter by, or nil for
199
+ # unfiltered
200
+ # @param verbose [Boolean] true iff we should output friend names with
201
+ # nicknames, locations, and hashtags; false for names only
181
202
  # @return [Array] a list of all friend names
182
- def list_friends(location_name:)
203
+ def list_friends(location_name:, tagged:, verbose:)
183
204
  fs = @friends
184
205
 
185
206
  # Filter by location if a name is passed.
@@ -188,7 +209,10 @@ module Friends
188
209
  fs = fs.select { |friend| friend.location_name == location.name }
189
210
  end
190
211
 
191
- fs.map(&:name)
212
+ # Filter by hashtag if one is passed.
213
+ fs = fs.select { |friend| friend.hashtags.include? tagged } if tagged
214
+
215
+ verbose ? fs.map(&:to_s) : fs.map(&:name)
192
216
  end
193
217
 
194
218
  # List your favorite friends.
@@ -214,9 +238,11 @@ module Friends
214
238
  # unfiltered
215
239
  # @param location_name [String] the name of a location to filter by, or nil
216
240
  # for unfiltered
241
+ # @param tagged [String] the name of a hashtag to filter by, or nil for
242
+ # unfiltered
217
243
  # @return [Array] a list of all activity text values
218
244
  # @raise [FriendsError] if 0 or 2+ friends match the given `with` text
219
- def list_activities(limit:, with:, location_name:)
245
+ def list_activities(limit:, with:, location_name:, tagged:)
220
246
  acts = @activities
221
247
 
222
248
  # Filter by friend name if argument is passed.
@@ -231,10 +257,15 @@ module Friends
231
257
  acts = acts.select { |act| act.includes_location?(location: location) }
232
258
  end
233
259
 
260
+ # Filter by tag if argument is passed.
261
+ unless tagged.nil?
262
+ acts = acts.select { |act| act.includes_hashtag?(hashtag: tagged) }
263
+ end
264
+
234
265
  # If we need to, trim the list.
235
266
  acts = acts.take(limit) unless limit.nil?
236
267
 
237
- acts.map(&:display_text)
268
+ acts.map(&:to_s)
238
269
  end
239
270
 
240
271
  # List all location names in the friends file.
@@ -243,6 +274,28 @@ module Friends
243
274
  @locations.map(&:name)
244
275
  end
245
276
 
277
+ # @param from [String] one of: ["activities", "friends", nil]
278
+ # If not nil, limits the hashtags returned to only those from either
279
+ # activities or friends.
280
+ # @return [Array] a sorted list of all hashtags in activity descriptions
281
+ def list_hashtags(from:)
282
+ output = Set.new
283
+
284
+ unless from == "friends" # If from is "activities" or nil.
285
+ @activities.each_with_object(output) do |activity, set|
286
+ set.merge(activity.hashtags)
287
+ end
288
+ end
289
+
290
+ unless from == "activities" # If from is "friends" or nil.
291
+ @friends.each_with_object(output) do |friend, set|
292
+ set.merge(friend.hashtags)
293
+ end
294
+ end
295
+
296
+ output.sort_by(&:downcase)
297
+ end
298
+
246
299
  # Find data points for graphing activities over time.
247
300
  # Optionally filter by a friend to see a given relationship over time.
248
301
  #
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module Friends
3
- VERSION = "0.19".freeze
3
+ VERSION = "0.20".freeze
4
4
  end
@@ -95,8 +95,8 @@ describe Friends::Activity do
95
95
  it { subject.description.must_equal description }
96
96
  end
97
97
 
98
- describe "#display_text" do
99
- subject { activity.display_text }
98
+ describe "#to_s" do
99
+ subject { activity.to_s }
100
100
 
101
101
  it do
102
102
  subject.
@@ -456,6 +456,40 @@ describe Friends::Activity do
456
456
  end
457
457
  end
458
458
 
459
+ describe "#hashtags" do
460
+ subject { activity.hashtags }
461
+
462
+ describe "when the activity has no hashtags" do
463
+ let(:activity) { Friends::Activity.new(str: "Enormous ball pit!") }
464
+ it { subject.must_be :empty? }
465
+ end
466
+
467
+ describe "when the activity has hashtags" do
468
+ let(:activity) { Friends::Activity.new(str: "Party! #fun #crazy #fun") }
469
+ it { subject.must_equal Set.new(["#fun", "#crazy"]) }
470
+ end
471
+ end
472
+
473
+ describe "#includes_hashtag?" do
474
+ subject { activity.includes_hashtag?(hashtag: hashtag) }
475
+ let(:activity) { Friends::Activity.new(str: "Enormous ball pit! #fun") }
476
+
477
+ describe "when the given hashtag is not in the activity" do
478
+ let(:hashtag) { "#garbage" }
479
+ it { subject.must_equal false }
480
+ end
481
+
482
+ describe "when the given word is in the activity but not as a hashtag" do
483
+ let(:hashtag) { "#ball" }
484
+ it { subject.must_equal false }
485
+ end
486
+
487
+ describe "when the given hashtag is in the activity" do
488
+ let(:hashtag) { "#fun" }
489
+ it { subject.must_equal true }
490
+ end
491
+ end
492
+
459
493
  describe "#friend_names" do
460
494
  subject { activity.friend_names }
461
495
 
data/test/friend_spec.rb CHANGED
@@ -62,7 +62,7 @@ describe Friends::Friend do
62
62
 
63
63
  describe "when the nickname is present" do
64
64
  let(:friend) do
65
- Friends::Friend.new(name: friend_name, nickname_str: "a.k.a. Jake")
65
+ Friends::Friend.new(name: friend_name, nickname_str: "Jake")
66
66
  end
67
67
 
68
68
  it "removes the nickname" do
@@ -79,6 +79,45 @@ describe Friends::Friend do
79
79
  end
80
80
  end
81
81
 
82
+ describe "#add_hashtag" do
83
+ subject { friend.add_hashtag("#college") }
84
+
85
+ it "adds the nickname" do
86
+ subject
87
+ friend.hashtags.must_include("#college")
88
+ end
89
+
90
+ it "does not keep duplicates" do
91
+ # Add the same nickname twice. Do not use `subject` because it's memoized.
92
+ friend.add_hashtag("#college")
93
+ friend.add_hashtag("#college")
94
+
95
+ friend.hashtags.must_equal ["#college"]
96
+ end
97
+ end
98
+
99
+ describe "#remove_hashtag" do
100
+ subject { friend.remove_hashtag("#school") }
101
+
102
+ describe "when the hashtag is present" do
103
+ let(:friend) do
104
+ Friends::Friend.new(name: friend_name, hashtags_str: "#school #work")
105
+ end
106
+
107
+ it "removes the nickname" do
108
+ friend.instance_variable_get(:@hashtags).must_equal ["#school", "#work"]
109
+ subject
110
+ friend.instance_variable_get(:@hashtags).must_equal ["#work"]
111
+ end
112
+ end
113
+
114
+ describe "when the nickname is not present" do
115
+ it "raises an error if the nickname is not found" do
116
+ proc { subject }.must_raise Friends::FriendsError
117
+ end
118
+ end
119
+ end
120
+
82
121
  describe "#n_activities" do
83
122
  subject { friend.n_activities }
84
123
 
@@ -34,7 +34,11 @@ describe Friends::Introvert do
34
34
  let(:args) { { filename: filename } }
35
35
  let(:introvert) { Friends::Introvert.new(args) }
36
36
  let(:friend_names) { ["George Washington Carver", "Betsy Ross"] }
37
- let(:friends) { friend_names.map { |name| Friends::Friend.new(name: name) } }
37
+ let(:friends) do
38
+ friend_names.map do |name|
39
+ Friends::Friend.new(name: name, hashtags_str: "#test")
40
+ end
41
+ end
38
42
  let(:activities) do
39
43
  [
40
44
  Friends::Activity.new(
@@ -110,20 +114,18 @@ describe Friends::Introvert do
110
114
  end
111
115
 
112
116
  describe "#list_friends" do
113
- subject { introvert.list_friends(location_name: location_name) }
114
-
115
- describe "when no location name has been passed" do
116
- let(:location_name) { nil }
117
-
118
- it "lists the names of friends" do
119
- stub_friends(friends) do
120
- subject.must_equal friend_names
121
- end
122
- end
117
+ subject do
118
+ introvert.list_friends(
119
+ location_name: location_name,
120
+ tagged: tagged,
121
+ verbose: verbose
122
+ )
123
123
  end
124
124
 
125
125
  describe "when a location name has been passed" do
126
126
  let(:location_name) { "Atlantis" }
127
+ let(:tagged) { nil }
128
+ let(:verbose) { false }
127
129
  let(:friends) do
128
130
  [
129
131
  Friends::Friend.new(name: "Mark Watney", location_name: "Mars"),
@@ -133,7 +135,29 @@ describe Friends::Introvert do
133
135
  ]
134
136
  end
135
137
 
136
- it "lists the names of friends" do
138
+ it "lists the names of friends in that location" do
139
+ stub_friends(friends) do
140
+ stub_locations(locations) do
141
+ subject.must_equal ["Aquaman", "Shark-Boy"]
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ describe "when a hashtag has been passed" do
148
+ let(:location_name) { nil }
149
+ let(:tagged) { "#superhero" }
150
+ let(:verbose) { false }
151
+ let(:friends) do
152
+ [
153
+ Friends::Friend.new(name: "Mark Watney"),
154
+ Friends::Friend.new(name: "Aquaman", hashtags_str: "#superhero"),
155
+ Friends::Friend.new(name: "Shark-Boy", hashtags_str: "#superhero"),
156
+ Friends::Friend.new(name: "Ms. Nowhere")
157
+ ]
158
+ end
159
+
160
+ it "lists the names of friends in that location" do
137
161
  stub_friends(friends) do
138
162
  stub_locations(locations) do
139
163
  subject.must_equal ["Aquaman", "Shark-Boy"]
@@ -141,6 +165,32 @@ describe Friends::Introvert do
141
165
  end
142
166
  end
143
167
  end
168
+
169
+ describe "when not verbose" do
170
+ let(:verbose) { false }
171
+ let(:location_name) { nil }
172
+ let(:tagged) { nil }
173
+ it "lists the names of friends" do
174
+ stub_friends(friends) do
175
+ subject.must_equal friend_names
176
+ end
177
+ end
178
+ end
179
+
180
+ describe "when verbose" do
181
+ let(:verbose) { true }
182
+ let(:location_name) { nil }
183
+ let(:tagged) { nil }
184
+ it "lists the names and details of friends" do
185
+ stub_friends(friends) do
186
+ # Just check that there's a difference between the verbose and
187
+ # non-verbose versions of friends (otherwise our test is useless).
188
+ subject.wont_equal friend_names
189
+
190
+ subject.must_equal friends.map(&:to_s)
191
+ end
192
+ end
193
+ end
144
194
  end
145
195
 
146
196
  describe "#add_friend" do
@@ -151,7 +201,9 @@ describe Friends::Introvert do
151
201
  it "adds the given friend" do
152
202
  stub_friends(friends) do
153
203
  subject
154
- introvert.list_friends(location_name: nil).
204
+ introvert.
205
+ instance_variable_get(:@friends).
206
+ map(&:name).
155
207
  must_include new_friend_name
156
208
  end
157
209
  end
@@ -214,17 +266,77 @@ describe Friends::Introvert do
214
266
  end
215
267
  end
216
268
 
269
+ describe "#list_hashtags" do
270
+ subject { introvert.list_hashtags(from: from) }
271
+
272
+ let(:activities) do
273
+ [
274
+ Friends::Activity.new(str: "Lunch in the park. #picnic #food"),
275
+ Friends::Activity.new(str: "Fancy dinner. #food #swanky")
276
+ ]
277
+ end
278
+
279
+ let(:friends) do
280
+ [
281
+ Friends::Friend.new(name: "Grace Hopper", hashtags_str: "#coder #navy"),
282
+ Friends::Friend.new(name: "James Bond", hashtags_str: "#cool #swanky")
283
+ ]
284
+ end
285
+
286
+ describe "when from flag is nil" do
287
+ let(:from) { nil }
288
+ it "lists all hashtags in sorted order" do
289
+ stub_activities(activities) do
290
+ stub_friends(friends) do
291
+ subject.must_equal [
292
+ "#coder",
293
+ "#cool",
294
+ "#food",
295
+ "#navy",
296
+ "#picnic",
297
+ "#swanky"
298
+ ]
299
+ end
300
+ end
301
+ end
302
+ end
303
+
304
+ describe 'when from flag is "activities"' do
305
+ let(:from) { "activities" }
306
+ it "lists all activity hashtags in sorted order" do
307
+ stub_activities(activities) do
308
+ stub_friends(friends) do
309
+ subject.must_equal ["#food", "#picnic", "#swanky"]
310
+ end
311
+ end
312
+ end
313
+ end
314
+
315
+ describe 'when from flag is "friends"' do
316
+ let(:from) { "friends" }
317
+ it "lists all friend hashtags in sorted order" do
318
+ stub_activities(activities) do
319
+ stub_friends(friends) do
320
+ subject.must_equal ["#coder", "#cool", "#navy", "#swanky"]
321
+ end
322
+ end
323
+ end
324
+ end
325
+ end
326
+
217
327
  describe "#list_activities" do
218
328
  subject do
219
329
  introvert.list_activities(
220
330
  limit: limit,
221
331
  with: with,
222
- location_name: location_name
332
+ location_name: location_name,
333
+ tagged: tagged
223
334
  )
224
335
  end
225
336
  let(:limit) { nil }
226
337
  let(:with) { nil }
227
338
  let(:location_name) { nil }
339
+ let(:tagged) { nil }
228
340
 
229
341
  describe "when the limit is lower than the number of activities" do
230
342
  let(:limit) { 1 }
@@ -271,7 +383,7 @@ describe Friends::Introvert do
271
383
 
272
384
  it "lists the activities" do
273
385
  stub_activities(activities) do
274
- subject.must_equal activities.map(&:display_text)
386
+ subject.must_equal activities.map(&:to_s)
275
387
  end
276
388
  end
277
389
  end
@@ -308,7 +420,7 @@ describe Friends::Introvert do
308
420
  stub_friends(friends) do
309
421
  stub_activities(activities) do
310
422
  # Only one activity has that friend.
311
- subject.must_equal activities[0..0].map(&:display_text)
423
+ subject.must_equal activities[0..0].map(&:to_s)
312
424
  end
313
425
  end
314
426
  end
@@ -320,7 +432,7 @@ describe Friends::Introvert do
320
432
 
321
433
  it "lists the activities" do
322
434
  stub_activities(activities) do
323
- subject.must_equal activities.map(&:display_text)
435
+ subject.must_equal activities.map(&:to_s)
324
436
  end
325
437
  end
326
438
  end
@@ -375,13 +487,60 @@ describe Friends::Introvert do
375
487
  stub_locations(locations) do
376
488
  stub_activities(activities) do
377
489
  # Only one activity has that friend.
378
- subject.must_equal activities[0..0].map(&:display_text)
490
+ subject.must_equal activities[0..0].map(&:to_s)
379
491
  end
380
492
  end
381
493
  end
382
494
  end
383
495
  end
384
496
  end
497
+
498
+ describe "when not filtering by a hashtag" do
499
+ let(:tagged) { nil }
500
+
501
+ it "lists the activities" do
502
+ stub_activities(activities) do
503
+ subject.must_equal activities.map(&:to_s)
504
+ end
505
+ end
506
+ end
507
+
508
+ describe "when filtering by a hashtag" do
509
+ let(:activities) do
510
+ [
511
+ Friends::Activity.new(str: "Tennis after work. #exercise #tennis"),
512
+ Friends::Activity.new(str: "Wimbledon! #tennis"),
513
+ Friends::Activity.new(str: "Drinks after work. #beer")
514
+ ]
515
+ end
516
+
517
+ describe "when the hashtag ('#hashtag') is not used at all" do
518
+ let(:tagged) { "#garbage" }
519
+ it "returns no results" do
520
+ stub_activities(activities) do
521
+ subject.must_equal []
522
+ end
523
+ end
524
+ end
525
+
526
+ describe "when the hashtag ('#hashtag') is used once" do
527
+ let(:tagged) { "#beer" }
528
+ it "returns the activity with that hashtag" do
529
+ stub_activities(activities) do
530
+ subject.must_equal [activities.last.to_s]
531
+ end
532
+ end
533
+ end
534
+
535
+ describe "when the hashtag ('#hashtag') is used multiple times" do
536
+ let(:tagged) { "#tennis" }
537
+ it "returns the activities with that hashtag" do
538
+ stub_activities(activities) do
539
+ subject.must_equal activities[0..1].map(&:to_s)
540
+ end
541
+ end
542
+ end
543
+ end
385
544
  end
386
545
 
387
546
  describe "#add_activity" do
@@ -426,24 +585,6 @@ describe Friends::Introvert do
426
585
  end
427
586
  end
428
587
  end
429
-
430
- describe "when given names with leading and trailing spaces" do
431
- let(:new_name) { " David Bowie " }
432
- let(:old_name) { friend_names.last + " " }
433
- subject do
434
- introvert.rename_friend(old_name: old_name, new_name: new_name)
435
- end
436
-
437
- it "correctly strips the spaces" do
438
- stub_friends(friends) do
439
- stub_activities(activities) do
440
- subject
441
- introvert.activities.first.description.must_include "David Bowie"
442
- introvert.activities.last.description.must_include "David Bowie"
443
- end
444
- end
445
- end
446
- end
447
588
  end
448
589
 
449
590
  describe "#rename_location" do
@@ -498,25 +639,6 @@ describe Friends::Introvert do
498
639
  end
499
640
  end
500
641
  end
501
-
502
- describe "when given names with leading and trailing spaces" do
503
- let(:new_name) { " Paris, France " }
504
- let(:old_name) { " Paris " }
505
- subject do
506
- introvert.rename_location(old_name: old_name, new_name: new_name)
507
- end
508
-
509
- it "correctly strips the spaces" do
510
- stub_locations(locations) do
511
- stub_activities(activities) do
512
- subject
513
- introvert.activities.map do |activity|
514
- activity.description.include? new_name
515
- end.must_equal [true, true, false]
516
- end
517
- end
518
- end
519
- end
520
642
  end
521
643
 
522
644
  describe "#set_location" do
@@ -554,8 +676,32 @@ describe Friends::Introvert do
554
676
  end
555
677
 
556
678
  it "returns the modified friend" do
557
- friend = Friends::Friend.new(name: "Jeff",
558
- nickname_str: "a.k.a. The Dude")
679
+ friend = Friends::Friend.new(name: "Jeff", nickname_str: "The Dude")
680
+ stub_friends([friend]) do
681
+ subject.must_equal friend
682
+ end
683
+ end
684
+ end
685
+
686
+ describe "#add_hashtag" do
687
+ subject do
688
+ introvert.add_hashtag(name: friend_names.first, hashtag: "#school")
689
+ end
690
+
691
+ it "returns the modified friend" do
692
+ stub_friends(friends) do
693
+ subject.must_equal friends.first
694
+ end
695
+ end
696
+ end
697
+
698
+ describe "#remove_hashtag" do
699
+ subject do
700
+ introvert.remove_hashtag(name: "Jeff", hashtag: "#school")
701
+ end
702
+
703
+ it "returns the modified friend" do
704
+ friend = Friends::Friend.new(name: "Jeff", hashtags_str: "#school")
559
705
  stub_friends([friend]) do
560
706
  subject.must_equal friend
561
707
  end
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.19'
4
+ version: '0.20'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jacob Evelyn
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-02 00:00:00.000000000 Z
11
+ date: 2016-05-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chronic