timeboss 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.rspec +2 -0
- data/.travis.yml +13 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +207 -0
- data/Rakefile +5 -0
- data/lib/tasks/calendars.rake +15 -0
- data/lib/tasks/timeboss.rake +6 -0
- data/lib/timeboss.rb +2 -0
- data/lib/timeboss/calendar.rb +28 -0
- data/lib/timeboss/calendar/day.rb +36 -0
- data/lib/timeboss/calendar/half.rb +18 -0
- data/lib/timeboss/calendar/month.rb +18 -0
- data/lib/timeboss/calendar/parser.rb +52 -0
- data/lib/timeboss/calendar/period.rb +69 -0
- data/lib/timeboss/calendar/quarter.rb +18 -0
- data/lib/timeboss/calendar/support/formatter.rb +31 -0
- data/lib/timeboss/calendar/support/month_based.rb +51 -0
- data/lib/timeboss/calendar/support/month_basis.rb +18 -0
- data/lib/timeboss/calendar/support/shiftable.rb +51 -0
- data/lib/timeboss/calendar/support/unit.rb +59 -0
- data/lib/timeboss/calendar/waypoints.rb +87 -0
- data/lib/timeboss/calendar/week.rb +45 -0
- data/lib/timeboss/calendar/year.rb +18 -0
- data/lib/timeboss/calendars.rb +30 -0
- data/lib/timeboss/calendars/broadcast.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/month_based_spec.rb +69 -0
- data/spec/calendar/support/unit_spec.rb +90 -0
- data/spec/calendar/week_spec.rb +83 -0
- data/spec/calendars/broadcast_spec.rb +777 -0
- data/spec/calendars_spec.rb +42 -0
- data/spec/spec_helper.rb +12 -0
- data/timeboss.gemspec +30 -0
- metadata +188 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module TimeBoss
|
3
|
+
class Calendar
|
4
|
+
class Period
|
5
|
+
attr_reader :begin, :end
|
6
|
+
delegate :start_date, to: :begin
|
7
|
+
delegate :end_date, to: :end
|
8
|
+
|
9
|
+
%i[name title to_s].each do |message|
|
10
|
+
define_method(message) do
|
11
|
+
text = self.begin.send(message)
|
12
|
+
text = "#{text} #{Parser::RANGE_DELIMITER} #{self.end.send(message)}" unless self.end == self.begin
|
13
|
+
text
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
%w[week month quarter half year].each do |size|
|
18
|
+
define_method(size.pluralize) do
|
19
|
+
entry = calendar.send("#{size}_for", self.begin.start_date)
|
20
|
+
build_entries entry
|
21
|
+
end
|
22
|
+
|
23
|
+
define_method(size) do
|
24
|
+
entries = send(size.pluralize)
|
25
|
+
return nil unless entries.length == 1
|
26
|
+
entries.first
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def current?
|
31
|
+
to_range.include?(Date.today)
|
32
|
+
end
|
33
|
+
|
34
|
+
def days
|
35
|
+
to_range.map { |d| Day.new(calendar, d) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def day
|
39
|
+
entries = days
|
40
|
+
return nil unless entries.length == 1
|
41
|
+
entries.first
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_range
|
45
|
+
@_to_range ||= start_date .. end_date
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
attr_reader :calendar
|
51
|
+
|
52
|
+
def initialize(calendar, begin_basis, end_basis = nil)
|
53
|
+
@calendar = calendar
|
54
|
+
@begin = begin_basis
|
55
|
+
@end = end_basis || @begin
|
56
|
+
end
|
57
|
+
|
58
|
+
def build_entries(entry)
|
59
|
+
return [] if entry.start_date > self.end.end_date
|
60
|
+
entries = [entry]
|
61
|
+
while entry.end_date < self.end.end_date
|
62
|
+
entry = entry.next
|
63
|
+
entries << entry
|
64
|
+
end
|
65
|
+
entries
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative './support/month_based'
|
3
|
+
|
4
|
+
module TimeBoss
|
5
|
+
class Calendar
|
6
|
+
class Quarter < Support::MonthBased
|
7
|
+
NUM_MONTHS = 3
|
8
|
+
|
9
|
+
def name
|
10
|
+
"#{year_index}Q#{index}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def title
|
14
|
+
"Q#{index} #{year_index}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative './shiftable'
|
3
|
+
|
4
|
+
module TimeBoss
|
5
|
+
class Calendar
|
6
|
+
module Support
|
7
|
+
private
|
8
|
+
|
9
|
+
class Formatter
|
10
|
+
PERIODS = Shiftable::PERIODS.reverse.map(&:to_sym).drop(1)
|
11
|
+
attr_reader :unit, :periods
|
12
|
+
|
13
|
+
def initialize(unit, periods)
|
14
|
+
@unit = unit
|
15
|
+
@periods = PERIODS & periods.map(&:to_sym).push(unit.class.type.to_sym)
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
base, text = 'year', unit.year.name
|
20
|
+
periods.each do |period|
|
21
|
+
sub = unit.send(period) or break
|
22
|
+
index = sub.send("in_#{base}")
|
23
|
+
text += "#{period[0].upcase}#{index}"
|
24
|
+
base = period
|
25
|
+
end
|
26
|
+
text
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative './unit'
|
3
|
+
|
4
|
+
module TimeBoss
|
5
|
+
class Calendar
|
6
|
+
module Support
|
7
|
+
class MonthBased < Unit
|
8
|
+
attr_reader :year_index, :index
|
9
|
+
|
10
|
+
def initialize(calendar, year_index, index, start_date, end_date)
|
11
|
+
super(calendar, start_date, end_date)
|
12
|
+
@year_index = year_index
|
13
|
+
@index = index
|
14
|
+
end
|
15
|
+
|
16
|
+
def next
|
17
|
+
if index == max_index
|
18
|
+
calendar.send(self.class.type, year_index + 1, 1)
|
19
|
+
else
|
20
|
+
calendar.send(self.class.type, year_index, index + 1)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def previous
|
25
|
+
if index == 1
|
26
|
+
calendar.send(self.class.type, year_index - 1, max_index)
|
27
|
+
else
|
28
|
+
calendar.send(self.class.type, year_index, index - 1)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
"#{name}: #{start_date} thru #{end_date}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def weeks
|
37
|
+
base = calendar.year(year_index)
|
38
|
+
num_weeks = (((base.end_date - base.start_date) + 1) / 7.0).to_i
|
39
|
+
num_weeks.times.map { |i| Week.new(calendar, year_index, i + 1, base.start_date + (i * 7).days, base.start_date + ((i * 7) + 6).days) }
|
40
|
+
.select { |w| w.start_date.between?(start_date, end_date) }
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def max_index
|
46
|
+
12 / self.class::NUM_MONTHS
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module TimeBoss
|
2
|
+
class Calendar
|
3
|
+
module Support
|
4
|
+
class MonthBasis
|
5
|
+
attr_reader :year, :month
|
6
|
+
|
7
|
+
def initialize(year, month)
|
8
|
+
@year = year
|
9
|
+
@month = month
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_range
|
13
|
+
@_to_range ||= start_date .. end_date
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module TimeBoss
|
3
|
+
class Calendar
|
4
|
+
module Support
|
5
|
+
module Shiftable
|
6
|
+
PERIODS = %w[day week month quarter half year]
|
7
|
+
|
8
|
+
PERIODS.each do |period|
|
9
|
+
periods = period.pluralize
|
10
|
+
|
11
|
+
define_method periods do
|
12
|
+
calendar.send("#{periods}_for", self)
|
13
|
+
end
|
14
|
+
|
15
|
+
define_method period do
|
16
|
+
entries = send(periods)
|
17
|
+
return nil unless entries.length == 1
|
18
|
+
entries.first
|
19
|
+
end
|
20
|
+
|
21
|
+
define_method "in_#{period}" do
|
22
|
+
base = send(periods)
|
23
|
+
return unless base.length == 1
|
24
|
+
base.first.send(self.class.type.to_s.pluralize).find_index { |p| p == self } + 1
|
25
|
+
end
|
26
|
+
|
27
|
+
define_method "#{periods}_ago" do |offset|
|
28
|
+
base_offset = send("in_#{period}") or return
|
29
|
+
(calendar.send("this_#{period}") - offset).send(self.class.type.to_s.pluralize)[base_offset - 1]
|
30
|
+
end
|
31
|
+
|
32
|
+
define_method "last_#{period}" do
|
33
|
+
send("#{periods}_ago", 1)
|
34
|
+
end
|
35
|
+
|
36
|
+
define_method "this_#{period}" do
|
37
|
+
send("#{periods}_ago", 0)
|
38
|
+
end
|
39
|
+
|
40
|
+
define_method "#{periods}_hence" do |offset|
|
41
|
+
send("#{periods}_ago", offset * -1)
|
42
|
+
end
|
43
|
+
|
44
|
+
define_method "next_#{period}" do
|
45
|
+
send("#{periods}_hence", 1)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative './shiftable'
|
3
|
+
require_relative './formatter'
|
4
|
+
|
5
|
+
module TimeBoss
|
6
|
+
class Calendar
|
7
|
+
module Support
|
8
|
+
class Unit
|
9
|
+
include Shiftable
|
10
|
+
attr_reader :calendar, :start_date, :end_date
|
11
|
+
|
12
|
+
def self.type
|
13
|
+
self.name.demodulize.underscore
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(calendar, start_date, end_date)
|
17
|
+
@calendar = calendar
|
18
|
+
@start_date = start_date
|
19
|
+
@end_date = end_date
|
20
|
+
end
|
21
|
+
|
22
|
+
def ==(entry)
|
23
|
+
self.class == entry.class && self.start_date == entry.start_date && self.end_date == entry.end_date
|
24
|
+
end
|
25
|
+
|
26
|
+
def format(*periods)
|
27
|
+
Formatter.new(self, periods.presence || Formatter::PERIODS).to_s
|
28
|
+
end
|
29
|
+
|
30
|
+
def thru(unit)
|
31
|
+
Period.new(calendar, self, unit)
|
32
|
+
end
|
33
|
+
|
34
|
+
def current?
|
35
|
+
Date.today.between?(start_date, end_date)
|
36
|
+
end
|
37
|
+
|
38
|
+
def offset(value)
|
39
|
+
method = value.negative? ? :previous : :next
|
40
|
+
base = self
|
41
|
+
value.abs.times { base = base.send(method) }
|
42
|
+
base
|
43
|
+
end
|
44
|
+
|
45
|
+
def +(value)
|
46
|
+
offset(value)
|
47
|
+
end
|
48
|
+
|
49
|
+
def -(value)
|
50
|
+
offset(-value)
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_range
|
54
|
+
@_to_range ||= start_date .. end_date
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module TimeBoss
|
3
|
+
class Calendar
|
4
|
+
module Waypoints
|
5
|
+
%i[month quarter half year].each do |type|
|
6
|
+
klass = TimeBoss::Calendar.const_get(type.to_s.classify)
|
7
|
+
size = klass.const_get("NUM_MONTHS")
|
8
|
+
|
9
|
+
define_method type do |year_index, index = 1|
|
10
|
+
month = (index * size) - size + 1
|
11
|
+
months = (month .. month + size - 1).map { |i| basis.new(year_index, i) }
|
12
|
+
klass.new(self, year_index, index, months.first.start_date, months.last.end_date)
|
13
|
+
end
|
14
|
+
|
15
|
+
define_method "#{type}_for" do |date|
|
16
|
+
window = send(type, date.year - 1, 1)
|
17
|
+
while true
|
18
|
+
break window if window.to_range.include?(date)
|
19
|
+
window = window.next
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
%i[day week month quarter half year].each do |type|
|
25
|
+
define_method("this_#{type}") { send("#{type}_for", Date.today) }
|
26
|
+
define_method("last_#{type}") { send("this_#{type}").previous }
|
27
|
+
define_method("next_#{type}") { send("this_#{type}").next }
|
28
|
+
|
29
|
+
define_method "#{type.to_s.pluralize}_for" do |entry|
|
30
|
+
first = send("#{type}_for", entry.start_date)
|
31
|
+
constituents = [first]
|
32
|
+
until first.end_date >= entry.end_date
|
33
|
+
first = first.next
|
34
|
+
constituents << first
|
35
|
+
end
|
36
|
+
constituents
|
37
|
+
end
|
38
|
+
|
39
|
+
define_method "#{type.to_s.pluralize}_back" do |quantity|
|
40
|
+
windows = []
|
41
|
+
window = send("this_#{type}")
|
42
|
+
while quantity > 0
|
43
|
+
windows << window.dup
|
44
|
+
window = window.previous
|
45
|
+
quantity -= 1
|
46
|
+
end
|
47
|
+
windows.reverse
|
48
|
+
end
|
49
|
+
|
50
|
+
define_method "#{type.to_s.pluralize}_ago" do |quantity|
|
51
|
+
send("#{type.to_s.pluralize}_back", quantity + 1).first
|
52
|
+
end
|
53
|
+
|
54
|
+
define_method type.to_s.pluralize do |quantity|
|
55
|
+
windows = []
|
56
|
+
window = send("this_#{type}")
|
57
|
+
while quantity > 0
|
58
|
+
windows << window.dup
|
59
|
+
window = window.next
|
60
|
+
quantity -= 1
|
61
|
+
end
|
62
|
+
windows
|
63
|
+
end
|
64
|
+
|
65
|
+
define_method "#{type.to_s.pluralize}_hence" do |quantity|
|
66
|
+
send(type.to_s.pluralize, quantity + 1).last
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
%i[yesterday today tomorrow].each do |period|
|
71
|
+
define_method(period) { Day.new(self, Date.send(period)) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def day(year_index, index)
|
75
|
+
year(year_index).days[index - 1]
|
76
|
+
end
|
77
|
+
|
78
|
+
def day_for(date)
|
79
|
+
Day.new(self, date)
|
80
|
+
end
|
81
|
+
|
82
|
+
def week_for(date)
|
83
|
+
year_for(date).weeks.find { |w| w.to_range.include?(date) }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative './support/unit'
|
3
|
+
|
4
|
+
module TimeBoss
|
5
|
+
class Calendar
|
6
|
+
class Week < Support::Unit
|
7
|
+
attr_reader :year_index, :index
|
8
|
+
|
9
|
+
def initialize(calendar, year_index, index, start_date, end_date)
|
10
|
+
super(calendar, start_date, end_date)
|
11
|
+
@year_index = year_index
|
12
|
+
@index = index
|
13
|
+
end
|
14
|
+
|
15
|
+
def name
|
16
|
+
"#{year_index}W#{index}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def title
|
20
|
+
"Week of #{start_date.strftime('%B %-d, %Y')}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
"#{name}: #{start_date} thru #{end_date}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def previous
|
28
|
+
if index == 1
|
29
|
+
(calendar.year_for(start_date) - 1).weeks.last
|
30
|
+
else
|
31
|
+
self.class.new(calendar, year_index, index - 1, start_date - 1.week, end_date - 1.week)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def next
|
36
|
+
weeks = calendar.year_for(start_date).weeks
|
37
|
+
if index == weeks.last.index
|
38
|
+
self.class.new(calendar, year_index + 1, 1, start_date + 1.week, end_date + 1.week)
|
39
|
+
else
|
40
|
+
weeks[index]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative './support/month_based'
|
3
|
+
|
4
|
+
module TimeBoss
|
5
|
+
class Calendar
|
6
|
+
class Year < Support::MonthBased
|
7
|
+
NUM_MONTHS = 12
|
8
|
+
|
9
|
+
def name
|
10
|
+
year_index.to_s
|
11
|
+
end
|
12
|
+
|
13
|
+
def title
|
14
|
+
name
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|