timeboss 1.0.5 → 1.1.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: 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