friends 0.28 → 0.29

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -4
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +2 -0
  5. data/CHANGELOG.md +13 -0
  6. data/README.md +35 -5
  7. data/Rakefile +1 -1
  8. data/bin/friends +7 -379
  9. data/friends.gemspec +3 -1
  10. data/lib/friends/activity.rb +4 -4
  11. data/lib/friends/commands/add.rb +64 -0
  12. data/lib/friends/commands/clean.rb +9 -0
  13. data/lib/friends/commands/edit.rb +12 -0
  14. data/lib/friends/commands/graph.rb +56 -0
  15. data/lib/friends/commands/list.rb +143 -0
  16. data/lib/friends/commands/remove.rb +27 -0
  17. data/lib/friends/commands/rename.rb +30 -0
  18. data/lib/friends/commands/set.rb +14 -0
  19. data/lib/friends/commands/stats.rb +11 -0
  20. data/lib/friends/commands/suggest.rb +20 -0
  21. data/lib/friends/commands/update.rb +29 -0
  22. data/lib/friends/graph.rb +7 -7
  23. data/lib/friends/introvert.rb +23 -18
  24. data/lib/friends/version.rb +1 -1
  25. data/test/commands/add/activity_spec.rb +379 -0
  26. data/test/commands/add/friend_spec.rb +30 -0
  27. data/test/commands/add/location_spec.rb +30 -0
  28. data/test/commands/add/nickname_spec.rb +50 -0
  29. data/test/commands/add/tag_spec.rb +65 -0
  30. data/test/commands/clean_spec.rb +39 -0
  31. data/test/commands/graph_spec.rb +147 -0
  32. data/test/commands/help_spec.rb +45 -0
  33. data/test/commands/list/activities_spec.rb +136 -0
  34. data/test/commands/list/favorite/friends_spec.rb +77 -0
  35. data/test/commands/list/favorite/locations_spec.rb +82 -0
  36. data/test/commands/list/friends_spec.rb +76 -0
  37. data/test/commands/list/locations_spec.rb +35 -0
  38. data/test/commands/list/tags_spec.rb +58 -0
  39. data/test/commands/remove/nickname_spec.rb +63 -0
  40. data/test/commands/remove/tag_spec.rb +64 -0
  41. data/test/commands/rename/friend_spec.rb +55 -0
  42. data/test/commands/rename/location_spec.rb +43 -0
  43. data/test/commands/set/location_spec.rb +54 -0
  44. data/test/commands/stats_spec.rb +41 -0
  45. data/test/commands/suggest_spec.rb +86 -0
  46. data/test/commands/update_spec.rb +13 -0
  47. data/test/helper.rb +114 -0
  48. metadata +89 -15
  49. data/test/activity_spec.rb +0 -597
  50. data/test/friend_spec.rb +0 -241
  51. data/test/graph_spec.rb +0 -92
  52. data/test/introvert_spec.rb +0 -969
  53. data/test/location_spec.rb +0 -60
