timeboss 0.1.0 → 0.2.3

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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -4
  3. data/.replit +2 -0
  4. data/README.md +15 -8
  5. data/bin/tbsh +1 -1
  6. data/lib/tasks/calendars.rake +4 -2
  7. data/lib/timeboss/calendar.rb +9 -1
  8. data/lib/timeboss/calendar/day.rb +0 -1
  9. data/lib/timeboss/calendar/half.rb +0 -1
  10. data/lib/timeboss/calendar/month.rb +0 -1
  11. data/lib/timeboss/calendar/parser.rb +0 -1
  12. data/lib/timeboss/calendar/period.rb +0 -1
  13. data/lib/timeboss/calendar/quarter.rb +0 -1
  14. data/lib/timeboss/calendar/support/formatter.rb +3 -2
  15. data/lib/timeboss/calendar/support/month_basis.rb +0 -2
  16. data/lib/timeboss/calendar/support/monthly_unit.rb +0 -2
  17. data/lib/timeboss/calendar/support/navigable.rb +0 -1
  18. data/lib/timeboss/calendar/support/shiftable.rb +218 -29
  19. data/lib/timeboss/calendar/support/translatable.rb +92 -0
  20. data/lib/timeboss/calendar/support/unit.rb +4 -1
  21. data/lib/timeboss/calendar/waypoints.rb +0 -1
  22. data/lib/timeboss/calendar/waypoints/absolute.rb +0 -1
  23. data/lib/timeboss/calendar/waypoints/relative.rb +0 -1
  24. data/lib/timeboss/calendar/week.rb +1 -1
  25. data/lib/timeboss/calendar/year.rb +0 -1
  26. data/lib/timeboss/calendars.rb +0 -1
  27. data/lib/timeboss/calendars/gregorian.rb +28 -0
  28. data/lib/timeboss/version.rb +1 -1
  29. data/spec/calendar/support/monthly_unit_spec.rb +6 -0
  30. data/spec/calendar/week_spec.rb +6 -1
  31. data/spec/calendars/gregorian_spec.rb +685 -0
  32. metadata +7 -49
  33. data/doc/TimeBoss.html +0 -146
  34. data/doc/TimeBoss/Calendar.html +0 -137
  35. data/doc/TimeBoss/Calendar/Day.html +0 -594
  36. data/doc/TimeBoss/Calendar/Half.html +0 -396
  37. data/doc/TimeBoss/Calendar/Month.html +0 -396
  38. data/doc/TimeBoss/Calendar/Parser.html +0 -386
  39. data/doc/TimeBoss/Calendar/Period.html +0 -841
  40. data/doc/TimeBoss/Calendar/Quarter.html +0 -396
  41. data/doc/TimeBoss/Calendar/Support.html +0 -131
  42. data/doc/TimeBoss/Calendar/Support/Formatter.html +0 -459
  43. data/doc/TimeBoss/Calendar/Support/MonthBased.html +0 -591
  44. data/doc/TimeBoss/Calendar/Support/MonthBasis.html +0 -437
  45. data/doc/TimeBoss/Calendar/Support/MonthlyUnit.html +0 -591
  46. data/doc/TimeBoss/Calendar/Support/Navigable.html +0 -723
  47. data/doc/TimeBoss/Calendar/Support/Shiftable.html +0 -138
  48. data/doc/TimeBoss/Calendar/Support/Unit.html +0 -1299
  49. data/doc/TimeBoss/Calendar/Waypoints.html +0 -155
  50. data/doc/TimeBoss/Calendar/Waypoints/Absolute.html +0 -1378
  51. data/doc/TimeBoss/Calendar/Waypoints/Relative.html +0 -4308
  52. data/doc/TimeBoss/Calendar/Week.html +0 -671
  53. data/doc/TimeBoss/Calendar/Year.html +0 -319
  54. data/doc/TimeBoss/Calendars.html +0 -336
  55. data/doc/TimeBoss/Calendars/Broadcast.html +0 -221
  56. data/doc/TimeBoss/Calendars/Broadcast/Basis.html +0 -278
  57. data/doc/TimeBoss/Calendars/Entry.html +0 -399
  58. data/doc/TimeBoss/Support.html +0 -115
  59. data/doc/TimeBoss/Support/Shellable.html +0 -249
  60. data/doc/_index.html +0 -416
  61. data/doc/class_list.html +0 -51
  62. data/doc/css/common.css +0 -1
  63. data/doc/css/full_list.css +0 -58
  64. data/doc/css/style.css +0 -496
  65. data/doc/file.README.html +0 -299
  66. data/doc/file_list.html +0 -56
  67. data/doc/frames.html +0 -17
  68. data/doc/index.html +0 -299
  69. data/doc/js/app.js +0 -314
  70. data/doc/js/full_list.js +0 -216
  71. data/doc/js/jquery.js +0 -4
  72. data/doc/method_list.html +0 -1139
  73. data/doc/top-level-namespace.html +0 -110
  74. data/lib/timeboss/calendar/support.rb +0 -8
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+ module TimeBoss
3
+ class Calendar
4
+ module Support
5
+ module Translatable
6
+ PERIODS = %w[day week month quarter half year]
7
+
8
+ PERIODS.each do |period|
9
+ periods = period.pluralize
10
+
11
+ define_method(periods) { calendar.send("#{periods}_for", self) }
12
+
13
+ define_method(period) do
14
+ entries = send(periods)
15
+ return nil unless entries.length == 1
16
+ entries.first
17
+ end
18
+ end
19
+
20
+ #
21
+ # i hate this
22
+ #
23
+
24
+ ### Days
25
+
26
+ # @!method days
27
+ # Get a list of days that fall within this unit.
28
+ # @return [Array<Calendar::Day>]
29
+
30
+ # @!method day
31
+ # Get the day this unit represents.
32
+ # Returns nil if no single day can be identified.
33
+ # @return [Array<Calendar::Day>, nil]
34
+
35
+ ### Weeks
36
+
37
+ # @!method weeks
38
+ # Get a list of weeks that fall within this unit.
39
+ # @return [Array<Calendar::Week>]
40
+
41
+ # @!method week
42
+ # Get the week this unit represents.
43
+ # Returns nil if no single week can be identified.
44
+ # @return [Array<Calendar::Week>, nil]
45
+
46
+ ### Months
47
+
48
+ # @!method months
49
+ # Get a list of months that fall within this unit.
50
+ # @return [Array<Calendar::Month>]
51
+
52
+ # @!method month
53
+ # Get the month this unit represents.
54
+ # Returns nil if no single month can be identified.
55
+ # @return [Array<Calendar::Month>, nil]
56
+
57
+ ### Quarters
58
+
59
+ # @!method quarters
60
+ # Get a list of quarters that fall within this unit.
61
+ # @return [Array<Calendar::Quarter>]
62
+
63
+ # @!method quarter
64
+ # Get the quarter this unit represents.
65
+ # Returns nil if no single quarter can be identified.
66
+ # @return [Array<Calendar::Quarter>, nil]
67
+
68
+ ### Halves
69
+
70
+ # @!method halves
71
+ # Get a list of halves that fall within this unit.
72
+ # @return [Array<Calendar::Half>]
73
+
74
+ # @!method half
75
+ # Get the half this unit represents.
76
+ # Returns nil if no single half can be identified.
77
+ # @return [Array<Calendar::Half>, nil]
78
+
79
+ ### Years
80
+
81
+ # @!method years
82
+ # Get a list of years that fall within this unit.
83
+ # @return [Array<Calendar::Year>]
84
+
85
+ # @!method year
86
+ # Get the year this unit represents.
87
+ # Returns nil if no single year can be identified.
88
+ # @return [Array<Calendar::Year>, nil]
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,17 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative './navigable'
3
+ require_relative './translatable'
3
4
  require_relative './shiftable'
