friends 0.28 → 0.29

Sign up to get free protection for your applications and to get access to all the features.
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