timeboss 0.0.10 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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