timeboss 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5bd79ad6e3e02088a3dc23390a8aef1a37d2db8a
4
+ data.tar.gz: 43e17929b3ffc20b7372282db1ca9024e87e11e3
5
+ SHA512:
6
+ metadata.gz: d72e587075701c343742b094537502127206ef2bc8beac51759d5e05bf35eb8306b1cea2161d3de76ed187a85cf04166e91273249ca399a44babeca35f4c17fd
7
+ data.tar.gz: 37660d7992674849d783c35c662bf6c1fa921363a25c7a95afbdb1855d94583d3d469e8374ab67b2a6edeb8d8c8ef0e11e509a1cb46e6da27a40f07ee66d8cf5
@@ -0,0 +1,2 @@
1
+ /*.gem
2
+ /Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.4
4
+ - 2.5
5
+ - 2.6
6
+ script:
7
+ - bundle exec rspec
8
+ deploy:
9
+ provider: rubygems
10
+ api_key:
11
+ secure: LKpYdRJImFZgpZ8NP5jah0pWUUwlmrXIW+dCfyFitm4Xf91XkZ3n4/g1WaUoZwmb/9FAian7PR/3XPuiNSEitr3J8DdbJMH9A/CG3VSyIgj/NE9w5Zl/mC6Pz8F0Ry7iSQ3kjhiW74927vRxc6VenHkm/oaBOwz3NfZQCoIEo27rDW1Xu+K4RviPL76k53Pm06C3xupibQ2m2+OKqwW8QlPCfcN1Q4Vkv/gokXjxGlxBYxxvEfZzCc3ul6TZTWeJgkSsfha6/MjHUj2q40sY+m4MMOrgdWGqau9xayJSQuDdP8c0C6hSyL2arHwbPDSO/4hsm0/h0Alfwrmm4sQmPcI2gVowL7yM44vTtTnuigRKhOFhgaNJH+mNTCGtAxH9Ke5Vx0ZjRKpWAXeE1Js1WARNX5gNj5jhZeyeQn5USV/YLJSqiB188Y0lnt3qdLc7h5FJlXWGgKf3+EPnnPupLaSkL9VWdMCVcP1hlp2CUhJo7dBKANzPiUP+sAsSuA3Ez681OOXPjRTfXFhfBSCd7XBnkvw/iZQeIiiZNbh/3CTKQ0tkJIwnnLR4qKsty3KZHH2cqxYfQkaSjmKDeHymQywWqPO9rUL6oxUz2chkLeiv9iEk/t8pTp+RBRx2K1fVB7/tZ1Ys+eGZBdS1lIrRmvvcVE0g6mAfcwhhyJHuPsk=
12
+ on:
13
+ tags: true
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Kevin McDonald
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,207 @@
1
+ # TimeBoss [![Build Status](https://travis-ci.com/kevinstuffandthings/timeboss.svg?branch=master)](https://travis-ci.com/kevinstuffandthings/timeboss)
2
+ A gem providing convenient navigation of the [Broadcast Calendar](https://en.wikipedia.org/wiki/Broadcast_calendar).
3
+
4
+ _This is a work in progress. Check back soon for the initial release!_
5
+
6
+ ## Installation
7
+ Add this line to your application's Gemfile:
8
+ ```ruby
9
+ # update with the version of your choice
10
+ gem 'timeboss', '0.0.1', git: "https://github.com/kevinstuffandthings/timeboss.git"
11
+ ```
12
+
13
+ And then execute:
14
+ ```bash
15
+ $ bundle install
16
+ ```
17
+
18
+ Or install it yourself as:
19
+ ```bash
20
+ $ gem install timeboss
21
+ ```
22
+
23
+ ## Usage
24
+ Supports `year`, `half`, `quarter`, `month`, `week`, and `day`.
25
+
26
+ Prepare your calendar for use:
27
+ ```ruby
28
+ require 'timeboss/calendars/broadcast'
29
+
30
+ calendar = TimeBoss::Calendars::Broadcast.new
31
+ # => #<TimeBoss::Calendars::Broadcast:0x007f82d50f0af0 @basis=TimeBoss::Calendars::Broadcast::Basis>
32
+ ```
33
+
34
+ You can ask simple questions of the calendar:
35
+ ```ruby
36
+ period = calendar.parse('2019Q4') # or '2018', or '2019-12-21', or '2020W32', or '2020M3W2'
37
+ # => #<TimeBoss::Calendar::Quarter:0x007f82d50e2478>
38
+ period.to_s
39
+ # => "2019Q4: 2019-09-30 thru 2019-12-29"
40
+ period.next.start_date.to_s # try previous, too!
41
+ # => "2019-12-30"
42
+ (period + 3).start_date.to_s # try subtraction, too!
43
+ # => "2020-06-29"
44
+ period.offset(3).start_date.to_s # works with negatives, too!
45
+ # => "2020-06-29"
46
+ period.current? # does today fall within this period?
47
+ # => false
48
+
49
+ calendar.year_for(Date.parse('2018-12-31')).to_s
50
+ # => "2019: 2018-12-31 thru 2019-12-29"
51
+
52
+ calendar.this_month.to_s # your results may vary
53
+ # => "2019M12: 2019-11-25 thru 2019-12-29"
54
+
55
+ calendar.years_back(2).map { |y| y.start_date.to_s } # run in 2020
56
+ # => ["2018-12-31", "2019-12-30"]
57
+
58
+ calendar.months_ago(3).name # run in 2020M7
59
+ # => "2020M4"
60
+
61
+ calendar.weeks_hence(3).name # run in 2020W29
62
+ # => "2020W32"
63
+ ```
64
+
65
+ The resulting periods can be formatted a variety of (parsable) ways:
66
+ ```ruby
67
+ entry = calendar.parse('2020M24')
68
+ entry.format
69
+ # => "2020H1Q2M3W2"
70
+
71
+ entry.format(:quarter)
72
+ # => "2020Q2W11"
73
+
74
+ entry.format(:quarter, :month)
75
+ # => "2020Q2M3W2"
76
+ ```
77
+
78
+ Each type of period can give you information about its constituent periods:
79
+ ```ruby
80
+ calendar.this_month.weeks.map(&:to_s)
81
+ # => ["2020M1W1: 2019-12-30 thru 2020-01-05", "2020M1W2: 2020-01-06 thru 2020-01-12", "2020M1W3: 2020-01-13 thru 2020-01-19", "2020M1W4: 2020-01-20 thru 2020-01-26"]
82
+
83
+ calendar.this_year.weeks.last.to_s
84
+ # => "2020W52: 2020-12-21 thru 2020-12-27"
85
+
86
+ calendar.last_month.quarter.title # today is 2020-07-15
87
+ # => "Q2 2020"
88
+
89
+ calendar.parse('2020Q1').months.map(&:name)
90
+ # => ["2020M1", "2020M2", "2020M3"]
91
+ ```
92
+
93
+ Period shifting is easy. Note that the shifts are relative to today, not the base date. A shift examines the base period to find its offset into the shifting period size, and project it relative to now.
94
+ ```ruby
95
+ calendar.parse('Q3').years_ago(5).title
96
+ # => "Q3 2015"
97
+
98
+ week = calendar.this_week # run 2020W29
99
+ "#{week.name}: #{week.in_quarter} of #{week.quarter.name}; #{week.in_year} of #{week.year.name}"
100
+ # => "2020W29: 3 of 2020Q3; 29 of 2020"
101
+
102
+ # run 2020W29, this generates the same as above, because shifts are relative to date run!
103
+ week = calendar.parse('2014W29').this_week
104
+ "#{week.name}: #{week.in_quarter} of #{week.quarter.name}; #{week.in_year} of #{week.year.name}"
105
+ # => "2020W29: 3 of 2020Q3; 29 of 2020"
106
+
107
+ calendar.this_week.next_year.to_s # run 2020W29
108
+ # => "2021W29: 2021-07-12 thru 2021-07-18"
109
+ ```
110
+
111
+ Complicated range expressions can be parsed using the `..` range operator, or evaluated with `thru`:
112
+ ```ruby
113
+ calendar.parse('2020M1 .. 2020M2').weeks.map(&:title)
114
+ # => ["Week of December 30, 2019", "Week of January 6, 2020", "Week of January 13, 2020", "Week of January 20, 2020", "Week of January 27, 2020", "Week of February 3, 2020", "Week of February 10, 2020", "Week of February 17, 2020"]
115
+
116
+ calendar.this_quarter.thru(calendar.this_quarter+2).months.map(&:name) # run in 2020Q3
117
+ # => ["2020M7", "2020M8", "2020M9", "2020M10", "2020M11", "2020M12", "2021M1", "2021M2", "2021M3"]
118
+
119
+ period = calendar.parse('2020W3..2020Q1')
120
+ "#{period.name}: from #{period.start_date} thru #{period.end_date} (current=#{period.current?})"
121
+ # => "2020W3 .. 2020Q1: from 2020-01-13 thru 2020-03-29 (current=false)"
122
+ ```
123
+
124
+ The examples above are just samples. Try different periods, operators, etc.
125
+
126
+ ### Shell
127
+ To open an IRB shell for the broadcast calendar, use the `timeboss:calendars:broadcast:shell` rake task.
128
+ You will find yourself in the context of an instantiated `TimeBoss::Calendars::Broadcast` object:
129
+ ```bash
130
+ $ rake timeboss:calendars:broadcast:shell
131
+ 2.4.1 :001 > next_quarter
132
+ => #<TimeBoss::Calendar::Quarter:0x007fe04c16a1c8 @calendar=#<TimeBoss::Calendars::Broadcast:0x007fe04c1a0458 @basis=TimeBoss::Calendars::Broadcast::Basis>, @year_index=2020, @index=4, @start_date=#<Date: 2020-09-28 ((2459121j,0s,0n),+0s,2299161j)>, @end_date=#<Date: 2020-12-27 ((2459211j,0s,0n),+0s,2299161j)>>
133
+ 2.4.1 :002 > last_year
134
+ => #<TimeBoss::Calendar::Year:0x007fe04c161ca8 @calendar=#<TimeBoss::Calendars::Broadcast:0x007fe04c1a0458 @basis=TimeBoss::Calendars::Broadcast::Basis>, @year_index=2019, @index=1, @start_date=#<Date: 2018-12-31 ((2458484j,0s,0n),+0s,2299161j)>, @end_date=#<Date: 2019-12-29 ((2458847j,0s,0n),+0s,2299161j)>>
135
+ 2.4.1 :003 > parse('this_quarter .. this_quarter+4').months.map(&:name)
136
+ => ["2020M7", "2020M8", "2020M9", "2020M10", "2020M11", "2020M12", "2021M1", "2021M2", "2021M3", "2021M4", "2021M5", "2021M6", "2021M7", "2021M8", "2021M9"]
137
+ ```
138
+
139
+ _Having trouble with the shell? If you are using the examples from the [Usage](#Usage) section, keep in mind that the shell is already in the context of the calendar -- so you don't need to specify the receiver!_
140
+
141
+ ## Creating new calendars
142
+ To create a custom calendar, simply extend the `TimeBoss::Calendar` class, and implement a new `TimeBoss::Calendar::Support::MonthBasis` for it.
143
+
144
+ ```ruby
145
+ require 'timeboss/calendar'
146
+
147
+ module MyCalendars
148
+ class AugustFiscal < TimeBoss::Calendar
149
+ def initialize
150
+ # For each calendar, operation, the class will be instantiated with an ordinal value
151
+ # for `year` and `month`. It is the instance's job to translate those ordinals into
152
+ # `start_date` and `end_date` values, based on the desired behavior of the calendar.
153
+ # With month rules defined, TimeBoss will be able to navigate all the relative periods
154
+ # within the calendar.
155
+ super(basis: Basis)
156
+ end
157
+
158
+ private
159
+
160
+ class Basis < TimeBoss::Calendar::Support::MonthBasis
161
+ # In this example, August is the first month of the fiscal year. So an incoming 2020/1
162
+ # value would translate to a gregorian 2019/8.
163
+ START_MONTH = 8
164
+
165
+ def start_date
166
+ @_start_date ||= begin
167
+ date = Date.civil(year_index, month_index, 1)
168
+ date - (date.wday + 7) % 7 # In this calendar, months start Sunday.
169
+ end
170
+ end
171
+
172
+ def end_date
173
+ @_end_date ||= begin
174
+ date = Date.civil(year_index, month_index, -1)
175
+ date - (date.wday + 1)
176
+ end
177
+ end
178
+
179
+ private
180
+
181
+ def month_index
182
+ ((month + START_MONTH - 2) % 12) + 1
183
+ end
184
+
185
+ def year_index
186
+ month >= START_MONTH ? year : year - 1
187
+ end
188
+ end
189
+ end
190
+ end
191
+ ```
192
+
193
+ With the new calendar implemented, it can be accessed in one of 2 ways:
194
+ - via traditional instantiation:
195
+ ```ruby
196
+ calendar = MyCalendars::AugustFiscal.new
197
+ calendar.this_year
198
+ ```
199
+ - via `TimeBoss::Calendars`:
200
+ ```ruby
201
+ require 'timeboss/calendars'
202
+ calendar = TimeBoss::Calendars[:august_fiscal]
203
+ calendar.this_year
204
+ ```
205
+
206
+ ## TODO
207
+ - [ ] Add gem deployment via [Travis-CI](https://docs.travis-ci.com/user/deployment/rubygems/#:~:text=Travis%20CI%20can%20automatically%20release,RubyGems%20after%20a%20successful%20build.&text=If%20you%20tag%20a%20commit,tags%20are%20uploaded%20to%20GitHub.&text=You%20will%20be%20prompted%20to,key%20on%20the%20command%20line.)
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ require "bundler/setup"
3
+ require "bundler/gem_tasks"
4
+
5
+ Dir.glob('lib/tasks/*.rake').each { |r| load r }
@@ -0,0 +1,15 @@
1
+ require './lib/timeboss/calendars'
2
+
3
+ namespace :timeboss do
4
+ namespace :calendars do
5
+ TimeBoss::Calendars.each do |entry|
6
+ namespace entry.name do
7
+ desc "Open a shell with the #{entry.name} calendar"
8
+ task shell: ['timeboss:init'] do
9
+ require 'timeboss/support/shellable'
10
+ TimeBoss::Support::Shellable.open(entry.calendar)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ namespace :timeboss do
2
+ task :init do
3
+ require './lib/timeboss'
4
+ require './lib/timeboss/calendars'
5
+ end
6
+ end
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require "timeboss/version"
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/inflector'
3
+ require 'active_support/core_ext/numeric/time'
4
+
5
+ %w[day week month quarter half year].each { |f| require_relative "./calendar/#{f}" }
6
+ %w[waypoints period parser].each { |f| require_relative "./calendar/#{f}" }
7
+ require_relative './calendar/support/month_basis'
8
+
9
+ module TimeBoss
10
+ class Calendar
11
+ include Waypoints
12
+ delegate :parse, to: :parser
13
+
14
+ protected
15
+
16
+ attr_reader :basis
17
+
18
+ def initialize(basis:)
19
+ @basis = basis
20
+ end
21
+
22
+ private
23
+
24
+ def parser
25
+ @_parser ||= Parser.new(self)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ require_relative './support/unit'
3
+
4
+ module TimeBoss
5
+ class Calendar
6
+ class Day < Support::Unit
7
+ def initialize(calendar, start_date)
8
+ super(calendar, start_date, start_date)
9
+ end
10
+
11
+ def name
12
+ start_date.to_s
13
+ end
14
+
15
+ def title
16
+ start_date.strftime('%B %-d, %Y')
17
+ end
18
+
19
+ def to_s
20
+ name
21
+ end
22
+
23
+ def index
24
+ @_index ||= (start_date - calendar.year_for(start_date).start_date).to_i + 1
25
+ end
26
+
27
+ def previous
28
+ self.class.new(calendar, start_date - 1.day)
29
+ end
30
+
31
+ def next
32
+ self.class.new(calendar, start_date + 1.day)
33
+ end
34
+ end
35
+ end
36
+ 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 Half < Support::MonthBased
7
+ NUM_MONTHS = 6
8
+
9
+ def name
10
+ "#{year_index}H#{index}"
11
+ end
12
+
13
+ def title
14
+ "H#{index} #{year_index}"
15
+ end
16
+ end
17
+ end
18
+ 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 Month < Support::MonthBased
7
+ NUM_MONTHS = 1
8
+
9
+ def name
10
+ "#{year_index}M#{index}"
11
+ end
12
+
13
+ def title
14
+ "#{Date::MONTHNAMES[index]} #{year_index}"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+ module TimeBoss
3
+ class Calendar
4
+ class Parser
5
+ RANGE_DELIMITER = '..'
6
+ InvalidPeriodIdentifierError = Class.new(StandardError)
7
+ attr_reader :calendar
8
+
9
+ def initialize(calendar)
10
+ @calendar = calendar
11
+ end
12
+
13
+ def parse(identifier = nil)
14
+ return parse_identifier(identifier.presence) unless identifier&.include?(RANGE_DELIMITER)
15
+ bases = identifier.split(RANGE_DELIMITER).map { |i| parse_identifier(i.strip) } unless identifier.nil?
16
+ bases ||= [parse_identifier(nil)]
17
+ Period.new(calendar, *bases)
18
+ rescue ArgumentError
19
+ raise InvalidPeriodIdentifierError
20
+ end
21
+
22
+ private
23
+
24
+ def parse_identifier(identifier)
25
+ captures = identifier&.match(/^([^-]+)(\s*[+-]\s*[0-9]+)$/)&.captures
26
+ base, offset = captures || [identifier, '0']
27
+ period = parse_period(base&.strip) or raise InvalidPeriodIdentifierError
28
+ period.offset(offset.gsub(/\s+/, '').to_i)
29
+ end
30
+
31
+ def parse_period(identifier)
32
+ return calendar.send(identifier) if calendar.respond_to?(identifier.to_s)
33
+ parse_term(identifier || Date.today.year.to_s)
34
+ end
35
+
36
+ def parse_term(identifier)
37
+ return Day.new(calendar, Date.parse(identifier)) if identifier.match?(/^[0-9]{4}-?[01][0-9]-?[0-3][0-9]$/)
38
+
39
+ raise InvalidPeriodIdentifierError unless identifier.match?(/^[HQMWD0-9]+$/)
40
+ period = if identifier.to_i == 0 then calendar.this_year else calendar.year(identifier.to_i) end
41
+ %w[half quarter month week day].each do |size|
42
+ prefix = size[0].upcase
43
+ next unless identifier.include?(prefix)
44
+ junk, identifier = identifier.split(prefix)
45
+ raise InvalidPeriodIdentifierError if junk.match?(/\D/)
46
+ period = period.send(size.pluralize)[identifier.to_i - 1] or raise InvalidPeriodIdentifierError
47
+ end
48
+ period
49
+ end
50
+ end
51
+ end
52
+ end