montrose 0.12.0 → 0.14.0

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +24 -32
  3. data/.gitignore +0 -3
  4. data/.rubocop.yml +5 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +24 -0
  7. data/Gemfile +4 -9
  8. data/Gemfile.lock +148 -0
  9. data/README.md +20 -22
  10. data/bin/bundle-all +5 -0
  11. data/gemfiles/activesupport_5.2.gemfile +1 -12
  12. data/gemfiles/activesupport_5.2.gemfile.lock +107 -0
  13. data/gemfiles/activesupport_6.0.gemfile +1 -12
  14. data/gemfiles/activesupport_6.0.gemfile.lock +108 -0
  15. data/gemfiles/activesupport_6.1.gemfile +1 -12
  16. data/gemfiles/activesupport_6.1.gemfile.lock +108 -0
  17. data/gemfiles/activesupport_7.0.gemfile +5 -0
  18. data/gemfiles/activesupport_7.0.gemfile.lock +106 -0
  19. data/lib/montrose/chainable.rb +0 -1
  20. data/lib/montrose/clock.rb +54 -9
  21. data/lib/montrose/day.rb +83 -0
  22. data/lib/montrose/frequency.rb +60 -27
  23. data/lib/montrose/hour.rb +22 -0
  24. data/lib/montrose/ical.rb +128 -0
  25. data/lib/montrose/minute.rb +22 -0
  26. data/lib/montrose/month.rb +47 -0
  27. data/lib/montrose/month_day.rb +25 -0
  28. data/lib/montrose/options.rb +56 -65
  29. data/lib/montrose/recurrence.rb +18 -12
  30. data/lib/montrose/rule/during.rb +7 -15
  31. data/lib/montrose/rule/minute_of_hour.rb +25 -0
  32. data/lib/montrose/rule/nth_day_of_month.rb +0 -2
  33. data/lib/montrose/rule/nth_day_of_year.rb +0 -2
  34. data/lib/montrose/rule/time_of_day.rb +1 -1
  35. data/lib/montrose/rule.rb +18 -16
  36. data/lib/montrose/schedule.rb +7 -7
  37. data/lib/montrose/stack.rb +1 -2
  38. data/lib/montrose/time_of_day.rb +48 -0
  39. data/lib/montrose/utils.rb +0 -38
  40. data/lib/montrose/version.rb +1 -1
  41. data/lib/montrose/week.rb +20 -0
  42. data/lib/montrose/year_day.rb +25 -0
  43. data/lib/montrose.rb +20 -8
  44. data/montrose.gemspec +4 -3
  45. metadata +42 -11
  46. data/Appraisals +0 -13
  47. data/bin/appraisal +0 -17
