friends 0.34 → 0.35
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 +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +79 -20
- data/RELEASING.md +7 -6
- data/friends.md +4 -0
- data/lib/friends/activity.rb +4 -291
- data/lib/friends/commands/add.rb +15 -13
- data/lib/friends/commands/list.rb +51 -47
- data/lib/friends/commands/stats.rb +3 -0
- data/lib/friends/event.rb +298 -0
- data/lib/friends/introvert.rb +109 -57
- data/lib/friends/note.rb +19 -0
- data/lib/friends/version.rb +1 -1
- data/test/add_event_helper.rb +410 -0
- data/test/commands/add/activity_spec.rb +6 -350
- data/test/commands/add/note_spec.rb +55 -0
- data/test/commands/clean_spec.rb +25 -3
- data/test/commands/list/notes_spec.rb +179 -0
- data/test/commands/list/tags_spec.rb +24 -0
- data/test/commands/stats_spec.rb +100 -7
- data/test/helper.rb +12 -0
- metadata +10 -2
data/lib/friends/introvert.rb
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
require "set"
|
|
8
8
|
|
|
9
9
|
require "friends/activity"
|
|
10
|
+
require "friends/note"
|
|
10
11
|
require "friends/friend"
|
|
11
12
|
require "friends/graph"
|
|
12
13
|
require "friends/location"
|
|
@@ -16,6 +17,7 @@ module Friends
|
|
|
16
17
|
class Introvert
|
|
17
18
|
DEFAULT_FILENAME = "./friends.md".freeze
|
|
18
19
|
ACTIVITIES_HEADER = "### Activities:".freeze
|
|
20
|
+
NOTES_HEADER = "### Notes:".freeze
|
|
19
21
|
FRIENDS_HEADER = "### Friends:".freeze
|
|
20
22
|
LOCATIONS_HEADER = "### Locations:".freeze
|
|
21
23
|
|
|
@@ -33,7 +35,10 @@ module Friends
|
|
|
33
35
|
File.open(@filename, "w") do |file|
|
|
34
36
|
file.puts(ACTIVITIES_HEADER)
|
|
35
37
|
stable_sort(@activities).each { |act| file.puts(act.serialize) }
|
|
36
|
-
file.puts # Blank line separating activities from
|
|
38
|
+
file.puts # Blank line separating activities from notes.
|
|
39
|
+
file.puts(NOTES_HEADER)
|
|
40
|
+
stable_sort(@notes).each { |note| file.puts(note.serialize) }
|
|
41
|
+
file.puts # Blank line separating notes from friends.
|
|
37
42
|
file.puts(FRIENDS_HEADER)
|
|
38
43
|
@friends.sort.each { |friend| file.puts(friend.serialize) }
|
|
39
44
|
file.puts # Blank line separating friends from locations.
|
|
@@ -64,12 +69,20 @@ module Friends
|
|
|
64
69
|
# @param serialization [String] the serialized activity
|
|
65
70
|
# @return [Activity] the added activity
|
|
66
71
|
def add_activity(serialization:)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
Activity.deserialize(serialization).tap do |activity|
|
|
73
|
+
activity.highlight_description(introvert: self) if activity.description
|
|
74
|
+
@activities.unshift(activity)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
# Add a note.
|
|
79
|
+
# @param serialization [String] the serialized note
|
|
80
|
+
# @return [Note] the added note
|
|
81
|
+
def add_note(serialization:)
|
|
82
|
+
Note.deserialize(serialization).tap do |note|
|
|
83
|
+
note.highlight_description(introvert: self) if note.description
|
|
84
|
+
@notes.unshift(note)
|
|
85
|
+
end
|
|
73
86
|
end
|
|
74
87
|
|
|
75
88
|
# Add a location.
|
|
@@ -226,36 +239,16 @@ module Friends
|
|
|
226
239
|
list_favorite_things(:location, limit: limit)
|
|
227
240
|
end
|
|
228
241
|
|
|
229
|
-
#
|
|
230
|
-
# @
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
# @param location_name [String] the name of a location to filter by, or
|
|
235
|
-
# nil for unfiltered
|
|
236
|
-
# @param tagged [Array<String>] the names of tags to filter by, or empty for
|
|
237
|
-
# unfiltered
|
|
238
|
-
# @param since_date [Date] a date on or after which to find activities, or nil for unfiltered
|
|
239
|
-
# @param until_date [Date] a date before or on which to find activities, or nil for unfiltered
|
|
240
|
-
# @return [Array] a list of all activity text values
|
|
241
|
-
# @raise [ArgumentError] if limit is present but limit < 1
|
|
242
|
-
# @raise [FriendsError] if friend, location or tag cannot be found or
|
|
243
|
-
# is ambiguous
|
|
244
|
-
def list_activities(limit:, with:, location_name:, tagged:, since_date:, until_date:)
|
|
245
|
-
raise ArgumentError, "Limit must be positive" if limit && limit < 1
|
|
246
|
-
|
|
247
|
-
acts = filtered_activities(
|
|
248
|
-
with: with,
|
|
249
|
-
location_name: location_name,
|
|
250
|
-
tagged: tagged,
|
|
251
|
-
since_date: since_date,
|
|
252
|
-
until_date: until_date
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
# If we need to, trim the list.
|
|
256
|
-
acts = acts.take(limit) unless limit.nil?
|
|
242
|
+
# See `list_events` for all of the parameters we can pass.
|
|
243
|
+
# @return [Array] a list of all activities' text values
|
|
244
|
+
def list_activities(**args)
|
|
245
|
+
list_events(events: @activities, **args)
|
|
246
|
+
end
|
|
257
247
|
|
|
258
|
-
|
|
248
|
+
# See `list_events` for all of the parameters we can pass.
|
|
249
|
+
# @return [Array] a list of all notes' text values
|
|
250
|
+
def list_notes(**args)
|
|
251
|
+
list_events(events: @notes, **args)
|
|
259
252
|
end
|
|
260
253
|
|
|
261
254
|
# List all location names in the friends file.
|
|
@@ -264,20 +257,26 @@ module Friends
|
|
|
264
257
|
@locations.map(&:name)
|
|
265
258
|
end
|
|
266
259
|
|
|
267
|
-
# @param from [
|
|
268
|
-
# If not
|
|
269
|
-
# activities or friends.
|
|
260
|
+
# @param from [Array] containing any of: ["activities", "friends", "notes"]
|
|
261
|
+
# If not empty, limits the tags returned to only those from either
|
|
262
|
+
# activities, notes, or friends.
|
|
270
263
|
# @return [Array] a sorted list of all tags in activity descriptions
|
|
271
264
|
def list_tags(from:)
|
|
272
265
|
output = Set.new
|
|
273
266
|
|
|
274
|
-
|
|
267
|
+
if from.empty? || from.include?("activities")
|
|
275
268
|
@activities.each_with_object(output) do |activity, set|
|
|
276
269
|
set.merge(activity.tags)
|
|
277
270
|
end
|
|
278
271
|
end
|
|
279
272
|
|
|
280
|
-
|
|
273
|
+
if from.empty? || from.include?("notes")
|
|
274
|
+
@notes.each_with_object(output) do |note, set|
|
|
275
|
+
set.merge(note.tags)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
if from.empty? || from.include?("friends")
|
|
281
280
|
@friends.each_with_object(output) do |friend, set|
|
|
282
281
|
set.merge(friend.tags)
|
|
283
282
|
end
|
|
@@ -310,7 +309,8 @@ module Friends
|
|
|
310
309
|
# @raise [FriendsError] if friend, location or tag cannot be found or
|
|
311
310
|
# is ambiguous
|
|
312
311
|
def graph(with:, location_name:, tagged:, since_date:, until_date:)
|
|
313
|
-
filtered_activities_to_graph =
|
|
312
|
+
filtered_activities_to_graph = filtered_events(
|
|
313
|
+
events: @activities,
|
|
314
314
|
with: with,
|
|
315
315
|
location_name: location_name,
|
|
316
316
|
tagged: tagged,
|
|
@@ -324,7 +324,8 @@ module Friends
|
|
|
324
324
|
# activities might not include others in the full range (for instance,
|
|
325
325
|
# if only one filtered activity matches a query, we don't want to only
|
|
326
326
|
# show unfiltered activities that occurred on that specific day).
|
|
327
|
-
all_activities_to_graph =
|
|
327
|
+
all_activities_to_graph = filtered_events(
|
|
328
|
+
events: @activities,
|
|
328
329
|
with: [],
|
|
329
330
|
location_name: nil,
|
|
330
331
|
tagged: [],
|
|
@@ -461,16 +462,66 @@ module Friends
|
|
|
461
462
|
@activities.size
|
|
462
463
|
end
|
|
463
464
|
|
|
465
|
+
# @return [Integer] the total number of locations
|
|
466
|
+
def total_locations
|
|
467
|
+
@locations.size
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# @return [Integer] the total number of notes
|
|
471
|
+
def total_notes
|
|
472
|
+
@notes.size
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# @return [Integer] the total number of tags
|
|
476
|
+
def total_tags
|
|
477
|
+
list_tags(from: []).size
|
|
478
|
+
end
|
|
479
|
+
|
|
464
480
|
# @return [Integer] the number of days elapsed between
|
|
465
481
|
# the first and last activity
|
|
466
482
|
def elapsed_days
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
483
|
+
events = @activities + @notes
|
|
484
|
+
return 0 if events.size < 2
|
|
485
|
+
sorted_events = events.sort
|
|
486
|
+
(sorted_events.first.date - sorted_events.last.date).to_i
|
|
470
487
|
end
|
|
471
488
|
|
|
472
489
|
private
|
|
473
490
|
|
|
491
|
+
# List all event details.
|
|
492
|
+
# @param events [Array<Event>] the base events to list, either @activities or @notes
|
|
493
|
+
# @param limit [Integer] the number of events to return, or nil for no
|
|
494
|
+
# limit
|
|
495
|
+
# @param with [Array<String>] the names of friends to filter by, or empty for
|
|
496
|
+
# unfiltered
|
|
497
|
+
# @param location_name [String] the name of a location to filter by, or
|
|
498
|
+
# nil for unfiltered
|
|
499
|
+
# @param tagged [Array<String>] the names of tags to filter by, or empty for
|
|
500
|
+
# unfiltered
|
|
501
|
+
# @param since_date [Date] a date on or after which to find events, or nil for unfiltered
|
|
502
|
+
# @param until_date [Date] a date before or on which to find events, or nil for unfiltered
|
|
503
|
+
# @return [Array] a list of all event (activity or note) text values
|
|
504
|
+
# @raise [ArgumentError] if limit is present but limit < 1
|
|
505
|
+
# @raise [FriendsError] if friend, location or tag cannot be found or
|
|
506
|
+
# is ambiguous
|
|
507
|
+
def list_events(events:, limit:, with:, location_name:, tagged:, since_date:, until_date:)
|
|
508
|
+
raise ArgumentError, "Limit must be positive" if limit && limit < 1
|
|
509
|
+
|
|
510
|
+
events = filtered_events(
|
|
511
|
+
events: events,
|
|
512
|
+
with: with,
|
|
513
|
+
location_name: location_name,
|
|
514
|
+
tagged: tagged,
|
|
515
|
+
since_date: since_date,
|
|
516
|
+
until_date: until_date
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
# If we need to, trim the list.
|
|
520
|
+
events = events.take(limit) unless limit.nil?
|
|
521
|
+
|
|
522
|
+
events.map(&:to_s)
|
|
523
|
+
end
|
|
524
|
+
|
|
474
525
|
# @param arr [Array] an unsorted array
|
|
475
526
|
# @return [Array] a stably-sorted array
|
|
476
527
|
def stable_sort(arr)
|
|
@@ -478,6 +529,7 @@ module Friends
|
|
|
478
529
|
end
|
|
479
530
|
|
|
480
531
|
# Filter activities by friend, location and tag
|
|
532
|
+
# @param events [Array<Event>] the base events to list, either @activities or @notes
|
|
481
533
|
# @param with [Array<String>] the names of friends to filter by, or empty for
|
|
482
534
|
# unfiltered
|
|
483
535
|
# @param location_name [String] the name of a location to filter by, or
|
|
@@ -489,35 +541,33 @@ module Friends
|
|
|
489
541
|
# @return [Array] an array of activities
|
|
490
542
|
# @raise [FriendsError] if friend, location or tag cannot be found or
|
|
491
543
|
# is ambiguous
|
|
492
|
-
def
|
|
493
|
-
acts = @activities
|
|
494
|
-
|
|
544
|
+
def filtered_events(events:, with:, location_name:, tagged:, since_date:, until_date:)
|
|
495
545
|
# Filter by friend name if argument is passed.
|
|
496
546
|
unless with.empty?
|
|
497
547
|
friends = with.map { |name| thing_with_name_in(:friend, name) }
|
|
498
|
-
|
|
499
|
-
friends.all? { |friend|
|
|
548
|
+
events = events.select do |event|
|
|
549
|
+
friends.all? { |friend| event.includes_friend?(friend) }
|
|
500
550
|
end
|
|
501
551
|
end
|
|
502
552
|
|
|
503
553
|
# Filter by location name if argument is passed.
|
|
504
554
|
unless location_name.nil?
|
|
505
555
|
location = thing_with_name_in(:location, location_name)
|
|
506
|
-
|
|
556
|
+
events = events.select { |event| event.includes_location?(location) }
|
|
507
557
|
end
|
|
508
558
|
|
|
509
559
|
# Filter by tag if argument is passed.
|
|
510
560
|
unless tagged.empty?
|
|
511
|
-
|
|
512
|
-
tagged.all? { |tag|
|
|
561
|
+
events = events.select do |event|
|
|
562
|
+
tagged.all? { |tag| event.includes_tag?(tag) }
|
|
513
563
|
end
|
|
514
564
|
end
|
|
515
565
|
|
|
516
566
|
# Filter by date if arguments are passed.
|
|
517
|
-
|
|
518
|
-
|
|
567
|
+
events = events.select { |event| event.date >= since_date } unless since_date.nil?
|
|
568
|
+
events = events.select { |event| event.date <= until_date } unless until_date.nil?
|
|
519
569
|
|
|
520
|
-
|
|
570
|
+
events
|
|
521
571
|
end
|
|
522
572
|
|
|
523
573
|
# @param type [Symbol] one of: [:friend, :location]
|
|
@@ -612,6 +662,7 @@ module Friends
|
|
|
612
662
|
def read_file
|
|
613
663
|
@friends = []
|
|
614
664
|
@activities = []
|
|
665
|
+
@notes = []
|
|
615
666
|
@locations = []
|
|
616
667
|
|
|
617
668
|
return unless File.exist?(@filename)
|
|
@@ -650,7 +701,7 @@ module Friends
|
|
|
650
701
|
end
|
|
651
702
|
|
|
652
703
|
# If we made it here, we couldn't recognize a header.
|
|
653
|
-
bad_line("
|
|
704
|
+
bad_line("a valid header", line_num)
|
|
654
705
|
end
|
|
655
706
|
|
|
656
707
|
# If we made it this far, we're parsing objects in a class.
|
|
@@ -670,6 +721,7 @@ module Friends
|
|
|
670
721
|
ParsingStage = Struct.new(:id, :klass)
|
|
671
722
|
PARSING_STAGES = [
|
|
672
723
|
ParsingStage.new(:activities, Activity),
|
|
724
|
+
ParsingStage.new(:notes, Note),
|
|
673
725
|
ParsingStage.new(:friends, Friend),
|
|
674
726
|
ParsingStage.new(:locations, Location)
|
|
675
727
|
].freeze
|
data/lib/friends/note.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Note represents a note--a dated record of something
|
|
4
|
+
# about friends or locations that isn't an activity
|
|
5
|
+
# you did. Notes are a good way to remember things big
|
|
6
|
+
# and small, like when your friends move, change jobs,
|
|
7
|
+
# get engaged, or just what food allergies they have
|
|
8
|
+
# for the next time you have them over for dinner.
|
|
9
|
+
|
|
10
|
+
require "friends/event"
|
|
11
|
+
|
|
12
|
+
module Friends
|
|
13
|
+
class Note < Event
|
|
14
|
+
# @return [Regexp] the string of what we expected during deserialization
|
|
15
|
+
def self.deserialization_expectation
|
|
16
|
+
"[YYYY-MM-DD]: [Note]"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/friends/version.rb
CHANGED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
def date_parsing_specs(test_stdout: true)
|
|
2
|
+
describe "date parsing" do
|
|
3
|
+
let(:description) { "Test." }
|
|
4
|
+
|
|
5
|
+
describe "when date is in YYYY-MM-DD" do
|
|
6
|
+
let(:date) { "2017-01-01" }
|
|
7
|
+
|
|
8
|
+
it { line_added "- #{date}: #{description}" }
|
|
9
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: #{description}\"" } if test_stdout
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe "when date is in MM-DD-YYYY" do
|
|
13
|
+
let(:date) { "01-02-2017" }
|
|
14
|
+
|
|
15
|
+
it { line_added "- 2017-01-02: #{description}" }
|
|
16
|
+
it { stdout_only "#{capitalized_event} added: \"2017-01-02: #{description}\"" } if test_stdout
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe "when date is invalid" do
|
|
20
|
+
let(:date) { "2017-02-30" }
|
|
21
|
+
|
|
22
|
+
it { line_added "- 2017-03-02: #{description}" }
|
|
23
|
+
it { stdout_only "#{capitalized_event} added: \"2017-03-02: #{description}\"" } if test_stdout
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe "when date is natural language and in full" do
|
|
27
|
+
let(:date) { "February 23rd, 2017" }
|
|
28
|
+
|
|
29
|
+
it { line_added "- 2017-02-23: #{description}" }
|
|
30
|
+
it { stdout_only "#{capitalized_event} added: \"2017-02-23: #{description}\"" } if test_stdout
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe "when date is natural language and only month and day" do
|
|
34
|
+
# We use two days rather than just one to avoid strange behavior around
|
|
35
|
+
# edge cases when the test is being run right around midnight.
|
|
36
|
+
let(:two_days_in_seconds) { 2 * 24 * 60 * 60 }
|
|
37
|
+
let(:raw_date) { Time.now + two_days_in_seconds }
|
|
38
|
+
let(:date) { raw_date.strftime("%B %d") }
|
|
39
|
+
let(:expected_year) { raw_date.strftime("%Y").to_i - 1 }
|
|
40
|
+
let(:expected_date_str) { "#{expected_year}-#{raw_date.strftime('%m-%d')}" }
|
|
41
|
+
|
|
42
|
+
it { line_added "- #{expected_date_str}: #{description}" }
|
|
43
|
+
if test_stdout
|
|
44
|
+
it { stdout_only "#{capitalized_event} added: \"#{expected_date_str}: #{description}\"" }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def description_parsing_specs(test_stdout: true)
|
|
51
|
+
describe "description parsing" do
|
|
52
|
+
let(:date) { Date.today.strftime }
|
|
53
|
+
|
|
54
|
+
describe "when description includes a friend's full name (case insensitive)" do
|
|
55
|
+
let(:description) { "Lunch with grace hopper." }
|
|
56
|
+
|
|
57
|
+
it { line_added "- #{date}: Lunch with **Grace Hopper**." }
|
|
58
|
+
if test_stdout
|
|
59
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with Grace Hopper.\"" }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
describe "when description includes a friend's first name (case insensitive)" do
|
|
64
|
+
let(:description) { "Lunch with grace." }
|
|
65
|
+
|
|
66
|
+
it { line_added "- #{date}: Lunch with **Grace Hopper**." }
|
|
67
|
+
if test_stdout
|
|
68
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with Grace Hopper.\"" }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe "when description has a friend's first name and last initial (case insensitive)" do
|
|
73
|
+
describe "when followed by no period" do
|
|
74
|
+
let(:description) { "Lunch with grace h" }
|
|
75
|
+
|
|
76
|
+
it { line_added "- #{date}: Lunch with **Grace Hopper**" }
|
|
77
|
+
if test_stdout
|
|
78
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with Grace Hopper\"" }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe "when followed by a period at the end of a sentence" do
|
|
83
|
+
let(:description) { "Met grace h. So fun!" }
|
|
84
|
+
|
|
85
|
+
it { line_added "- #{date}: Met **Grace Hopper**. So fun!" }
|
|
86
|
+
if test_stdout
|
|
87
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Met Grace Hopper. So fun!\"" }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe "when followed by a period at the end of the description" do
|
|
92
|
+
let(:description) { "Lunch with grace h." }
|
|
93
|
+
|
|
94
|
+
it { line_added "- #{date}: Lunch with **Grace Hopper**." }
|
|
95
|
+
if test_stdout
|
|
96
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with Grace Hopper.\"" }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe "when followed by a period in the middle of a sentence" do
|
|
101
|
+
let(:description) { "Met grace h. at 12." }
|
|
102
|
+
|
|
103
|
+
it { line_added "- #{date}: Met **Grace Hopper** at 12." }
|
|
104
|
+
if test_stdout
|
|
105
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Met Grace Hopper at 12.\"" }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe "when description includes a friend's nickname (case insensitive)" do
|
|
111
|
+
let(:description) { "Lunch with the admiral." }
|
|
112
|
+
|
|
113
|
+
it { line_added "- #{date}: Lunch with **Grace Hopper**." }
|
|
114
|
+
if test_stdout
|
|
115
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with Grace Hopper.\"" }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
describe "when description includes a friend's nickname which contains a name" do
|
|
120
|
+
let(:description) { "Lunch with Amazing Grace." }
|
|
121
|
+
|
|
122
|
+
it { line_added "- #{date}: Lunch with **Grace Hopper**." }
|
|
123
|
+
if test_stdout
|
|
124
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with Grace Hopper.\"" }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
describe "when description includes a friend's name at the beginning of a word" do
|
|
129
|
+
# Capitalization reduces chance of a false positive.
|
|
130
|
+
let(:description) { "Gracefully strolled." }
|
|
131
|
+
|
|
132
|
+
it { line_added "- #{date}: Gracefully strolled." }
|
|
133
|
+
if test_stdout
|
|
134
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Gracefully strolled.\"" }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
describe "when description includes a friend's name at the end of a word" do
|
|
139
|
+
# Capitalization reduces chance of a false positive.
|
|
140
|
+
let(:description) { "The service was a disGrace." }
|
|
141
|
+
|
|
142
|
+
it { line_added "- #{date}: The service was a disGrace." }
|
|
143
|
+
if test_stdout
|
|
144
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: The service was a disGrace.\"" }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
describe "when description includes a friend's name in the middle of a word" do
|
|
149
|
+
# Capitalization reduces chance of a false positive.
|
|
150
|
+
let(:description) { "The service was disGraceful." }
|
|
151
|
+
|
|
152
|
+
it { line_added "- #{date}: The service was disGraceful." }
|
|
153
|
+
if test_stdout
|
|
154
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: The service was disGraceful.\"" }
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe "when a friend's name is escaped with a backslash" do
|
|
159
|
+
# We have to use four backslashes here because of Ruby's backslash escaping; when this
|
|
160
|
+
# goes through all of the layers of this test it emerges on the other side as a single one.
|
|
161
|
+
let(:description) { "Dinner with \\\\Grace Kelly." }
|
|
162
|
+
|
|
163
|
+
it { line_added "- #{date}: Dinner with Grace Kelly." }
|
|
164
|
+
if test_stdout
|
|
165
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Dinner with Grace Kelly.\"" }
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
describe "hyphenated name edge cases" do
|
|
170
|
+
describe "when one name precedes another before a hyphen" do
|
|
171
|
+
let(:description) { "Shopped w/ Mary-Kate." }
|
|
172
|
+
|
|
173
|
+
# Make sure "Mary" is a closer friend than "Mary-Kate" so we know our
|
|
174
|
+
# test result isn't due to chance.
|
|
175
|
+
let(:content) do
|
|
176
|
+
<<-FILE
|
|
177
|
+
### Activities:
|
|
178
|
+
- 2017-01-01: Singing with **Mary Poppins**.
|
|
179
|
+
|
|
180
|
+
### Notes:
|
|
181
|
+
|
|
182
|
+
### Friends:
|
|
183
|
+
- Mary Poppins
|
|
184
|
+
- Mary-Kate Olsen
|
|
185
|
+
|
|
186
|
+
### Locations:
|
|
187
|
+
FILE
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it { line_added "- #{date}: Shopped w/ **Mary-Kate Olsen**." }
|
|
191
|
+
if test_stdout
|
|
192
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Shopped w/ Mary-Kate Olsen.\"" }
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
describe "when one name follows another after a hyphen" do
|
|
197
|
+
let(:description) { "Shopped w/ Mary-Kate." }
|
|
198
|
+
|
|
199
|
+
# Make sure "Kate" is a closer friend than "Mary-Kate" so we know our
|
|
200
|
+
# test result isn't due to chance.
|
|
201
|
+
let(:content) do
|
|
202
|
+
<<-FILE
|
|
203
|
+
### Activities:
|
|
204
|
+
- 2017-01-01: Improv with **Kate Winslet**.
|
|
205
|
+
|
|
206
|
+
### Notes:
|
|
207
|
+
|
|
208
|
+
### Friends:
|
|
209
|
+
- Kate Winslet
|
|
210
|
+
- Mary-Kate Olsen
|
|
211
|
+
|
|
212
|
+
### Locations:
|
|
213
|
+
FILE
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it { line_added "- #{date}: Shopped w/ **Mary-Kate Olsen**." }
|
|
217
|
+
if test_stdout
|
|
218
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Shopped w/ Mary-Kate Olsen.\"" }
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
describe "when one name is contained within another via hyphens" do
|
|
223
|
+
let(:description) { "Met Mary-Jo-Kate." }
|
|
224
|
+
|
|
225
|
+
# Make sure "Jo" is a closer friend than "Mary-Jo-Kate" so we know our
|
|
226
|
+
# test result isn't due to chance.
|
|
227
|
+
let(:content) do
|
|
228
|
+
<<-FILE
|
|
229
|
+
### Activities:
|
|
230
|
+
- 2017-01-01: Singing with **Jo Stafford**.
|
|
231
|
+
|
|
232
|
+
### Notes:
|
|
233
|
+
|
|
234
|
+
### Friends:
|
|
235
|
+
- Jo Stafford
|
|
236
|
+
- Mary-Jo-Kate Olsen
|
|
237
|
+
|
|
238
|
+
### Locations:
|
|
239
|
+
FILE
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it { line_added "- #{date}: Met **Mary-Jo-Kate Olsen**." }
|
|
243
|
+
if test_stdout
|
|
244
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Met Mary-Jo-Kate Olsen.\"" }
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
describe "when description has a friend's name with leading asterisks" do
|
|
250
|
+
let(:description) { "Lunch with **Grace Hopper." }
|
|
251
|
+
|
|
252
|
+
it { line_added "- #{date}: Lunch with **Grace Hopper." }
|
|
253
|
+
if test_stdout
|
|
254
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with **Grace Hopper.\"" }
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
describe "when description has a friend's name with trailing asterisks" do
|
|
259
|
+
# Note: We can't guarantee that "Grace Hopper**" doesn't match because the "Grace" isn't
|
|
260
|
+
# surrounded by asterisks.
|
|
261
|
+
let(:description) { "Lunch with Grace**." }
|
|
262
|
+
|
|
263
|
+
it { line_added "- #{date}: Lunch with Grace**." }
|
|
264
|
+
if test_stdout
|
|
265
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch with Grace**.\"" }
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
describe "when description has a friend's name multiple times" do
|
|
270
|
+
let(:description) { "Grace! Grace!!!" }
|
|
271
|
+
|
|
272
|
+
it { line_added "- #{date}: **Grace Hopper**! **Grace Hopper**!!!" }
|
|
273
|
+
if test_stdout
|
|
274
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Grace Hopper! Grace Hopper!!!\"" }
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
describe "when description has a name with multiple friend matches" do
|
|
279
|
+
describe "when there is useful context from past activities" do
|
|
280
|
+
let(:description) { "Met John + Elizabeth." }
|
|
281
|
+
|
|
282
|
+
# Create a past activity in which Elizabeth Cady Stanton did something
|
|
283
|
+
# with John Cage. Then, create past activities to make Elizabeth II a
|
|
284
|
+
# better friend than Elizabeth Cady Stanton.
|
|
285
|
+
let(:content) do
|
|
286
|
+
<<-FILE
|
|
287
|
+
### Activities:
|
|
288
|
+
- 2017-01-05: Picnic with **Elizabeth Cady Stanton** and **John Cage**.
|
|
289
|
+
- 2017-01-04: Got lunch with **Elizabeth II**.
|
|
290
|
+
- 2017-01-03: Ice skated with **Elizabeth II**.
|
|
291
|
+
|
|
292
|
+
### Notes:
|
|
293
|
+
|
|
294
|
+
### Friends:
|
|
295
|
+
- Elizabeth Cady Stanton
|
|
296
|
+
- Elizabeth II
|
|
297
|
+
- John Cage
|
|
298
|
+
|
|
299
|
+
### Locations:
|
|
300
|
+
FILE
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Elizabeth II is the better friend, but historical activities have
|
|
304
|
+
# had Elizabeth Cady Stanton and John Cage together. Thus, we should
|
|
305
|
+
# interpret "Elizabeth" as Elizabeth Cady Stanton.
|
|
306
|
+
it { line_added "- #{date}: Met **John Cage** + **Elizabeth Cady Stanton**." }
|
|
307
|
+
if test_stdout
|
|
308
|
+
it do
|
|
309
|
+
stdout_only "#{capitalized_event} added: "\
|
|
310
|
+
"\"#{date}: Met John Cage + Elizabeth Cady Stanton.\""
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
describe "when there is no useful context from past activities" do
|
|
316
|
+
let(:description) { "Dinner with John and Elizabeth." }
|
|
317
|
+
|
|
318
|
+
# Create a past activity in which Elizabeth Cady Stanton did something
|
|
319
|
+
# with John Cage. Then, create past activities to make Elizabeth II a
|
|
320
|
+
# better friend than Elizabeth Cady Stanton.
|
|
321
|
+
let(:content) do
|
|
322
|
+
<<-FILE
|
|
323
|
+
### Activities:
|
|
324
|
+
- 2017-01-03: Ice skated with **Elizabeth II**.
|
|
325
|
+
|
|
326
|
+
### Notes:
|
|
327
|
+
|
|
328
|
+
### Friends:
|
|
329
|
+
- Elizabeth Cady Stanton
|
|
330
|
+
- Elizabeth II
|
|
331
|
+
- John Cage
|
|
332
|
+
|
|
333
|
+
### Locations:
|
|
334
|
+
FILE
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Pick the "Elizabeth" with more activities.
|
|
338
|
+
it { line_added "- #{date}: Dinner with **John Cage** and **Elizabeth II**." }
|
|
339
|
+
if test_stdout
|
|
340
|
+
it do
|
|
341
|
+
stdout_only "#{capitalized_event} added: "\
|
|
342
|
+
"\"#{date}: Dinner with John Cage and Elizabeth II.\""
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
describe "when description contains a location name (case insensitive)" do
|
|
349
|
+
let(:description) { "Lunch at a cafe in paris." }
|
|
350
|
+
|
|
351
|
+
it { line_added "- #{date}: Lunch at a cafe in _Paris_." }
|
|
352
|
+
if test_stdout
|
|
353
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Lunch at a cafe in Paris.\"" }
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
describe "when description contains both names and locations" do
|
|
358
|
+
let(:description) { "Grace and I went to Atlantis and then Paris for lunch with George." }
|
|
359
|
+
|
|
360
|
+
it do
|
|
361
|
+
line_added "- #{date}: **Grace Hopper** and I went to _Atlantis_ and then _Paris_ for "\
|
|
362
|
+
"lunch with **George Washington Carver**."
|
|
363
|
+
end
|
|
364
|
+
if test_stdout
|
|
365
|
+
it do
|
|
366
|
+
stdout_only "#{capitalized_event} added: \"#{date}: Grace Hopper and I went to "\
|
|
367
|
+
"Atlantis and then Paris for lunch with George Washington Carver.\""
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def parsing_specs(event:)
|
|
375
|
+
let(:capitalized_event) { event.to_s.capitalize }
|
|
376
|
+
|
|
377
|
+
describe "when given a date and a description in the command" do
|
|
378
|
+
subject { run_cmd("add #{event} #{date}: #{description}") }
|
|
379
|
+
|
|
380
|
+
date_parsing_specs
|
|
381
|
+
description_parsing_specs
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
describe "when given only a date in the command" do
|
|
385
|
+
subject { run_cmd("add #{event} #{date}", stdin_data: description) }
|
|
386
|
+
|
|
387
|
+
# We don't try to test the STDOUT here because our command prompt produces other STDOUT that's
|
|
388
|
+
# hard to test.
|
|
389
|
+
date_parsing_specs(test_stdout: false)
|
|
390
|
+
description_parsing_specs(test_stdout: false)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
describe "when given only a description in the command" do
|
|
394
|
+
subject { run_cmd("add #{event} #{description}") }
|
|
395
|
+
|
|
396
|
+
# We don't test date parsing since in this case the date is always inferred to be today.
|
|
397
|
+
|
|
398
|
+
description_parsing_specs
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
describe "when given neither a date nor a description in the command" do
|
|
402
|
+
subject { run_cmd("add #{event}", stdin_data: description) }
|
|
403
|
+
|
|
404
|
+
# We don't test date parsing since in this case the date is always inferred to be today.
|
|
405
|
+
|
|
406
|
+
# We don't try to test the STDOUT here because our command prompt produces other STDOUT that's
|
|
407
|
+
# hard to test.
|
|
408
|
+
description_parsing_specs(test_stdout: false)
|
|
409
|
+
end
|
|
410
|
+
end
|