timeboss 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e9e42e59807ef4268bea48fffb572839eca05a8ae52309990ccb478c1431c42
4
- data.tar.gz: 7077b857d5d72786cb092cf329f422bc1f7f7868a54e970164f8c7834a00c3c2
3
+ metadata.gz: 7a66336923993e3b4319dd8d0dc925d936ba7e6897f50f1c679a23f37a35e8f8
4
+ data.tar.gz: 3925a5fd244c6fac67ba026f0f3738398bc67f6cc75e015f8eea21caf78f5d84
5
5
  SHA512:
6
- metadata.gz: 7e50564efbd452f31fc7bf7939c3a0cb458aff1fd68d4279187efc013a65dad05e5ec88acf3957fe8f54874d6f795dd4e3c05aecb0199df7117ea105a4b03225
7
- data.tar.gz: 102c86505d6ef3b5f152b76a865d3379d5e07200e01f5c02f9ea82a830b903d503398f804bc530fc439a1a90d374e878386f825137477a3dfb59c5e735fe8acd
6
+ metadata.gz: 87ebf1f8c61e12cd086c33336abca2b5a76ee59cc6117711f166672bd9958c753eb68e38e29ed6aeaccb927520eabaf819da50a62b11879186778f97c982c632
7
+ data.tar.gz: 5bc4914fb3f90bb391fb0e3bb852d25764f926492fccc74ff4ecc56ba321fbad9030fb3812423d4c2ed8bc2c61b2e79b9def7ed34a7789061a2367f122888da1
data/.replit ADDED
@@ -0,0 +1,2 @@
1
+ language = "ruby"
2
+ run = "bundle exec rake timeboss:calendars:broadcast:repl"
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # TimeBoss [![Build Status](https://travis-ci.com/kevinstuffandthings/timeboss.svg?branch=master)](https://travis-ci.com/kevinstuffandthings/timeboss) [![Gem Version](https://badge.fury.io/rb/timeboss.svg)](https://badge.fury.io/rb/timeboss)
1
+ # TimeBoss [![Build Status](https://travis-ci.com/kevinstuffandthings/timeboss.svg?branch=master)](https://travis-ci.com/kevinstuffandthings/timeboss) [![Gem Version](https://badge.fury.io/rb/timeboss.svg)](https://badge.fury.io/rb/timeboss) [![Run on Repl.it](https://repl.it/badge/github/kevinstuffandthings/timeboss)](https://repl.it/github/kevinstuffandthings/timeboss)
2
2
 
