timeboss 0.0.5

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