timeboss 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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