3
3
  A gem providing convenient navigation of the [Broadcast Calendar](https://en.wikipedia.org/wiki/Broadcast_calendar), the standard Gregorian calendar, and is easily extensible to support multiple financial calendars.
4
4
 
@@ -135,12 +135,12 @@ period = calendar.parse('2020W3..2020Q1')
135
135
 
136
136
  The examples above are just samples. Try different periods, operators, etc. All of the non-week-based operations will work similarly on the `TimeBoss::Calendars::Gregorian` calendar.
137
137
 
138
- ### Shell
139
- To open an IRB shell for the broadcast calendar, use the `tbsh` executable, or the `timeboss:calendars:broadcast:shell` rake task.
138
+ ### REPL
139
+ To open a REPL for the broadcast calendar, use the `tbsh` executable, or the `timeboss:calendars:broadcast:repl` rake task.
140
140
 
141
141
  For the Gregorian calendar (or any other implemented calendars), supply the name on the command line.
142
142
  - `tbsh gregorian`
143
- - `rake timeboss:calendars:gregorian:shell`
143
+ - `rake timeboss:calendars:gregorian:repl`
144
144
 
145
145
  You will find yourself in the context of an instantiated `TimeBoss::Calendar` object:
146
146
 
@@ -154,7 +154,7 @@ $ tbsh
154
154
  => ["2020M7", "2020M8", "2020M9", "2020M10", "2020M11", "2020M12", "2021M1", "2021M2", "2021M3", "2021M4", "2021M5", "2021M6", "2021M7", "2021M8", "2021M9"]
155
155
  ```
156
156
 
157
- _Having trouble with the shell? If you are using the examples from the [Usage](#Usage) section, keep in mind that the shell is already in the context of the calendar -- so you don't need to specify the receiver!_
157
+ _Having trouble with the REPL? If you are using the examples from the [Usage](#Usage) section, keep in mind that the REPL is already in the context of the calendar -- so you don't need to specify the receiver!_
158
158
 
159
159
  ## Creating new calendars
160
160
  To create a custom calendar, simply extend the `TimeBoss::Calendar` class, and implement a new `TimeBoss::Calendar::Support::MonthBasis` for it.
@@ -221,6 +221,10 @@ With the new calendar implemented, it can be accessed in one of 2 ways:
221
221
 
222
222
  ```ruby
223
223
  require 'timeboss/calendars'
224
+ TimeBoss::Calendars.register(:august_fiscal, MyCalendars::AugustFiscal)
225
+
226
+ # You'll get a cached instance of your calendar here.
227
+ # Handy when switching back and forth between different calendar implementations.
224
228
  calendar = TimeBoss::Calendars[:august_fiscal]
225
229
  calendar.this_year
226
230
  ```
@@ -9,11 +9,13 @@ namespace :timeboss do
9
9
  puts entry.calendar.parse(args[:expression])
10
10
  end
11
11
 
12
- desc "Open a shell with the #{entry.name} calendar"
13
- task shell: ['timeboss:init'] do
12
+ desc "Open a REPL with the #{entry.name} calendar"
13
+ task repl: ['timeboss:init'] do
14
14
  require 'timeboss/support/shellable'
15
15
  TimeBoss::Support::Shellable.open(entry.calendar)
16
16
  end
17
+
18
+ task shell: ["timeboss:calendars:#{entry.name}:repl"]
17
19
  end
18
20
  end
19
21
  end
@@ -25,6 +25,7 @@ module TimeBoss
25
25
  def name
26
26
  self.class.to_s.demodulize.underscore
27
27
  end
28
+ alias_method :to_s, :name
28
29
 
29
30
  # Get a friendly title for this calendar.
30
31
  # @return [String]
@@ -32,10 +33,20 @@ module TimeBoss
32
33
  name.titleize
33
34
  end
34
35
 
36
+ # Can this calendar support weeks?
37
+ # For custom calendars, this value can generally not be overridden.
38
+ # But for calendars like our Gregorian implementation, weeks are irrelevant, and should be suppressed.
39
+ # @return [Boolean]
35
40
  def supports_weeks?
36
- false
41
+ true
37
42
  end
38
43
 
44
+ def self.register!
45
+ return unless TimeBoss::Calendars.method_defined?(:register)
46
+ TimeBoss::Calendars.register(self.name.to_s.demodulize.underscore, self)
47
+ end
48
+ private_class_method :register!
49
+
39
50
  protected
40
51
 
41
52
  attr_reader :basis
@@ -11,7 +11,8 @@ module TimeBoss
11
11
  end
12
12
 
13
13
  def parse(identifier = nil)
14
- return parse_identifier(identifier.presence) unless identifier&.include?(RANGE_DELIMITER)
14
+ return nil unless (identifier || '').strip.length > 0
15
+ return parse_identifier(identifier) unless identifier&.include?(RANGE_DELIMITER)
15
16
  bases = identifier.split(RANGE_DELIMITER).map { |i| parse_identifier(i.strip) } unless identifier.nil?
16
17
  bases ||= [parse_identifier(nil)]
17
18
  Period.new(calendar, *bases)
@@ -2,6 +2,8 @@ module TimeBoss
2
2
  class Calendar
3
3
  module Support
4
4
  # @abstract
5
+ # A MonthBasis must define a `#start_date` and `#end_date` method.
6
+ # These methods should be calculated based on the incoming `#year` and `#month` values.
5
7
  class MonthBasis
6
8
  attr_reader :year, :month
7
9
 
@@ -10,8 +10,9 @@ module TimeBoss
10
10
 
11
11
  define_method(periods) { calendar.send("#{periods}_for", self) }
12
12
 
13
- define_method(period) do
13
+ define_method(period) do |index = nil|
14
14
  entries = send(periods)
15
+ return entries[index - 1] unless index.nil?
15
16
  return nil unless entries.length == 1
16
17
  entries.first
17
18
  end
@@ -27,7 +28,7 @@ module TimeBoss
27
28
  # Get a list of days that fall within this unit.
28
29
  # @return [Array<Calendar::Day>]
29
30
 
30
- # @!method day
31
+ # @!method day(index = nil)
31
32
  # Get the day this unit represents.
32
33
  # Returns nil if no single day can be identified.
33
34
  # @return [Array<Calendar::Day>, nil]
@@ -38,7 +39,7 @@ module TimeBoss
38
39
  # Get a list of weeks that fall within this unit.
39
40
  # @return [Array<Calendar::Week>]
40
41
 
41
- # @!method week
42
+ # @!method week(index = nil)
42
43
  # Get the week this unit represents.
43
44
  # Returns nil if no single week can be identified.
44
45
  # @return [Array<Calendar::Week>, nil]
@@ -49,7 +50,7 @@ module TimeBoss
49
50
  # Get a list of months that fall within this unit.
50
51
  # @return [Array<Calendar::Month>]
51
52
 
52
- # @!method month
53
+ # @!method month(index = nil)
53
54
  # Get the month this unit represents.
54
55
  # Returns nil if no single month can be identified.
55
56
  # @return [Array<Calendar::Month>, nil]
@@ -60,7 +61,7 @@ module TimeBoss
60
61
  # Get a list of quarters that fall within this unit.
61
62
  # @return [Array<Calendar::Quarter>]
62
63
 
63
- # @!method quarter
64
+ # @!method quarter(index = nil)
64
65
  # Get the quarter this unit represents.
65
66
  # Returns nil if no single quarter can be identified.
66
67
  # @return [Array<Calendar::Quarter>, nil]
@@ -71,7 +72,7 @@ module TimeBoss
71
72
  # Get a list of halves that fall within this unit.
72
73
  # @return [Array<Calendar::Half>]
73
74
 
74
- # @!method half
75
+ # @!method half(index = nil)
75
76
  # Get the half this unit represents.
76
77
  # Returns nil if no single half can be identified.
77
78
  # @return [Array<Calendar::Half>, nil]
@@ -82,7 +83,7 @@ module TimeBoss
82
83
  # Get a list of years that fall within this unit.
83
84
  # @return [Array<Calendar::Year>]
84
85
 
85
- # @!method year
86
+ # @!method year(index = nil)
86
87
  # Get the year this unit represents.
87
88
  # Returns nil if no single year can be identified.
88
89
  # @return [Array<Calendar::Year>, nil]
@@ -1,28 +1,33 @@
1
1
  # frozen_string_literal: true
2
- require 'active_support/core_ext/class/subclasses'
3
2
  require_relative 'calendar'
4
3
 
5
- Dir[File.expand_path('../calendars/*.rb', __FILE__)].each { |f| require f }
6
-
7
4
  module TimeBoss
8
5
  module Calendars
9
6
  extend self
10
7
  extend Enumerable
11
8
  delegate :each, :length, to: :all
12
9
 
10
+ # Register a new calendar
11
+ # @return [Entry]
12
+ def register(name, klass)
13
+ Entry.new(name.to_sym, klass).tap do |entry|
14
+ (@entries ||= {})[name.to_sym] = entry
15
+ end
16
+ end
17
+
13
18
  # Retrieve a list of all registered calendars.
14
19
  # @return [Array<Entry>]
15
20
  def all
16
- @_all ||= TimeBoss::Calendar.subclasses.map do |klass|
17
- Entry.new(klass.to_s.demodulize.underscore.to_sym, klass)
18
- end
21
+ return if @entries.nil?
22
+ @entries.values.sort_by { |e| e.name.to_s }
19
23
  end
20
24
 
21
25
  # Retrieve an instance of the specified named calendar.
22
26
  # @param name [String, Symbol] the name of the calendar to retrieve.
23
27
  # @return [Calendar]
24
28
  def [](name)
25
- find { |e| e.name == name&.to_sym }&.calendar
29
+ return if @entries.nil?
30
+ @entries[name&.to_sym]&.calendar
26
31
  end
27
32
 
28
33
  private
@@ -44,3 +49,5 @@ module TimeBoss
44
49
  end
45
50
  end
46
51
  end
52
+
53
+ Dir[File.expand_path('../calendars/*.rb', __FILE__)].each { |f| require f }
@@ -4,14 +4,12 @@ require_relative '../calendar'
4
4
  module TimeBoss
5
5
  module Calendars
6
6
  class Broadcast < Calendar
7
+ register!
8
+
7
9
  def initialize
8
10
  super(basis: Basis)
9
11
  end
10
12
 
11
- def supports_weeks?
12
- true
13
- end
14
-
15
13
  private
16
14
 
17
15
  class Basis < Calendar::Support::MonthBasis
@@ -4,10 +4,16 @@ require_relative '../calendar'
4
4
  module TimeBoss
5
5
  module Calendars
6
6
  class Gregorian < Calendar
7
+ register!
8
+
7
9
  def initialize
8
10
  super(basis: Basis)
9
11
  end
10
12
 
13
+ def supports_weeks?
14
+ false
15
+ end
16
+
11
17
  private
12
18
 
13
19
  class Basis < Calendar::Support::MonthBasis
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module TimeBoss
3
- VERSION = "0.2.1"
3
+ VERSION = "0.3.0"
4
4
  end
@@ -493,10 +493,9 @@ module TimeBoss
493
493
  expect(date.end_date).to eq Date.parse('2017-04-08')
494
494
  end
495
495
 
496
- it 'gives you this year if you give it nothing' do
497
- year = subject.this_year
498
- expect(subject.parse(nil)).to eq year
499
- expect(subject.parse('')).to eq year
496
+ it 'gives you nothing if you give it nothing' do
497
+ expect(subject.parse(nil)).to be nil
498
+ expect(subject.parse('')).to be nil
500
499
  end
501
500
  end
502
501
 
@@ -0,0 +1,684 @@
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 nothing if you give it nothing' do
479
+ expect(subject.parse(nil)).to be nil
480
+ expect(subject.parse('')).to be nil
481
+ end
482
+ end
483
+
484
+ context 'expressions' do
485
+ it 'can parse waypoints' do
486
+ result = subject.parse('this_year')
487
+ expect(result).to be_a TimeBoss::Calendar::Year
488
+ expect(result).to be_current
489
+ end
490
+
491
+ it 'can parse mathematic expressions' do
492
+ result = subject.parse('this_month + 2')
493
+ expect(result).to be_a TimeBoss::Calendar::Month
494
+ expect(result).to eq subject.months_ahead(2)
495
+ end
496
+
497
+ context 'ranges' do
498
+ before(:each) { allow(subject).to receive(:this_year).and_return subject.year(2018) }
499
+ let(:result) { subject.parse('this_year-2 .. this_year') }
500
+
501
+ it 'can parse range expressions' do
502
+ expect(result).to be_a TimeBoss::Calendar::Period
503
+ expect(result.to_s).to eq "2016: 2016-01-01 thru 2016-12-31 .. 2018: 2018-01-01 thru 2018-12-31"
504
+ end
505
+
506
+ it 'can get an overall start date for a range' do
507
+ expect(result.start_date).to eq Date.parse('2016-01-01')
508
+ end
509
+
510
+ it 'can get an overall end date for a range' do
511
+ expect(result.end_date).to eq Date.parse('2018-12-31')
512
+ end
513
+
514
+ context 'sub-periods' do
515
+ it 'can get the months included in a range' do
516
+ entries = result.months
517
+ entries.each { |e| expect(e).to be_a TimeBoss::Calendar::Month }
518
+ expect(entries.map(&:name)).to include('2016M1', '2016M9', '2017M3', '2018M12')
519
+ end
520
+
521
+ it 'cannot get the weeks included in a range' do
522
+ expect { result.weeks }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
523
+ end
524
+
525
+ it 'can get the days included in a range' do
526
+ entries = result.days
527
+ entries.each { |e| expect(e).to be_a TimeBoss::Calendar::Day }
528
+ expect(entries.map(&:name)).to include('2016-01-01', '2016-05-12', '2017-09-22', '2018-12-31')
529
+ end
530
+ end
531
+ end
532
+ end
533
+
534
+ context 'shifting' do
535
+ context 'from day' do
536
+ let(:basis) { subject.parse('2020-04-21') }
537
+
538
+ it 'cannot shift to a different week' do
539
+ expect { basis.last_week }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
540
+ expect { basis.in_week }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
541
+ end
542
+
543
+ it 'can shift to a different quarter' do
544
+ allow(subject).to receive(:this_quarter).and_return subject.parse('2020Q3')
545
+ result = basis.quarters_ago(2)
546
+ expect(result).to be_a TimeBoss::Calendar::Day
547
+ expect(result.to_s).to eq '2020-01-21'
548
+ expect(basis.in_quarter).to eq 21
549
+ end
550
+
551
+ it 'can shift to a different year' do
552
+ allow(subject).to receive(:this_year).and_return subject.parse('2019')
553
+ result = basis.years_ahead(3)
554
+ expect(result).to be_a TimeBoss::Calendar::Day
555
+ expect(result.to_s).to eq '2022-04-22'
556
+ expect(basis.in_year).to eq 112
557
+ end
558
+ end
559
+
560
+ context 'from month' do
561
+ let(:basis) { subject.parse('2017M4') }
562
+
563
+ it 'cannot shift to a different week' do
564
+ expect { basis.last_week }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
565
+ expect { basis.in_week }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
566
+ end
567
+
568
+ it 'can shift to a different year' do
569
+ allow(subject).to receive(:this_year).and_return subject.parse('2020')
570
+ result = basis.years_ahead(4)
571
+ expect(result).to be_a TimeBoss::Calendar::Month
572
+ expect(result.name).to eq '2024M4'
573
+ expect(basis.in_year).to eq 4
574
+ end
575
+ end
576
+
577
+ context 'from quarter' do
578
+ let(:basis) { subject.parse('2018Q2') }
579
+
580
+ it 'cannot shift to a different month' do
581
+ expect(basis.months_ago(4)).to be nil
582
+ expect(basis.in_month).to be nil
583
+ end
584
+
585
+ it 'can shift to a different half' do
586
+ allow(subject).to receive(:this_half).and_return subject.parse('2020H1')
587
+ result = basis.last_half
588
+ expect(result).to be_a TimeBoss::Calendar::Quarter
589
+ expect(result.name).to eq '2019Q4'
590
+ expect(basis.in_half).to eq 2
591
+ end
592
+ end
593
+
594
+ context 'from year' do
595
+ let(:basis) { subject.parse('2014') }
596
+
597
+ it 'cannot shift to a different half' do
598
+ expect(basis.next_half).to be nil
599
+ expect(basis.in_half).to be nil
600
+ end
601
+
602
+ it 'shifts to a different year, but knows how useless that is' do
603
+ allow(subject).to receive(:this_year).and_return subject.parse('2020')
604
+ result = basis.years_ago(2)
605
+ expect(result).to be_a TimeBoss::Calendar::Year
606
+ expect(result.name).to eq '2018'
607
+ expect(basis.in_year).to eq 1
608
+ end
609
+ end
610
+ end
611
+
612
+ context 'units' do
613
+ let(:calendar) { described_class.new }
614
+
615
+ context 'day' do
616
+ let(:start_date) { Date.parse('2019-09-30') }
617
+ let(:subject) { TimeBoss::Calendar::Day.new(calendar, start_date) }
618
+
619
+ context 'links' do
620
+ it 'can get its previous' do
621
+ expect(subject.previous.name).to eq '2019-09-29'
622
+ end
623
+
624
+ it 'can get its next' do
625
+ expect(subject.next.name).to eq '2019-10-01'
626
+ end
627
+
628
+ it 'can offset backwards' do
629
+ expect(subject.offset(-3).name).to eq '2019-09-27'
630
+ expect((subject - 3).name).to eq '2019-09-27'
631
+ end
632
+
633
+ it 'can offset forwards' do
634
+ expect(subject.offset(4).name).to eq '2019-10-04'
635
+ expect((subject + 4).name).to eq '2019-10-04'
636
+ end
637
+ end
638
+ end
639
+
640
+ context 'quarter' do
641
+ let(:start_date) { Date.parse('2019-10-01') }
642
+ let(:end_date) { Date.parse('2019-12-31') }
643
+ let(:subject) { TimeBoss::Calendar::Quarter.new(calendar, 2019, 4, start_date, end_date) }
644
+
645
+ context 'links' do
646
+ it 'can get the next quarter' do
647
+ quarter = subject.next
648
+ expect(quarter.to_s).to include('2020Q1', '2020-01-01', '2020-03-31')
649
+ end
650
+
651
+ it 'can get the next next quarter' do
652
+ quarter = subject.next.next
653
+ expect(quarter.to_s).to include('2020Q2', '2020-04-01', '2020-06-30')
654
+ end
655
+
656
+ it 'can get the next next previous quarter' do
657
+ quarter = subject.next.next.previous
658
+ expect(quarter.to_s).to include('2020Q1', '2020-01-01', '2020-03-31')
659
+ end
660
+
661
+ it 'can get the next previous quarter' do
662
+ quarter = subject.next.previous
663
+ expect(quarter.to_s).to eq subject.to_s
664
+ end
665
+
666
+ it 'can get the previous quarter' do
667
+ quarter = subject.previous
668
+ expect(quarter.to_s).to include('2019Q3', '2019-07-01', '2019-09-30')
669
+ end
670
+
671
+ it 'can offset backwards' do
672
+ expect(subject.offset(-4).name).to eq '2018Q4'
673
+ expect((subject - 4).name).to eq '2018Q4'
674
+ end
675
+
676
+ it 'can offset forwards' do
677
+ expect(subject.offset(2).name).to eq '2020Q2'
678
+ expect((subject + 2).name).to eq '2020Q2'
679
+ end
680
+ end
681
+ end
682
+ end
683
+ end
684
+ end
@@ -1,11 +1,13 @@
1
1
  module TimeBoss
2
2
  describe Calendars do
3
- class MyTestCalendar < TimeBoss::Calendar
3
+ class MyCal < TimeBoss::Calendar
4
4
  def initialize
5
5
  super(basis: nil)
6
6
  end
7
7
  end
8
8
 
9
+ TimeBoss::Calendars.register(:my_amazing_calendar, MyCal)
10
+
9
11
  describe '#all' do
10
12
  let(:all) { described_class.all }
11
13
 
@@ -17,7 +19,7 @@ module TimeBoss
17
19
 
18
20
  context 'enumerability' do
19
21
  it 'can get a list of names' do
20
- expect(described_class.map(&:name)).to include(:broadcast, :my_test_calendar)
22
+ expect(described_class.map(&:name)).to include(:broadcast, :my_amazing_calendar)
21
23
  end
22
24
  end
23
25
  end
@@ -34,10 +36,10 @@ module TimeBoss
34
36
  end
35
37
 
36
38
  it 'can return a new calendar' do
37
- c1 = described_class[:my_test_calendar]
38
- expect(c1).to be_instance_of MyTestCalendar
39
- expect(c1.name).to eq 'my_test_calendar'
40
- expect(c1.title).to eq 'My Test Calendar'
39
+ c1 = described_class[:my_amazing_calendar]
40
+ expect(c1).to be_instance_of MyCal
41
+ expect(c1.name).to eq 'my_cal'
42
+ expect(c1.title).to eq 'My Cal'
41
43
  end
42
44
 
43
45
  it 'can graceully give you nothing' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timeboss
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin McDonald
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-07-19 00:00:00.000000000 Z
11
+ date: 2020-07-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -133,6 +133,7 @@ files:
133
133
  - ".github/ISSUE_TEMPLATE/bug_report.md"
134
134
  - ".github/ISSUE_TEMPLATE/feature_request.md"
135
135
  - ".gitignore"
136
+ - ".replit"
136
137
  - ".rspec"
137
138
  - ".travis.yml"
138
139
  - ".yardopts"
@@ -175,6 +176,7 @@ files:
175
176
  - spec/calendar/support/unit_spec.rb
176
177
  - spec/calendar/week_spec.rb
177
178
  - spec/calendars/broadcast_spec.rb
179
+ - spec/calendars/gregorian_spec.rb
178
180
  - spec/calendars_spec.rb
179
181
  - spec/spec_helper.rb
180
182
  - timeboss.gemspec
@@ -197,7 +199,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
197
199
  - !ruby/object:Gem::Version
198
200
  version: '0'
199
201
  requirements: []
200
- rubygems_version: 3.0.8
202
+ rubyforge_project:
203
+ rubygems_version: 2.7.7
201
204
  signing_key:
202
205
  specification_version: 4
203
206
  summary: Broadcast Calendar navigation in Ruby made simple
@@ -208,5 +211,6 @@ test_files:
208
211
  - spec/calendar/support/unit_spec.rb
209
212
  - spec/calendar/week_spec.rb
210
213
  - spec/calendars/broadcast_spec.rb
214
+ - spec/calendars/gregorian_spec.rb
211
215
  - spec/calendars_spec.rb
212
216
  - spec/spec_helper.rb