4
5
  require_relative './formatter'
5
6
 
6
7
  module TimeBoss
7
8
  class Calendar
8
9
  module Support
9
- # A unit is the lowest-level base of all days/weeks/months, etc.
10
10
  class Unit
11
11
  include Navigable
12
+ include Translatable
12
13
  include Shiftable
13
14
  attr_reader :calendar, :start_date, :end_date
14
15
 
16
+ UnsupportedUnitError = Class.new(StandardError)
17
+
15
18
  def self.type
16
19
  self.name.demodulize.underscore
17
20
  end
@@ -4,7 +4,6 @@
4
4
 
5
5
  module TimeBoss
6
6
  class Calendar
7
- # Provides implementations for known periods within a calendar.
8
7
  module Waypoints
9
8
  include Absolute
10
9
  include Relative
@@ -2,7 +2,6 @@
2
2
  module TimeBoss
3
3
  class Calendar
4
4
  module Waypoints
5
- # Provides implementation for absolute periods within a calendar.
6
5
  module Absolute
7
6
  %i[month quarter half year].each do |type|
8
7
  klass = TimeBoss::Calendar.const_get(type.to_s.classify)
@@ -2,7 +2,6 @@
2
2
  module TimeBoss
3
3
  class Calendar
4
4
  module Waypoints
5
- # Provides implementation for periods relative to today within a calendar.
6
5
  module Relative
7
6
  %i[day week month quarter half year].each do |type|
8
7
  types = type.to_s.pluralize
@@ -3,9 +3,9 @@ require_relative './support/unit'
3
3
 
4
4
  module TimeBoss
5
5
  class Calendar
6
- # Representation of a single week within a calendar.
7
6
  class Week < Support::Unit
8
7
  def initialize(calendar, start_date, end_date)
8
+ raise UnsupportedUnitError unless calendar.supports_weeks?
9
9
  super(calendar, start_date, end_date)
10
10
  end
11
11
 
@@ -3,7 +3,6 @@ require_relative './support/monthly_unit'
3
3
 
4
4
  module TimeBoss
5
5
  class Calendar
6
- # Representation of a 12-month period within a calendar.
7
6
  class Year < Support::MonthlyUnit
8
7
  NUM_MONTHS = 12
9
8
 
@@ -5,7 +5,6 @@ require_relative 'calendar'
5
5
  Dir[File.expand_path('../calendars/*.rb', __FILE__)].each { |f| require f }
