timeboss 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +13 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +207 -0
  8. data/Rakefile +5 -0
  9. data/lib/tasks/calendars.rake +15 -0
  10. data/lib/tasks/timeboss.rake +6 -0
  11. data/lib/timeboss.rb +2 -0
  12. data/lib/timeboss/calendar.rb +28 -0
  13. data/lib/timeboss/calendar/day.rb +36 -0
  14. data/lib/timeboss/calendar/half.rb +18 -0
  15. data/lib/timeboss/calendar/month.rb +18 -0
  16. data/lib/timeboss/calendar/parser.rb +52 -0
  17. data/lib/timeboss/calendar/period.rb +69 -0
  18. data/lib/timeboss/calendar/quarter.rb +18 -0
  19. data/lib/timeboss/calendar/support/formatter.rb +31 -0
  20. data/lib/timeboss/calendar/support/month_based.rb +51 -0
  21. data/lib/timeboss/calendar/support/month_basis.rb +18 -0
  22. data/lib/timeboss/calendar/support/shiftable.rb +51 -0
  23. data/lib/timeboss/calendar/support/unit.rb +59 -0
  24. data/lib/timeboss/calendar/waypoints.rb +87 -0
  25. data/lib/timeboss/calendar/week.rb +45 -0
  26. data/lib/timeboss/calendar/year.rb +18 -0
  27. data/lib/timeboss/calendars.rb +30 -0
  28. data/lib/timeboss/calendars/broadcast.rb +30 -0
  29. data/lib/timeboss/support/shellable.rb +17 -0
  30. data/lib/timeboss/version.rb +4 -0
  31. data/spec/calendar/day_spec.rb +60 -0
  32. data/spec/calendar/quarter_spec.rb +32 -0
  33. data/spec/calendar/support/month_based_spec.rb +69 -0
  34. data/spec/calendar/support/unit_spec.rb +90 -0
  35. data/spec/calendar/week_spec.rb +83 -0
  36. data/spec/calendars/broadcast_spec.rb +777 -0
  37. data/spec/calendars_spec.rb +42 -0
  38. data/spec/spec_helper.rb +12 -0
  39. data/timeboss.gemspec +30 -0
  40. metadata +188 -0
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/core_ext/class/subclasses'
3
+ require './lib/timeboss/calendar'
4
+ Dir['./lib/timeboss/calendars/*.rb'].each { |f| require f }
5
+
6
+ module TimeBoss
7
+ module Calendars
8
+ extend self
9
+ extend Enumerable
10
+ delegate :each, :length, to: :all
11
+
12
+ def all
13
+ @_all ||= TimeBoss::Calendar.subclasses.map do |klass|
14
+ Entry.new(klass.to_s.demodulize.underscore.to_sym, klass)
15
+ end
16
+ end
17
+
18
+ def [](name)
19
+ find { |e| e.name == name.to_sym }&.calendar
20
+ end
21
+
22
+ private
23
+
24
+ Entry = Struct.new(:name, :klass) do
25
+ def calendar
26
+ @_calendar ||= klass.new
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ require_relative '../calendar'
3
+
4
+ module TimeBoss
5
+ module Calendars
6
+ class Broadcast < Calendar
7
+ def initialize
8
+ super(basis: Basis)
9
+ end
10
+
11
+ private
12
+
13
+ class Basis < Calendar::Support::MonthBasis
14
+ def start_date
15
+ @_start_date ||= begin
16
+ date = Date.civil(year, month, 1)
17
+ date - (date.wday + 6) % 7
18
+ end
19
+ end
20
+
21
+ def end_date
22
+ @_end_date ||= begin
23
+ date = Date.civil(year, month, -1)
24
+ date - date.wday
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ module TimeBoss
2
+ module Support
3
+ module Shellable
4
+ def self.open(context)
5
+ context.extend(self).open_shell
6
+ end
7
+
8
+ def open_shell
9
+ require 'irb'
10
+ IRB.setup nil
11
+ IRB.conf[:MAIN_CONTEXT] = IRB::Irb.new.context
12
+ require 'irb/ext/multi-irb'
13
+ IRB.irb nil, self
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module TimeBoss
3
+ VERSION = "0.0.5"
4
+ end
@@ -0,0 +1,60 @@
1
+ module TimeBoss
2
+ class Calendar
3
+ describe Day do
4
+ let(:calendar) { instance_double(TimeBoss::Calendar) }
5
+ let(:start_date) { Date.parse('2019-09-30') }
6
+ let(:subject) { described_class.new(calendar, start_date) }
7
+
8
+ it 'knows its stuff' do
9
+ expect(subject.start_date).to eq start_date
10
+ expect(subject.end_date).to eq start_date
11
+ expect(subject.to_range).to eq start_date..start_date
12
+ end
13
+
14
+ it 'knows its name' do
15
+ expect(subject.name).to eq start_date.to_s
16
+ end
17
+
18
+ it 'knows its title' do
19
+ expect(subject.title).to eq 'September 30, 2019'
20
+ end
21
+
22
+ it 'can stringify itself' do
23
+ expect(subject.to_s).to eq subject.name
24
+ end
25
+
26
+ describe '#index' do
27
+ before(:each) { allow(calendar).to receive(:year_for).with(start_date).and_return double(start_date: start_date - 3) }
28
+
29
+ it 'gets its index within the year' do
30
+ expect(subject.index).to eq 4
31
+ end
32
+ end
33
+
34
+ describe '#current?' do
35
+ it 'knows when it is' do
36
+ allow(Date).to receive(:today).and_return start_date
37
+ expect(subject).to be_current
38
+ end
39
+
40
+ it 'knows when it is not' do
41
+ expect(subject).not_to be_current
42
+ end
43
+ end
44
+
45
+ context 'navigation' do
46
+ it 'can get the previous date' do
47
+ result = subject.previous
48
+ expect(result).to be_a described_class
49
+ expect(result.start_date).to eq start_date - 1.day
50
+ end
51
+
52
+ it 'can get the next date' do
53
+ result = subject.next
54
+ expect(result).to be_a described_class
55
+ expect(result.start_date).to eq start_date + 1.day
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,32 @@
1
+ module TimeBoss
2
+ class Calendar
3
+ describe Quarter do
4
+ let(:calendar) { instance_double(TimeBoss::Calendar) }
5
+ let(:start_date) { Date.parse('2019-09-30') }
6
+ let(:end_date) { Date.parse('2019-12-29') }
7
+ let(:subject) { described_class.new(calendar, 2019, 4, start_date, end_date) }
8
+
9
+ it 'knows its stuff' do
10
+ expect(subject.name).to eq '2019Q4'
11
+ expect(subject.start_date).to eq start_date
12
+ expect(subject.end_date).to eq end_date
13
+ expect(subject.to_range).to eq start_date..end_date
14
+ end
15
+
16
+ it 'can stringify itself' do
17
+ expect(subject.to_s).to include('2019Q4', start_date.to_s, end_date.to_s)
18
+ end
19
+
20
+ describe '#current?' do
21
+ it 'knows when it is' do
22
+ allow(Date).to receive(:today).and_return start_date
23
+ expect(subject).to be_current
24
+ end
25
+
26
+ it 'knows when it is not' do
27
+ expect(subject).not_to be_current
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,69 @@
1
+ module TimeBoss
2
+ class Calendar
3
+ module Support
4
+ describe MonthBased do
5
+ class ChunkMonthBased < described_class
6
+ NUM_MONTHS = 2
7
+
8
+ def name
9
+ "#{year_index}C#{index}"
10
+ end
11
+ end
12
+ let(:described_class) { ChunkMonthBased }
13
+ let(:calendar) { double }
14
+ let(:start_date) { Date.parse('2018-06-25') }
15
+ let(:end_date) { Date.parse('2018-08-26') }
16
+ let(:subject) { described_class.new(calendar, 2018, 4, start_date, end_date) }
17
+
18
+ it 'knows its stuff' do
19
+ expect(subject.start_date).to eq start_date
20
+ expect(subject.end_date).to eq end_date
21
+ expect(subject.to_range).to eq start_date..end_date
22
+ end
23
+
24
+ it 'can stringify itself' do
25
+ expect(subject.to_s).to eq "2018C4: 2018-06-25 thru 2018-08-26"
26
+ end
27
+
28
+ describe '#weeks' do
29
+ let(:base) { double(start_date: Date.parse('2018-01-01'), end_date: Date.parse('2018-12-30')) }
30
+ before(:each) { allow(calendar).to receive(:year).with(2018).and_return base }
31
+
32
+ it 'can get the relevant weeks for the period' do
33
+ result = subject.weeks
34
+ result.each { |w| expect(w).to be_instance_of TimeBoss::Calendar::Week }
35
+ expect(result.map(&:name)).to eq %w[2018W26 2018W27 2018W28 2018W29 2018W30 2018W31 2018W32 2018W33 2018W34]
36
+ end
37
+ end
38
+
39
+ context 'navigation' do
40
+ let(:result) { double }
41
+
42
+ describe '#previous' do
43
+ it 'moves easily within itself' do
44
+ expect(calendar).to receive(:chunk_month_based).with(48, 3).and_return result
45
+ expect(described_class.new(calendar, 48, 4, nil, nil).previous).to eq result
46
+ end
47
+
48
+ it 'flips to the previous container' do
49
+ expect(calendar).to receive(:chunk_month_based).with(47, 6).and_return result
50
+ expect(described_class.new(calendar, 48, 1, nil, nil).previous).to eq result
51
+ end
52
+ end
53
+
54
+ describe '#next' do
55
+ it 'moves easily within itself' do
56
+ expect(calendar).to receive(:chunk_month_based).with(48, 3).and_return result
57
+ expect(described_class.new(calendar, 48, 2, nil, nil).next).to eq result
58
+ end
59
+
60
+ it 'flips to the previous container' do
61
+ expect(calendar).to receive(:chunk_month_based).with(48, 1).and_return result
62
+ expect(described_class.new(calendar, 47, 6, nil, nil).next).to eq result
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,90 @@
1
+ module TimeBoss
2
+ class Calendar
3
+ module Support
4
+ describe Unit do
5
+ class ChunkUnit < described_class
6
+ end
7
+ let(:described_class) { ChunkUnit }
8
+ let(:calendar) { double }
9
+ let(:start_date) { Date.parse('2018-06-25') }
10
+ let(:end_date) { Date.parse('2018-08-26') }
11
+ let(:subject) { described_class.new(calendar, start_date, end_date) }
12
+
13
+ it 'knows its stuff' do
14
+ expect(subject.start_date).to eq start_date
15
+ expect(subject.end_date).to eq end_date
16
+ expect(subject.to_range).to eq start_date..end_date
17
+ end
18
+
19
+ describe '#current?' do
20
+ it 'is not current right now' do
21
+ expect(subject).not_to be_current
22
+ end
23
+
24
+ it 'is current when today falls in the middle' do
25
+ allow(Date).to receive(:today).and_return start_date + 3.days
26
+ expect(subject).to be_current
27
+ end
28
+ end
29
+
30
+ context 'periods' do
31
+ before(:each) do
32
+ allow(calendar).to receive(:days_for).with(subject).and_return %w[D1 D2 D3 D4 D5 D6 D7 D8]
33
+ allow(calendar).to receive(:weeks_for).with(subject).and_return %w[W1 W2 W3 W4]
34
+ allow(calendar).to receive(:months_for).with(subject).and_return %w[M1 M2 M3]
35
+ allow(calendar).to receive(:quarters_for).with(subject).and_return %w[Q1 Q2]
36
+ allow(calendar).to receive(:halves_for).with(subject).and_return %w[H1]
37
+ allow(calendar).to receive(:years_for).with(subject).and_return %w[Y1]
38
+ end
39
+
40
+ it 'knows about its days' do
41
+ expect(subject.days).to eq %w[D1 D2 D3 D4 D5 D6 D7 D8]
42
+ expect(subject.day).to be nil
43
+ end
44
+
45
+ it 'knows about its weeks' do
46
+ expect(subject.weeks).to eq %w[W1 W2 W3 W4]
47
+ expect(subject.week).to be nil
48
+ end
49
+
50
+ it 'knows about its months' do
51
+ expect(subject.months).to eq %w[M1 M2 M3]
52
+ expect(subject.month).to be nil
53
+ end
54
+
55
+ it 'knows about its quarters' do
56
+ expect(subject.quarters).to eq %w[Q1 Q2]
57
+ expect(subject.quarter).to be nil
58
+ end
59
+
60
+ it 'knows about its halves' do
61
+ expect(subject.halves).to eq %w[H1]
62
+ expect(subject.half).to eq 'H1'
63
+ end
64
+
65
+ it 'knows about its years' do
66
+ expect(subject.years).to eq %w[Y1]
67
+ expect(subject.year).to eq 'Y1'
68
+ end
69
+ end
70
+
71
+ context 'navigation' do
72
+ let(:result) { double }
73
+
74
+ describe '#offset' do
75
+ end
76
+
77
+ it 'can increment' do
78
+ expect(subject).to receive(:offset).with(7).and_return result
79
+ expect(subject + 7).to eq result
80
+ end
81
+
82
+ it 'can decrement' do
83
+ expect(subject).to receive(:offset).with(-23).and_return result
84
+ expect(subject - 23).to eq result
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,83 @@
1
+ module TimeBoss
2
+ class Calendar
3
+ describe Week do
4
+ let(:calendar) { instance_double(TimeBoss::Calendar) }
5
+ let(:start_date) { Date.parse('2048-04-06') }
6
+ let(:end_date) { Date.parse('2048-04-12') }
7
+ let(:subject) { described_class.new(calendar, 2048, 15, start_date, end_date) }
8
+
9
+ it 'knows its stuff' do
10
+ expect(subject.start_date).to eq start_date
11
+ expect(subject.end_date).to eq end_date
12
+ expect(subject.to_range).to eq start_date..end_date
13
+ end
14
+
15
+ it 'knows its name' do
16
+ expect(subject.name).to eq '2048W15'
17
+ end
18
+
19
+ it 'knows its title' do
20
+ expect(subject.title).to eq "Week of April 6, 2048"
21
+ end
22
+
23
+ it 'can stringify itself' do
24
+ expect(subject.to_s).to include(subject.name, start_date.to_s, end_date.to_s)
25
+ end
26
+
27
+ describe '#current?' do
28
+ it 'knows when it is' do
29
+ allow(Date).to receive(:today).and_return start_date
30
+ expect(subject).to be_current
31
+ end
32
+
33
+ it 'knows when it is not' do
34
+ expect(subject).not_to be_current
35
+ end
36
+ end
37
+
38
+ context 'navigation' do
39
+ let(:calendar) { TimeBoss::Calendars::Broadcast.new }
40
+
41
+ describe '#previous' do
42
+ it 'can back up simply' do
43
+ result = subject.previous
44
+ expect(result).to be_a described_class
45
+ expect(result.to_s).to eq "2048W14: 2048-03-30 thru 2048-04-05"
46
+ end
47
+
48
+ it 'can wrap to the previous 52-week year' do
49
+ result = described_class.new(calendar, 2022, 1, Date.parse('2021-12-27'), Date.parse('2022-01-02')).previous
50
+ expect(result).to be_a described_class
51
+ expect(result.to_s).to eq "2021W52: 2021-12-20 thru 2021-12-26"
52
+ end
53
+
54
+ it 'can wrap to the previous 53-week year' do
55
+ result = described_class.new(calendar, 2024, 1, Date.parse('2024-01-01'), Date.parse('2024-01-07')).previous
56
+ expect(result).to be_a described_class
57
+ expect(result.to_s).to eq "2023W53: 2023-12-25 thru 2023-12-31"
58
+ end
59
+ end
60
+
61
+ describe '#next' do
62
+ it 'can move forward simply' do
63
+ result = subject.next
64
+ expect(result).to be_a described_class
65
+ expect(result.to_s).to eq "2048W16: 2048-04-13 thru 2048-04-19"
66
+ end
67
+
68
+ it 'can wrap from week 52 to the next year' do
69
+ result = described_class.new(calendar, 2021, 52, Date.parse('2021-12-20'), Date.parse('2021-12-26')).next
70
+ expect(result).to be_a described_class
71
+ expect(result.to_s).to eq "2022W1: 2021-12-27 thru 2022-01-02"
72
+ end
73
+
74
+ it 'can wrap from week 53 to the next year' do
75
+ result = described_class.new(calendar, 2023, 53, Date.parse('2023-12-25'), Date.parse('2023-12-31')).next
76
+ expect(result).to be_a described_class
77
+ expect(result.to_s).to eq "2024W1: 2024-01-01 thru 2024-01-07"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,777 @@
1
+ module TimeBoss
2
+ describe Calendars::Broadcast do
3
+ let(:subject) { described_class.new }
4
+
5
+ context 'quarters' do
6
+ describe '#quarter' do
7
+ it 'knows 2017Q2' do
8
+ quarter = subject.quarter(2017, 2)
9
+ expect(quarter.name).to eq '2017Q2'
10
+ expect(quarter.title).to eq 'Q2 2017'
11
+ expect(quarter.year_index).to eq 2017
12
+ expect(quarter.index).to eq 2
13
+ expect(quarter.start_date).to eq Date.parse('2017-03-27')
14
+ expect(quarter.end_date).to eq Date.parse('2017-06-25')
15
+ expect(quarter.to_range).to eq quarter.start_date..quarter.end_date
16
+ end
17
+
18
+ it 'knows 2018Q3' do
19
+ quarter = subject.quarter(2018, 3)
20
+ expect(quarter.name).to eq '2018Q3'
21
+ expect(quarter.title).to eq 'Q3 2018'
22
+ expect(quarter.year_index).to eq 2018
23
+ expect(quarter.index).to eq 3
24
+ expect(quarter.start_date).to eq Date.parse('2018-06-25')
25
+ expect(quarter.end_date).to eq Date.parse('2018-09-30')
26
+ expect(quarter.to_range).to eq quarter.start_date..quarter.end_date
27
+ end
28
+
29
+ it 'knows 2019Q4' do
30
+ quarter = subject.quarter(2019, 4)
31
+ expect(quarter.year_index).to eq 2019
32
+ expect(quarter.index).to eq 4
33
+ expect(quarter.name).to eq '2019Q4'
34
+ expect(quarter.title).to eq 'Q4 2019'
35
+ expect(quarter.start_date).to eq Date.parse('2019-09-30')
36
+ expect(quarter.end_date).to eq Date.parse('2019-12-29')
37
+ expect(quarter.to_range).to eq quarter.start_date..quarter.end_date
38
+ end
39
+ end
40
+
41
+ describe '#quarter_for' do
42
+ it 'knows what quarter 2018-06-27 is in' do
43
+ quarter = subject.quarter_for(Date.parse('2018-06-27'))
44
+ expect(quarter.name).to eq '2018Q3'
45
+ end
46
+
47
+ it 'knows what quarter 2018-06-22 is in' do
48
+ quarter = subject.quarter_for(Date.parse('2018-06-22'))
49
+ expect(quarter.name).to eq '2018Q2'
50
+ end
51
+ end
52
+
53
+ describe '#quarters_for' do
54
+ it 'knows what quarters are in 2020' do
55
+ basis = subject.year(2020)
56
+ periods = subject.quarters_for(basis)
57
+ expect(periods.map(&:name)).to eq %w[2020Q1 2020Q2 2020Q3 2020Q4]
58
+ end
59
+
60
+ it 'knows what quarter 2018M7 is in' do
61
+ basis = subject.month(2018, 7)
62
+ periods = subject.quarters_for(basis)
63
+ expect(periods.map(&:name)).to eq %w[2018Q3]
64
+ end
65
+ end
66
+
67
+ describe '#this_quarter' do
68
+ let(:today) { double }
69
+ let(:quarter) { double }
70
+
71
+ it 'gets the quarter for today' do
72
+ allow(Date).to receive(:today).and_return today
73
+ expect(subject).to receive(:quarter_for).with(today).and_return quarter
74
+ expect(subject.this_quarter).to eq quarter
75
+ end
76
+ end
77
+
78
+ describe '#format' do
79
+ let(:entry) { subject.quarter(2015, 3) }
80
+
81
+ it 'can do a default format' do
82
+ expect(entry.format).to eq '2015H2Q1'
83
+ end
84
+
85
+ it 'can format with only the quarter' do
86
+ expect(entry.format(:quarter)).to eq '2015Q3'
87
+ end
88
+
89
+ it 'ignores stupidity' do
90
+ expect(entry.format(:day, :banana)).to eq '2015Q3'
91
+ end
92
+ end
93
+
94
+ context 'relative' do
95
+ let(:this_quarter) { subject.quarter(2015, 3) }
96
+ let(:quarter) { double }
97
+ before(:each) { allow(subject).to receive(:this_quarter).and_return this_quarter }
98
+
99
+ it 'can get the last quarter' do
100
+ allow(this_quarter).to receive(:previous).and_return quarter
101
+ expect(subject.last_quarter).to eq quarter
102
+ end
103
+
104
+ it 'can get the next quarter' do
105
+ allow(this_quarter).to receive(:next).and_return quarter
106
+ expect(subject.next_quarter).to eq quarter
107
+ end
108
+
109
+ it 'can get some number of quarters' do
110
+ quarters = subject.quarters(5)
111
+ expect(quarters.length).to eq 5
112
+ quarters.each { |q| expect(q).to be_a TimeBoss::Calendar::Quarter }
113
+ expect(quarters.map(&:name)).to eq ['2015Q3', '2015Q4', '2016Q1', '2016Q2', '2016Q3']
114
+ end
115
+
116
+ it 'can get a quarter hence' do
117
+ quarter = subject.quarters_hence(4)
118
+ expect(quarter).to be_a TimeBoss::Calendar::Quarter
119
+ expect(quarter.name).to eq '2016Q3'
120
+ end
121
+
122
+ it 'can get some number of quarters back' do
123
+ quarters = subject.quarters_back(5)
124
+ expect(quarters.length).to eq 5
125
+ quarters.each { |q| expect(q).to be_a TimeBoss::Calendar::Quarter }
126
+ expect(quarters.map(&:name)).to eq ['2014Q3', '2014Q4', '2015Q1', '2015Q2', '2015Q3']
127
+ end
128
+
129
+ it 'can get a quarter ago' do
130
+ quarter = subject.quarters_ago(4)
131
+ expect(quarter).to be_a TimeBoss::Calendar::Quarter
132
+ expect(quarter.name).to eq '2014Q3'
133
+ end
134
+ end
135
+ end
136
+
137
+ context 'months' do
138
+ describe '#month' do
139
+ it 'knows 2017M2' do
140
+ month = subject.month(2017, 2)
141
+ expect(month.name).to eq '2017M2'
142
+ expect(month.title).to eq 'February 2017'
143
+ expect(month.year_index).to eq 2017
144
+ expect(month.index).to eq 2
145
+ expect(month.start_date).to eq Date.parse('2017-01-30')
146
+ expect(month.end_date).to eq Date.parse('2017-02-26')
147
+ expect(month.to_range).to eq month.start_date..month.end_date
148
+ end
149
+
150
+ it 'knows 2018M3' do
151
+ month = subject.month(2018, 3)
152
+ expect(month.name).to eq '2018M3'
153
+ expect(month.title).to eq 'March 2018'
154
+ expect(month.year_index).to eq 2018
155
+ expect(month.index).to eq 3
156
+ expect(month.start_date).to eq Date.parse('2018-02-26')
157
+ expect(month.end_date).to eq Date.parse('2018-03-25')
158
+ expect(month.to_range).to eq month.start_date..month.end_date
159
+ end
160
+
161
+ it 'knows 2019M11' do
162
+ month = subject.month(2019, 11)
163
+ expect(month.year_index).to eq 2019
164
+ expect(month.index).to eq 11
165
+ expect(month.name).to eq '2019M11'
166
+ expect(month.title).to eq 'November 2019'
167
+ expect(month.start_date).to eq Date.parse('2019-10-28')
168
+ expect(month.end_date).to eq Date.parse('2019-11-24')
169
+ expect(month.to_range).to eq month.start_date..month.end_date
170
+ end
171
+ end
172
+
173
+ describe '#month_for' do
174
+ it 'knows what month 2018-06-27 is in' do
175
+ month = subject.month_for(Date.parse('2018-06-27'))
176
+ expect(month.name).to eq '2018M7'
177
+ end
178
+
179
+ it 'knows what month 2018-06-22 is in' do
180
+ month = subject.month_for(Date.parse('2018-06-22'))
181
+ expect(month.name).to eq '2018M6'
182
+ end
183
+ end
184
+
185
+ describe '#months_for' do
186
+ it 'knows what months are in 2020' do
187
+ basis = subject.year(2020)
188
+ periods = subject.months_for(basis)
189
+ expect(periods.map(&:name)).to eq %w[2020M1 2020M2 2020M3 2020M4 2020M5 2020M6 2020M7 2020M8 2020M9 2020M10 2020M11 2020M12]
190
+ end
191
+
192
+ it 'knows what months are in 2018Q2' do
193
+ basis = subject.parse('2018Q2')
194
+ periods = subject.months_for(basis)
195
+ expect(periods.map(&:name)).to eq %w[2018M4 2018M5 2018M6]
196
+ end
197
+
198
+ it 'knows what month 2019-12-12 is in' do
199
+ basis = subject.parse('2019-12-12')
200
+ periods = subject.months_for(basis)
201
+ expect(periods.map(&:name)).to eq %w[2019M12]
202
+ end
203
+ end
204
+
205
+ describe '#this_month' do
206
+ let(:today) { double }
207
+ let(:month) { double }
208
+
209
+ it 'gets the month for today' do
210
+ allow(Date).to receive(:today).and_return today
211
+ expect(subject).to receive(:month_for).with(today).and_return month
212
+ expect(subject.this_month).to eq month
213
+ end
214
+ end
215
+
216
+ describe '#format' do
217
+ let(:entry) { subject.month(2015, 8) }
218
+
219
+ it 'can do a default format' do
220
+ expect(entry.format).to eq '2015H2Q1M2'
221
+ end
222
+
223
+ it 'can format with only the quarter' do
224
+ expect(entry.format(:quarter)).to eq '2015Q3M2'
225
+ end
226
+
227
+ it 'ignores stupidity' do
228
+ expect(entry.format(:banana, :half, :week)).to eq '2015H2M2'
229
+ end
230
+ end
231
+
232
+ context 'relative' do
233
+ let(:this_month) { subject.month(2015, 3) }
234
+ let(:month) { double }
235
+ before(:each) { allow(subject).to receive(:this_month).and_return this_month }
236
+
237
+ it 'can get the last month' do
238
+ allow(this_month).to receive(:previous).and_return month
239
+ expect(subject.last_month).to eq month
240
+ end
241
+
242
+ it 'can get the next month' do
243
+ allow(this_month).to receive(:next).and_return month
244
+ expect(subject.next_month).to eq month
245
+ end
246
+
247
+ it 'can get some number of months' do
248
+ months = subject.months(5)
249
+ expect(months.length).to eq 5
250
+ months.each { |m| expect(m).to be_a TimeBoss::Calendar::Month }
251
+ expect(months.map(&:name)).to eq ['2015M3', '2015M4', '2015M5', '2015M6', '2015M7']
252
+ end
253
+
254
+ it 'can get a month hence' do
255
+ month = subject.months_hence(4)
256
+ expect(month).to be_a TimeBoss::Calendar::Month
257
+ expect(month.name).to eq '2015M7'
258
+ end
259
+
260
+ it 'can get some number of months back' do
261
+ months = subject.months_back(5)
262
+ expect(months.length).to eq 5
263
+ months.each { |m| expect(m).to be_a TimeBoss::Calendar::Month }
264
+ expect(months.map(&:name)).to eq ['2014M11', '2014M12', '2015M1', '2015M2', '2015M3']
265
+ end
266
+
267
+ it 'can get a month ago' do
268
+ month = subject.months_ago(4)
269
+ expect(month).to be_a TimeBoss::Calendar::Month
270
+ expect(month.name).to eq '2014M11'
271
+ end
272
+ end
273
+ end
274
+
275
+ context 'weeks' do
276
+ before(:each) { allow(Date).to receive(:today).and_return Date.parse('2019-08-23') }
277
+
278
+ it 'knows this week 'do
279
+ expect(subject.this_week.name).to eq '2019W34'
280
+ expect(subject.this_week.title).to eq 'Week of August 19, 2019'
281
+ end
282
+
283
+ it 'knows last week' do
284
+ expect(subject.last_week.name).to eq '2019W33'
285
+ expect(subject.last_week.title).to eq 'Week of August 12, 2019'
286
+ end
287
+
288
+ it 'knows next week' do
289
+ expect(subject.next_week.name).to eq '2019W35'
290
+ expect(subject.next_week.title).to eq 'Week of August 26, 2019'
291
+ end
292
+ end
293
+
294
+ context 'years' do
295
+ describe '#year' do
296
+ it 'knows 2016' do
297
+ year = subject.year(2016)
298
+ expect(year.name).to eq '2016'
299
+ expect(year.title).to eq '2016'
300
+ expect(year.year_index).to eq 2016
301
+ expect(year.index).to eq 1
302
+ expect(year.start_date).to eq Date.parse('2015-12-28')
303
+ expect(year.end_date).to eq Date.parse('2016-12-25')
304
+ expect(year.to_range).to eq year.start_date..year.end_date
305
+ end
306
+
307
+ it 'knows 2017' do
308
+ year = subject.year(2017)
309
+ expect(year.name).to eq '2017'
310
+ expect(year.title).to eq '2017'
311
+ expect(year.year_index).to eq 2017
312
+ expect(year.index).to eq 1
313
+ expect(year.start_date).to eq Date.parse('2016-12-26')
314
+ expect(year.end_date).to eq Date.parse('2017-12-31')
315
+ expect(year.to_range).to eq year.start_date..year.end_date
316
+ end
317
+
318
+ it 'knows 2018' do
319
+ year = subject.year(2018)
320
+ expect(year.name).to eq '2018'
321
+ expect(year.title).to eq '2018'
322
+ expect(year.year_index).to eq 2018
323
+ expect(year.index).to eq 1
324
+ expect(year.start_date).to eq Date.parse('2018-01-01')
325
+ expect(year.end_date).to eq Date.parse('2018-12-30')
326
+ expect(year.to_range).to eq year.start_date..year.end_date
327
+ end
328
+ end
329
+
330
+ describe '#year_for' do
331
+ it 'knows what year 2018-04-07 is in' do
332
+ year = subject.year_for(Date.parse('2018-04-07'))
333
+ expect(year.name).to eq '2018'
334
+ end
335
+
336
+ it 'knows what year 2016-12-27 is in' do
337
+ year = subject.year_for(Date.parse('2016-12-27'))
338
+ expect(year.name).to eq '2017'
339
+ end
340
+ end
341
+
342
+ describe '#years_for' do
343
+ it 'knows what years are in 2020 (duh)' do
344
+ basis = subject.year(2020)
345
+ periods = subject.years_for(basis)
346
+ expect(periods.map(&:name)).to eq %w[2020]
347
+ end
348
+
349
+ it 'knows what year 2018Q2 is in' do
350
+ basis = subject.parse('2018Q2')
351
+ periods = subject.years_for(basis)
352
+ expect(periods.map(&:name)).to eq %w[2018]
353
+ end
354
+
355
+ it 'knows what years 2019-12-12 is in' do
356
+ basis = subject.parse('2019-12-12')
357
+ periods = subject.years_for(basis)
358
+ expect(periods.map(&:name)).to eq %w[2019]
359
+ end
360
+ end
361
+
362
+ describe '#this_year' do
363
+ let(:today) { double }
364
+ let(:year) { double }
365
+
366
+ it 'gets the year for today' do
367
+ allow(Date).to receive(:today).and_return today
368
+ expect(subject).to receive(:year_for).with(today).and_return year
369
+ expect(subject.this_year).to eq year
370
+ end
371
+ end
372
+
373
+ describe '#format' do
374
+ let(:entry) { subject.parse('2020W24') }
375
+
376
+ it 'can do a default format' do
377
+ expect(entry.format).to eq '2020H1Q2M3W2'
378
+ end
379
+
380
+ it 'can format with only the quarter' do
381
+ expect(entry.format(:quarter)).to eq '2020Q2W11'
382
+ end
383
+
384
+ it 'can format with only the quarter + month' do
385
+ expect(entry.format(:quarter, :month)).to eq '2020Q2M3W2'
386
+ expect(entry.format(:month, :quarter)).to eq '2020Q2M3W2'
387
+ end
388
+
389
+ it 'ignores stupidity' do
390
+ expect(entry.format(:day, :month, :banana)).to eq '2020M6W2'
391
+ end
392
+ end
393
+
394
+ context 'relative' do
395
+ let(:this_year) { subject.year(2015) }
396
+ let(:year) { double }
397
+ before(:each) { allow(subject).to receive(:this_year).and_return this_year }
398
+
399
+ it 'can get the last year' do
400
+ allow(this_year).to receive(:previous).and_return year
401
+ expect(subject.last_year).to eq year
402
+ end
403
+
404
+ it 'can get the next year' do
405
+ allow(this_year).to receive(:next).and_return year
406
+ expect(subject.next_year).to eq year
407
+ end
408
+
409
+ it 'can get some number of years' do
410
+ years = subject.years(5)
411
+ expect(years.length).to eq 5
412
+ years.each { |y| expect(y).to be_a TimeBoss::Calendar::Year }
413
+ expect(years.map(&:name)).to eq ['2015', '2016', '2017', '2018', '2019']
414
+ end
415
+
416
+ it 'can get some number of years back' do
417
+ years = subject.years_back(5)
418
+ expect(years.length).to eq 5
419
+ years.each { |y| expect(y).to be_a TimeBoss::Calendar::Year }
420
+ expect(years.map(&:name)).to eq ['2011', '2012', '2013', '2014', '2015']
421
+ end
422
+ end
423
+ end
424
+
425
+ describe '#parse' do
426
+ it 'can parse a year' do
427
+ date = subject.parse('2018')
428
+ expect(date).to be_a TimeBoss::Calendar::Year
429
+ expect(date.name).to eq '2018'
430
+ end
431
+
432
+ it 'can parse a quarter identifier' do
433
+ date = subject.parse('2017Q2')
434
+ expect(date).to be_a TimeBoss::Calendar::Quarter
435
+ expect(date.name).to eq '2017Q2'
436
+ end
437
+
438
+ it 'can parse a month identifier' do
439
+ date = subject.parse('2017M4')
440
+ expect(date).to be_a TimeBoss::Calendar::Month
441
+ expect(date.name).to eq '2017M4'
442
+ end
443
+
444
+ it 'can parse a week within a year' do
445
+ date = subject.parse('2018W37')
446
+ expect(date).to be_a TimeBoss::Calendar::Week
447
+ expect(date.name).to eq '2018W37'
448
+ end
449
+
450
+ it 'can parse a week within a quarter' do
451
+ date = subject.parse('2017Q2W2')
452
+ expect(date).to be_a TimeBoss::Calendar::Week
453
+ expect(date.name).to eq '2017W15'
454
+ end
455
+
456
+ it 'can parse a week within a month' do
457
+ date = subject.parse('2017M4W1')
458
+ expect(date).to be_a TimeBoss::Calendar::Week
459
+ expect(date.name).to eq '2017W14'
460
+ end
461
+
462
+ it 'can parse a date' do
463
+ date = subject.parse('2017-04-08')
464
+ expect(date).to be_a TimeBoss::Calendar::Day
465
+ expect(date.start_date).to eq Date.parse('2017-04-08')
466
+ expect(date.end_date).to eq Date.parse('2017-04-08')
467
+ end
468
+
469
+ it 'can parse an aesthetically displeasing date' do
470
+ date = subject.parse('20170408')
471
+ expect(date).to be_a TimeBoss::Calendar::Day
472
+ expect(date.start_date).to eq Date.parse('2017-04-08')
473
+ expect(date.end_date).to eq Date.parse('2017-04-08')
474
+ end
475
+
476
+ it 'gives you this year if you give it nothing' do
477
+ year = subject.this_year
478
+ expect(subject.parse(nil)).to eq year
479
+ expect(subject.parse('')).to eq year
480
+ end
481
+ end
482
+
483
+ context 'expressions' do
484
+ it 'can parse waypoints' do
485
+ result = subject.parse('this_year')
486
+ expect(result).to be_a TimeBoss::Calendar::Year
487
+ expect(result).to be_current
488
+ end
489
+
490
+ it 'can parse mathematic expressions' do
491
+ result = subject.parse('this_month + 2')
492
+ expect(result).to be_a TimeBoss::Calendar::Month
493
+ expect(result).to eq subject.months_hence(2)
494
+ end
495
+
496
+ context 'ranges' do
497
+ before(:each) { allow(subject).to receive(:this_year).and_return subject.year(2018) }
498
+ let(:result) { subject.parse('this_year-2 .. this_year') }
499
+
500
+ it 'can parse range expressions' do
501
+ expect(result).to be_a TimeBoss::Calendar::Period
502
+ expect(result.to_s).to eq "2016: 2015-12-28 thru 2016-12-25 .. 2018: 2018-01-01 thru 2018-12-30"
503
+ end
504
+
505
+ it 'can get an overall start date for a range' do
506
+ expect(result.start_date).to eq Date.parse('2015-12-28')
507
+ end
508
+
509
+ it 'can get an overall end date for a range' do
510
+ expect(result.end_date).to eq Date.parse('2018-12-30')
511
+ end
512
+
513
+ context 'sub-periods' do
514
+ it 'can get the months included in a range' do
515
+ entries = result.months
516
+ entries.each { |e| expect(e).to be_a TimeBoss::Calendar::Month }
517
+ expect(entries.map(&:name)).to include('2016M1', '2016M9', '2017M3', '2018M12')
518
+ end
519
+
520
+ it 'can get the weeks included in a range' do
521
+ entries = result.weeks
522
+ entries.each { |e| expect(e).to be_a TimeBoss::Calendar::Week }
523
+ expect(entries.map(&:name)).to include('2016W1', '2016W38', '2017W15', '2017W53', '2018W52')
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('2015-12-28', '2016-04-30', '2017-09-22', '2018-12-30')
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 'can shift to a different week' do
540
+ allow(subject).to receive(:this_week).and_return subject.parse('2020W23')
541
+ result = basis.last_week
542
+ expect(result).to be_a TimeBoss::Calendar::Day
543
+ expect(result.to_s).to eq '2020-05-26'
544
+ expect(basis.in_week).to eq 2
545
+ end
546
+
547
+ it 'can shift to a different quarter' do
548
+ allow(subject).to receive(:this_quarter).and_return subject.parse('2020Q3')
549
+ result = basis.quarters_ago(2)
550
+ expect(result).to be_a TimeBoss::Calendar::Day
551
+ expect(result.to_s).to eq '2020-01-21'
552
+ expect(basis.in_quarter).to eq 23
553
+ end
554
+
555
+ it 'can shift to a different year' do
556
+ allow(subject).to receive(:this_year).and_return subject.parse('2019')
557
+ result = basis.years_hence(3)
558
+ expect(result).to be_a TimeBoss::Calendar::Day
559
+ expect(result.to_s).to eq '2022-04-19'
560
+ expect(basis.in_year).to eq 114
561
+ end
562
+ end
563
+
564
+ context 'from week' do
565
+ let(:basis) { subject.parse('2017W8') }
566
+
567
+ it 'cannot shift to a different day' do
568
+ expect(basis.last_day).to be nil
569
+ expect(basis.in_day).to be nil
570
+ end
571
+
572
+ it 'can shift to a different month' do
573
+ allow(subject).to receive(:this_month).and_return subject.parse('2020M4')
574
+ result = basis.next_month
575
+ expect(result).to be_a TimeBoss::Calendar::Week
576
+ expect(result.to_s).to eq '2020W20: 2020-05-11 thru 2020-05-17'
577
+ expect(basis.in_month).to eq 3
578
+ end
579
+
580
+ it 'can shift to a different half' do
581
+ allow(subject).to receive(:this_half).and_return subject.parse('2019H1')
582
+ result = basis.last_half
583
+ expect(result).to be_a TimeBoss::Calendar::Week
584
+ expect(result.to_s).to eq '2018W33: 2018-08-13 thru 2018-08-19'
585
+ expect(basis.in_half).to eq 8
586
+ end
587
+ end
588
+
589
+ context 'from month' do
590
+ let(:basis) { subject.parse('2017M4') }
591
+
592
+ it 'cannot shift to a different week' do
593
+ expect(basis.last_week).to be nil
594
+ expect(basis.in_week).to be nil
595
+ end
596
+
597
+ it 'can shift to a different year' do
598
+ allow(subject).to receive(:this_year).and_return subject.parse('2020')
599
+ result = basis.years_hence(4)
600
+ expect(result).to be_a TimeBoss::Calendar::Month
601
+ expect(result.name).to eq '2024M4'
602
+ expect(basis.in_year).to eq 4
603
+ end
604
+ end
605
+
606
+ context 'from quarter' do
607
+ let(:basis) { subject.parse('2018Q2') }
608
+
609
+ it 'cannot shift to a different month' do
610
+ expect(basis.months_ago(4)).to be nil
611
+ expect(basis.in_month).to be nil
612
+ end
613
+
614
+ it 'can shift to a different half' do
615
+ allow(subject).to receive(:this_half).and_return subject.parse('2020H1')
616
+ result = basis.last_half
617
+ expect(result).to be_a TimeBoss::Calendar::Quarter
618
+ expect(result.name).to eq '2019Q4'
619
+ expect(basis.in_half).to eq 2
620
+ end
621
+ end
622
+
623
+ context 'from year' do
624
+ let(:basis) { subject.parse('2014') }
625
+
626
+ it 'cannot shift to a different half' do
627
+ expect(basis.next_half).to be nil
628
+ expect(basis.in_half).to be nil
629
+ end
630
+
631
+ it 'shifts to a different year, but knows how useless that is' do
632
+ allow(subject).to receive(:this_year).and_return subject.parse('2020')
633
+ result = basis.years_ago(2)
634
+ expect(result).to be_a TimeBoss::Calendar::Year
635
+ expect(result.name).to eq '2018'
636
+ expect(basis.in_year).to eq 1
637
+ end
638
+ end
639
+ end
640
+
641
+ context 'units' do
642
+ let(:calendar) { described_class.new }
643
+
644
+ context 'day' do
645
+ let(:start_date) { Date.parse('2019-09-30') }
646
+ let(:subject) { TimeBoss::Calendar::Day.new(calendar, start_date) }
647
+
648
+ context 'links' do
649
+ it 'can get its previous' do
650
+ expect(subject.previous.name).to eq '2019-09-29'
651
+ end
652
+
653
+ it 'can get its next' do
654
+ expect(subject.next.name).to eq '2019-10-01'
655
+ end
656
+
657
+ it 'can offset backwards' do
658
+ expect(subject.offset(-3).name).to eq '2019-09-27'
659
+ expect((subject - 3).name).to eq '2019-09-27'
660
+ end
661
+
662
+ it 'can offset forwards' do
663
+ expect(subject.offset(4).name).to eq '2019-10-04'
664
+ expect((subject + 4).name).to eq '2019-10-04'
665
+ end
666
+ end
667
+ end
668
+
669
+ context 'week' do
670
+ context 'links' do
671
+ context 'within year' do
672
+ let(:parent) { calendar.parse('2020') }
673
+ let(:week) { parent.weeks.first }
674
+
675
+ it 'knows itself first' do
676
+ expect(week.to_s).to include('2020W1', '2019-12-30', '2020-01-05')
677
+ end
678
+
679
+ it 'can get its next week' do
680
+ subject = week.next
681
+ expect(subject).to be_a TimeBoss::Calendar::Week
682
+ expect(subject.to_s).to include('2020W2', '2020-01-06', '2020-01-12')
683
+ end
684
+
685
+ it 'can get its previous week' do
686
+ subject = week.previous
687
+ expect(subject).to be_a TimeBoss::Calendar::Week
688
+ expect(subject.to_s).to include('2019W52', '2019-12-23', '2019-12-29')
689
+ end
690
+
691
+ it 'can offset backwards' do
692
+ expect(week.offset(-4).name).to eq '2019W49'
693
+ expect((week - 4).name).to eq '2019W49'
694
+ end
695
+
696
+ it 'can offset forwards' do
697
+ expect((week + 2).name).to eq '2020W3'
698
+ end
699
+ end
700
+
701
+ context 'within quarter' do
702
+ let(:parent) { calendar.parse('2019Q3') }
703
+ let(:week) { parent.weeks.last }
704
+
705
+ it 'knows itself first' do
706
+ expect(week.to_s).to include('2019W39', '2019-09-23', '2019-09-29')
707
+ end
708
+
709
+ it 'can get its next week' do
710
+ subject = week.next
711
+ expect(subject).to be_a TimeBoss::Calendar::Week
712
+ expect(subject.to_s).to include('2019W40', '2019-09-30', '2019-10-06')
713
+ end
714
+
715
+ it 'can get its previous week' do
716
+ subject = week.previous
717
+ expect(subject).to be_a TimeBoss::Calendar::Week
718
+ expect(subject.to_s).to include('2019W38', '2019-09-16', '2019-09-22')
719
+ end
720
+
721
+ it 'can offset backwards' do
722
+ expect(week.offset(-4).name).to eq '2019W35'
723
+ expect((week - 4).name).to eq '2019W35'
724
+ end
725
+
726
+ it 'can offset forwards' do
727
+ expect((week + 2).name).to eq '2019W41'
728
+ end
729
+ end
730
+ end
731
+ end
732
+
733
+ context 'quarter' do
734
+ let(:start_date) { Date.parse('2019-09-30') }
735
+ let(:end_date) { Date.parse('2019-12-29') }
736
+ let(:subject) { TimeBoss::Calendar::Quarter.new(calendar, 2019, 4, start_date, end_date) }
737
+
738
+ context 'links' do
739
+ it 'can get the next quarter' do
740
+ quarter = subject.next
741
+ expect(quarter.to_s).to include('2020Q1', '2019-12-30', '2020-03-29')
742
+ end
743
+
744
+ it 'can get the next next quarter' do
745
+ quarter = subject.next.next
746
+ expect(quarter.to_s).to include('2020Q2', '2020-03-30', '2020-06-28')
747
+ end
748
+
749
+ it 'can get the next next previous quarter' do
750
+ quarter = subject.next.next.previous
751
+ expect(quarter.to_s).to include('2020Q1', '2019-12-30', '2020-03-29')
752
+ end
753
+
754
+ it 'can get the next previous quarter' do
755
+ quarter = subject.next.previous
756
+ expect(quarter.to_s).to eq subject.to_s
757
+ end
758
+
759
+ it 'can get the previous quarter' do
760
+ quarter = subject.previous
761
+ expect(quarter.to_s).to include('2019Q3', '2019-07-01', '2019-09-29')
762
+ end
763
+
764
+ it 'can offset backwards' do
765
+ expect(subject.offset(-4).name).to eq '2018Q4'
766
+ expect((subject - 4).name).to eq '2018Q4'
767
+ end
768
+
769
+ it 'can offset forwards' do
770
+ expect(subject.offset(2).name).to eq '2020Q2'
771
+ expect((subject + 2).name).to eq '2020Q2'
772
+ end
773
+ end
774
+ end
775
+ end
776
+ end
777
+ end