@@ -0,0 +1,108 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ montrose (0.13.0)
5
+ activesupport (>= 5.2, < 7.1)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activesupport (6.1.7.6)
11
+ concurrent-ruby (~> 1.0, >= 1.0.2)
12
+ i18n (>= 1.6, < 2)
13
+ minitest (>= 5.1)
14
+ tzinfo (~> 2.0)
15
+ zeitwerk (~> 2.3)
16
+ ast (2.4.2)
17
+ base64 (0.1.1)
18
+ concurrent-ruby (1.2.2)
19
+ coveralls (0.8.23)
20
+ json (>= 1.8, < 3)
21
+ simplecov (~> 0.16.1)
22
+ term-ansicolor (~> 1.3)
23
+ thor (>= 0.19.4, < 2.0)
24
+ tins (~> 1.6)
25
+ docile (1.4.0)
26
+ i18n (1.14.1)
27
+ concurrent-ruby (~> 1.0)
28
+ json (2.6.3)
29
+ language_server-protocol (3.17.0.3)
30
+ lint_roller (1.1.0)
31
+ m (1.6.1)
32
+ method_source (>= 0.6.7)
33
+ rake (>= 0.9.2.2)
34
+ method_source (1.0.0)
35
+ minitest (5.19.0)
36
+ parallel (1.23.0)
37
+ parser (3.2.2.3)
38
+ ast (~> 2.4.1)
39
+ racc
40
+ racc (1.7.1)
41
+ rainbow (3.1.1)
42
+ rake (13.0.6)
43
+ regexp_parser (2.8.1)
44
+ rexml (3.2.6)
45
+ rubocop (1.56.1)
46
+ base64 (~> 0.1.1)
47
+ json (~> 2.3)
48
+ language_server-protocol (>= 3.17.0)
49
+ parallel (~> 1.10)
50
+ parser (>= 3.2.2.3)
51
+ rainbow (>= 2.2.2, < 4.0)
52
+ regexp_parser (>= 1.8, < 3.0)
53
+ rexml (>= 3.2.5, < 4.0)
54
+ rubocop-ast (>= 1.28.1, < 2.0)
55
+ ruby-progressbar (~> 1.7)
56
+ unicode-display_width (>= 2.4.0, < 3.0)
57
+ rubocop-ast (1.29.0)
58
+ parser (>= 3.2.1.0)
59
+ rubocop-performance (1.19.0)
60
+ rubocop (>= 1.7.0, < 2.0)
61
+ rubocop-ast (>= 0.4.0)
62
+ ruby-progressbar (1.13.0)
63
+ simplecov (0.16.1)
64
+ docile (~> 1.1)
65
+ json (>= 1.8, < 3)
66
+ simplecov-html (~> 0.10.0)
67
+ simplecov-html (0.10.2)
68
+ standard (1.31.0)
69
+ language_server-protocol (~> 3.17.0.2)
70
+ lint_roller (~> 1.0)
71
+ rubocop (~> 1.56.0)
72
+ standard-custom (~> 1.0.0)
73
+ standard-performance (~> 1.2)
74
+ standard-custom (1.0.2)
75
+ lint_roller (~> 1.0)
76
+ rubocop (~> 1.50)
77
+ standard-performance (1.2.0)
78
+ lint_roller (~> 1.1)
79
+ rubocop-performance (~> 1.19.0)
80
+ sync (0.5.0)
81
+ term-ansicolor (1.7.1)
82
+ tins (~> 1.0)
83
+ thor (1.2.2)
84
+ timecop (0.9.8)
85
+ tins (1.32.1)
86
+ sync
87
+ tzinfo (2.0.6)
88
+ concurrent-ruby (~> 1.0)
89
+ unicode-display_width (2.4.2)
90
+ yard (0.9.34)
91
+ zeitwerk (2.6.11)
92
+
93
+ PLATFORMS
94
+ arm64-darwin-22
95
+
96
+ DEPENDENCIES
97
+ activesupport (~> 6.1)
98
+ coveralls
99
+ m
100
+ minitest
101
+ montrose!
102
+ rake (>= 12.3.3)
103
+ standard
104
+ timecop
105
+ yard
106
+
107
+ BUNDLED WITH
108
+ 2.4.10
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "activesupport", "~> 7.0"
4
+
5
+ gemspec path: "../"
@@ -0,0 +1,106 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ montrose (0.13.0)
5
+ activesupport (>= 5.2, < 7.1)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activesupport (7.0.7.2)
11
+ concurrent-ruby (~> 1.0, >= 1.0.2)
12
+ i18n (>= 1.6, < 2)
13
+ minitest (>= 5.1)
14
+ tzinfo (~> 2.0)
15
+ ast (2.4.2)
16
+ base64 (0.1.1)
17
+ concurrent-ruby (1.2.2)
18
+ coveralls (0.8.23)
19
+ json (>= 1.8, < 3)
20
+ simplecov (~> 0.16.1)
21
+ term-ansicolor (~> 1.3)
22
+ thor (>= 0.19.4, < 2.0)
23
+ tins (~> 1.6)
24
+ docile (1.4.0)
25
+ i18n (1.14.1)
26
+ concurrent-ruby (~> 1.0)
27
+ json (2.6.3)
28
+ language_server-protocol (3.17.0.3)
29
+ lint_roller (1.1.0)
30
+ m (1.6.1)
31
+ method_source (>= 0.6.7)
32
+ rake (>= 0.9.2.2)
33
+ method_source (1.0.0)
34
+ minitest (5.19.0)
35
+ parallel (1.23.0)
36
+ parser (3.2.2.3)
37
+ ast (~> 2.4.1)
38
+ racc
39
+ racc (1.7.1)
40
+ rainbow (3.1.1)
41
+ rake (13.0.6)
42
+ regexp_parser (2.8.1)
43
+ rexml (3.2.6)
44
+ rubocop (1.56.1)
45
+ base64 (~> 0.1.1)
46
+ json (~> 2.3)
47
+ language_server-protocol (>= 3.17.0)
48
+ parallel (~> 1.10)
49
+ parser (>= 3.2.2.3)
50
+ rainbow (>= 2.2.2, < 4.0)
51
+ regexp_parser (>= 1.8, < 3.0)
52
+ rexml (>= 3.2.5, < 4.0)
53
+ rubocop-ast (>= 1.28.1, < 2.0)
54
+ ruby-progressbar (~> 1.7)
55
+ unicode-display_width (>= 2.4.0, < 3.0)
56
+ rubocop-ast (1.29.0)
57
+ parser (>= 3.2.1.0)
58
+ rubocop-performance (1.19.0)
59
+ rubocop (>= 1.7.0, < 2.0)
60
+ rubocop-ast (>= 0.4.0)
61
+ ruby-progressbar (1.13.0)
62
+ simplecov (0.16.1)
63
+ docile (~> 1.1)
64
+ json (>= 1.8, < 3)
65
+ simplecov-html (~> 0.10.0)
66
+ simplecov-html (0.10.2)
67
+ standard (1.31.0)
68
+ language_server-protocol (~> 3.17.0.2)
69
+ lint_roller (~> 1.0)
70
+ rubocop (~> 1.56.0)
71
+ standard-custom (~> 1.0.0)
72
+ standard-performance (~> 1.2)
73
+ standard-custom (1.0.2)
74
+ lint_roller (~> 1.0)
75
+ rubocop (~> 1.50)
76
+ standard-performance (1.2.0)
77
+ lint_roller (~> 1.1)
78
+ rubocop-performance (~> 1.19.0)
79
+ sync (0.5.0)
80
+ term-ansicolor (1.7.1)
81
+ tins (~> 1.0)
82
+ thor (1.2.2)
83
+ timecop (0.9.8)
84
+ tins (1.32.1)
85
+ sync
86
+ tzinfo (2.0.6)
87
+ concurrent-ruby (~> 1.0)
88
+ unicode-display_width (2.4.2)
89
+ yard (0.9.34)
90
+
91
+ PLATFORMS
92
+ arm64-darwin-22
93
+
94
+ DEPENDENCIES
95
+ activesupport (~> 7.0)
96
+ coveralls
97
+ m
98
+ minitest
99
+ montrose!
100
+ rake (>= 12.3.3)
101
+ standard
102
+ timecop
103
+ yard
104
+
105
+ BUNDLED WITH
106
+ 2.4.10
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "montrose/options"
4
3
  require "montrose/refinements/array_concat"
