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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +24 -32
- data/.gitignore +0 -3
- data/.rubocop.yml +5 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +24 -0
- data/Gemfile +4 -9
- data/Gemfile.lock +148 -0
- data/README.md +20 -22
- data/bin/bundle-all +5 -0
- data/gemfiles/activesupport_5.2.gemfile +1 -12
- data/gemfiles/activesupport_5.2.gemfile.lock +107 -0
- data/gemfiles/activesupport_6.0.gemfile +1 -12
- data/gemfiles/activesupport_6.0.gemfile.lock +108 -0
- data/gemfiles/activesupport_6.1.gemfile +1 -12
- data/gemfiles/activesupport_6.1.gemfile.lock +108 -0
- data/gemfiles/activesupport_7.0.gemfile +5 -0
- data/gemfiles/activesupport_7.0.gemfile.lock +106 -0
- data/lib/montrose/chainable.rb +0 -1
- data/lib/montrose/clock.rb +54 -9
- data/lib/montrose/day.rb +83 -0
- data/lib/montrose/frequency.rb +60 -27
- data/lib/montrose/hour.rb +22 -0
- data/lib/montrose/ical.rb +128 -0
- data/lib/montrose/minute.rb +22 -0
- data/lib/montrose/month.rb +47 -0
- data/lib/montrose/month_day.rb +25 -0
- data/lib/montrose/options.rb +56 -65
- data/lib/montrose/recurrence.rb +18 -12
- data/lib/montrose/rule/during.rb +7 -15
- data/lib/montrose/rule/minute_of_hour.rb +25 -0
- data/lib/montrose/rule/nth_day_of_month.rb +0 -2
- data/lib/montrose/rule/nth_day_of_year.rb +0 -2
- data/lib/montrose/rule/time_of_day.rb +1 -1
- data/lib/montrose/rule.rb +18 -16
- data/lib/montrose/schedule.rb +7 -7
- data/lib/montrose/stack.rb +1 -2
- data/lib/montrose/time_of_day.rb +48 -0
- data/lib/montrose/utils.rb +0 -38
- data/lib/montrose/version.rb +1 -1
- data/lib/montrose/week.rb +20 -0
- data/lib/montrose/year_day.rb +25 -0
- data/lib/montrose.rb +20 -8
- data/montrose.gemspec +4 -3
- metadata +42 -11
- data/Appraisals +0 -13
- 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,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
|
data/lib/montrose/chainable.rb
CHANGED
data/lib/montrose/clock.rb
CHANGED
@@ -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,
|
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 =
|
20
|
+
@time = next_time(true)
|
21
21
|
end
|
22
22
|
|
23
23
|
def peek
|
24
|
-
|
24
|
+
next_time(false)
|
25
|
+
end
|
25
26
|
|
26
|
-
|
27
|
-
times = @at.map { |hour, min, sec = 0| @time.change(hour: hour, min: min, sec: sec) }
|
27
|
+
private
|
28
28
|
|
29
|
-
|
29
|
+
def next_time(tick)
|
30
|
+
return @start_time if @time.nil?
|
30
31
|
|
31
|
-
|
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
|
data/lib/montrose/day.rb
ADDED
@@ -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
|
data/lib/montrose/frequency.rb
CHANGED
@@ -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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
38
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
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
|