timeboss 0.0.10 → 0.2.2

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.
@@ -5,25 +5,36 @@ module TimeBoss
5
5
  class Calendar
6
6
  class Week < Support::Unit
7
7
  def initialize(calendar, start_date, end_date)
8
+ raise UnsupportedUnitError unless calendar.supports_weeks?
8
9
  super(calendar, start_date, end_date)
9
10
  end
10
11
 
12
+ # Get a simple representation of this week.
13
+ # @return [String] (e.g. "2020W32")
11
14
  def name
12
15
  "#{year_index}W#{index}"
13
16
  end
14
17
 
18
+ # Get a "pretty" representation of this week.
19
+ # @return [String] (e.g. "Week of August 3, 2020")
15
20
  def title
16
21
  "Week of #{start_date.strftime('%B %-d, %Y')}"
17
22
  end
18
23
 
24
+ # Get a stringified representation of this week.
25
+ # @return [String] (e.g. "2020W32: 2020-08-03 thru 2020-08-09")
19
26
  def to_s
20
27
  "#{name}: #{start_date} thru #{end_date}"
21
28
  end
22
29
 
30
+ # Get the index of this week within its containing year.
31
+ # @return [Integer]
23
32
  def index
24
33
  @_index ||= (((start_date - year.start_date) + 1) / 7.0).to_i + 1
25
34
  end
26
35
 
36
+ # Get the year number for this week.
37
+ # @return [Integer] (e.g. 2020)
27
38
  def year_index
28
39
  @_year_index ||= year.year_index
29
40
  end
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
- require_relative './support/month_based'
2
+ require_relative './support/monthly_unit'
3
3
 
4
4
  module TimeBoss
5
5
  class Calendar
6
- class Year < Support::MonthBased
6
+ class Year < Support::MonthlyUnit
7
7
  NUM_MONTHS = 12
8
8
 
9
+ # Get a simple representation of this year.
10
+ # @return [String] (e.g. "2020")
9
11
  def name
10
12
  year_index.to_s
11
13
  end
12
14
 
13
- def title
14
- name
15
- end
15
+ alias_method :title, :name
16
16
  end
17
17
  end
18
18
  end
@@ -10,12 +10,17 @@ module TimeBoss
10
10
  extend Enumerable
11
11
  delegate :each, :length, to: :all
12
12
 
13
+ # Retrieve a list of all registered calendars.
14
+ # @return [Array<Entry>]
13
15
  def all
14
16
  @_all ||= TimeBoss::Calendar.subclasses.map do |klass|
15
17
  Entry.new(klass.to_s.demodulize.underscore.to_sym, klass)
16
18
  end
17
19
  end
18
20
 
21
+ # Retrieve an instance of the specified named calendar.
22
+ # @param name [String, Symbol] the name of the calendar to retrieve.
23
+ # @return [Calendar]
19
24
  def [](name)
20
25
  find { |e| e.name == name&.to_sym }&.calendar
21
26
  end
@@ -23,6 +28,16 @@ module TimeBoss
23
28
  private
24
29
 
25
30
  Entry = Struct.new(:name, :klass) do
31
+ # @!method name
32
+ # Get the name of the calendar referenced in this entry.
33
+ # @return [Symbol]
34
+
35
+ # @!method klass
36
+ # The class implementing this calendar.
37
+ # @return [Class<Calendar>]
38
+
39
+ # Get an instance of the calendar referenced in this entry.
40
+ # @return [Calendar]
26
41
  def calendar
27
42
  @_calendar ||= klass.new
28
43
  end
@@ -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.0.10"
3
+ VERSION = "0.2.2"
4
4
  end
@@ -1,15 +1,15 @@
1
1
  module TimeBoss
2
2
  class Calendar
3
3
  module Support
4
- describe MonthBased do
5
- class ChunkMonthBased < described_class
4
+ describe MonthlyUnit do
5
+ class MonthBasedChunk < described_class
6
6
  NUM_MONTHS = 2
7
7
 
8
8
  def name
9
9
  "#{year_index}C#{index}"
10
10
  end
11
11
  end
12
- let(:described_class) { ChunkMonthBased }
12
+ let(:described_class) { MonthBasedChunk }
13
13
  let(:calendar) { double }