5
4
 
6
5
  module Montrose
@@ -10,32 +10,32 @@ module Montrose
10
10
  @every = @options.fetch(:every) { fail ConfigurationError, "Required option :every not provided" }
11
11
  @interval = @options.fetch(:interval)
12
12
  @start_time = @options.fetch(:start_time)
13
- @at = @options.fetch(:at, nil)
13
+ @at = @options.fetch(:at, []).sort
14
14
  end
15
15
 
16
16
  # Advances time to new unit by increment and sets
17
17
  # new time as "current" time for next tick
18
18
  #
19
19
  def tick
20
- @time = peek
20
+ @time = next_time(true)
21
21
  end
22
22
 
23
23
  def peek
24
- return @start_time if @time.nil?
24
+ next_time(false)
25
+ end
25
26
 
26
- if @at
27
- times = @at.map { |hour, min, sec = 0| @time.change(hour: hour, min: min, sec: sec) }
27
+ private
28
28
 
29
- (min_next = times.select { |t| t > @time }.min) && (return min_next)
29
+ def next_time(tick)
30
+ return @start_time if @time.nil?
30
31
 
31
- advance_step(times.min || @time)
32
+ if @at.present?
33
+ next_time_at(@time, tick)
32
34
  else
33
35
  advance_step(@time)
