friends 0.47 → 0.52

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.
@@ -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