14
14
  let(:start_date) { Date.parse('2018-06-25') }
15
15
  let(:end_date) { Date.parse('2018-08-26') }
@@ -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
@@ -51,24 +57,24 @@ module TimeBoss
51
57
 
52
58
  describe '#previous' do
53
59
  it 'moves easily within itself' do
54
- expect(calendar).to receive(:chunk_month_based).with(48, 3).and_return result
60
+ expect(calendar).to receive(:month_based_chunk).with(48, 3).and_return result
55
61
  expect(described_class.new(calendar, 48, 4, nil, nil).previous).to eq result
56
62
  end
57
63
 
58
64
  it 'flips to the previous container' do
59
- expect(calendar).to receive(:chunk_month_based).with(47, 6).and_return result
65
+ expect(calendar).to receive(:month_based_chunk).with(47, 6).and_return result
60
66
  expect(described_class.new(calendar, 48, 1, nil, nil).previous).to eq result
61
67
  end
62
68
  end
63
69
 
64
70
  describe '#next' do
65
71
  it 'moves easily within itself' do
66
- expect(calendar).to receive(:chunk_month_based).with(48, 3).and_return result
72
+ expect(calendar).to receive(:month_based_chunk).with(48, 3).and_return result
67
73
  expect(described_class.new(calendar, 48, 2, nil, nil).next).to eq result
68
74
  end
69
75
 
70
76
  it 'flips to the previous container' do
71
- expect(calendar).to receive(:chunk_month_based).with(48, 1).and_return result
77
+ expect(calendar).to receive(:month_based_chunk).with(48, 1).and_return result
72
78
  expect(described_class.new(calendar, 47, 6, nil, nil).next).to eq result
73
79
  end
74
80
  end
@@ -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
@@ -133,8 +133,8 @@ module TimeBoss
133
133
  expect(quarters.map(&:name)).to eq ['2015Q3', '2015Q4', '2016Q1', '2016Q2', '2016Q3']
134
134
  end
135
135
 
136
- it 'can get a quarter hence' do
137
- quarter = subject.quarters_hence(4)
136
+ it 'can get a quarter ahead' do
137
+ quarter = subject.quarters_ahead(4)
138
138
  expect(quarter).to be_a TimeBoss::Calendar::Quarter
139
139
  expect(quarter.name).to eq '2016Q3'
140
140
  end
@@ -271,8 +271,8 @@ module TimeBoss
271
271
  expect(months.map(&:name)).to eq ['2015M3', '2015M4', '2015M5', '2015M6', '2015M7']
272
272
  end
273
273
 
274
- it 'can get a month hence' do
275
- month = subject.months_hence(4)
274
+ it 'can get a month ahead' do
275
+ month = subject.months_ahead(4)
276
276
  expect(month).to be_a TimeBoss::Calendar::Month
277
277
  expect(month.name).to eq '2015M7'
278
278
  end
@@ -510,7 +510,7 @@ module TimeBoss
510
510
  it 'can parse mathematic expressions' do
511
511
  result = subject.parse('this_month + 2')
512
512
  expect(result).to be_a TimeBoss::Calendar::Month
513
- expect(result).to eq subject.months_hence(2)
513
+ expect(result).to eq subject.months_ahead(2)
514
514
  end
515
515
 
516
516
  context 'ranges' do
@@ -574,7 +574,7 @@ module TimeBoss
574
574
 
575
575
  it 'can shift to a different year' do
576
576
  allow(subject).to receive(:this_year).and_return subject.parse('2019')
577
- result = basis.years_hence(3)
577
+ result = basis.years_ahead(3)
578
578
  expect(result).to be_a TimeBoss::Calendar::Day
579
579
  expect(result.to_s).to eq '2022-04-19'
580
580
  expect(basis.in_year).to eq 114
@@ -616,7 +616,7 @@ module TimeBoss
616
616
 
617
617
  it 'can shift to a different year' do
618
618
  allow(subject).to receive(:this_year).and_return subject.parse('2020')
619
- result = basis.years_hence(4)
619
+ result = basis.years_ahead(4)
620
620
  expect(result).to be_a TimeBoss::Calendar::Month
621
621
  expect(result.name).to eq '2024M4'
622
622
  expect(basis.in_year).to eq 4
@@ -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