@@ -1,597 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "./test/helper"
4
-
5
- describe Friends::Activity do
6
- let(:date) { Date.today - 1 }
7
- let(:date_s) { date.to_s }
8
- let(:friend1) { Friends::Friend.new(name: "Elizabeth Cady Stanton") }
9
- let(:friend2) { Friends::Friend.new(name: "John Cage") }
10
- let(:description) do
11
- "Lunch with **#{friend1.name}** and **#{friend2.name}** on _The Moon_ "\
12
- "after hanging out in _Atlantis_."
13
- end
14
- let(:partition) { Friends::Activity::DATE_PARTITION }
15
- let(:activity) do
16
- Friends::Activity.new(str: "#{date_s}#{partition}#{description}")
17
- end
18
-
19
- describe ".deserialize" do
20
- subject { Friends::Activity.deserialize(serialized_str) }
21
-
22
- describe "when serialized string is empty" do
23
- let(:serialized_str) { "" }
24
-
25
- it "defaults date to today and sets no description" do
26
- today = Date.today - 7
27
-
28
- # We stub out Date.today to guarantee that it is always the same even
29
- # when the date changes in the middle of the test's execution. To ensure
30
- # this technique actually works, we move our reference time backward by
31
- # a week.
32
- Date.stub(:today, today) do
33
- new_activity = subject
34
- new_activity.date.must_equal today
35
- new_activity.description.must_equal ""
36
- end
37
- end
38
- end
39
-
40
- describe "when string is well-formed" do
41
- let(:serialized_str) { "#{date_s}: #{description}" }
42
-
43
- it "creates an activity with the correct date and description" do
44
- new_activity = subject
45
- new_activity.date.must_equal date
46
- new_activity.description.must_equal description
47
- end
48
- end
49
-
50
- describe "when date is written in natural language" do
51
- let(:serialized_str) { "Yesterday: #{description}" }
52
-
53
- it "creates an activity with the correct date and description" do
54
- now = Time.now + 604800
55
-
56
- # Chronic uses Time.now for parsing, so we stub this to prevent racy
57
- # behavior when the date changes in the middle of test execution. To
58
- # ensure this technique actually works, we move our reference time
59
- # backward by a week.
60
- Time.stub(:now, now) do
61
- new_activity = subject
62
- new_activity.date.must_equal (now.to_date - 1)
63
- new_activity.description.must_equal description
64
- end
65
- end
66
- end
67
-
68
- describe "when no date is present" do
69
- let(:serialized_str) { description }
70
-
71
- it "defaults to today" do
72
- today = Date.today - 7
73
-
74
- # We stub out Date.today to guarantee that it is always the same even
75
- # when the date changes in the middle of the test's execution. To ensure
76
- # this technique actually works, we move our reference time backward by
77
- # a week.
78
- Date.stub(:today, today) { subject.date.must_equal today }
79
- end
80
- end
81
-
82
- describe "when no description is present" do
83
- let(:serialized_str) { date_s }
84
-
85
- it "leaves description blank" do
86
- new_activity = subject
87
- new_activity.date.must_equal date
88
- new_activity.description.must_equal ""
89
- end
90
- end
91
- end
92
-
93
- describe "#new" do
94
- subject { activity }
95
-
96
- it { subject.date.must_equal date }
97
- it { subject.description.must_equal description }
98
- end
99
-
100
- describe "#to_s" do
101
- subject { activity.to_s }
102
-
103
- it do
104
- subject.
105
- must_equal "#{Paint[date_s, :bold]}: "\
106
- "Lunch with #{Paint[friend1.name, :bold, :magenta]} and "\
107
- "#{Paint[friend2.name, :bold, :magenta]} on "\
108
- "#{Paint['The Moon', :bold, :yellow]} after hanging out in "\
109
- "#{Paint['Atlantis', :bold, :yellow]}."
110
- end
111
- end
112
-
113
- describe "#serialize" do
114
- subject { activity.serialize }
115
-
116
- it do
117
- subject.
118
- must_equal "#{Friends::Activity::SERIALIZATION_PREFIX}#{date_s}: "\
119
- "#{description}"
120
- end
121
- end
122
-
123
- describe "#highlight_description" do
124
- # Add helpers to set internal states for friends and activities.
125
- def stub_friends(val)
126
- old_val = introvert.instance_variable_get(:@friends)
127
- introvert.instance_variable_set(:@friends, val)
128
- introvert.send(:set_n_activities!, :friend)
129
- yield
130
- introvert.instance_variable_set(:@friends, old_val)
131
- end
132
-
133
- def stub_activities(val)
134
- old_val = introvert.instance_variable_get(:@activities)
135
- introvert.instance_variable_set(:@activities, val)
136
- introvert.send(:set_n_activities!, :friend)
137
- introvert.send(:set_n_activities!, :location)
138
- yield
139
- introvert.instance_variable_set(:@activities, old_val)
140
- end
141
-
142
- def stub_locations(val)
143
- old_val = introvert.instance_variable_get(:@locations)
144
- introvert.instance_variable_set(:@locations, val)
145
- introvert.send(:set_n_activities!, :location)
146
- yield
147
- introvert.instance_variable_set(:@locations, old_val)
148
- end
149
-
150
- let(:locations) do
151
- [
152
- Friends::Location.new(name: "Atlantis"),
153
- Friends::Location.new(name: "The Moon")
154
- ]
155
- end
156
- let(:friends) { [friend1, friend2] }
157
- let(:introvert) { Friends::Introvert.new }
158
- subject do
159
- stub_friends(friends) do
160
- stub_locations(locations) do
161
- activity.highlight_description(introvert: introvert)
162
- end
163
- end
164
- end
165
-
166
- it "finds all friends and locations" do
167
- subject
168
- activity.description.must_equal "Lunch with **#{friend1.name}** and "\
169
- "**#{friend2.name}** on _The Moon_ "\
170
- "after hanging out in _Atlantis_."
171
- end
172
-
173
- describe "when description has first names" do
174
- let(:description) { "Lunch with Elizabeth and John." }
175
- it "matches friends" do
176
- subject
177
- activity.description.
178
- must_equal "Lunch with **#{friend1.name}** and **#{friend2.name}**."
179
- end
180
- end
181
-
182
- describe "when description has nicknames" do
183
- let(:description) { "Lunch with Lizzy and Johnny." }
184
- it "matches friends" do
185
- friend1.add_nickname("Lizzy")
186
- friend2.add_nickname("Johnny")
187
- subject
188
- activity.description.
189
- must_equal "Lunch with **#{friend1.name}** and **#{friend2.name}**."
190
- end
191
- end
192
-
193
- describe "when discription has nicknames which contain first names" do
194
- let(:nickname1) { "Awesome #{friend1.name}" }
195
- let(:nickname2) { "Long #{friend2.name} Silver" }
196
- let(:description) { "Lunch with #{nickname1} and #{nickname2}." }
197
- it "matches friends" do
198
- friend1.add_nickname(nickname1)
199
- friend2.add_nickname(nickname2)
200
- subject
201
- activity.description.
202
- must_equal "Lunch with **#{friend1.name}** and **#{friend2.name}**."
203
- end
204
- end
205
-
206
- describe 'when description ends with "<first name> <last name initial>"' do
207
- let(:description) { "Lunch with John C" }
208
- it "matches the friend" do
209
- subject
210
- activity.description.
211
- must_equal "Lunch with **#{friend2.name}**"
212
- end
213
- end
214
-
215
- describe 'when description ends with "<first name> <last name initial>".' do
216
- let(:description) { "Lunch with John C." }
217
- it "matches the friend and keeps the period" do
218
- subject
219
- activity.description.
220
- must_equal "Lunch with **#{friend2.name}**."
221
- end
222
- end
223
-
224
- describe "when description has \"<first name> <last name initial>\" in "\
225
- "the middle of a sentence" do
226
- let(:description) { "Lunch with John C in the park." }
227
- it "matches the friend" do
228
- subject
229
- activity.description.
230
- must_equal "Lunch with **#{friend2.name}** in the park."
231
- end
232
- end
233
-
234
- describe "when description has \"<first name> <last name initial>.\" in "\
235
- "the middle of a sentence" do
236
- let(:description) { "Lunch with John C. in the park." }
237
- it "matches the friend and swallows the period" do
238
- subject
239
- activity.description.
240
- must_equal "Lunch with **#{friend2.name}** in the park."
241
- end
242
- end
243
-
244
- describe "when description has \"<first name> <last name initial>\". at "\
245
- "the end of a sentence" do
246
- let(:description) { "Lunch with John C. It was great!" }
247
- it "matches the friend and keeps the period" do
248
- subject
249
- activity.description.
250
- must_equal "Lunch with **#{friend2.name}**. It was great!"
251
- end
252
- end
253
-
254
- describe "when names are not entered case-sensitively" do
255
- let(:description) { "Lunch with elizabeth cady stanton." }
256
- it "matches friends" do
257
- subject
258
- activity.description.must_equal "Lunch with **Elizabeth Cady Stanton**."
259
- end
260
- end
261
-
262
- describe "when name is at beginning of word" do
263
- let(:description) { "Field trip to the Johnson Co." }
264
- it "does not match a friend" do
265
- subject
266
- # No match found.
267
- activity.description.must_equal "Field trip to the Johnson Co."
268
- end
269
- end
270
-
271
- describe "when name is in middle of word" do
272
- let(:description) { "Field trip to the JimJohnJames Co." }
273
- it "does not match a friend" do
274
- subject
275
- # No match found.
276
- activity.description.must_equal "Field trip to the JimJohnJames Co."
277
- end
278
- end
279
-
280
- describe "when one name ends another after a hyphen" do
281
- let(:friend1) { Friends::Friend.new(name: "Mary-Kate Olsen") }
282
- let(:friend2) { Friends::Friend.new(name: "Kate Winslet") }
283
- let(:description) { "Shopping with Mary-Kate." }
284
-
285
- it "gives precedence to the larger name" do
286
- # Make sure "Kate" is a closer friend than "Mary-Kate" so we know our
287
- # test result isn't due to chance.
288
- friend1.n_activities = 0
289
- friend2.n_activities = 10
290
-
291
- subject
292
- activity.description.must_equal "Shopping with **Mary-Kate Olsen**."
293
- end
294
- end
295
-
296
- describe "when one name preceeds another before a hyphen" do
297
- let(:friend1) { Friends::Friend.new(name: "Mary-Kate Olsen") }
298
- let(:friend2) { Friends::Friend.new(name: "Mary Poppins") }
299
- let(:description) { "Shopping with Mary-Kate." }
300
-
301
- it "gives precedence to the larger name" do
302
- # Make sure "Kate" is a closer friend than "Mary-Kate" so we know our
303
- # test result isn't due to chance.
304
- friend1.n_activities = 0
305
- friend2.n_activities = 10
306
-
307
- subject
308
- activity.description.must_equal "Shopping with **Mary-Kate Olsen**."
309
- end
310
- end
311
-
312
- describe "when one name is contained within another via a hyphen" do
313
- let(:friend1) { Friends::Friend.new(name: "Mary-Jo-Kate Olsen") }
314
- let(:friend2) { Friends::Friend.new(name: "Jo Stafford") }
315
- let(:description) { "Shopping with Mary-Jo-Kate." }
316
-
317
- it "gives precedence to the larger name" do
318
- # Make sure "Kate" is a closer friend than "Mary-Kate" so we know our
319
- # test result isn't due to chance.
320
- friend1.n_activities = 0
321
- friend2.n_activities = 10
322
-
323
- subject
324
- activity.description.must_equal "Shopping with **Mary-Jo-Kate Olsen**."
325
- end
326
- end
327
-
328
- describe "when name is at end of word" do
329
- let(:description) { "Field trip to the JimJohn Co." }
330
- it "does not match a friend" do
331
- subject
332
- # No match found.
333
- activity.description.must_equal "Field trip to the JimJohn Co."
334
- end
335
- end
336
-
337
- describe "when name is escaped with a backslash" do
338
- # We have to use two backslashes here because that's how Ruby encodes one.
339
- let(:description) { "Dinner with \\Elizabeth Cady Stanton." }
340
- it "does not match a friend and removes the backslash" do
341
- subject
342
- # No match found.
343
- activity.description.must_equal "Dinner with Elizabeth Cady Stanton."
344
- end
345
- end
346
-
347
- describe "when name has leading asterisks" do
348
- let(:description) { "Dinner with **Elizabeth Cady Stanton." }
349
- it "does not match a friend" do
350
- subject
351
- # No match found.
352
- activity.description.must_equal "Dinner with **Elizabeth Cady Stanton."
353
- end
354
- end
355
-
356
- describe "when name has ending asterisks" do
357
- let(:description) { "Dinner with Elizabeth**." }
358
- it "does not match a friend" do
359
- subject
360
-
361
- # Note: for now we can't guarantee that "Elizabeth Cady Stanton**" won't
362
- # match, because the Elizabeth isn't surrounded by asterisks.
363
- activity.description.must_equal "Dinner with Elizabeth**."
364
- end
365
- end
366
-
367
- describe "when a friend's name is mentioned multiple times" do
368
- let(:description) { "Dinner with Elizabeth. Elizabeth made us pasta." }
369
- it "highlights all occurrences of the friend's name" do
370
- subject
371
- activity.description.
372
- must_equal "Dinner with **Elizabeth Cady Stanton**."\
373
- " **Elizabeth Cady Stanton** made us pasta."
374
- end
375
- end
376
-
377
- describe "when there are multiple matches" do
378
- describe "when there is context from past activities" do
379
- let(:description) { "Dinner with Elizabeth and John." }
380
- let(:friends) do
381
- [
382
- friend1,
383
- friend2,
384
- Friends::Friend.new(name: "Elizabeth II")
385
- ]
386
- end
387
-
388
- it "chooses a match based on the context" do
389
- # Create a past activity in which Elizabeth Cady Stanton did something
390
- # with John Cage. Then, create past activities to make Elizabeth II a
391
- # better friend than Elizabeth Cady Stanton.
392
- old_activities = [
393
- Friends::Activity.new(
394
- str: "#{date_s}#{partition}Picnic with "\
395
- "**Elizabeth Cady Stanton** and **John Cage**."
396
- ),
397
- Friends::Activity.new(
398
- str: "#{date_s}#{partition}Got lunch with **Elizabeth II**."
399
- ),
400
- Friends::Activity.new(
401
- str: "#{date_s}#{partition}Ice skated with **Elizabeth II**."
402
- )
403
- ]
404
-
405
- # Elizabeth II is the better friend, but historical activities have
406
- # had Elizabeth Cady Stanton and John Cage together. Thus, we should
407
- # interpret "Elizabeth" as Elizabeth Cady Stanton.
408
- stub_activities(old_activities) { subject }
409
-
410
- activity.description.
411
- must_equal "Dinner with **Elizabeth Cady Stanton** and "\
412
- "**John Cage**."
413
- end
414
- end
415
-
416
- describe "when there is no context from past activities" do
417
- let(:description) { "Dinner with Elizabeth." }
418
-
419
- it "falls back to choosing the better friend" do
420
- friend2.name = "Elizabeth II"
421
-
422
- # Give a past activity to Elizabeth II.
423
- old_activity = Friends::Activity.new(
424
- str: "#{date_s}#{partition}Do something with **Elizabeth II**."
425
- )
426
-
427
- stub_activities([old_activity]) { subject }
428
-
429
- # Pick the friend with more activities.
430
- activity.description.must_equal "Dinner with **Elizabeth II**."
431
- end
432
- end
433
- end
434
- end
435
-
436
- describe "#includes_location?" do
437
- subject { activity.includes_location?(loc) }
438
- let(:loc) { Friends::Location.new(name: "Atlantis") }
439
-
440
- describe "when the given location is in the activity" do
441
- let(:activity) { Friends::Activity.new(str: "Explored _#{loc.name}_") }
442
- it { subject.must_equal true }
443
- end
444
-
445
- describe "when the given location is not in the activity" do
446
- let(:activity) { Friends::Activity.new(str: "Explored _Elsewhere_") }
447
- it { subject.must_equal false }
448
- end
449
- end
450
-
451
- describe "#includes_friend?" do
452
- subject { activity.includes_friend?(friend) }
453
-
454
- describe "when the given friend is in the activity" do
455
- let(:friend) { friend1 }
456
- it { subject.must_equal true }
457
- end
458
-
459
- describe "when the given friend is not in the activity" do
460
- let(:friend) { Friends::Friend.new(name: "Claude Debussy") }
461
- it { subject.must_equal false }
462
- end
463
- end
464
-
465
- describe "#tags" do
466
- subject { activity.tags }
467
-
468
- describe "when the activity has no tags" do
469
- let(:activity) { Friends::Activity.new(str: "Enormous ball pit!") }
470
- it { subject.must_be :empty? }
471
- end
472
-
473
- describe "when the activity has tags" do
474
- let(:activity) { Friends::Activity.new(str: "Party! @fun @crazy @fun") }
475
- it { subject.must_equal Set.new(["@fun", "@crazy"]) }
476
- end
477
- end
478
-
479
- describe "#includes_tag?" do
480
- subject { activity.includes_tag?(tag) }
481
- let(:activity) { Friends::Activity.new(str: "Enormous ball pit! @fun") }
482
-
483
- describe "when the given tag is not in the activity" do
484
- let(:tag) { "@garbage" }
485
- it { subject.must_equal false }
486
- end
487
-
488
- describe "when the given word is in the activity but not as a tag" do
489
- let(:tag) { "@ball" }
490
- it { subject.must_equal false }
491
- end
492
-
493
- describe "when the given tag is in the activity" do
494
- let(:tag) { "@fun" }
495
- it { subject.must_equal true }
496
- end
497
- end
498
-
499
- describe "#friend_names" do
500
- subject { activity.friend_names }
501
-
502
- it "returns a list of friend names" do
503
- names = subject
504
-
505
- # We don't assert that the output must be in a specific order because we
506
- # don't care about the order and it is subject to change.
507
- names.size.must_equal 2
508
- names.must_include "Elizabeth Cady Stanton"
509
- names.must_include "John Cage"
510
- end
511
-
512
- describe "when a friend is mentioned more than once" do
513
- let(:description) { "Lunch with **John Cage**. **John Cage** can eat!" }
514
-
515
- it "removes duplicate names" do
516
- subject.must_equal ["John Cage"]
517
- end
518
- end
519
- end
520
-
521
- describe "#<=>" do
522
- it "sorts by reverse-date" do
523
- past_act = Friends::Activity.new(str: "Yesterday: Dummy")
524
- future_act = Friends::Activity.new(str: "Tomorrow: Dummy")
525
- [past_act, future_act].sort.must_equal [future_act, past_act]
526
- end
527
- end
528
-
529
- describe "#update_friend_name" do
530
- let(:description) { "Lunch with **John Candy**." }
531
- subject do
532
- activity.update_friend_name(
533
- old_name: "John Candy",
534
- new_name: "John Cleese"
535
- )
536
- end
537
-
538
- it "renames the given friend in the description" do
539
- subject.must_equal "Lunch with **John Cleese**."
540
- end
541
-
542
- describe "when the description contains a fragment of the old name" do
543
- let(:description) { "Lunch with **John Candy** at Johnny's Diner." }
544
-
545
- it "only replaces the name" do
546
- subject.must_equal "Lunch with **John Cleese** at Johnny's Diner."
547
- end
548
- end
549
-
550
- describe "when the description contains the complete old name" do
551
- let(:description) { "Coffee with **John** at John's Studio." }
552
- subject do
553
- activity.update_friend_name(old_name: "John", new_name: "Joe")
554
- end
555
-
556
- it "only replaces the actual name" do
557
- subject.must_equal "Coffee with **Joe** at John's Studio."
558
- end
559
- end
560
- end
561
-
562
- describe "#update_location_name" do
563
- let(:description) { "Lunch in _Paris_." }
564
- subject do
565
- activity.update_location_name(
566
- old_name: "Paris",
567
- new_name: "Paris, France"
568
- )
569
- end
570
-
571
- it "renames the given friend in the description" do
572
- subject.must_equal "Lunch in _Paris, France_."
573
- end
574
-
575
- describe "when the description contains a fragment of the old name" do
576
- let(:description) { "Lunch in _Paris_ at the Parisian Café." }
577
-
578
- it "only replaces the name" do
579
- subject.must_equal "Lunch in _Paris, France_ at the Parisian Café."
580
- end
581
- end
582
-
583
- describe "when the description contains the complete old name" do
584
- let(:description) { "Lunch in _Paris_ at The Paris Café." }
585
- subject do
586
- activity.update_location_name(
587
- old_name: "Paris",
588
- new_name: "Paris, France"
589
- )
590
- end
591
-
592
- it "only replaces the actual name" do
593
- subject.must_equal "Lunch in _Paris, France_ at The Paris Café."
594
- end
595
- end
596
- end
597
- end