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.
- checksums.yaml +4 -4
- data/.gitignore +5 -3
- data/.yardopts +1 -0
- data/README.md +12 -5
- data/bin/tbsh +1 -1
- data/lib/timeboss.rb +4 -0
- data/lib/timeboss/calendar.rb +21 -0
- data/lib/timeboss/calendar/day.rb +9 -3
- data/lib/timeboss/calendar/half.rb +6 -2
- data/lib/timeboss/calendar/month.rb +6 -2
- data/lib/timeboss/calendar/period.rb +25 -11
- data/lib/timeboss/calendar/quarter.rb +6 -2
- data/lib/timeboss/calendar/support/formatter.rb +4 -2
- data/lib/timeboss/calendar/support/month_basis.rb +1 -0
- data/lib/timeboss/calendar/support/{month_based.rb → monthly_unit.rb} +5 -1
- data/lib/timeboss/calendar/support/navigable.rb +24 -1
- data/lib/timeboss/calendar/support/shiftable.rb +218 -28
- data/lib/timeboss/calendar/support/translatable.rb +92 -0
- data/lib/timeboss/calendar/support/unit.rb +27 -0
- data/lib/timeboss/calendar/waypoints.rb +5 -55
- data/lib/timeboss/calendar/waypoints/absolute.rb +113 -0
- data/lib/timeboss/calendar/waypoints/relative.rb +267 -0
- data/lib/timeboss/calendar/week.rb +11 -0
- data/lib/timeboss/calendar/year.rb +5 -5
- data/lib/timeboss/calendars.rb +15 -0
- data/lib/timeboss/calendars/gregorian.rb +28 -0
- data/lib/timeboss/version.rb +1 -1
- data/spec/calendar/support/{month_based_spec.rb → monthly_unit_spec.rb} +13 -7
- data/spec/calendar/week_spec.rb +6 -1
- data/spec/calendars/broadcast_spec.rb +7 -7
- data/spec/calendars/gregorian_spec.rb +685 -0
- data/timeboss.gemspec +1 -0
- metadata +27 -7
@@ -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/
|
2
|
+
require_relative './support/monthly_unit'
|
3
3
|
|
4
4
|
module TimeBoss
|
5
5
|
class Calendar
|
6
|
-
class Year < Support::
|
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
|
-
|
14
|
-
name
|
15
|
-
end
|
15
|
+
alias_method :title, :name
|
16
16
|
end
|
17
17
|
end
|
18
18
|
end
|
data/lib/timeboss/calendars.rb
CHANGED
@@ -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
|
data/lib/timeboss/version.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
module TimeBoss
|
2
2
|
class Calendar
|
3
3
|
module Support
|
4
|
-
describe
|
5
|
-
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) {
|
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(:
|
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(:
|
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(:
|
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(:
|
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
|
data/spec/calendar/week_spec.rb
CHANGED
@@ -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
|
137
|
-
quarter = subject.
|
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
|
275
|
-
month = subject.
|
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.
|
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.
|
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.
|
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
|