timeboss 0.3.1

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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +23 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  4. data/.gitignore +5 -0
  5. data/.replit +2 -0
  6. data/.rspec +2 -0
  7. data/.travis.yml +16 -0
  8. data/.yardopts +1 -0
  9. data/CODE_OF_CONDUCT.md +76 -0
  10. data/Gemfile +3 -0
  11. data/LICENSE.txt +22 -0
  12. data/README.md +233 -0
  13. data/Rakefile +5 -0
  14. data/bin/tbsh +15 -0
  15. data/lib/tasks/calendars.rake +22 -0
  16. data/lib/tasks/timeboss.rake +6 -0
  17. data/lib/timeboss.rb +6 -0
  18. data/lib/timeboss/calendar.rb +64 -0
  19. data/lib/timeboss/calendar/day.rb +48 -0
  20. data/lib/timeboss/calendar/half.rb +22 -0
  21. data/lib/timeboss/calendar/month.rb +22 -0
  22. data/lib/timeboss/calendar/parser.rb +53 -0
  23. data/lib/timeboss/calendar/period.rb +154 -0
  24. data/lib/timeboss/calendar/quarter.rb +22 -0
  25. data/lib/timeboss/calendar/support/formatter.rb +33 -0
  26. data/lib/timeboss/calendar/support/month_basis.rb +21 -0
  27. data/lib/timeboss/calendar/support/monthly_unit.rb +55 -0
  28. data/lib/timeboss/calendar/support/navigable.rb +72 -0
  29. data/lib/timeboss/calendar/support/shiftable.rb +241 -0
  30. data/lib/timeboss/calendar/support/translatable.rb +93 -0
  31. data/lib/timeboss/calendar/support/unit.rb +88 -0
  32. data/lib/timeboss/calendar/waypoints.rb +12 -0
  33. data/lib/timeboss/calendar/waypoints/absolute.rb +113 -0
  34. data/lib/timeboss/calendar/waypoints/relative.rb +267 -0
  35. data/lib/timeboss/calendar/week.rb +53 -0
  36. data/lib/timeboss/calendar/year.rb +18 -0
  37. data/lib/timeboss/calendars.rb +53 -0
  38. data/lib/timeboss/calendars/broadcast.rb +32 -0
  39. data/lib/timeboss/calendars/gregorian.rb +30 -0
  40. data/lib/timeboss/support/shellable.rb +17 -0
  41. data/lib/timeboss/version.rb +4 -0
  42. data/spec/calendar/day_spec.rb +60 -0
  43. data/spec/calendar/quarter_spec.rb +32 -0
  44. data/spec/calendar/support/monthly_unit_spec.rb +85 -0
  45. data/spec/calendar/support/unit_spec.rb +90 -0
  46. data/spec/calendar/week_spec.rb +80 -0
  47. data/spec/calendars/broadcast_spec.rb +796 -0
  48. data/spec/calendars/gregorian_spec.rb +684 -0
  49. data/spec/calendars_spec.rb +50 -0
  50. data/spec/spec_helper.rb +12 -0
  51. data/timeboss.gemspec +31 -0
  52. metadata +215 -0
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ require_relative './support/unit'
3
+
4
+ module TimeBoss
5
+ class Calendar
6
+ class Week < Support::Unit
7
+ def initialize(calendar, start_date, end_date)
8
+ raise UnsupportedUnitError unless calendar.supports_weeks?
9
+ super(calendar, start_date, end_date)
10
+ end
11
+
12
+ # Get a simple representation of this week.
13
+ # @return [String] (e.g. "2020W32")
14
+ def name
15
+ "#{year_index}W#{index}"
16
+ end
17
+
18
+ # Get a "pretty" representation of this week.
19
+ # @return [String] (e.g. "Week of August 3, 2020")
20
+ def title
21
+ "Week of #{start_date.strftime('%B %-d, %Y')}"
22
+ end
23
+
24
+ # Get a stringified representation of this week.
25
+ # @return [String] (e.g. "2020W32: 2020-08-03 thru 2020-08-09")
26
+ def to_s
27
+ "#{name}: #{start_date} thru #{end_date}"
28
+ end
29
+
30
+ # Get the index of this week within its containing year.
31
+ # @return [Integer]
32
+ def index
33
+ @_index ||= (((start_date - year.start_date) + 1) / 7.0).to_i + 1
34
+ end
35
+
36
+ # Get the year number for this week.
37
+ # @return [Integer] (e.g. 2020)
38
+ def year_index
39
+ @_year_index ||= year.year_index
40
+ end
41
+
42
+ private
43
+
44
+ def down
45
+ self.class.new(calendar, start_date - 1.week, end_date - 1.week)
46
+ end
47
+
48
+ def up
49
+ self.class.new(calendar, start_date + 1.week, end_date + 1.week)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ require_relative './support/monthly_unit'
3
+
4
+ module TimeBoss
5
+ class Calendar
6
+ class Year < Support::MonthlyUnit
7
+ NUM_MONTHS = 12
8
+
9
+ # Get a simple representation of this year.
10
+ # @return [String] (e.g. "2020")
11
+ def name
12
+ year_index.to_s
13
+ end
14
+
15
+ alias_method :title, :name
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'calendar'
3
+
4
+ module TimeBoss
5
+ module Calendars
6
+ extend self
7
+ extend Enumerable
8
+ delegate :each, :length, to: :all
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
+
18
+ # Retrieve a list of all registered calendars.
19
+ # @return [Array<Entry>]
20
+ def all
21
+ return if @entries.nil?
22
+ @entries.values.sort_by { |e| e.name.to_s }
23
+ end
24
+
25
+ # Retrieve an instance of the specified named calendar.
26
+ # @param name [String, Symbol] the name of the calendar to retrieve.
27
+ # @return [Calendar]
28
+ def [](name)
29
+ return if @entries.nil?
30
+ @entries[name&.to_sym]&.calendar
31
+ end
32
+
33
+ private
34
+
35
+ Entry = Struct.new(:name, :klass) do
36
+ # @!method name
37
+ # Get the name of the calendar referenced in this entry.
38
+ # @return [Symbol]
39
+
40
+ # @!method klass
41
+ # The class implementing this calendar.
42
+ # @return [Class<Calendar>]
43
+
44
+ # Get an instance of the calendar referenced in this entry.
45
+ # @return [Calendar]
46
+ def calendar
47
+ @_calendar ||= klass.new
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ Dir[File.expand_path('../calendars/*.rb', __FILE__)].each { |f| require f }
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ require_relative '../calendar'
3
+
4
+ module TimeBoss
5
+ module Calendars
6
+ class Broadcast < Calendar
7
+ register!
8
+
9
+ def initialize
10
+ super(basis: Basis)
11
+ end
12
+
13
+ private
14
+
15
+ class Basis < Calendar::Support::MonthBasis
16
+ def start_date
17
+ @_start_date ||= begin
18
+ date = Date.civil(year, month, 1)
19
+ date - (date.wday + 6) % 7
20
+ end
21
+ end
22
+
23
+ def end_date
24
+ @_end_date ||= begin
25
+ date = Date.civil(year, month, -1)
26
+ date - date.wday
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ require_relative '../calendar'
3
+
4
+ module TimeBoss
5
+ module Calendars
6
+ class Gregorian < Calendar
7
+ register!
8
+
9
+ def initialize
10
+ super(basis: Basis)
11
+ end
12
+
13
+ def supports_weeks?
14
+ false
15
+ end
16
+
17
+ private
18
+
19
+ class Basis < Calendar::Support::MonthBasis
20
+ def start_date
21
+ @_start_date ||= Date.civil(year, month, 1)
22
+ end
23
+
24
+ def end_date
25
+ @_end_date ||= Date.civil(year, month, -1)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ module TimeBoss
2
+ module Support
3
+ module Shellable
4
+ def self.open(context)
5
+ context.extend(self).open_shell
6
+ end
7
+
8
+ def open_shell
9
+ require 'irb'
10
+ IRB.setup nil
11
+ IRB.conf[:MAIN_CONTEXT] = IRB::Irb.new.context
12
+ require 'irb/ext/multi-irb'
13
+ IRB.irb nil, self
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module TimeBoss
3
+ VERSION = "0.3.1"
4
+ end
@@ -0,0 +1,60 @@
1
+ module TimeBoss
2
+ class Calendar
3
+ describe Day do
4
+ let(:calendar) { instance_double(TimeBoss::Calendar) }
5
+ let(:start_date) { Date.parse('2019-09-30') }
6
+ let(:subject) { described_class.new(calendar, start_date) }
7
+
8
+ it 'knows its stuff' do
9
+ expect(subject.start_date).to eq start_date
10
+ expect(subject.end_date).to eq start_date
11
+ expect(subject.to_range).to eq start_date..start_date
12
+ end
13
+
14
+ it 'knows its name' do
15
+ expect(subject.name).to eq start_date.to_s
16
+ end
17
+
18
+ it 'knows its title' do
19
+ expect(subject.title).to eq 'September 30, 2019'
20
+ end
21
+
22
+ it 'can stringify itself' do
23
+ expect(subject.to_s).to eq subject.name
24
+ end
25
+
26
+ describe '#index' do
27
+ before(:each) { allow(subject).to receive(:year).and_return double(start_date: start_date - 3) }
28
+
29
+ it 'gets its index within the year' do
30
+ expect(subject.index).to eq 4
31
+ end
32
+ end
33
+
34
+ describe '#current?' do
35
+ it 'knows when it is' do
36
+ allow(Date).to receive(:today).and_return start_date
37
+ expect(subject).to be_current
38
+ end
39
+
40
+ it 'knows when it is not' do
41
+ expect(subject).not_to be_current
42
+ end
43
+ end
44
+
45
+ context 'navigation' do
46
+ it 'can get the previous date' do
47
+ result = subject.previous
48
+ expect(result).to be_a described_class
49
+ expect(result.start_date).to eq start_date - 1.day
50
+ end
51
+
52
+ it 'can get the next date' do
53
+ result = subject.next
54
+ expect(result).to be_a described_class
55
+ expect(result.start_date).to eq start_date + 1.day
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,32 @@
1
+ module TimeBoss
2
+ class Calendar
3
+ describe Quarter do
4
+ let(:calendar) { instance_double(TimeBoss::Calendar) }
5
+ let(:start_date) { Date.parse('2019-09-30') }
6
+ let(:end_date) { Date.parse('2019-12-29') }
7
+ let(:subject) { described_class.new(calendar, 2019, 4, start_date, end_date) }
8
+
9
+ it 'knows its stuff' do
10
+ expect(subject.name).to eq '2019Q4'
11
+ expect(subject.start_date).to eq start_date
12
+ expect(subject.end_date).to eq end_date
13
+ expect(subject.to_range).to eq start_date..end_date
14
+ end
15
+
16
+ it 'can stringify itself' do
17
+ expect(subject.to_s).to include('2019Q4', start_date.to_s, end_date.to_s)
18
+ end
19
+
20
+ describe '#current?' do
21
+ it 'knows when it is' do
22
+ allow(Date).to receive(:today).and_return start_date
23
+ expect(subject).to be_current
24
+ end
25
+
26
+ it 'knows when it is not' do
27
+ expect(subject).not_to be_current
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,85 @@
1
+ module TimeBoss
2
+ class Calendar
3
+ module Support
4
+ describe MonthlyUnit do
5
+ class MonthBasedChunk < described_class
6
+ NUM_MONTHS = 2
7
+
8
+ def name
9
+ "#{year_index}C#{index}"
10
+ end
11
+ end
12
+ let(:described_class) { MonthBasedChunk }
13
+ let(:calendar) { double }
14
+ let(:start_date) { Date.parse('2018-06-25') }
15
+ let(:end_date) { Date.parse('2018-08-26') }
16
+ let(:subject) { described_class.new(calendar, 2018, 4, start_date, end_date) }
17
+
18
+ it 'knows its stuff' do
19
+ expect(subject.start_date).to eq start_date
20
+ expect(subject.end_date).to eq end_date
21
+ expect(subject.to_range).to eq start_date..end_date
22
+ end
23
+
24
+ it 'can stringify itself' do
25
+ expect(subject.to_s).to eq "2018C4: 2018-06-25 thru 2018-08-26"
26
+ end
27
+
28
+ describe '#weeks' do
29
+ let(:base) { double(start_date: Date.parse('2018-01-01'), end_date: Date.parse('2018-12-30')) }
30
+ before(:each) { allow(calendar).to receive(:year).with(2018).and_return base }
31
+
32
+ it 'can get the relevant weeks for the period' do
33
+ allow(calendar).to receive(:supports_weeks?).and_return true
34
+ result = subject.weeks
35
+ result.each { |w| expect(w).to be_instance_of TimeBoss::Calendar::Week }
36
+ expect(result.map { |w| w.start_date.to_s }).to eq [
37
+ '2018-06-25',
38
+ '2018-07-02',
39
+ '2018-07-09',
40
+ '2018-07-16',
41
+ '2018-07-23',
42
+ '2018-07-30',
43
+ '2018-08-06',
44
+ '2018-08-13',
45
+ '2018-08-20'
46
+ ]
47
+ end
48
+
49
+ it 'blows up when weeks are not supported' do
50
+ allow(calendar).to receive(:supports_weeks?).and_return false
51
+ expect { subject.weeks }.to raise_error TimeBoss::Calendar::Support::Unit::UnsupportedUnitError
52
+ end
53
+ end
54
+
55
+ context 'navigation' do
56
+ let(:result) { double }
57
+
58
+ describe '#previous' do
59
+ it 'moves easily within itself' do
60
+ expect(calendar).to receive(:month_based_chunk).with(48, 3).and_return result
61
+ expect(described_class.new(calendar, 48, 4, nil, nil).previous).to eq result
62
+ end
63
+
64
+ it 'flips to the previous container' do
65
+ expect(calendar).to receive(:month_based_chunk).with(47, 6).and_return result
66
+ expect(described_class.new(calendar, 48, 1, nil, nil).previous).to eq result
67
+ end
68
+ end
69
+
70
+ describe '#next' do
71
+ it 'moves easily within itself' do
72
+ expect(calendar).to receive(:month_based_chunk).with(48, 3).and_return result
73
+ expect(described_class.new(calendar, 48, 2, nil, nil).next).to eq result
74
+ end
75
+
76
+ it 'flips to the previous container' do
77
+ expect(calendar).to receive(:month_based_chunk).with(48, 1).and_return result
78
+ expect(described_class.new(calendar, 47, 6, nil, nil).next).to eq result
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,90 @@
1
+ module TimeBoss
2
+ class Calendar
3
+ module Support
4
+ describe Unit do
5
+ class ChunkUnit < described_class
6
+ end
7
+ let(:described_class) { ChunkUnit }
8
+ let(:calendar) { double }
9
+ let(:start_date) { Date.parse('2018-06-25') }
10
+ let(:end_date) { Date.parse('2018-08-26') }
11
+ let(:subject) { described_class.new(calendar, start_date, end_date) }
12
+
13
+ it 'knows its stuff' do
14
+ expect(subject.start_date).to eq start_date
15
+ expect(subject.end_date).to eq end_date
16
+ expect(subject.to_range).to eq start_date..end_date
17
+ end
18
+
19
+ describe '#current?' do
20
+ it 'is not current right now' do
21
+ expect(subject).not_to be_current
22
+ end
23
+
24
+ it 'is current when today falls in the middle' do
25
+ allow(Date).to receive(:today).and_return start_date + 3.days
26
+ expect(subject).to be_current
27
+ end
28
+ end
29
+
30
+ context 'periods' do
31
+ before(:each) do
32
+ allow(calendar).to receive(:days_for).with(subject).and_return %w[D1 D2 D3 D4 D5 D6 D7 D8]
33
+ allow(calendar).to receive(:weeks_for).with(subject).and_return %w[W1 W2 W3 W4]
34
+ allow(calendar).to receive(:months_for).with(subject).and_return %w[M1 M2 M3]
35
+ allow(calendar).to receive(:quarters_for).with(subject).and_return %w[Q1 Q2]
36
+ allow(calendar).to receive(:halves_for).with(subject).and_return %w[H1]
37
+ allow(calendar).to receive(:years_for).with(subject).and_return %w[Y1]
38
+ end
39
+
40
+ it 'knows about its days' do
41
+ expect(subject.days).to eq %w[D1 D2 D3 D4 D5 D6 D7 D8]
42
+ expect(subject.day).to be nil
43
+ end
44
+
45
+ it 'knows about its weeks' do
46
+ expect(subject.weeks).to eq %w[W1 W2 W3 W4]
47
+ expect(subject.week).to be nil
48
+ end
49
+
50
+ it 'knows about its months' do
51
+ expect(subject.months).to eq %w[M1 M2 M3]
52
+ expect(subject.month).to be nil
53
+ end
54
+
55
+ it 'knows about its quarters' do
56
+ expect(subject.quarters).to eq %w[Q1 Q2]
57
+ expect(subject.quarter).to be nil
58
+ end
59
+
60
+ it 'knows about its halves' do
61
+ expect(subject.halves).to eq %w[H1]
62
+ expect(subject.half).to eq 'H1'
63
+ end
64
+
65
+ it 'knows about its years' do
66
+ expect(subject.years).to eq %w[Y1]
67
+ expect(subject.year).to eq 'Y1'
68
+ end
69
+ end
70
+
71
+ context 'navigation' do
72
+ let(:result) { double }
73
+
74
+ describe '#offset' do
75
+ end
76
+
77
+ it 'can increment' do
78
+ expect(subject).to receive(:offset).with(7).and_return result
79
+ expect(subject + 7).to eq result
80
+ end
81
+
82
+ it 'can decrement' do
83
+ expect(subject).to receive(:offset).with(-23).and_return result
84
+ expect(subject - 23).to eq result
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end