34
36
  end
35
37
  end
36
38
 
37
- private
38
-
39
39
  def advance_step(time)
40
40
  time.advance(step)
41
41
  end
@@ -54,6 +54,51 @@ module Montrose
54
54
  unit_step(:year)
55
55
  end
56
56
 
57
+ # @private
58
+ #
59
+ # Returns next time using :at option. Tries to calculate
60
+ # a time for the current date by incrementing the index
61
+ # of the :at option. Once all items have been exhausted
62
+ # the minimum time is generated for the current date and
63
+ # we advance to the next date based on interval
64
+ #
65
+ def next_time_at(time, tick)
66
+ if current_at_index && (next_time = time_at(time, current_at_index + 1))
67
+ @current_at_index += 1 if tick
68
+
69
+ next_time
70
+ else
71
+ min_time = time_at(time, 0)
72
+ @current_at_index = 0 if tick
73
+
74
+ advance_step(min_time)
75
+ end
76
+ end
77
+
78
+ # @private
79
+ #
80
+ # Returns time with hour, minute and second from :at option
81
+ # at specified index
82
+ #
83
+ def time_at(time, index)
84
+ parts = @at[index]
85
+
86
+ return unless parts
87
+
88
+ hour, min, sec = parts
89
+ time.change(hour: hour, min: min, sec: sec || 0)
90
+ end
91
+
92
+ # @private
93
+ #
94
+ # Keep track of which index we are currently at for :at option.
95
+ #
96
+ def current_at_index
97
+ @current_at_index ||= @at.index do |hour, min, sec = 0|
98
+ @start_time.hour == hour && @start_time.min == min && @start_time.sec == sec
99
+ end
100
+ end
101
+
57
102
  # @private
58
103
  #
59
104
  # Returns hash representing unit and amount to advance time
