friends 0.47 → 0.52

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.1's default-less keyword arguments and default UTF-8
27
- # encoding.
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", "~> 12.3"
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 _Marie's Diner_. George had to cancel at the last minute. @food
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
- - Marie's Diner
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: global_options[: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 Semverse::Version.coerce(remote_version) >
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]
@@ -14,8 +14,8 @@ module Friends
14
14
  class Event
15
15
  extend Serializable
16
16
 
17
- SERIALIZATION_PREFIX = "- ".freeze
18
- DATE_PARTITION = ": ".freeze
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 moved_to_location
142
- @description[/(?<=[mM]oved to _)\w[^_]*(?=_)/]
141
+ def default_location
142
+ @default_location ||= @description[/(?<=to _)\w[^_]*(?=_)/]
143
143
  end
144
144
 
145
145
  # @param friend [Friend] the friend to test
@@ -10,15 +10,13 @@ module Friends
10
10
  class Friend
11
11
  extend Serializable
12
12
 
13
- SERIALIZATION_PREFIX = "- ".freeze
14
- NICKNAME_PREFIX = "a.k.a. ".freeze
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 Metrics/LineLength
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 && tags_str.split(/\s+/) || []
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, and then
138
- # (via lookahead) spacing followed by a lowercase letter. This matches
139
- # the "Jake E." part of something like "Jake E. and I went skiing." This
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
- ([chunks.first, "#{chunks.last[0]}\.(?=#{splitter}(?-i)[a-z])"] if chunks.size > 1),
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
@@ -4,7 +4,7 @@
4
4
 
5
5
  module Friends
6
6
  class Graph
7
- DATE_FORMAT = "%b %Y".freeze
7
+ DATE_FORMAT = "%b %Y"
8
8
  SCALED_SIZE = 20
9
9
 
10
10
  # @param filtered_activities [Array<Friends::Activity>] a list of activities to highlight in
@@ -16,10 +16,10 @@ require "friends/friends_error"
16
16
 
17
17
  module Friends
18
18
  class Introvert
19
- ACTIVITIES_HEADER = "### Activities:".freeze
20
- NOTES_HEADER = "### Notes:".freeze
21
- FRIENDS_HEADER = "### Friends:".freeze
22
- LOCATIONS_HEADER = "### Locations:".freeze
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.moved_to_location if activity.moved_to_location
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 => ex # rubocop:disable Style/RescueStandardError
723
- bad_line(ex, line_num)
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
@@ -9,7 +9,7 @@ module Friends
9
9
  class Location
10
10
  extend Serializable
11
11
 
12
- SERIALIZATION_PREFIX = "- ".freeze
12
+ SERIALIZATION_PREFIX = "- "
13
13
 
14
14
  # @return [Regexp] the regex for capturing groups in deserialization
15
15
  def self.deserialization_regex
@@ -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 = "(?<!\\\\)".freeze
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 = "(?<!\\*\\*)".freeze
14
- NO_TRAILING_ASTERISKS = "(?!\\*\\*)".freeze
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 = "(?<!_)".freeze
19
- NO_TRAILING_UNDERSCORES = "(?!_)".freeze
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])".freeze
23
- NO_TRAILING_ALPHABETICALS = "(?![A-z])".freeze
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)".freeze
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
@@ -26,7 +26,7 @@ module Serializable
26
26
  reduce(:merge).
27
27
  reject { |_, v| v.nil? }
28
28
 
29
- new(args)
29
+ new(**args)
30
30
  end
31
31
 
32
32
  class SerializationError < StandardError
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Friends
4
- VERSION = "0.47".freeze
4
+ VERSION = "0.52"
5
5
  end
@@ -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 went to Atlantis and then Paris for lunch with George." }
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 went to _Atlantis_ and then _Paris_ for "\
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 went to "\
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