timeboss 0.1.0 → 0.2.3

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