@@ -0,0 +1,83 @@
1
+ module Montrose
2
+ class Day
3
+ extend Montrose::Utils
4
+
5
+ NAMES = ::Date::DAYNAMES
6
+ TWO_LETTER_ABBREVIATIONS = %w[SU MO TU WE TH FR SA].freeze
7
+ THREE_LETTER_ABBREVIATIONS = %w[SUN MON TUE WED THU FRI SAT]
8
+ NUMBERS = NAMES.map.with_index { |_n, i| i.to_s }
9
+
10
+ ICAL_MATCH = /(?<ordinal>[+-]?\d+)?(?<day>[A-Z]{2})/ # e.g. 1FR
11
+
12
+ class << self
13
+ def parse(arg)
14
+ case arg
15
+ when Hash
16
+ parse_entries(arg.entries)
17
+ when String
18
+ parse(arg.split(","))
19
+ else
20
+ parse_entries(map_arg(arg) { |value| parse_value(value) })
21
+ end
22
+ end
23
+
24
+ def parse_entries(entries)
25
+ hash = Hash.new { |h, k| h[k] = [] }
26
+ result = entries.each_with_object(hash) { |(k, v), hash|
27
+ index = number!(k)
28
+ hash[index] = hash[index] + [*v]
29
+ }
30
+ result.values.all?(&:empty?) ? result.keys : result
31
+ end
32
+
33
+ def parse_value(value)
34
+ parse_ical(value) || [number!(value), nil]
35
+ end
36
+
37
+ def parse_ical(value)
38
+ (match = ICAL_MATCH.match(value.to_s)) || (return nil)
39
+ index = number!(match[:day])
40
+ ordinal = match[:ordinal]&.to_i
41
+ [index, ordinal]
42
+ end
43
+
44
+ def map_arg(arg, &block)
45
+ return nil unless arg.present?
46
+
47
+ Array(arg).map(&block)
48
+ end
49
+
50
+ def names
51
+ NAMES
52
+ end
53
+
54
+ def number(name)
55
+ case name
56
+ when 0..6
57
+ name
58
+ when Symbol, String
59
+ string = name.to_s.downcase
60
+ NAMES.index(string.titleize) ||
61
+ TWO_LETTER_ABBREVIATIONS.index(string.upcase) ||
62
+ THREE_LETTER_ABBREVIATIONS.index(string.upcase) ||
63
+ number(to_index(string))
64
+ when Array
65
+ number name.first
66
+ end
67
+ end
68
+
69
+ def number!(name)
70
+ number(name) || raise(ConfigurationError,
71
+ "Did not recognize day #{name}, must be one of #{(names + abbreviations + numbers).inspect}")
72
+ end
73
+
74
+ def numbers
75
+ NUMBERS
76
+ end
77
+
78
+ def abbreviations
79
+ TWO_LETTER_ABBREVIATIONS + THREE_LETTER_ABBREVIATIONS
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,14 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "montrose/errors"
4
- require "montrose/options"
5
-
6
3
  module Montrose
7
4
  # Abstract class for special recurrence rule required
8
5
  # in all instances of Recurrence. Frequency describes
9
6
  # the base recurrence interval.
10
7
  #
11
8
  class Frequency
9
+ autoload :Daily, "montrose/frequency/daily"
10
+ autoload :Hourly, "montrose/frequency/hourly"
11
+ autoload :Minutely, "montrose/frequency/minutely"
12
+ autoload :Monthly, "montrose/frequency/monthly"
13
+ autoload :Secondly, "montrose/frequency/secondly"
14
+ autoload :Weekly, "montrose/frequency/weekly"
15
+ autoload :Yearly, "montrose/frequency/yearly"
16
+
12
17
  include Montrose::Rule
13
18
 
