timeboss 0.0.5

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 (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +13 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +207 -0
  8. data/Rakefile +5 -0
  9. data/lib/tasks/calendars.rake +15 -0
  10. data/lib/tasks/timeboss.rake +6 -0
  11. data/lib/timeboss.rb +2 -0
  12. data/lib/timeboss/calendar.rb +28 -0
  13. data/lib/timeboss/calendar/day.rb +36 -0
  14. data/lib/timeboss/calendar/half.rb +18 -0
  15. data/lib/timeboss/calendar/month.rb +18 -0
  16. data/lib/timeboss/calendar/parser.rb +52 -0
  17. data/lib/timeboss/calendar/period.rb +69 -0
  18. data/lib/timeboss/calendar/quarter.rb +18 -0
  19. data/lib/timeboss/calendar/support/formatter.rb +31 -0
  20. data/lib/timeboss/calendar/support/month_based.rb +51 -0
  21. data/lib/timeboss/calendar/support/month_basis.rb +18 -0
  22. data/lib/timeboss/calendar/support/shiftable.rb +51 -0
  23. data/lib/timeboss/calendar/support/unit.rb +59 -0
  24. data/lib/timeboss/calendar/waypoints.rb +87 -0
  25. data/lib/timeboss/calendar/week.rb +45 -0
  26. data/lib/timeboss/calendar/year.rb +18 -0
  27. data/lib/timeboss/calendars.rb +30 -0
  28. data/lib/timeboss/calendars/broadcast.rb +30 -0
  29. data/lib/timeboss/support/shellable.rb +17 -0
  30. data/lib/timeboss/version.rb +4 -0
  31. data/spec/calendar/day_spec.rb +60 -0
  32. data/spec/calendar/quarter_spec.rb +32 -0
  33. data/spec/calendar/support/month_based_spec.rb +69 -0
  34. data/spec/calendar/support/unit_spec.rb +90 -0
  35. data/spec/calendar/week_spec.rb +83 -0
  36. data/spec/calendars/broadcast_spec.rb +777 -0
  37. data/spec/calendars_spec.rb +42 -0
  38. data/spec/spec_helper.rb +12 -0
  39. data/timeboss.gemspec +30 -0
  40. 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