timeboss 1.0.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +1 -1
- data/README.md +7 -2
- data/lib/timeboss/calendar/period.rb +4 -2
- data/lib/timeboss/calendar/support/has_fiscal_weeks.rb +17 -0
- data/lib/timeboss/calendar/support/has_iso_weeks.rb +30 -0
- data/lib/timeboss/calendar/support/monthly_unit.rb +2 -3
- data/lib/timeboss/calendar/support/unit.rb +6 -0
- data/lib/timeboss/calendar.rb +3 -3
- data/lib/timeboss/calendars/broadcast.rb +2 -0
- data/lib/timeboss/calendars/gregorian.rb +2 -4
- data/lib/timeboss/version.rb +1 -1
- data/lib/timeboss.rb +1 -0
- data/spec/calendar/support/monthly_unit_spec.rb +26 -21
- data/spec/calendars/gregorian_spec.rb +129 -20
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3091d788f78273741aeb0206a87765fed72e3908099c2596b70589740aa63c26
|
4
|
+
data.tar.gz: 2a0cd5b28cc9b05c07d3fe06aef86644876864b7b24eda946105706d0626622f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f58a6f56a3bf0cb2f44a00aee3b5a4d09fdbcd4b2cd8b06c2942fa4eefc508b0afff19dd577bef26bd7b5f187ca1ccb380e5efdd5b357da829ad2936d36c12ff
|
7
|
+
data.tar.gz: 5f2b1b5013f49c81e3e5f9066e9e4c3063ddd70231a7199010a79b822bc3531ead0d0bf285d9d045fb0434dcfa6e0a5564aac138a8b25b07e2a374b1c049faf9
|
data/.github/workflows/ruby.yml
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# TimeBoss  [](https://badge.fury.io/rb/timeboss)
|
1
|
+
# TimeBoss  [](https://badge.fury.io/rb/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
|
|
@@ -160,7 +160,7 @@ $ tbsh
|
|
160
160
|
If you want to try things out locally without installing the gem or updating your ruby environment, you can use [Docker](https://docker.com):
|
161
161
|
|
162
162
|
```bash
|
163
|
-
$ docker run --rm -it ruby:
|
163
|
+
$ docker run --rm -it ruby:3.0-slim /bin/bash -c "gem install timeboss shellable >/dev/null && tbsh"
|
164
164
|
```
|
165
165
|
|
166
166
|
_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!_
|
@@ -170,9 +170,14 @@ To create a custom calendar, simply extend the `TimeBoss::Calendar` class, and i
|
|
170
170
|
|
171
171
|
```ruby
|
172
172
|
require 'timeboss/calendar'
|
173
|
+
require 'timeboss/calendar/support/has_fiscal_weeks'
|
173
174
|
|
174
175
|
module MyCalendars
|
175
176
|
class AugustFiscal < TimeBoss::Calendar
|
177
|
+
# The calendar we wish to implement has "fiscal weeks", meaning that the weeks start on
|
178
|
+
# the day of the containing period.
|
179
|
+
include TimeBoss::Calendar::Support::HasFiscalWeeks
|
180
|
+
|
176
181
|
def initialize
|
177
182
|
# For each calendar, operation, the class will be instantiated with an ordinal value
|
178
183
|
# for `year` and `month`. It is the instance's job to translate those ordinals into
|
@@ -113,8 +113,10 @@ module TimeBoss
|
|
113
113
|
|
114
114
|
%w[day week month quarter half year].each do |size|
|
115
115
|
define_method(size.pluralize) do
|
116
|
-
entry = calendar.public_send("#{size}_for", self.begin.start_date)
|
117
|
-
build_entries
|
116
|
+
entry = calendar.public_send("#{size}_for", self.begin.start_date) || self.begin.public_send(size, 1)
|
117
|
+
entries = build_entries(entry)
|
118
|
+
entries.pop if size == "week" && self.end.next.public_send(size, 1) == entries.last
|
119
|
+
entries
|
118
120
|
end
|
119
121
|
|
120
122
|
define_method(size) do |index = nil|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TimeBoss
|
4
|
+
class Calendar
|
5
|
+
module Support
|
6
|
+
module HasFiscalWeeks
|
7
|
+
def weeks_in(year:)
|
8
|
+
num_weeks = (((year.end_date - year.start_date) + 1) / 7.0).to_i
|
9
|
+
num_weeks.times.map do |i|
|
10
|
+
start_date = year.start_date + (i * 7).days
|
11
|
+
Week.new(self, start_date, start_date + 6.days)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TimeBoss
|
4
|
+
class Calendar
|
5
|
+
module Support
|
6
|
+
module HasIsoWeeks
|
7
|
+
def weeks_in(year:)
|
8
|
+
weeks = []
|
9
|
+
start_date = Date.commercial(year.year_index)
|
10
|
+
end_date = Date.commercial(year.next.year_index)
|
11
|
+
while start_date < end_date
|
12
|
+
weeks << Week.new(self, start_date, start_date + 6.days)
|
13
|
+
start_date += 7.days
|
14
|
+
end
|
15
|
+
weeks
|
16
|
+
end
|
17
|
+
|
18
|
+
class Week < Calendar::Week
|
19
|
+
def index
|
20
|
+
start_date.cweek
|
21
|
+
end
|
22
|
+
|
23
|
+
def year
|
24
|
+
calendar.year(start_date.cwyear)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -23,10 +23,9 @@ module TimeBoss
|
|
23
23
|
# Get a list of weeks contained within this period.
|
24
24
|
# @return [Array<Week>]
|
25
25
|
def weeks
|
26
|
+
raise UnsupportedUnitError unless calendar.supports_weeks?
|
26
27
|
base = calendar.year(year_index)
|
27
|
-
|
28
|
-
num_weeks.times.map { |i| Week.new(calendar, base.start_date + (i * 7).days, base.start_date + ((i * 7) + 6).days) }
|
29
|
-
.select { |w| w.start_date.between?(start_date, end_date) }
|
28
|
+
calendar.weeks_in(year: base).select { |w| (w.dates & dates).count >= 4 }
|
30
29
|
end
|
31
30
|
|
32
31
|
private
|
data/lib/timeboss/calendar.rb
CHANGED
@@ -35,11 +35,11 @@ module TimeBoss
|
|
35
35
|
end
|
36
36
|
|
37
37
|
# Can this calendar support weeks?
|
38
|
-
#
|
39
|
-
#
|
38
|
+
# To support weeks, a calendar must implement a `#weeks_in(year:)` method that returns an array of
|
39
|
+
# `Calendar::Week` objects.
|
40
40
|
# @return [Boolean]
|
41
41
|
def supports_weeks?
|
42
|
-
|
42
|
+
respond_to?(:weeks_in)
|
43
43
|
end
|
44
44
|
|
45
45
|
def self.register!
|
@@ -1,10 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "../calendar"
|
4
|
+
require_relative "../calendar/support/has_fiscal_weeks"
|
4
5
|
|
5
6
|
module TimeBoss
|
6
7
|
module Calendars
|
7
8
|
class Broadcast < Calendar
|
9
|
+
include Support::HasFiscalWeeks
|
8
10
|
register!
|
9
11
|
|
10
12
|
def initialize
|
@@ -1,20 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "../calendar"
|
4
|
+
require_relative "../calendar/support/has_iso_weeks"
|
4
5
|
|
5
6
|
module TimeBoss
|
6
7
|
module Calendars
|
7
8
|
class Gregorian < Calendar
|
9
|
+
include Support::HasIsoWeeks
|
8
10
|
register!
|
9
11
|
|
10
12
|
def initialize
|
11
13
|
super(basis: Basis)
|
12
14
|
end
|
13
15
|
|
14
|
-
def supports_weeks?
|
15
|
-
false
|
16
|
-
end
|
17
|
-
|
18
16
|
private
|
19
17
|
|
20
18
|
class Basis < Calendar::Support::MonthBasis
|
data/lib/timeboss/version.rb
CHANGED
data/lib/timeboss.rb
CHANGED
@@ -11,7 +11,7 @@ module TimeBoss
|
|
11
11
|
|
12
12
|
describe MonthlyUnit do
|
13
13
|
let(:described_class) { TestMonthBasedChunk }
|
14
|
-
let(:calendar) { double }
|
14
|
+
let(:calendar) { double(supports_weeks?: false, weeks_in: nil) }
|
15
15
|
let(:start_date) { Date.parse("2018-06-25") }
|
16
16
|
let(:end_date) { Date.parse("2018-08-26") }
|
17
17
|
let(:subject) { described_class.new(calendar, 2018, 4, start_date, end_date) }
|
@@ -27,29 +27,34 @@ module TimeBoss
|
|
27
27
|
end
|
28
28
|
|
29
29
|
describe "#weeks" do
|
30
|
-
let(:
|
31
|
-
before(:each) { allow(calendar).to receive(:
|
30
|
+
let(:supports_weeks) { false }
|
31
|
+
before(:each) { allow(calendar).to receive(:supports_weeks?).and_return supports_weeks }
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
expect(result.map { |w| w.start_date.to_s }).to eq [
|
38
|
-
"2018-06-25",
|
39
|
-
"2018-07-02",
|
40
|
-
"2018-07-09",
|
41
|
-
"2018-07-16",
|
42
|
-
"2018-07-23",
|
43
|
-
"2018-07-30",
|
44
|
-
"2018-08-06",
|
45
|
-
"2018-08-13",
|
46
|
-
"2018-08-20"
|
47
|
-
]
|
33
|
+
context "unsupported" do
|
34
|
+
it "blows up when weeks are not supported" do
|
35
|
+
expect { subject.weeks }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
|
36
|
+
end
|
48
37
|
end
|
49
38
|
|
50
|
-
|
51
|
-
|
52
|
-
|
39
|
+
context "supported" do
|
40
|
+
let(:supports_weeks) { true }
|
41
|
+
let(:base) { double(start_date: Date.parse("2018-01-01"), end_date: Date.parse("2018-12-30")) }
|
42
|
+
let(:weeks) do
|
43
|
+
["2018-04-19", "2018-06-25", "2018-07-16", "2018-08-20", "2018-09-30"].map do |date|
|
44
|
+
start_date = Date.parse(date)
|
45
|
+
TimeBoss::Calendar::Week.new(calendar, start_date, start_date + 6.days)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
before(:each) do
|
49
|
+
allow(calendar).to receive(:year).with(2018).and_return base
|
50
|
+
allow(calendar).to receive(:weeks_in).with(year: base).and_return weeks
|
51
|
+
end
|
52
|
+
|
53
|
+
it "can get the relevant weeks for the period" do
|
54
|
+
result = subject.weeks
|
55
|
+
result.each { |w| expect(w).to be_instance_of TimeBoss::Calendar::Week }
|
56
|
+
expect(result.map { |w| w.start_date.to_s }).to eq ["2018-06-25", "2018-07-16", "2018-08-20"]
|
57
|
+
end
|
53
58
|
end
|
54
59
|
end
|
55
60
|
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module TimeBoss
|
2
2
|
describe Calendars::Gregorian do
|
3
|
-
|
4
|
-
|
3
|
+
subject { described_class.new }
|
5
4
|
context "days" do
|
6
5
|
it "can get today" do
|
7
6
|
day = subject.today
|
@@ -293,10 +292,80 @@ module TimeBoss
|
|
293
292
|
end
|
294
293
|
|
295
294
|
context "weeks" do
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
expect
|
295
|
+
before(:each) { allow(Date).to receive(:today).and_return Date.parse("2019-08-23") }
|
296
|
+
|
297
|
+
it "knows this week " do
|
298
|
+
expect(subject.this_week.name).to eq "2019W34"
|
299
|
+
expect(subject.this_week.title).to eq "Week of August 19, 2019"
|
300
|
+
end
|
301
|
+
|
302
|
+
it "knows last week" do
|
303
|
+
expect(subject.last_week.name).to eq "2019W33"
|
304
|
+
expect(subject.last_week.title).to eq "Week of August 12, 2019"
|
305
|
+
end
|
306
|
+
|
307
|
+
it "knows next week" do
|
308
|
+
expect(subject.next_week.name).to eq "2019W35"
|
309
|
+
expect(subject.next_week.title).to eq "Week of August 26, 2019"
|
310
|
+
end
|
311
|
+
|
312
|
+
describe "over the years" do
|
313
|
+
[
|
314
|
+
[2010, "2010-01-04", 52, "2010-12-27"],
|
315
|
+
[2011, "2011-01-03", 52, "2011-12-26"],
|
316
|
+
[2012, "2012-01-02", 52, "2012-12-24"],
|
317
|
+
[2013, "2012-12-31", 52, "2013-12-23"],
|
318
|
+
[2014, "2013-12-30", 52, "2014-12-22"],
|
319
|
+
[2015, "2014-12-29", 53, "2015-12-28"],
|
320
|
+
[2016, "2016-01-04", 52, "2016-12-26"],
|
321
|
+
[2017, "2017-01-02", 52, "2017-12-25"],
|
322
|
+
[2018, "2018-01-01", 52, "2018-12-24"],
|
323
|
+
[2019, "2018-12-31", 52, "2019-12-23"],
|
324
|
+
[2020, "2019-12-30", 53, "2020-12-28"],
|
325
|
+
[2021, "2021-01-04", 52, "2021-12-27"],
|
326
|
+
[2022, "2022-01-03", 52, "2022-12-26"],
|
327
|
+
[2023, "2023-01-02", 52, "2023-12-25"],
|
328
|
+
[2024, "2024-01-01", 52, "2024-12-23"],
|
329
|
+
[2025, "2024-12-30", 52, "2025-12-22"],
|
330
|
+
[2026, "2025-12-29", 53, "2026-12-28"],
|
331
|
+
[2027, "2027-01-04", 52, "2027-12-27"],
|
332
|
+
[2028, "2028-01-03", 52, "2028-12-25"],
|
333
|
+
[2029, "2029-01-01", 52, "2029-12-24"],
|
334
|
+
[2030, "2029-12-31", 52, "2030-12-23"]
|
335
|
+
].each do |year_number, first_start, num_weeks, last_start|
|
336
|
+
context "year #{year_number}" do
|
337
|
+
let(:year) { subject.year(year_number) }
|
338
|
+
let(:weeks) { year.weeks }
|
339
|
+
|
340
|
+
it "knows the first week of #{year_number} starts on #{first_start}" do
|
341
|
+
expect(weeks.first.start_date).to eq Date.parse(first_start)
|
342
|
+
end
|
343
|
+
|
344
|
+
it "can navigate to the second week of the year" do
|
345
|
+
week = weeks.first.next
|
346
|
+
expect(week.start_date).to eq Date.parse(first_start) + 7.days
|
347
|
+
expect(week.year).to eq year
|
348
|
+
end
|
349
|
+
|
350
|
+
it "knows that every week is 7 days" do
|
351
|
+
weeks.each { |w| expect(w.end_date - w.start_date).to eq 6 }
|
352
|
+
end
|
353
|
+
|
354
|
+
it "knows there are #{num_weeks} in #{year_number}" do
|
355
|
+
expect(weeks.count).to eq num_weeks
|
356
|
+
end
|
357
|
+
|
358
|
+
it "knows the last week of #{year_number} starts on #{last_start}" do
|
359
|
+
expect(weeks.first.start_date).to eq Date.parse(first_start)
|
360
|
+
end
|
361
|
+
|
362
|
+
it "can navigate to the first week of the next year" do
|
363
|
+
week = weeks.last.next
|
364
|
+
expect(week.start_date).to eq Date.parse(last_start) + 7.days
|
365
|
+
expect(week.year).to eq year.next
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
300
369
|
end
|
301
370
|
end
|
302
371
|
|
@@ -394,7 +463,7 @@ module TimeBoss
|
|
394
463
|
let(:entry) { subject.parse("2020D201") }
|
395
464
|
|
396
465
|
it "can do a default format" do
|
397
|
-
expect(entry.format).to eq "
|
466
|
+
expect(entry.format).to eq "2020H2Q1M1W3D7"
|
398
467
|
end
|
399
468
|
end
|
400
469
|
end
|
@@ -449,16 +518,25 @@ module TimeBoss
|
|
449
518
|
expect(date.name).to eq "2017M4"
|
450
519
|
end
|
451
520
|
|
452
|
-
it "
|
453
|
-
|
521
|
+
it "can parse a week within a year" do
|
522
|
+
date = subject.parse("2018W37")
|
523
|
+
expect(date).to be_a TimeBoss::Calendar::Week
|
524
|
+
expect(date).to be_instance_of TimeBoss::Calendars::Gregorian::Week
|
525
|
+
expect(date.name).to eq "2018W37"
|
454
526
|
end
|
455
527
|
|
456
|
-
it "
|
457
|
-
|
528
|
+
it "can parse a week within a quarter" do
|
529
|
+
date = subject.parse("2017Q2W2")
|
530
|
+
expect(date).to be_a TimeBoss::Calendar::Week
|
531
|
+
expect(date).to be_instance_of TimeBoss::Calendars::Gregorian::Week
|
532
|
+
expect(date.name).to eq "2017W15"
|
458
533
|
end
|
459
534
|
|
460
|
-
it "
|
461
|
-
|
535
|
+
it "can parse a week within a month" do
|
536
|
+
date = subject.parse("2017M4W1")
|
537
|
+
expect(date).to be_a TimeBoss::Calendar::Week
|
538
|
+
expect(date).to be_instance_of TimeBoss::Calendars::Gregorian::Week
|
539
|
+
expect(date.name).to eq "2017W14"
|
462
540
|
end
|
463
541
|
|
464
542
|
it "can parse a date" do
|
@@ -518,8 +596,11 @@ module TimeBoss
|
|
518
596
|
expect(entries.map(&:name)).to include("2016M1", "2016M9", "2017M3", "2018M12")
|
519
597
|
end
|
520
598
|
|
521
|
-
it "
|
522
|
-
|
599
|
+
it "can get the weeks included in a range" do
|
600
|
+
entries = result.weeks
|
601
|
+
entries.each { |e| expect(e).to be_a TimeBoss::Calendars::Gregorian::Week }
|
602
|
+
expect(entries.count).to eq 156
|
603
|
+
expect(entries.map(&:name)).to include("2016W1", "2016W38", "2017W15", "2017W52", "2018W52")
|
523
604
|
end
|
524
605
|
|
525
606
|
it "can get the days included in a range" do
|
@@ -535,9 +616,12 @@ module TimeBoss
|
|
535
616
|
context "from day" do
|
536
617
|
let(:basis) { subject.parse("2020-04-21") }
|
537
618
|
|
538
|
-
it "
|
539
|
-
|
540
|
-
|
619
|
+
it "can shift to a different week" do
|
620
|
+
allow(subject).to receive(:this_week).and_return subject.parse("2020W23")
|
621
|
+
result = basis.last_week
|
622
|
+
expect(result).to be_a TimeBoss::Calendar::Day
|
623
|
+
expect(result.to_s).to eq "2020-05-26"
|
624
|
+
expect(basis.in_week).to eq 2
|
541
625
|
end
|
542
626
|
|
543
627
|
it "can shift to a different quarter" do
|
@@ -557,12 +641,37 @@ module TimeBoss
|
|
557
641
|
end
|
558
642
|
end
|
559
643
|
|
644
|
+
context "from week" do
|
645
|
+
let(:basis) { subject.parse("2017W8") }
|
646
|
+
|
647
|
+
it "cannot shift to a different day" do
|
648
|
+
expect(basis.last_day).to be nil
|
649
|
+
expect(basis.in_day).to be nil
|
650
|
+
end
|
651
|
+
|
652
|
+
it "can shift to a different month" do
|
653
|
+
allow(subject).to receive(:this_month).and_return subject.parse("2020M4")
|
654
|
+
result = basis.next_month
|
655
|
+
expect(result).to be_a TimeBoss::Calendars::Gregorian::Week
|
656
|
+
expect(result.to_s).to eq "2020W22: 2020-05-25 thru 2020-05-31"
|
657
|
+
expect(basis.in_month).to eq 4
|
658
|
+
end
|
659
|
+
|
660
|
+
it "can shift to a different half" do
|
661
|
+
allow(subject).to receive(:this_half).and_return subject.parse("2019H1")
|
662
|
+
result = basis.last_half
|
663
|
+
expect(result).to be_a TimeBoss::Calendars::Gregorian::Week
|
664
|
+
expect(result.to_s).to eq "2018W34: 2018-08-20 thru 2018-08-26"
|
665
|
+
expect(basis.in_half).to eq 8
|
666
|
+
end
|
667
|
+
end
|
668
|
+
|
560
669
|
context "from month" do
|
561
670
|
let(:basis) { subject.parse("2017M4") }
|
562
671
|
|
563
672
|
it "cannot shift to a different week" do
|
564
|
-
expect
|
565
|
-
expect
|
673
|
+
expect(basis.last_week).to be nil
|
674
|
+
expect(basis.in_week).to be nil
|
566
675
|
end
|
567
676
|
|
568
677
|
it "can shift to a different year" 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: 1.0
|
4
|
+
version: 1.1.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:
|
11
|
+
date: 2022-01-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -184,6 +184,8 @@ files:
|
|
184
184
|
- lib/timeboss/calendar/period.rb
|
185
185
|
- lib/timeboss/calendar/quarter.rb
|
186
186
|
- lib/timeboss/calendar/support/formatter.rb
|
187
|
+
- lib/timeboss/calendar/support/has_fiscal_weeks.rb
|
188
|
+
- lib/timeboss/calendar/support/has_iso_weeks.rb
|
187
189
|
- lib/timeboss/calendar/support/month_basis.rb
|
188
190
|
- lib/timeboss/calendar/support/monthly_unit.rb
|
189
191
|
- lib/timeboss/calendar/support/navigable.rb
|