6
6
 
7
7
  module TimeBoss
8
- # A home for specific calendar implementations.
9
8
  module Calendars
10
9
  extend self
11
10
  extend Enumerable
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ require_relative '../calendar'
3
+
4
+ module TimeBoss
5
+ module Calendars
6
+ class Gregorian < Calendar
7
+ def initialize
8
+ super(basis: Basis)
9
+ end
10
+
11
+ def supports_weeks?
12
+ false
13
+ end
14
+
15
+ private
16
+
17
+ class Basis < Calendar::Support::MonthBasis
18
+ def start_date
19
+ @_start_date ||= Date.civil(year, month, 1)
20
+ end
21
+
22
+ def end_date
23
+ @_end_date ||= Date.civil(year, month, -1)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module TimeBoss
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.3"
4
4
  end
@@ -30,6 +30,7 @@ module TimeBoss
30
30
  before(:each) { allow(calendar).to receive(:year).with(2018).and_return base }
31
31
 
32
32
  it 'can get the relevant weeks for the period' do
33
+ allow(calendar).to receive(:supports_weeks?).and_return true
33
34
  result = subject.weeks
34
35
  result.each { |w| expect(w).to be_instance_of TimeBoss::Calendar::Week }
35
36
  expect(result.map { |w| w.start_date.to_s }).to eq [
@@ -44,6 +45,11 @@ module TimeBoss
44
45
  '2018-08-20'
45
46
  ]
46
47
  end
48
+
49
+ it 'blows up when weeks are not supported' do
50
+ allow(calendar).to receive(:supports_weeks?).and_return false
51
+ expect { subject.weeks }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
52
+ end
47
53
  end
48
54
 
49
55
  context 'navigation' do
@@ -1,11 +1,16 @@
1
1
  module TimeBoss
2
2
  class Calendar
3
3
  describe Week do
4
- let(:calendar) { instance_double(TimeBoss::Calendar) }
4
+ let(:calendar) { instance_double(TimeBoss::Calendar, supports_weeks?: true) }
5
5
  let(:start_date) { Date.parse('2048-04-06') }
6
6
  let(:end_date) { Date.parse('2048-04-12') }
7
7
  let(:subject) { described_class.new(calendar, start_date, end_date) }
8
8
 
9
+ it "doesn't even exist if its calendar doesn't support weeks" do
10
+ allow(calendar).to receive(:supports_weeks?).and_return false
11
+ expect { subject }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
12
+ end
13
+
9
14
  it 'knows its stuff' do
10
15
  expect(subject.start_date).to eq start_date
11
16
  expect(subject.end_date).to eq end_date