14
19
  FREQUENCY_TERMS = {
@@ -25,24 +30,60 @@ module Montrose
25
30
 
26
31
  attr_reader :time, :starts
27
32
 
28
- # Factory method for instantiating the appropriate Frequency
29
- # subclass.
30
- #
31
- def self.from_options(opts)
32
- frequency = opts.fetch(:every) { fail ConfigurationError, "Please specify the :every option" }
33
- class_name = FREQUENCY_TERMS.fetch(frequency.to_s) {
34
- fail "Don't know how to enumerate every: #{frequency}"
35
- }
33
+ class << self
34
+ def parse(input)
35
+ if input.respond_to?(:parts)
36
+ frequency, interval = duration_to_frequency_parts(input)
37
+ {every: frequency.to_s.singularize.to_sym, interval: interval}
38
+ elsif input.is_a?(Numeric)
39
+ frequency, interval = numeric_to_frequency_parts(input)
40
+ {every: frequency, interval: interval}
41
+ else
42
+ {every: Frequency.assert(input)}
43
+ end
44
+ end
36
45
 
37
- Montrose::Frequency.const_get(class_name).new(opts)
38
- end
46
+ # Factory method for instantiating the appropriate Frequency
47
+ # subclass.
48
+ #
49
+ def from_options(opts)
50
+ frequency = opts.fetch(:every) { fail ConfigurationError, "Please specify the :every option" }
51
+ class_name = FREQUENCY_TERMS.fetch(frequency.to_s) {
52
+ fail "Don't know how to enumerate every: #{frequency}"
53
+ }
54
+
55
+ Montrose::Frequency.const_get(class_name).new(opts)
56
+ end
39
57
 
40
- # @private
41
- def self.assert(frequency)
42
- FREQUENCY_TERMS.key?(frequency.to_s) || fail(ConfigurationError,
43
- "Don't know how to enumerate every: #{frequency}")
58
+ def from_term(term)
59
+ FREQUENCY_TERMS.invert.map { |k, v| [k.downcase, v] }.to_h.fetch(term.downcase) do
60
+ fail "Don't know how to convert #{term} to a Montrose frequency"
61
+ end
62
+ end
44
63
 
45
- frequency.to_sym
64
+ # @private
65
+ def assert(frequency)
66
+ FREQUENCY_TERMS.key?(frequency.to_s) || fail(ConfigurationError,
67
+ "Don't know how to enumerate every: #{frequency}")
68
+
69
+ frequency.to_sym
70
+ end
71
+
72
+ # @private
73
+ def numeric_to_frequency_parts(number)
74
+ parts = nil
75
+ %i[year month week day hour minute].each do |freq|
76
+ div, mod = number.divmod(1.send(freq))
77
+ parts = [freq, div]
78
+ return parts if mod.zero?
79
+ end
80
+ parts
81
+ end
82
+
83
+ # @private
84
+ def duration_to_frequency_parts(duration)
85
+ duration.parts.first
86
+ end
46
87
  end
47
88
 
48
89
  def initialize(opts = {})
@@ -63,15 +104,7 @@ module Montrose
63
104
  protected
64
105
 
65
106
  def interval_str
66
- @interval != 1 ? "*/#{@interval}" : "*"
107
+ (@interval != 1) ? "*/#{@interval}" : "*"
67
108
  end
68
109
  end
69
110
  end
70
-
71
- require "montrose/frequency/daily"
72
- require "montrose/frequency/hourly"
73
- require "montrose/frequency/minutely"
74
- require "montrose/frequency/monthly"
75
- require "montrose/frequency/secondly"
76
- require "montrose/frequency/weekly"
77
- require "montrose/frequency/yearly"
@@ -0,0 +1,22 @@
1
+ module Montrose
2
+ class Hour
3
+ HOURS_IN_DAY = 1.upto(24).to_a.freeze
4
+
5
+ class << self
6
+ def parse(arg)
7
+ case arg
8
+ when String
9
+ parse(arg.split(","))
10
+ else
11
+ Array(arg).map { |h| assert(h.to_i) }.presence
12
+ end
13
+ end
14
+
15
+ def assert(hour)
16
+ raise ConfigurationError, "Out of range: #{HOURS_IN_DAY.inspect} does not include #{hour}" unless HOURS_IN_DAY.include?(hour)
17
+
18
+ hour
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Montrose
4
+ class ICal
5
+ # DTSTART;TZID=US-Eastern:19970902T090000
6
+ # RRULE:FREQ=DAILY;INTERVAL=2
7
+ def self.parse(ical)
8
+ new(ical).parse
9
+ end
10
+
11
+ def initialize(ical)
12
+ @ical = ical
13
+ end
14
+
15
+ def parse
16
+ time_zone = extract_time_zone(@ical)
17
+
18
+ Time.use_zone(time_zone) do
19
+ Hash[*parse_properties(@ical)]
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def extract_time_zone(ical_string)
26
+ _label, time_string = ical_string.split("\n").grep(/^DTSTART/).join.split(";")
27
+ time_zone_rule, _ = time_string.split(":")
28
+ _label, time_zone = (time_zone_rule || "").split("=")
29
+ time_zone
30
+ end
31
+
32
+ # First pass parsing to normalize arbitrary line breaks
33
+ def property_lines(ical_string)
34
+ ical_string.split("\n").each_with_object([]) do |line, lines|
35
+ case line
36
+ when /^(DTSTART|DTEND|EXDATE|RDATE|RRULE)/
37
+ lines << line
38
+ else
39
+ (lines.last || lines << "")
40
+ lines.last << line
41
+ end
42
+ end
43
+ end
44
+
45
+ def parse_properties(ical_string)
46
+ property_lines(ical_string).flat_map do |line|
47
+ (property, value) = line.split(":")
48
+ (property, tzid) = property.split(";")
49
+
50
+ case property
51
+ when "DTSTART"
52
+ parse_dtstart(tzid, value)
53
+ when "DTEND"
54
+ warn "DTEND not currently supported!"
55
+ when "EXDATE"
56
+ parse_exdate(value)
57
+ when "RDATE"
58
+ warn "RDATE not currently supported!"
59
+ when "RRULE"
60
+ parse_rrule(value)
61
+ end
62
+ end
63
+ end
64
+
65
+ def parse_dtstart(tzid, time)
66
+ return [] unless time.present?
67
+
68
+ @starts_at = parse_time([tzid, time].compact.join(":"))
69
+
70
+ [:starts, @starts_at]
71
+ end
72
+
73
+ def parse_timezone(time_string)
74
+ time_zone_rule, _ = time_string.split(":")
75
+ _label, time_zone = (time_zone_rule || "").split("=")
76
+ time_zone
77
+ end
78
+
79
+ def parse_time(time_string)
80
+ time_zone = parse_timezone(time_string)
81
+ Montrose::Utils.parse_time(time_string).in_time_zone(time_zone)
82
+ end
83
+
84
+ def parse_exdate(exdate)
85
+ return [] unless exdate.present?
86
+
87
+ @except = Montrose::Utils.as_date(exdate) # only currently supports dates
88
+
89
+ [:except, @except]
90
+ end
91
+
92
+ def parse_rrule(rrule)
93
+ rrule.gsub(/\s+/, "").split(";").flat_map do |rule|
94
+ prop, value = rule.split("=")
95
+ case prop
96
+ when "FREQ"
97
+ [:every, Montrose::Frequency.from_term(value)]
98
+ when "INTERVAL"
99
+ [:interval, value.to_i]
100
+ when "COUNT"
101
+ [:total, value.to_i]
102
+ when "UNTIL"
103
+ [:until, parse_time(value)]
104
+ when "BYMINUTE"
105
+ [:minute, Montrose::Minute.parse(value)]
106
+ when "BYHOUR"
107
+ [:hour, Montrose::Hour.parse(value)]
108
+ when "BYMONTH"
109
+ [:month, Montrose::Month.parse(value)]
110
+ when "BYDAY"
111
+ [:day, Montrose::Day.parse(value)]
112
+ when "BYMONTHDAY"
113
+ [:mday, Montrose::MonthDay.parse(value)]
114
+ when "BYYEARDAY"
115
+ [:yday, Montrose::YearDay.parse(value)]
116
+ when "BYWEEKNO"
117
+ [:week, Montrose::Week.parse(value)]
118
+ when "WKST"
119
+ [:week_start, value]
120
+ when "BYSETPOS"
121
+ warn "BYSETPOS not currently supported!"
122
+ else
123
+ raise "Unrecognized rrule '#{rule}'"
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,22 @@
1
+ module Montrose
2
+ class Minute
3
+ MINUTES_IN_HOUR = 0.upto(59).to_a.freeze
4
+
5
+ class << self
6
+ def parse(arg)
7
+ case arg
8
+ when String
9
+ parse(arg.split(","))
10
+ else
11
+ Array(arg).map { |m| assert(m.to_i) }.presence
12
+ end
13
+ end
14
+
15
+ def assert(minute)
16
+ raise ConfigurationError, "Out of range: #{MINUTES_IN_HOUR.inspect} does not include #{minute}" unless MINUTES_IN_HOUR.include?(minute)
17
+
18
+ minute
19
+ end
20
+ end
21
+ end
22
+ end