friends 0.47 → 0.52
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/CONTRIBUTING.md +41 -36
- data/.github/FUNDING.yml +4 -0
- data/.github/ISSUE_TEMPLATE.md +1 -6
- data/.rubocop.yml +20 -5
- data/.travis.yml +5 -5
- data/CHANGELOG.md +78 -10
- data/Gemfile +2 -4
- data/README.md +31 -28
- data/RELEASING.md +1 -1
- data/bin/friends +3 -7
- data/friends.gemspec +3 -5
- data/friends.md +2 -2
- data/lib/friends/commands/edit.rb +1 -1
- data/lib/friends/commands/update.rb +2 -2
- data/lib/friends/event.rb +4 -4
- data/lib/friends/friend.rb +20 -13
- data/lib/friends/graph.rb +1 -1
- data/lib/friends/introvert.rb +47 -9
- data/lib/friends/location.rb +1 -1
- data/lib/friends/post_install_message.rb +1 -2
- data/lib/friends/regex_builder.rb +8 -8
- data/lib/friends/sem_ver_comparator.rb +20 -0
- data/lib/friends/serializable.rb +1 -1
- data/lib/friends/version.rb +1 -1
- data/test/add_event_helper.rb +59 -3
- data/test/commands/add/activity_spec.rb +294 -0
- data/test/commands/edit_spec.rb +33 -2
- data/test/commands/graph_spec.rb +4 -4
- data/test/commands/list/activities_spec.rb +55 -25
- data/test/commands/list/favorite/friends_spec.rb +1 -1
- data/test/commands/list/favorite/locations_spec.rb +8 -8
- data/test/commands/list/locations_spec.rb +1 -1
- data/test/commands/remove/tag_spec.rb +9 -0
- data/test/commands/rename/friend_spec.rb +2 -2
- data/test/commands/suggest_spec.rb +2 -2
- data/test/editor +2 -0
- data/test/helper.rb +7 -7
- metadata +7 -25
data/friends.gemspec
CHANGED
@@ -23,17 +23,15 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
24
24
|
spec.require_paths = ["lib"]
|
25
25
|
|
26
|
-
# We need Ruby 2.
|
27
|
-
|
28
|
-
spec.required_ruby_version = ">= 2.1"
|
26
|
+
# We need Ruby 2.3's safe navigation operator.
|
27
|
+
spec.required_ruby_version = ">= 2.3"
|
29
28
|
|
30
29
|
spec.add_dependency "chronic", "~> 0.10"
|
31
30
|
spec.add_dependency "gli", "~> 2.14"
|
32
31
|
spec.add_dependency "paint", "~> 2.0"
|
33
|
-
spec.add_dependency "semverse", ">= 2", "< 4"
|
34
32
|
spec.add_dependency "tty-pager", "~> 0.11"
|
35
33
|
|
36
34
|
spec.add_development_dependency "minitest", "~> 5.5"
|
37
35
|
spec.add_development_dependency "minitest-proveit", "~> 1.0"
|
38
|
-
spec.add_development_dependency "rake", "~>
|
36
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
39
37
|
end
|
data/friends.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
### Activities:
|
2
|
-
- 2018-11-01: **Grace Hopper** and I went to
|
2
|
+
- 2018-11-01: **Grace Hopper** and I went to _Martha's Vineyard_. George had to cancel at the last minute.
|
3
3
|
- 2018-01-04: Got lunch with **Grace Hopper** and **George Washington Carver**. @food
|
4
4
|
- 2017-12-31: Celebrated the new year in _Paris_ with **Marie Curie**. @partying
|
5
5
|
- 2017-11-15: Talked to **George Washington Carver** on the phone for an hour.
|
@@ -15,5 +15,5 @@
|
|
15
15
|
|
16
16
|
### Locations:
|
17
17
|
- Atlantis
|
18
|
-
-
|
18
|
+
- Martha's Vineyard
|
19
19
|
- Paris
|
@@ -10,7 +10,7 @@ command :edit do |edit|
|
|
10
10
|
|
11
11
|
# Mark the file for cleaning once the editor was closed correctly.
|
12
12
|
if Kernel.system("#{editor} #{filename}")
|
13
|
-
@introvert = Friends::Introvert.new(filename:
|
13
|
+
@introvert = Friends::Introvert.new(filename: filename)
|
14
14
|
@clean_command = true
|
15
15
|
@dirty = true
|
16
16
|
elsif !global_options[:quiet]
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "friends/post_install_message"
|
4
|
+
require "friends/sem_ver_comparator"
|
4
5
|
|
5
6
|
desc "Updates the `friends` program"
|
6
7
|
command :update do |update|
|
@@ -9,8 +10,7 @@ command :update do |update|
|
|
9
10
|
if match = `gem search friends`.match(/^friends\s\(([^\)]+)\)$/)
|
10
11
|
# rubocop:enable Lint/AssignmentInCondition
|
11
12
|
remote_version = match[1]
|
12
|
-
if
|
13
|
-
Semverse::Version.coerce(Friends::VERSION)
|
13
|
+
if Friends::SemVerComparator.greater?(remote_version, Friends::VERSION)
|
14
14
|
`gem update friends && gem cleanup friends`
|
15
15
|
|
16
16
|
unless global_options[:quiet]
|
data/lib/friends/event.rb
CHANGED
@@ -14,8 +14,8 @@ module Friends
|
|
14
14
|
class Event
|
15
15
|
extend Serializable
|
16
16
|
|
17
|
-
SERIALIZATION_PREFIX = "- "
|
18
|
-
DATE_PARTITION = ": "
|
17
|
+
SERIALIZATION_PREFIX = "- "
|
18
|
+
DATE_PARTITION = ": "
|
19
19
|
|
20
20
|
# @return [Regexp] the regex for capturing groups in deserialization
|
21
21
|
def self.deserialization_regex
|
@@ -138,8 +138,8 @@ module Friends
|
|
138
138
|
location_in_description?(location) || location_is_implicit?(location)
|
139
139
|
end
|
140
140
|
|
141
|
-
def
|
142
|
-
@description[/(?<=
|
141
|
+
def default_location
|
142
|
+
@default_location ||= @description[/(?<=to _)\w[^_]*(?=_)/]
|
143
143
|
end
|
144
144
|
|
145
145
|
# @param friend [Friend] the friend to test
|
data/lib/friends/friend.rb
CHANGED
@@ -10,15 +10,13 @@ module Friends
|
|
10
10
|
class Friend
|
11
11
|
extend Serializable
|
12
12
|
|
13
|
-
SERIALIZATION_PREFIX = "- "
|
14
|
-
NICKNAME_PREFIX = "a.k.a. "
|
13
|
+
SERIALIZATION_PREFIX = "- "
|
14
|
+
NICKNAME_PREFIX = "a.k.a. "
|
15
15
|
|
16
16
|
# @return [Regexp] the regex for capturing groups in deserialization
|
17
17
|
def self.deserialization_regex
|
18
18
|
# Note: this regex must be on one line because whitespace is important
|
19
|
-
# rubocop:disable
|
20
|
-
/(#{SERIALIZATION_PREFIX})?(?<name>[^\(\[@]*[^\(\[@\s])(\s+\(#{NICKNAME_PREFIX}(?<nickname_str>.+)\))?(\s+\[(?<location_name>[^\]]+)\])?(\s+(?<tags_str>(#{TAG_REGEX}\s*)+))?/
|
21
|
-
# rubocop:enable Metrics/LineLength
|
19
|
+
/(#{SERIALIZATION_PREFIX})?(?<name>[^\(\[@]*[^\(\[@\s])(\s+\(#{NICKNAME_PREFIX}(?<nickname_str>.+)\))?(\s+\[(?<location_name>[^\]]+)\])?(\s+(?<tags_str>(#{TAG_REGEX}\s*)+))?/ # rubocop:disable Layout/LineLength
|
22
20
|
end
|
23
21
|
|
24
22
|
# @return [Regexp] the string of what we expected during deserialization
|
@@ -34,11 +32,9 @@ module Friends
|
|
34
32
|
tags_str: nil
|
35
33
|
)
|
36
34
|
@name = name
|
37
|
-
@nicknames = nickname_str
|
38
|
-
nickname_str.split(" #{NICKNAME_PREFIX}") ||
|
39
|
-
[]
|
35
|
+
@nicknames = nickname_str&.split(" #{NICKNAME_PREFIX}") || []
|
40
36
|
@location_name = location_name
|
41
|
-
@tags = tags_str
|
37
|
+
@tags = tags_str&.split(/\s+/) || []
|
42
38
|
end
|
43
39
|
|
44
40
|
attr_accessor :name
|
@@ -134,12 +130,23 @@ module Friends
|
|
134
130
|
chunks, # Match a full name with the highest priority.
|
135
131
|
*@nicknames.map { |n| [n] },
|
136
132
|
|
137
|
-
# Match a first name followed by a last name initial, period
|
138
|
-
#
|
139
|
-
#
|
133
|
+
# Match a first name followed by a last name initial, period (that via
|
134
|
+
# lookahead is *NOT* a part of an ellipsis), and then (via lookahead)
|
135
|
+
# either:
|
136
|
+
# - other punctuation that would indicate we want to swallow the period
|
137
|
+
# (note that we do not include closing parentheses in this list because
|
138
|
+
# they could be part of an offset sentence), OR
|
139
|
+
# - anything, so long as the first alphabetical character afterwards is
|
140
|
+
# lowercase.
|
141
|
+
# This matches the "Jake E." part of something like "Jake E. and I went
|
142
|
+
# skiing." or "Jake E., Marie Curie, and I studied science." This
|
140
143
|
# allows us to correctly count the period as part of the name when it's
|
141
144
|
# in the middle of a sentence.
|
142
|
-
(
|
145
|
+
(
|
146
|
+
if chunks.size > 1
|
147
|
+
[chunks.first, "#{chunks.last[0]}\\.(?!\\.\\.)(?=([,!?;:—]+|(?-i)[^A-Z]+[a-z]))"]
|
148
|
+
end
|
149
|
+
),
|
143
150
|
|
144
151
|
# If the above doesn't match, we check for just the first name and then
|
145
152
|
# a last name initial. This matches the "Jake E" part of something like
|
data/lib/friends/graph.rb
CHANGED
data/lib/friends/introvert.rb
CHANGED
@@ -16,10 +16,10 @@ require "friends/friends_error"
|
|
16
16
|
|
17
17
|
module Friends
|
18
18
|
class Introvert
|
19
|
-
ACTIVITIES_HEADER = "### Activities:"
|
20
|
-
NOTES_HEADER = "### Notes:"
|
21
|
-
FRIENDS_HEADER = "### Friends:"
|
22
|
-
LOCATIONS_HEADER = "### Locations:"
|
19
|
+
ACTIVITIES_HEADER = "### Activities:"
|
20
|
+
NOTES_HEADER = "### Notes:"
|
21
|
+
FRIENDS_HEADER = "### Friends:"
|
22
|
+
LOCATIONS_HEADER = "### Locations:"
|
23
23
|
|
24
24
|
# @param filename [String] the name of the friends Markdown file
|
25
25
|
def initialize(filename:)
|
@@ -107,9 +107,11 @@ module Friends
|
|
107
107
|
|
108
108
|
activity.highlight_description(introvert: self)
|
109
109
|
|
110
|
-
@activities.unshift(activity)
|
111
|
-
|
112
110
|
@output << "Activity added: \"#{activity}\""
|
111
|
+
|
112
|
+
@output << default_location_output(activity) if activity.default_location
|
113
|
+
|
114
|
+
@activities.unshift(activity)
|
113
115
|
end
|
114
116
|
end
|
115
117
|
|
@@ -660,8 +662,9 @@ module Friends
|
|
660
662
|
|
661
663
|
def set_implicit_locations!
|
662
664
|
implicit_location = nil
|
665
|
+
# reverse_each here moves through the activities in chronological order
|
663
666
|
@activities.reverse_each do |activity|
|
664
|
-
implicit_location = activity.
|
667
|
+
implicit_location = activity.default_location if activity.default_location
|
665
668
|
activity.implicit_location = implicit_location if activity.description_location_names.empty?
|
666
669
|
end
|
667
670
|
end
|
@@ -685,6 +688,9 @@ module Friends
|
|
685
688
|
# Parse the line and update the parsing state.
|
686
689
|
state = parse_line!(line, line_num: line_num, state: state)
|
687
690
|
end
|
691
|
+
# sort the activities from earliest to latest, in case friends.md has been corrupted
|
692
|
+
@activities = stable_sort(@activities)
|
693
|
+
|
688
694
|
set_implicit_locations!
|
689
695
|
|
690
696
|
set_n_activities!(:friend)
|
@@ -719,8 +725,8 @@ module Friends
|
|
719
725
|
|
720
726
|
begin
|
721
727
|
instance_variable_get("@#{stage.id}") << stage.klass.deserialize(line)
|
722
|
-
rescue =>
|
723
|
-
bad_line(
|
728
|
+
rescue StandardError => e
|
729
|
+
bad_line(e, line_num)
|
724
730
|
end
|
725
731
|
|
726
732
|
state
|
@@ -777,5 +783,37 @@ module Friends
|
|
777
783
|
def bad_line(expected, line_num)
|
778
784
|
raise FriendsError, "Expected \"#{expected}\" on line #{line_num}"
|
779
785
|
end
|
786
|
+
|
787
|
+
# @param [Activity] the activity that was added by the user
|
788
|
+
# @return [String] specifying default location and its time range
|
789
|
+
def default_location_output(activity)
|
790
|
+
str = "Default location"
|
791
|
+
|
792
|
+
earlier_activities, later_activities = @activities.partition { |a| a.date <= activity.date }
|
793
|
+
|
794
|
+
earlier_activity_with_default_location = activity
|
795
|
+
|
796
|
+
earlier_activities.each do |a|
|
797
|
+
next unless a.default_location
|
798
|
+
|
799
|
+
break unless a.default_location == activity.default_location
|
800
|
+
|
801
|
+
earlier_activity_with_default_location = a
|
802
|
+
end
|
803
|
+
|
804
|
+
unless later_activities.empty?
|
805
|
+
str += " from #{Paint[earlier_activity_with_default_location.date, :bold]}"
|
806
|
+
|
807
|
+
later_activity = later_activities.find do |a|
|
808
|
+
a.default_location && a.default_location != activity.default_location
|
809
|
+
end
|
810
|
+
|
811
|
+
str += " to #{Paint[later_activity&.date || 'present', :bold]}"
|
812
|
+
end
|
813
|
+
|
814
|
+
str += " already" if earlier_activity_with_default_location != activity
|
815
|
+
|
816
|
+
"#{str} set to: \"#{activity.default_location}\""
|
817
|
+
end
|
780
818
|
end
|
781
819
|
end
|
data/lib/friends/location.rb
CHANGED
@@ -3,6 +3,5 @@
|
|
3
3
|
module Friends
|
4
4
|
POST_INSTALL_MESSAGE = "\nfriends is a volunteer project. If you find it useful, please "\
|
5
5
|
"consider making a small donation:\n\n\t"\
|
6
|
-
"https://github.com/JacobEvelyn/friends#contributing-its-encouraged\n\n"
|
7
|
-
freeze
|
6
|
+
"https://github.com/JacobEvelyn/friends#contributing-its-encouraged\n\n"
|
8
7
|
end
|
@@ -6,26 +6,26 @@ module Friends
|
|
6
6
|
class RegexBuilder
|
7
7
|
# We don't want to match strings that are "escaped" with a leading
|
8
8
|
# backslash.
|
9
|
-
NO_LEADING_BACKSLASH = "(?<!\\\\)"
|
9
|
+
NO_LEADING_BACKSLASH = "(?<!\\\\)"
|
10
10
|
|
11
11
|
# We don't want to match strings that are directly touching double asterisks
|
12
12
|
# as these are treated as sacred by our program.
|
13
|
-
NO_LEADING_ASTERISKS = "(?<!\\*\\*)"
|
14
|
-
NO_TRAILING_ASTERISKS = "(?!\\*\\*)"
|
13
|
+
NO_LEADING_ASTERISKS = "(?<!\\*\\*)"
|
14
|
+
NO_TRAILING_ASTERISKS = "(?!\\*\\*)"
|
15
15
|
|
16
16
|
# We don't want to match strings that are directly touching underscores
|
17
17
|
# as these are treated as sacred by our program.
|
18
|
-
NO_LEADING_UNDERSCORES = "(?<!_)"
|
19
|
-
NO_TRAILING_UNDERSCORES = "(?!_)"
|
18
|
+
NO_LEADING_UNDERSCORES = "(?<!_)"
|
19
|
+
NO_TRAILING_UNDERSCORES = "(?!_)"
|
20
20
|
|
21
21
|
# We don't want to match strings that are part of other words.
|
22
|
-
NO_LEADING_ALPHABETICALS = "(?<![A-z])"
|
23
|
-
NO_TRAILING_ALPHABETICALS = "(?![A-z])"
|
22
|
+
NO_LEADING_ALPHABETICALS = "(?<![A-z])"
|
23
|
+
NO_TRAILING_ALPHABETICALS = "(?![A-z])"
|
24
24
|
|
25
25
|
# We ignore case within the regex as opposed to globally to allow consumers
|
26
26
|
# of this API the ability to pass in strings that turn off this modifier
|
27
27
|
# with the "(?-i)" string.
|
28
|
-
IGNORE_CASE = "(?i)"
|
28
|
+
IGNORE_CASE = "(?i)"
|
29
29
|
|
30
30
|
def self.regex(str)
|
31
31
|
Regexp.new(
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Friends
|
4
|
+
module SemVerComparator
|
5
|
+
SEPARATOR = "."
|
6
|
+
NUMBER_REGEX = /\d+/.freeze
|
7
|
+
|
8
|
+
def self.greater?(version_a, version_b)
|
9
|
+
version_a.split(SEPARATOR).zip(version_b.split(SEPARATOR)) do |a, b|
|
10
|
+
a_num = a&.[](NUMBER_REGEX)&.to_i
|
11
|
+
b_num = b&.[](NUMBER_REGEX)&.to_i
|
12
|
+
return false if a_num.nil?
|
13
|
+
return true if b_num.nil? || a_num > b_num
|
14
|
+
return false if a_num < b_num
|
15
|
+
end
|
16
|
+
|
17
|
+
false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/friends/serializable.rb
CHANGED
data/lib/friends/version.rb
CHANGED
data/test/add_event_helper.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
def date_parsing_specs(test_stdout: true)
|
2
4
|
describe "date parsing" do
|
3
5
|
let(:description) { "Test." }
|
@@ -117,6 +119,60 @@ def description_parsing_specs(test_stdout: true)
|
|
117
119
|
it { stdout_only "#{capitalized_event} added: \"#{date}: Met Grace Hopper at 12.\"" }
|
118
120
|
end
|
119
121
|
end
|
122
|
+
|
123
|
+
describe "when followed by a period and a comma" do
|
124
|
+
let(:description) { "Met grace h., and others, at 12." }
|
125
|
+
|
126
|
+
it { line_added "- #{date}: Met **Grace Hopper**, and others, at 12." }
|
127
|
+
if test_stdout
|
128
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Met Grace Hopper, and others, at 12.\"" } # rubocop:disable Layout/LineLength
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe "when followed by a period, a comma, and a proper noun" do
|
133
|
+
let(:description) { "Met grace h., King James, and others at 12." }
|
134
|
+
|
135
|
+
it { line_added "- #{date}: Met **Grace Hopper**, King James, and others at 12." }
|
136
|
+
if test_stdout
|
137
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Met Grace Hopper, King James, and others at 12.\"" } # rubocop:disable Layout/LineLength
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe "when followed by a period and a complex series of sentence-ending punctuation" do
|
142
|
+
let(:description) { "Met someone—grace h.?! At 12." }
|
143
|
+
|
144
|
+
it { line_added "- #{date}: Met someone—**Grace Hopper**?! At 12." }
|
145
|
+
if test_stdout
|
146
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Met someone—Grace Hopper?! At 12.\"" } # rubocop:disable Layout/LineLength
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe "when followed by a period and a complex series of mid-sentence punctuation" do
|
151
|
+
let(:description) { "Met someone {grace h.}—at 12." }
|
152
|
+
|
153
|
+
it { line_added "- #{date}: Met someone {**Grace Hopper**}—at 12." }
|
154
|
+
if test_stdout
|
155
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Met someone {Grace Hopper}—at 12.\"" } # rubocop:disable Layout/LineLength
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe "when followed by a period as part of a sentence-ending ellipsis" do
|
160
|
+
let(:description) { "Met grace h... Great!" }
|
161
|
+
|
162
|
+
it { line_added "- #{date}: Met **Grace Hopper**... Great!" }
|
163
|
+
if test_stdout
|
164
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Met Grace Hopper... Great!\"" }
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
describe "when followed by a period as part of a mid-sentence ellipsis" do
|
169
|
+
let(:description) { "Met grace h... at 12." }
|
170
|
+
|
171
|
+
it { line_added "- #{date}: Met **Grace Hopper**... at 12." }
|
172
|
+
if test_stdout
|
173
|
+
it { stdout_only "#{capitalized_event} added: \"#{date}: Met Grace Hopper... at 12.\"" }
|
174
|
+
end
|
175
|
+
end
|
120
176
|
end
|
121
177
|
|
122
178
|
describe "when description includes a friend's nickname (case insensitive)" do
|
@@ -449,15 +505,15 @@ FILE
|
|
449
505
|
end
|
450
506
|
|
451
507
|
describe "when description contains both names and locations" do
|
452
|
-
let(:description) { "Grace and I
|
508
|
+
let(:description) { "Grace and I visited Atlantis and then Paris for lunch with George." }
|
453
509
|
|
454
510
|
it do
|
455
|
-
line_added "- #{date}: **Grace Hopper** and I
|
511
|
+
line_added "- #{date}: **Grace Hopper** and I visited _Atlantis_ and then _Paris_ for "\
|
456
512
|
"lunch with **George Washington Carver**."
|
457
513
|
end
|
458
514
|
if test_stdout
|
459
515
|
it do
|
460
|
-
stdout_only "#{capitalized_event} added: \"#{date}: Grace Hopper and I
|
516
|
+
stdout_only "#{capitalized_event} added: \"#{date}: Grace Hopper and I visited "\
|
461
517
|
"Atlantis and then Paris for lunch with George Washington Carver.\""
|
462
518
|
end
|
463
519
|
end
|
@@ -51,5 +51,299 @@ FILE
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
+
describe "adding default location" do
|
55
|
+
describe "when it is the latest activity" do
|
56
|
+
subject { run_cmd("add activity Moved to _Paris_") }
|
57
|
+
let(:content) do
|
58
|
+
<<-FILE
|
59
|
+
### Activities:
|
60
|
+
- #{preceding_activity}
|
61
|
+
|
62
|
+
### Notes:
|
63
|
+
|
64
|
+
### Friends:
|
65
|
+
|
66
|
+
### Locations:
|
67
|
+
FILE
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "when there is no preceding default location" do
|
71
|
+
let(:preceding_activity) { "2016-01-01: Went to the library." }
|
72
|
+
|
73
|
+
it "prints 'Default location set to [LOCATION]'" do
|
74
|
+
output = 'Default location set to: "Paris"'
|
75
|
+
assert_default_location_output(output)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "when preceding default location is different" do
|
80
|
+
let(:preceding_activity) { "2016-01-01: Moved to _Berlin_." }
|
81
|
+
|
82
|
+
it "prints 'Default location set to [LOCATION]'" do
|
83
|
+
output = 'Default location set to: "Paris"'
|
84
|
+
assert_default_location_output(output)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe "when preceding default location is the same" do
|
89
|
+
let(:preceding_activity) { "2016-01-01: Flew to _Paris_." }
|
90
|
+
|
91
|
+
it "prints 'Default location already set to [LOCATION]'" do
|
92
|
+
output = 'Default location already set to: "Paris"'
|
93
|
+
assert_default_location_output(output)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "when it is not the latest activity" do
|
99
|
+
subject { run_cmd("add activity 2009-01-01: Moved to _Paris_") }
|
100
|
+
let(:content) do
|
101
|
+
<<-FILE
|
102
|
+
### Activities:
|
103
|
+
- #{following_activity}
|
104
|
+
- #{preceding_activity}
|
105
|
+
|
106
|
+
### Notes:
|
107
|
+
|
108
|
+
### Friends:
|
109
|
+
|
110
|
+
### Locations:
|
111
|
+
FILE
|
112
|
+
end
|
113
|
+
|
114
|
+
describe "when there is no following default location" do
|
115
|
+
let(:following_activity) { "2019-01-01: Visited a cafe" }
|
116
|
+
|
117
|
+
describe "when there is no preceding default location" do
|
118
|
+
let(:preceding_activity) { "1999-01-01: Visited a library" }
|
119
|
+
|
120
|
+
it "prints 'Default location from [ADDED ACTIVITY DATE] to present set to [LOCATION]'" do
|
121
|
+
message = 'Default location from 2009-01-01 to present set to: "Paris"'
|
122
|
+
assert_default_location_output(message)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "when preceding default location is different" do
|
127
|
+
let(:preceding_activity) { "1999-01-01: Went to _Berlin_" }
|
128
|
+
|
129
|
+
it "prints 'Default location from [ADDED ACTIVITY DATE] to present set to [LOCATION]'" do
|
130
|
+
output = 'Default location from 2009-01-01 to present set to: "Paris"'
|
131
|
+
assert_default_location_output(output)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe "when preceding default location is same" do
|
136
|
+
let(:preceding_activity) { "1999-01-01: Went to _Paris_" }
|
137
|
+
|
138
|
+
it "prints 'Default location from [PRECEDING DEFAULT LOCATION ACTIVITY DATE] to " \
|
139
|
+
"present already set to [LOCATION]'" do
|
140
|
+
output = 'Default location from 1999-01-01 to present already set to: "Paris"'
|
141
|
+
assert_default_location_output(output)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe "when multiple preceding default locations are same and consecutive" do
|
146
|
+
let(:content) do
|
147
|
+
<<-FILE
|
148
|
+
### Activities:
|
149
|
+
- 2019-01-01: Visited a cafe
|
150
|
+
- 1999-01-01: Went to _Paris_
|
151
|
+
- 1989-01-01: Relocated to _Paris_
|
152
|
+
|
153
|
+
### Notes:
|
154
|
+
|
155
|
+
### Friends:
|
156
|
+
|
157
|
+
### Locations:
|
158
|
+
FILE
|
159
|
+
end
|
160
|
+
|
161
|
+
it "prints 'Default location from " \
|
162
|
+
"[EARLIEST CONSECUTIVE DEFAULT LOCATION ACTIVITY DATE] to " \
|
163
|
+
"present already set to [LOCATION]'" do
|
164
|
+
output = 'Default location from 1989-01-01 to present already set to: "Paris"'
|
165
|
+
assert_default_location_output(output)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe "when multiple preceding default locations are the same but not consecutive" do
|
170
|
+
let(:content) do
|
171
|
+
<<-FILE
|
172
|
+
### Activities:
|
173
|
+
- 2019-01-01: Visited a cafe
|
174
|
+
- 1999-01-01: Went to _Paris_
|
175
|
+
- 1989-01-01: Went to _Berlin_
|
176
|
+
- 1979-01-01: Relocated to _Paris_
|
177
|
+
|
178
|
+
### Notes:
|
179
|
+
|
180
|
+
### Friends:
|
181
|
+
|
182
|
+
### Locations:
|
183
|
+
FILE
|
184
|
+
end
|
185
|
+
|
186
|
+
it "prints 'Default location from " \
|
187
|
+
"[EARLIEST CONSECUTIVE DEFAULT LOCATION ACTIVITY DATE] to " \
|
188
|
+
"present already set to [LOCATION]'" do
|
189
|
+
output = 'Default location from 1999-01-01 to present already set to: "Paris"'
|
190
|
+
assert_default_location_output(output)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe "when there are no preceding default locations" do
|
196
|
+
let(:preceding_activity) { "1999-01-01: Visited a cafe" }
|
197
|
+
|
198
|
+
describe "when following default location is the same" do
|
199
|
+
let(:following_activity) { "2019-01-01: Went to _Paris_" }
|
200
|
+
|
201
|
+
it "prints 'Default location from [ADDED ACTIVITY DATE] to present set to [LOCATION]'" do
|
202
|
+
output = 'Default location from 2009-01-01 to present set to: "Paris"'
|
203
|
+
assert_default_location_output(output)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
describe "when following default location is different" do
|
208
|
+
let(:following_activity) { "2019-01-01: Went to _Berlin_" }
|
209
|
+
|
210
|
+
it "prints 'Default location from [ADDED ACTIVITY DATE] to " \
|
211
|
+
"[NEXT DIFFERENT DEFAULT LOCATION ACIVITY DATE] set to [LOCATION]'" do
|
212
|
+
output = 'Default location from 2009-01-01 to 2019-01-01 set to: "Paris"'
|
213
|
+
assert_default_location_output(output)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
describe "when multiple following default locations are the same and consecutive" do
|
218
|
+
let(:content) do
|
219
|
+
<<-FILE
|
220
|
+
### Activities:
|
221
|
+
- 2019-01-01: Went to _Paris_
|
222
|
+
- 2018-01-01: Relocated to _Paris_
|
223
|
+
|
224
|
+
### Notes:
|
225
|
+
|
226
|
+
### Friends:
|
227
|
+
|
228
|
+
### Locations:
|
229
|
+
FILE
|
230
|
+
end
|
231
|
+
|
232
|
+
it "prints 'Default location from [ADDED ACTIVITY DATE] to present set to [LOCATION]'" do
|
233
|
+
output = 'Default location from 2009-01-01 to present set to: "Paris"'
|
234
|
+
assert_default_location_output(output)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
describe "when multiple following default locations are the same but not consecutive" do
|
239
|
+
let(:content) do
|
240
|
+
<<-FILE
|
241
|
+
### Activities:
|
242
|
+
- 2019-01-01: Went to _Paris_
|
243
|
+
- 2018-01-01: Went to _Berlin_
|
244
|
+
- 2017-01-01: Relocated to _Paris_
|
245
|
+
|
246
|
+
### Notes:
|
247
|
+
|
248
|
+
### Friends:
|
249
|
+
|
250
|
+
### Locations:
|
251
|
+
FILE
|
252
|
+
end
|
253
|
+
|
254
|
+
it "prints 'Default location from [ADDED ACTIVITY DATE] to " \
|
255
|
+
"[NEXT DIFFERENT DEFAULT LOCATION ACIVITY DATE] set to [LOCATION]'" do
|
256
|
+
output = 'Default location from 2009-01-01 to 2018-01-01 set to: "Paris"'
|
257
|
+
assert_default_location_output(output)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
describe "when preceding default location is the same" do
|
263
|
+
let(:preceding_activity) { "1999-01-01: Went to _Paris_" }
|
264
|
+
|
265
|
+
describe "when following default location is the same" do
|
266
|
+
let(:following_activity) { "2019-01-01: Relocated to _Paris_" }
|
267
|
+
|
268
|
+
it "prints 'Default location from " \
|
269
|
+
"[PRECEDING ACTIVITY DATE] to present already set to [LOCATION]'" do
|
270
|
+
output = 'Default location from 1999-01-01 to present already set to: "Paris"'
|
271
|
+
assert_default_location_output(output)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
describe "when following default location is different" do
|
276
|
+
let(:following_activity) { "2019-01-01: Relocated to _Berlin_" }
|
277
|
+
|
278
|
+
it "prints 'Default location from " \
|
279
|
+
"[PRECEDING ACTIVITY DATE] to " \
|
280
|
+
"[FOLLOWING ACTIVITY DATE] set to [LOCATION]'" do
|
281
|
+
output = 'Default location from 1999-01-01 to 2019-01-01 already set to: "Paris"'
|
282
|
+
assert_default_location_output(output)
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
describe "when preceding default location is different" do
|
288
|
+
let(:preceding_activity) { "1999-01-01: Went to _Berlin_" }
|
289
|
+
|
290
|
+
describe "when following default location is the same" do
|
291
|
+
let(:following_activity) { "2019-01-01: Relocated to _Paris_" }
|
292
|
+
|
293
|
+
it "prints 'Default location from [ADDED ACTIVITY DATE] to present set to [LOCATION]'" do
|
294
|
+
output = 'Default location from 2009-01-01 to present set to: "Paris"'
|
295
|
+
assert_default_location_output(output)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
describe "when following default location is different" do
|
300
|
+
let(:following_activity) { "2019-01-01: Relocated to _Berlin_" }
|
301
|
+
|
302
|
+
it "prints 'Default location from " \
|
303
|
+
"[ADDED ACTIVITY DATE] to " \
|
304
|
+
"[FOLLOWING ACTIVITY DATE] set to [LOCATION]'" do
|
305
|
+
output = 'Default location from 2009-01-01 to 2019-01-01 set to: "Paris"'
|
306
|
+
assert_default_location_output(output)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
describe "when activities are out of order" do
|
312
|
+
let(:content) do
|
313
|
+
<<-FILE
|
314
|
+
### Activities:
|
315
|
+
- 2018-01-01: Went to _Berlin_
|
316
|
+
- 2019-01-01: Went to _Paris_
|
317
|
+
|
318
|
+
### Notes:
|
319
|
+
|
320
|
+
### Friends:
|
321
|
+
|
322
|
+
### Locations:
|
323
|
+
FILE
|
324
|
+
end
|
325
|
+
|
326
|
+
it "uses the sorted order for determining output" do
|
327
|
+
output = 'Default location from 2009-01-01 to 2018-01-01 set to: "Paris"'
|
328
|
+
assert_default_location_output(output)
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
54
334
|
parsing_specs(event: :activity)
|
335
|
+
|
336
|
+
private
|
337
|
+
|
338
|
+
def assert_default_location_output(expected_output)
|
339
|
+
output = select_default_activity_output(subject[:stdout])
|
340
|
+
|
341
|
+
value(output.size).must_equal(1)
|
342
|
+
value(output).must_include(expected_output)
|
343
|
+
end
|
344
|
+
|
345
|
+
def select_default_activity_output(output)
|
346
|
+
lines = output.split("\n")
|
347
|
+
lines.select { |line| line.include?("Default") }
|
348
|
+
end
|
55
349
|
end
|