@@ -0,0 +1,685 @@
1
+ module TimeBoss
2
+ describe Calendars::Gregorian do
3
+ let(:subject) { described_class.new }
4
+
5
+ context 'days' do
6
+ it 'can get today' do
7
+ day = subject.today
8
+ expect(day).to be_instance_of(TimeBoss::Calendar::Day)
9
+ expect(day.start_date).to eq Date.today
10
+ end
11
+
12
+ it 'can get yesterday' do
13
+ day = subject.yesterday
14
+ expect(day).to be_instance_of(TimeBoss::Calendar::Day)
15
+ expect(day.start_date).to eq Date.yesterday
16
+ end
17
+
18
+ it 'can get tomorrow' do
19
+ day = subject.tomorrow
20
+ expect(day).to be_instance_of(TimeBoss::Calendar::Day)
21
+ expect(day.start_date).to eq Date.tomorrow
22
+ end
23
+ end
24
+
25
+ context 'quarters' do
26
+ describe '#quarter' do
27
+ it 'knows 2017Q2' do
28
+ quarter = subject.quarter(2017, 2)
29
+ expect(quarter.name).to eq '2017Q2'
30
+ expect(quarter.title).to eq 'Q2 2017'
31
+ expect(quarter.year_index).to eq 2017
32
+ expect(quarter.index).to eq 2
33
+ expect(quarter.start_date).to eq Date.parse('2017-04-01')
34
+ expect(quarter.end_date).to eq Date.parse('2017-06-30')
35
+ expect(quarter.to_range).to eq quarter.start_date..quarter.end_date
36
+ end
37
+
38
+ it 'knows 2018Q3' do
39
+ quarter = subject.quarter(2018, 3)
40
+ expect(quarter.name).to eq '2018Q3'
41
+ expect(quarter.title).to eq 'Q3 2018'
42
+ expect(quarter.year_index).to eq 2018
43
+ expect(quarter.index).to eq 3
44
+ expect(quarter.start_date).to eq Date.parse('2018-07-01')
45
+ expect(quarter.end_date).to eq Date.parse('2018-09-30')
46
+ expect(quarter.to_range).to eq quarter.start_date..quarter.end_date
47
+ end
48
+
49
+ it 'knows 2019Q4' do
50
+ quarter = subject.quarter(2019, 4)
51
+ expect(quarter.year_index).to eq 2019
52
+ expect(quarter.index).to eq 4
53
+ expect(quarter.name).to eq '2019Q4'
54
+ expect(quarter.title).to eq 'Q4 2019'
55
+ expect(quarter.start_date).to eq Date.parse('2019-10-01')
56
+ expect(quarter.end_date).to eq Date.parse('2019-12-31')
57
+ expect(quarter.to_range).to eq quarter.start_date..quarter.end_date
58
+ end
59
+ end
60
+
61
+ describe '#quarter_for' do
62
+ it 'knows what quarter 2018-07-05 is in' do
63
+ quarter = subject.quarter_for(Date.parse('2018-07-05'))
64
+ expect(quarter.name).to eq '2018Q3'
65
+ end
66
+
67
+ it 'knows what quarter 2018-06-22 is in' do
68
+ quarter = subject.quarter_for(Date.parse('2018-06-22'))
69
+ expect(quarter.name).to eq '2018Q2'
70
+ end
71
+ end
72
+
73
+ describe '#quarters_for' do
74
+ it 'knows what quarters are in 2020' do
75
+ basis = subject.year(2020)
76
+ periods = subject.quarters_for(basis)
77
+ expect(periods.map(&:name)).to eq %w[2020Q1 2020Q2 2020Q3 2020Q4]
78
+ end
79
+
80
+ it 'knows what quarter 2018M7 is in' do
81
+ basis = subject.month(2018, 7)
82
+ periods = subject.quarters_for(basis)
83
+ expect(periods.map(&:name)).to eq %w[2018Q3]
84
+ end
85
+ end
86
+
87
+ describe '#this_quarter' do
88
+ let(:today) { double }
89
+ let(:quarter) { double }
90
+
91
+ it 'gets the quarter for today' do
92
+ allow(Date).to receive(:today).and_return today
93
+ expect(subject).to receive(:quarter_for).with(today).and_return quarter
94
+ expect(subject.this_quarter).to eq quarter
95
+ end
96
+ end
97
+
98
+ describe '#format' do
99
+ let(:entry) { subject.quarter(2015, 3) }
100
+
101
+ it 'can do a default format' do
102
+ expect(entry.format).to eq '2015H2Q1'
103
+ end
104
+
105
+ it 'can format with only the quarter' do
106
+ expect(entry.format(:quarter)).to eq '2015Q3'
107
+ end
108
+
109
+ it 'ignores stupidity' do
110
+ expect(entry.format(:day, :banana)).to eq '2015Q3'
111
+ end
112
+ end
113
+
114
+ context 'relative' do
115
+ let(:this_quarter) { subject.quarter(2015, 3) }
116
+ let(:quarter) { double }
117
+ before(:each) { allow(subject).to receive(:this_quarter).and_return this_quarter }
118
+
119
+ it 'can get the last quarter' do
120
+ allow(this_quarter).to receive(:previous).and_return quarter
121
+ expect(subject.last_quarter).to eq quarter
122
+ end
123
+
124
+ it 'can get the next quarter' do
125
+ allow(this_quarter).to receive(:next).and_return quarter
126
+ expect(subject.next_quarter).to eq quarter
127
+ end
128
+
129
+ it 'can get some number of quarters' do
130
+ quarters = subject.quarters(5)
131
+ expect(quarters.length).to eq 5
132
+ quarters.each { |q| expect(q).to be_a TimeBoss::Calendar::Quarter }
133
+ expect(quarters.map(&:name)).to eq ['2015Q3', '2015Q4', '2016Q1', '2016Q2', '2016Q3']
134
+ end
135
+
136
+ it 'can get a quarter ahead' do
137
+ quarter = subject.quarters_ahead(4)
138
+ expect(quarter).to be_a TimeBoss::Calendar::Quarter
139
+ expect(quarter.name).to eq '2016Q3'
140
+ end
141
+
142
+ it 'can get some number of quarters back' do
143
+ quarters = subject.quarters_back(5)
144
+ expect(quarters.length).to eq 5
145
+ quarters.each { |q| expect(q).to be_a TimeBoss::Calendar::Quarter }
146
+ expect(quarters.map(&:name)).to eq ['2014Q3', '2014Q4', '2015Q1', '2015Q2', '2015Q3']
147
+ end
148
+
149
+ it 'can get a quarter ago' do
150
+ quarter = subject.quarters_ago(4)
151
+ expect(quarter).to be_a TimeBoss::Calendar::Quarter
152
+ expect(quarter.name).to eq '2014Q3'
153
+ end
154
+ end
155
+ end
156
+
157
+ context 'months' do
158
+ describe '#month' do
159
+ it 'knows 2017M2' do
160
+ month = subject.month(2017, 2)
161
+ expect(month.name).to eq '2017M2'
162
+ expect(month.title).to eq 'February 2017'
163
+ expect(month.year_index).to eq 2017
164
+ expect(month.index).to eq 2
165
+ expect(month.start_date).to eq Date.parse('2017-02-01')
166
+ expect(month.end_date).to eq Date.parse('2017-02-28')
167
+ expect(month.to_range).to eq month.start_date..month.end_date
168
+ end
169
+
170
+ it 'knows 2018M3' do
171
+ month = subject.month(2018, 3)
172
+ expect(month.name).to eq '2018M3'
173
+ expect(month.title).to eq 'March 2018'
174
+ expect(month.year_index).to eq 2018
175
+ expect(month.index).to eq 3
176
+ expect(month.start_date).to eq Date.parse('2018-03-01')
177
+ expect(month.end_date).to eq Date.parse('2018-03-31')
178
+ expect(month.to_range).to eq month.start_date..month.end_date
179
+ end
180
+
181
+ it 'knows 2019M11' do
182
+ month = subject.month(2019, 11)
183
+ expect(month.year_index).to eq 2019
184
+ expect(month.index).to eq 11
185
+ expect(month.name).to eq '2019M11'
186
+ expect(month.title).to eq 'November 2019'
187
+ expect(month.start_date).to eq Date.parse('2019-11-01')
188
+ expect(month.end_date).to eq Date.parse('2019-11-30')
189
+ expect(month.to_range).to eq month.start_date..month.end_date
190
+ end
191
+ end
192
+
193
+ describe '#month_for' do
194
+ it 'knows what month 2018-07-05 is in' do
195
+ month = subject.month_for(Date.parse('2018-07-05'))
196
+ expect(month.name).to eq '2018M7'
197
+ end
198
+
199
+ it 'knows what month 2018-06-22 is in' do
200
+ month = subject.month_for(Date.parse('2018-06-22'))
201
+ expect(month.name).to eq '2018M6'
202
+ end
203
+ end
204
+
205
+ describe '#months_for' do
206
+ it 'knows what months are in 2020' do
207
+ basis = subject.year(2020)
208
+ periods = subject.months_for(basis)
209
+ expect(periods.map(&:name)).to eq %w[2020M1 2020M2 2020M3 2020M4 2020M5 2020M6 2020M7 2020M8 2020M9 2020M10 2020M11 2020M12]
210
+ end
211
+
212
+ it 'knows what months are in 2018Q2' do
213
+ basis = subject.parse('2018Q2')
214
+ periods = subject.months_for(basis)
215
+ expect(periods.map(&:name)).to eq %w[2018M4 2018M5 2018M6]
216
+ end
217
+
218
+ it 'knows what month 2019-12-12 is in' do
219
+ basis = subject.parse('2019-12-12')
220
+ periods = subject.months_for(basis)
221
+ expect(periods.map(&:name)).to eq %w[2019M12]
222
+ end
223
+ end
224
+
225
+ describe '#this_month' do
226
+ let(:today) { double }
227
+ let(:month) { double }
228
+
229
+ it 'gets the month for today' do
230
+ allow(Date).to receive(:today).and_return today
231
+ expect(subject).to receive(:month_for).with(today).and_return month
232
+ expect(subject.this_month).to eq month
233
+ end
234
+ end
235
+
236
+ describe '#format' do
237
+ let(:entry) { subject.month(2015, 8) }
238
+
239
+ it 'can do a default format' do
240
+ expect(entry.format).to eq '2015H2Q1M2'
241
+ end
242
+
243
+ it 'can format with only the quarter' do
244
+ expect(entry.format(:quarter)).to eq '2015Q3M2'
245
+ end
246
+
247
+ it 'ignores stupidity' do
248
+ expect(entry.format(:banana, :half, :week)).to eq '2015H2M2'
249
+ end
250
+ end
251
+
252
+ context 'relative' do
253
+ let(:this_month) { subject.month(2015, 3) }
254
+ let(:month) { double }
255
+ before(:each) { allow(subject).to receive(:this_month).and_return this_month }
256
+
257
+ it 'can get the last month' do
258
+ allow(this_month).to receive(:previous).and_return month
259
+ expect(subject.last_month).to eq month
260
+ end
261
+
262
+ it 'can get the next month' do
263
+ allow(this_month).to receive(:next).and_return month
264
+ expect(subject.next_month).to eq month
265
+ end
266
+
267
+ it 'can get some number of months' do
268
+ months = subject.months(5)
269
+ expect(months.length).to eq 5
270
+ months.each { |m| expect(m).to be_a TimeBoss::Calendar::Month }
271
+ expect(months.map(&:name)).to eq ['2015M3', '2015M4', '2015M5', '2015M6', '2015M7']
272
+ end
273
+
274
+ it 'can get a month ahead' do
275
+ month = subject.months_ahead(4)
276
+ expect(month).to be_a TimeBoss::Calendar::Month
277
+ expect(month.name).to eq '2015M7'
278
+ end
279
+
280
+ it 'can get some number of months back' do
281
+ months = subject.months_back(5)
282
+ expect(months.length).to eq 5
283
+ months.each { |m| expect(m).to be_a TimeBoss::Calendar::Month }
284
+ expect(months.map(&:name)).to eq ['2014M11', '2014M12', '2015M1', '2015M2', '2015M3']
285
+ end
286
+
287
+ it 'can get a month ago' do
288
+ month = subject.months_ago(4)
289
+ expect(month).to be_a TimeBoss::Calendar::Month
290
+ expect(month.name).to eq '2014M11'
291
+ end
292
+ end
293
+ end
294
+
295
+ context 'weeks' do
296
+ it 'is uninterested in weeks' do
297
+ expect { subject.this_week }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
298
+ expect { subject.parse('2020W3') }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
299
+ expect { subject.weeks_ago(2) }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
300
+ end
301
+ end
302
+
303
+ context 'years' do
304
+ describe '#year' do
305
+ it 'knows 2016' do
306
+ year = subject.year(2016)
307
+ expect(year.name).to eq '2016'
308
+ expect(year.title).to eq '2016'
309
+ expect(year.year_index).to eq 2016
310
+ expect(year.index).to eq 1
311
+ expect(year.start_date).to eq Date.parse('2016-01-01')
312
+ expect(year.end_date).to eq Date.parse('2016-12-31')
313
+ expect(year.to_range).to eq year.start_date..year.end_date
314
+ end
315
+
316
+ it 'knows 2017' do
317
+ year = subject.year(2017)
318
+ expect(year.name).to eq '2017'
319
+ expect(year.title).to eq '2017'
320
+ expect(year.year_index).to eq 2017
321
+ expect(year.index).to eq 1
322
+ expect(year.start_date).to eq Date.parse('2017-01-01')
323
+ expect(year.end_date).to eq Date.parse('2017-12-31')
324
+ expect(year.to_range).to eq year.start_date..year.end_date
325
+ end
326
+
327
+ it 'knows 2018' do
328
+ year = subject.year(2018)
329
+ expect(year.name).to eq '2018'
330
+ expect(year.title).to eq '2018'
331
+ expect(year.year_index).to eq 2018
332
+ expect(year.index).to eq 1
333
+ expect(year.start_date).to eq Date.parse('2018-01-01')
334
+ expect(year.end_date).to eq Date.parse('2018-12-31')
335
+ expect(year.to_range).to eq year.start_date..year.end_date
336
+ end
337
+ end
338
+
339
+ describe '#year_for' do
340
+ it 'knows what year 2018-04-07 is in' do
341
+ year = subject.year_for(Date.parse('2018-04-07'))
342
+ expect(year.name).to eq '2018'
343
+ end
344
+
345
+ it 'knows what year 2016-12-27 is in' do
346
+ year = subject.year_for(Date.parse('2016-12-27'))
347
+ expect(year.name).to eq '2016'
348
+ end
349
+ end
350
+
351
+ describe '#years_for' do
352
+ it 'knows what years are in 2020 (duh)' do
353
+ basis = subject.year(2020)
354
+ periods = subject.years_for(basis)
355
+ expect(periods.map(&:name)).to eq %w[2020]
356
+ end
357
+
358
+ it 'knows what year 2018Q2 is in' do
359
+ basis = subject.parse('2018Q2')
360
+ periods = subject.years_for(basis)
361
+ expect(periods.map(&:name)).to eq %w[2018]
362
+ end
363
+
364
+ it 'knows what years 2019-12-12 is in' do
365
+ basis = subject.parse('2019-12-12')
366
+ periods = subject.years_for(basis)
367
+ expect(periods.map(&:name)).to eq %w[2019]
368
+ end
369
+ end
370
+
371
+ describe '#this_year' do
372
+ let(:today) { double }
373
+ let(:year) { double }
374
+
375
+ it 'gets the year for today' do
376
+ allow(Date).to receive(:today).and_return today
377
+ expect(subject).to receive(:year_for).with(today).and_return year
378
+ expect(subject.this_year).to eq year
379
+ end
380
+ end
381
+
382
+ describe '#format' do
383
+ let(:entry) { subject.parse('2020M8') }
384
+
385
+ it 'can do a default format' do
386
+ expect(entry.format).to eq '2020H2Q1M2'
387
+ end
388
+
389
+ it 'can format with only the quarter' do
390
+ expect(entry.format(:quarter)).to eq '2020Q3M2'
391
+ end
392
+
393
+ context 'with days' do
394
+ let(:entry) { subject.parse('2020D201') }
395
+
396
+ it 'can do a default format' do
397
+ expect(entry.format).to eq '2020H2Q1M1D19'
398
+ end
399
+ end
400
+ end
401
+
402
+ context 'relative' do
403
+ let(:this_year) { subject.year(2015) }
404
+ let(:year) { double }
405
+ before(:each) { allow(subject).to receive(:this_year).and_return this_year }
406
+
407
+ it 'can get the last year' do
408
+ allow(this_year).to receive(:previous).and_return year
409
+ expect(subject.last_year).to eq year
410
+ end
411
+
412
+ it 'can get the next year' do
413
+ allow(this_year).to receive(:next).and_return year
414
+ expect(subject.next_year).to eq year
415
+ end
416
+
417
+ it 'can get some number of years' do
418
+ years = subject.years(5)
419
+ expect(years.length).to eq 5
420
+ years.each { |y| expect(y).to be_a TimeBoss::Calendar::Year }
421
+ expect(years.map(&:name)).to eq ['2015', '2016', '2017', '2018', '2019']
422
+ end
423
+
424
+ it 'can get some number of years back' do
425
+ years = subject.years_back(5)
426
+ expect(years.length).to eq 5
427
+ years.each { |y| expect(y).to be_a TimeBoss::Calendar::Year }
428
+ expect(years.map(&:name)).to eq ['2011', '2012', '2013', '2014', '2015']
429
+ end
430
+ end
431
+ end
432
+
433
+ describe '#parse' do
434
+ it 'can parse a year' do
435
+ date = subject.parse('2018')
436
+ expect(date).to be_a TimeBoss::Calendar::Year
437
+ expect(date.name).to eq '2018'
438
+ end
439
+
440
+ it 'can parse a quarter identifier' do
441
+ date = subject.parse('2017Q2')
442
+ expect(date).to be_a TimeBoss::Calendar::Quarter
443
+ expect(date.name).to eq '2017Q2'
444
+ end
445
+
446
+ it 'can parse a month identifier' do
447
+ date = subject.parse('2017M4')
448
+ expect(date).to be_a TimeBoss::Calendar::Month
449
+ expect(date.name).to eq '2017M4'
450
+ end
451
+
452
+ it 'cannot parse a week within a year' do
453
+ expect { subject.parse('2018W37') }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
454
+ end
455
+
456
+ it 'cannot parse a week within a quarter' do
457
+ expect { subject.parse('2017Q2W2') }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
458
+ end
459
+
460
+ it 'cannot parse a week within a month' do
461
+ expect { subject.parse('2017M4W1') }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
462
+ end
463
+
464
+ it 'can parse a date' do
465
+ date = subject.parse('2017-04-08')
466
+ expect(date).to be_a TimeBoss::Calendar::Day
467
+ expect(date.start_date).to eq Date.parse('2017-04-08')
468
+ expect(date.end_date).to eq Date.parse('2017-04-08')
469
+ end
470
+
471
+ it 'can parse an aesthetically displeasing date' do
472
+ date = subject.parse('20170408')
473
+ expect(date).to be_a TimeBoss::Calendar::Day
474
+ expect(date.start_date).to eq Date.parse('2017-04-08')
475
+ expect(date.end_date).to eq Date.parse('2017-04-08')
476
+ end
477
+
478
+ it 'gives you this year if you give it nothing' do
479
+ year = subject.this_year
480
+ expect(subject.parse(nil)).to eq year
481
+ expect(subject.parse('')).to eq year
482
+ end
483
+ end
484
+
485
+ context 'expressions' do
486
+ it 'can parse waypoints' do
487
+ result = subject.parse('this_year')
488
+ expect(result).to be_a TimeBoss::Calendar::Year
489
+ expect(result).to be_current
490
+ end
491
+
492
+ it 'can parse mathematic expressions' do
493
+ result = subject.parse('this_month + 2')
494
+ expect(result).to be_a TimeBoss::Calendar::Month
495
+ expect(result).to eq subject.months_ahead(2)
496
+ end
497
+
498
+ context 'ranges' do
499
+ before(:each) { allow(subject).to receive(:this_year).and_return subject.year(2018) }
500
+ let(:result) { subject.parse('this_year-2 .. this_year') }
501
+
502
+ it 'can parse range expressions' do
503
+ expect(result).to be_a TimeBoss::Calendar::Period
504
+ expect(result.to_s).to eq "2016: 2016-01-01 thru 2016-12-31 .. 2018: 2018-01-01 thru 2018-12-31"
505
+ end
506
+
507
+ it 'can get an overall start date for a range' do
508
+ expect(result.start_date).to eq Date.parse('2016-01-01')
509
+ end
510
+
511
+ it 'can get an overall end date for a range' do
512
+ expect(result.end_date).to eq Date.parse('2018-12-31')
513
+ end
514
+
515
+ context 'sub-periods' do
516
+ it 'can get the months included in a range' do
517
+ entries = result.months
518
+ entries.each { |e| expect(e).to be_a TimeBoss::Calendar::Month }
519
+ expect(entries.map(&:name)).to include('2016M1', '2016M9', '2017M3', '2018M12')
520
+ end
521
+
522
+ it 'cannot get the weeks included in a range' do
523
+ expect { result.weeks }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
524
+ end
525
+
526
+ it 'can get the days included in a range' do
527
+ entries = result.days
528
+ entries.each { |e| expect(e).to be_a TimeBoss::Calendar::Day }
529
+ expect(entries.map(&:name)).to include('2016-01-01', '2016-05-12', '2017-09-22', '2018-12-31')
530
+ end
531
+ end
532
+ end
533
+ end
534
+
535
+ context 'shifting' do
536
+ context 'from day' do
537
+ let(:basis) { subject.parse('2020-04-21') }
538
+
539
+ it 'cannot shift to a different week' do
540
+ expect { basis.last_week }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
541
+ expect { basis.in_week }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
542
+ end
543
+
544
+ it 'can shift to a different quarter' do
545
+ allow(subject).to receive(:this_quarter).and_return subject.parse('2020Q3')
546
+ result = basis.quarters_ago(2)
547
+ expect(result).to be_a TimeBoss::Calendar::Day
548
+ expect(result.to_s).to eq '2020-01-21'
549
+ expect(basis.in_quarter).to eq 21
550
+ end
551
+
552
+ it 'can shift to a different year' do
553
+ allow(subject).to receive(:this_year).and_return subject.parse('2019')
554
+ result = basis.years_ahead(3)
555
+ expect(result).to be_a TimeBoss::Calendar::Day
556
+ expect(result.to_s).to eq '2022-04-22'
557
+ expect(basis.in_year).to eq 112
558
+ end
559
+ end
560
+
561
+ context 'from month' do
562
+ let(:basis) { subject.parse('2017M4') }
563
+
564
+ it 'cannot shift to a different week' do
565
+ expect { basis.last_week }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
566
+ expect { basis.in_week }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
567
+ end
568
+
569
+ it 'can shift to a different year' do
570
+ allow(subject).to receive(:this_year).and_return subject.parse('2020')
571
+ result = basis.years_ahead(4)
572
+ expect(result).to be_a TimeBoss::Calendar::Month
573
+ expect(result.name).to eq '2024M4'
574
+ expect(basis.in_year).to eq 4
575
+ end
576
+ end
577
+
578
+ context 'from quarter' do
579
+ let(:basis) { subject.parse('2018Q2') }
580
+
581
+ it 'cannot shift to a different month' do
582
+ expect(basis.months_ago(4)).to be nil
583
+ expect(basis.in_month).to be nil
584
+ end
585
+
586
+ it 'can shift to a different half' do
587
+ allow(subject).to receive(:this_half).and_return subject.parse('2020H1')
588
+ result = basis.last_half
589
+ expect(result).to be_a TimeBoss::Calendar::Quarter
590
+ expect(result.name).to eq '2019Q4'
591
+ expect(basis.in_half).to eq 2
592
+ end
593
+ end
594
+
595
+ context 'from year' do
596
+ let(:basis) { subject.parse('2014') }
597
+
598
+ it 'cannot shift to a different half' do
599
+ expect(basis.next_half).to be nil
600
+ expect(basis.in_half).to be nil
601
+ end
602
+
603
+ it 'shifts to a different year, but knows how useless that is' do
604
+ allow(subject).to receive(:this_year).and_return subject.parse('2020')
605
+ result = basis.years_ago(2)
606
+ expect(result).to be_a TimeBoss::Calendar::Year
607
+ expect(result.name).to eq '2018'
608
+ expect(basis.in_year).to eq 1
609
+ end
610
+ end
611
+ end
612
+
613
+ context 'units' do
614
+ let(:calendar) { described_class.new }
615
+
616
+ context 'day' do
617
+ let(:start_date) { Date.parse('2019-09-30') }
618
+ let(:subject) { TimeBoss::Calendar::Day.new(calendar, start_date) }
619
+
620
+ context 'links' do
621
+ it 'can get its previous' do
622
+ expect(subject.previous.name).to eq '2019-09-29'
623
+ end
624
+
625
+ it 'can get its next' do
626
+ expect(subject.next.name).to eq '2019-10-01'
627
+ end
628
+
629
+ it 'can offset backwards' do
630
+ expect(subject.offset(-3).name).to eq '2019-09-27'
631
+ expect((subject - 3).name).to eq '2019-09-27'
632
+ end
633
+
634
+ it 'can offset forwards' do
635
+ expect(subject.offset(4).name).to eq '2019-10-04'
636
+ expect((subject + 4).name).to eq '2019-10-04'
637
+ end
638
+ end
639
+ end
640
+
641
+ context 'quarter' do
642
+ let(:start_date) { Date.parse('2019-10-01') }
643
+ let(:end_date) { Date.parse('2019-12-31') }
644
+ let(:subject) { TimeBoss::Calendar::Quarter.new(calendar, 2019, 4, start_date, end_date) }
645
+
646
+ context 'links' do
647
+ it 'can get the next quarter' do
648
+ quarter = subject.next
649
+ expect(quarter.to_s).to include('2020Q1', '2020-01-01', '2020-03-31')
650
+ end
651
+
652
+ it 'can get the next next quarter' do
653
+ quarter = subject.next.next
654
+ expect(quarter.to_s).to include('2020Q2', '2020-04-01', '2020-06-30')
655
+ end
656
+
657
+ it 'can get the next next previous quarter' do
658
+ quarter = subject.next.next.previous
659
+ expect(quarter.to_s).to include('2020Q1', '2020-01-01', '2020-03-31')
660
+ end
661
+
662
+ it 'can get the next previous quarter' do
663
+ quarter = subject.next.previous
664
+ expect(quarter.to_s).to eq subject.to_s
665
+ end
666
+
667
+ it 'can get the previous quarter' do
668
+ quarter = subject.previous
669
+ expect(quarter.to_s).to include('2019Q3', '2019-07-01', '2019-09-30')
670
+ end
671
+
672
+ it 'can offset backwards' do
673
+ expect(subject.offset(-4).name).to eq '2018Q4'
674
+ expect((subject - 4).name).to eq '2018Q4'
675
+ end
676
+
677
+ it 'can offset forwards' do
678
+ expect(subject.offset(2).name).to eq '2020Q2'
679
+ expect((subject + 2).name).to eq '2020Q2'
680
+ end
681
+ end
682
+ end
683
+ end
684
+ end
685
+ end