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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd2408c9c89006534d10c61c485b8adf4adc5fb8d3fd819ce904ed99c8985db8
4
- data.tar.gz: e27776f25a4fae89655cc04c07e91e8b607fcc50eb65090be0e6ecd16349c4e3
3
+ metadata.gz: 3091d788f78273741aeb0206a87765fed72e3908099c2596b70589740aa63c26
4
+ data.tar.gz: 2a0cd5b28cc9b05c07d3fe06aef86644876864b7b24eda946105706d0626622f
5
5
  SHA512:
6
- metadata.gz: 5b7e5694b2955fbf6043d2e52bc8d0e6e15cbedc8fdee19123473b0487f2a9ba48b88b5cf8bd6e0eeb93db2537724e12a9aeb00378e9cad4022dbc9fc01c8887
7
- data.tar.gz: aefad770303d38a8d2b31baa406cccee9ababea5b2415d6599ec2645e5fca418c61ccbe8f4b6f4aa29a7ead7aea3200b2bce0b83f58f6e6416fb7fb3c9f38b21
6
+ metadata.gz: f58a6f56a3bf0cb2f44a00aee3b5a4d09fdbcd4b2cd8b06c2942fa4eefc508b0afff19dd577bef26bd7b5f187ca1ccb380e5efdd5b357da829ad2936d36c12ff
7
+ data.tar.gz: 5f2b1b5013f49c81e3e5f9066e9e4c3063ddd70231a7199010a79b822bc3531ead0d0bf285d9d045fb0434dcfa6e0a5564aac138a8b25b07e2a374b1c049faf9
@@ -19,7 +19,7 @@ jobs:
19
19
  runs-on: ubuntu-latest
20
20
  strategy:
21
21
  matrix:
22
- ruby-version: ['2.6', '2.7', '3.0']
22
+ ruby-version: ['2.7', '3.0']
23
23
 
24
24
  steps:
25
25
  - uses: actions/checkout@v2
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # TimeBoss ![Build Status](https://github.com/kevinstuffandthings/timeboss/actions/workflows/ruby.yml/badge.svg) [![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)
1
+ # TimeBoss ![Build Status](https://github.com/kevinstuffandthings/timeboss/actions/workflows/ruby.yml/badge.svg) [![Gem Version](https://badge.fury.io/rb/timeboss.svg)](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:2.6.6-slim /bin/bash -c "gem install timeboss >/dev/null && tbsh"
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 entry
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
- num_weeks = (((base.end_date - base.start_date) + 1) / 7.0).to_i
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
@@ -87,6 +87,12 @@ module TimeBoss
87
87
  def inspect
88
88
  "#<#{self.class.name} start_date=#{start_date}, end_date=#{end_date}>"
89
89
  end
90
+
91
+ protected
92
+
93
+ def dates
94
+ @_dates ||= to_range.to_a
95
+ end
90
96
  end
91
97
  end
92
98
  end
@@ -35,11 +35,11 @@ module TimeBoss
35
35
  end
36
36
 
37
37
  # Can this calendar support weeks?
38
- # For custom calendars, this value can generally not be overridden.
39
- # But for calendars like our Gregorian implementation, weeks are irrelevant, and should be suppressed.
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
- true
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TimeBoss
4
- VERSION = "1.0.5"
4
+ VERSION = "1.1.0"
5
5
  end
data/lib/timeboss.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/isolated_execution_state"
3
4
  require "timeboss/version"
4
5
 
5
6
  # TimeBoss
@@ -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(:base) { double(start_date: Date.parse("2018-01-01"), end_date: Date.parse("2018-12-30")) }
31
- before(:each) { allow(calendar).to receive(:year).with(2018).and_return base }
30
+ let(:supports_weeks) { false }
31
+ before(:each) { allow(calendar).to receive(:supports_weeks?).and_return supports_weeks }
32
32
 
33
- it "can get the relevant weeks for the period" do
34
- allow(calendar).to receive(:supports_weeks?).and_return true
35
- result = subject.weeks
36
- result.each { |w| expect(w).to be_instance_of TimeBoss::Calendar::Week }
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
- it "blows up when weeks are not supported" do
51
- allow(calendar).to receive(:supports_weeks?).and_return false
52
- expect { subject.weeks }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
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
- let(:subject) { described_class.new }
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
- 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
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 "2020H2Q1M1D19"
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 "cannot parse a week within a year" do
453
- expect { subject.parse("2018W37") }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
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 "cannot parse a week within a quarter" do
457
- expect { subject.parse("2017Q2W2") }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
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 "cannot parse a week within a month" do
461
- expect { subject.parse("2017M4W1") }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
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 "cannot get the weeks included in a range" do
522
- expect { result.weeks }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
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 "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
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 { basis.last_week }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
565
- expect { basis.in_week }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
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.5
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: 2021-07-31 00:00:00.000000000 Z
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