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.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +23 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.gitignore +5 -0
- data/.replit +2 -0
- data/.rspec +2 -0
- data/.travis.yml +16 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +76 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +233 -0
- data/Rakefile +5 -0
- data/bin/tbsh +15 -0
- data/lib/tasks/calendars.rake +22 -0
- data/lib/tasks/timeboss.rake +6 -0
- data/lib/timeboss.rb +6 -0
- data/lib/timeboss/calendar.rb +64 -0
- data/lib/timeboss/calendar/day.rb +48 -0
- data/lib/timeboss/calendar/half.rb +22 -0
- data/lib/timeboss/calendar/month.rb +22 -0
- data/lib/timeboss/calendar/parser.rb +53 -0
- data/lib/timeboss/calendar/period.rb +154 -0
- data/lib/timeboss/calendar/quarter.rb +22 -0
- data/lib/timeboss/calendar/support/formatter.rb +33 -0
- data/lib/timeboss/calendar/support/month_basis.rb +21 -0
- data/lib/timeboss/calendar/support/monthly_unit.rb +55 -0
- data/lib/timeboss/calendar/support/navigable.rb +72 -0
- data/lib/timeboss/calendar/support/shiftable.rb +241 -0
- data/lib/timeboss/calendar/support/translatable.rb +93 -0
- data/lib/timeboss/calendar/support/unit.rb +88 -0
- data/lib/timeboss/calendar/waypoints.rb +12 -0
- data/lib/timeboss/calendar/waypoints/absolute.rb +113 -0
- data/lib/timeboss/calendar/waypoints/relative.rb +267 -0
- data/lib/timeboss/calendar/week.rb +53 -0
- data/lib/timeboss/calendar/year.rb +18 -0
- data/lib/timeboss/calendars.rb +53 -0
- data/lib/timeboss/calendars/broadcast.rb +32 -0
- data/lib/timeboss/calendars/gregorian.rb +30 -0
- data/lib/timeboss/support/shellable.rb +17 -0
- data/lib/timeboss/version.rb +4 -0
- data/spec/calendar/day_spec.rb +60 -0
- data/spec/calendar/quarter_spec.rb +32 -0
- data/spec/calendar/support/monthly_unit_spec.rb +85 -0
- data/spec/calendar/support/unit_spec.rb +90 -0
- data/spec/calendar/week_spec.rb +80 -0
- data/spec/calendars/broadcast_spec.rb +796 -0
- data/spec/calendars/gregorian_spec.rb +684 -0
- data/spec/calendars_spec.rb +50 -0
- data/spec/spec_helper.rb +12 -0
- data/timeboss.gemspec +31 -0
- 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,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
|