montrose 0.12.0 → 0.13.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 +7 -6
- data/Appraisals +7 -3
- data/CHANGELOG.md +24 -0
- data/README.md +20 -15
- data/gemfiles/activesupport_5.2.gemfile +1 -1
- data/gemfiles/activesupport_6.0.gemfile +1 -1
- data/gemfiles/activesupport_6.1.gemfile +1 -1
- data/gemfiles/activesupport_7.0.gemfile +16 -0
- data/lib/montrose/chainable.rb +0 -1
- data/lib/montrose/day.rb +83 -0
- data/lib/montrose/frequency.rb +59 -26
- 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 +11 -5
- 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/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 +2 -2
- metadata +19 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c4b7914b70270912f8acf6757a8f130112d9137c50c34b933714f0edf97ac3c1
|
4
|
+
data.tar.gz: d70713d758bf62b45a7a74196f3d2c5bcc0a4bcf48b899164d7b5d4e2afe6b4a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ea9a0877d09b1d360c3de7dc9b68a4fd9b6f64047728991d0398333777c18afa06f12fba1b2e178f53bd412f054314b125a174099a896c554cc53a337672fb5
|
7
|
+
data.tar.gz: f0b7431307846f0fa969352cfb98101ae2542ad37dd443e8324dd1480b01edfa81f8c06f92b6dc96f4278410f7cb221b7a1c1a487241a0faf8f5ee07fd763db3
|
data/.circleci/config.yml
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
version: 2.1
|
2
2
|
orbs:
|
3
|
-
ruby: circleci/ruby@1.
|
3
|
+
ruby: circleci/ruby@1.3.0
|
4
4
|
commands:
|
5
5
|
run-tests:
|
6
6
|
description: Run tests
|
@@ -37,7 +37,7 @@ jobs:
|
|
37
37
|
gemfile:
|
38
38
|
type: string
|
39
39
|
docker:
|
40
|
-
- image: '
|
40
|
+
- image: 'cimg/ruby:<< parameters.ruby_version >>'
|
41
41
|
environment:
|
42
42
|
GEMFILE_NAME: <<parameters.gemfile>>
|
43
43
|
steps:
|
@@ -53,13 +53,14 @@ workflows:
|
|
53
53
|
- test:
|
54
54
|
matrix:
|
55
55
|
parameters:
|
56
|
-
ruby_version: ['2.
|
56
|
+
ruby_version: ['2.6', '2.7', '3.0', '3.1']
|
57
57
|
gemfile:
|
58
58
|
[
|
59
59
|
'gemfiles/activesupport_5.2.gemfile',
|
60
60
|
'gemfiles/activesupport_6.0.gemfile',
|
61
61
|
'gemfiles/activesupport_6.1.gemfile',
|
62
|
+
'gemfiles/activesupport_7.0.gemfile',
|
62
63
|
]
|
63
|
-
|
64
|
-
|
65
|
-
|
64
|
+
exclude:
|
65
|
+
- ruby_version: '2.6'
|
66
|
+
gemfile: gemfiles/activesupport_7.0.gemfile
|
data/Appraisals
CHANGED
@@ -1,13 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
appraise "activesupport-7.0" do
|
4
|
+
gem "activesupport", "~> 7.0"
|
5
|
+
end
|
6
|
+
|
3
7
|
appraise "activesupport-6.1" do
|
4
|
-
gem "activesupport", "~> 6.1
|
8
|
+
gem "activesupport", "~> 6.1"
|
5
9
|
end
|
6
10
|
|
7
11
|
appraise "activesupport-6.0" do
|
8
|
-
gem "activesupport", "~> 6.0
|
12
|
+
gem "activesupport", "~> 6.0"
|
9
13
|
end
|
10
14
|
|
11
15
|
appraise "activesupport-5.2" do
|
12
|
-
gem "activesupport", "~> 5.2
|
16
|
+
gem "activesupport", "~> 5.2"
|
13
17
|
end
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,27 @@
|
|
1
|
+
### 0.12.0 - (2021-02-02)
|
2
|
+
|
3
|
+
* enhancements
|
4
|
+
* Adds `Montrose.covering` to disambiguate `Montrose.between` behavior
|
5
|
+
`#covering` provides recurrence masking behavior, i.e., only recurrences
|
6
|
+
within the given range will be emitted
|
7
|
+
* Added support for ActiveSupport 6 and Ruby 2.7
|
8
|
+
* Adds `Montrose#infinite?` and ensures `Montrose.finite?` returns a boolean
|
9
|
+
|
10
|
+
* bug fixes
|
11
|
+
* Fixes `Recurrence#include?` behavior for infinite recurrences with
|
12
|
+
intervals > 1
|
13
|
+
|
14
|
+
* breaking changes
|
15
|
+
* `Montrose.between` no longer provides masking behavior, which is now
|
16
|
+
provided by `Montrose.covering`. A global option can be used
|
17
|
+
`Montrose.enable_deprecated_between_masking = true` to retain the legacy
|
18
|
+
behavior for `Montrose.between`. This option will be removed in v1.0.
|
19
|
+
* Dropped official support for EOL'd rubies and ActiveSupport < 5.2
|
20
|
+
|
21
|
+
* miscellaneous
|
22
|
+
* switched from Travis to CircleCi for builds
|
23
|
+
* switched default branch to `main`
|
24
|
+
#
|
1
25
|
### 0.11.0 - (2019-08-16)
|
2
26
|
|
3
27
|
* enhancements
|
data/README.md
CHANGED
@@ -4,10 +4,10 @@
|
|
4
4
|
[](https://codeclimate.com/github/rossta/montrose)
|
5
5
|
[](https://coveralls.io/github/rossta/montrose?branch=master)
|
6
6
|
|
7
|
-
Montrose is an easy-to-use library for defining recurring events in Ruby. It uses a simple chaining system for building recurrences, inspired heavily by the design principles of [HTTP.rb](https://github.com/httprb/http) and rule definitions available in [Recurrence](https://github.com/fnando/recurrence).
|
7
|
+
Montrose is an easy-to-use library for defining recurring events in Ruby. It uses a simple chaining system for building enumerable recurrences, inspired heavily by the design principles of [HTTP.rb](https://github.com/httprb/http) and rule definitions available in [Recurrence](https://github.com/fnando/recurrence).
|
8
8
|
|
9
|
-
|
10
|
-
|
9
|
+
- [Introductory blog post](http://bit.ly/1PA68Zb)
|
10
|
+
- [NYC.rb
|
11
11
|
presentation](https://speakerdeck.com/rossta/recurring-events-with-montrose)
|
12
12
|
|
13
13
|
## Installation
|
@@ -32,17 +32,17 @@ Dealing with recurring events is hard. `Montrose` provides a simple interface fo
|
|
32
32
|
|
33
33
|
More specifically, this project intends to:
|
34
34
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
35
|
+
- model recurring events in Ruby
|
36
|
+
- embrace Ruby idioms
|
37
|
+
- support recent Rubies
|
38
|
+
- be reasonably performant
|
39
|
+
- serialize to yaml, hash, and [ical](http://www.kanzaki.com/docs/ical/rrule.html#basic) formats
|
40
|
+
- be suitable for integration with persistence libraries
|
41
41
|
|
42
42
|
What `Montrose` doesn't do:
|
43
43
|
|
44
|
-
|
45
|
-
|
44
|
+
- support all calendaring use cases under the sun
|
45
|
+
- schedule recurring jobs for your Rails app. Use one of these instead: [cron](https://en.wikipedia.org/wiki/Cron), [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler), [sidekiq-cron](https://github.com/ondrejbartas/sidekiq-cron), [sidetiq](https://github.com/tobiassvn/sidetiq), [whenever](https://github.com/javan/whenever)
|
46
46
|
|
47
47
|
## Concepts
|
48
48
|
|
@@ -392,13 +392,16 @@ end
|
|
392
392
|
# add after building
|
393
393
|
s << Montrose.yearly
|
394
394
|
```
|
395
|
+
|
395
396
|
The `Schedule#<<` method also accepts valid recurrence options as hashes:
|
397
|
+
|
396
398
|
```ruby
|
397
399
|
schedule = Montrose::Schedule.build do |s|
|
398
400
|
s << { day: { friday: [1] } }
|
399
401
|
s << { on: :tuesday }
|
400
402
|
end
|
401
403
|
```
|
404
|
+
|
402
405
|
A schedule acts like a collection of recurrence rules that also behaves as a single
|
403
406
|
stream of events:
|
404
407
|
|
@@ -419,7 +422,9 @@ class RecurringEvent < ApplicationRecord
|
|
419
422
|
|
420
423
|
end
|
421
424
|
```
|
425
|
+
|
422
426
|
`Montrose::Schedule` can also be serialized:
|
427
|
+
|
423
428
|
```ruby
|
424
429
|
class RecurringEvent < ApplicationRecord
|
425
430
|
serialize :recurrence, Montrose::Schedule
|
@@ -435,10 +440,10 @@ Montrose is named after the beautifully diverse and artistic [neighborhood in Ho
|
|
435
440
|
|
436
441
|
Check out following related projects, all of which have provided inspiration for `Montrose`.
|
437
442
|
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
443
|
+
- [ice_cube](https://github.com/seejohnrun/ice_cube)
|
444
|
+
- [recurrence](https://github.com/fnando/recurrence)
|
445
|
+
- [runt](https://github.com/mlipper/runt)
|
446
|
+
- [http.rb](https://github.com/httprb/http) - not a recurrence project, but inspirational to design, implementation, and interface of `Montrose`
|
442
447
|
|
443
448
|
## Development
|
444
449
|
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activesupport", "~> 7.0"
|
6
|
+
|
7
|
+
group :development do
|
8
|
+
gem "coveralls"
|
9
|
+
gem "yard"
|
10
|
+
gem "guard"
|
11
|
+
gem "guard-minitest"
|
12
|
+
gem "guard-rubocop"
|
13
|
+
gem "pry-byebug"
|
14
|
+
end
|
15
|
+
|
16
|
+
gemspec path: "../"
|
data/lib/montrose/chainable.rb
CHANGED
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 = {})
|
@@ -67,11 +108,3 @@ module Montrose
|
|
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
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Montrose
|
2
|
+
class Month
|
3
|
+
extend Montrose::Utils
|
4
|
+
|
5
|
+
NAMES = ::Date::MONTHNAMES # starts with nil to match 1-12 numbering
|
6
|
+
NUMBERS = NAMES.map.with_index { |_n, i| i.to_s }.slice(1, 12)
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def parse(value)
|
10
|
+
case value
|
11
|
+
when String
|
12
|
+
parse(value.split(",").compact)
|
13
|
+
when Array
|
14
|
+
value.map { |m|
|
15
|
+
Montrose::Month.number!(m)
|
16
|
+
}.presence
|
17
|
+
else
|
18
|
+
parse(Array(value))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def names
|
23
|
+
NAMES
|
24
|
+
end
|
25
|
+
|
26
|
+
def numbers
|
27
|
+
NUMBERS
|
28
|
+
end
|
29
|
+
|
30
|
+
def number(name)
|
31
|
+
case name
|
32
|
+
when Symbol, String
|
33
|
+
string = name.to_s
|
34
|
+
NAMES.index(string.titleize) || number(to_index(string))
|
35
|
+
when 1..12
|
36
|
+
name
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def number!(name)
|
41
|
+
numbers = NAMES.map.with_index { |_n, i| i.to_s }.slice(1, 12)
|
42
|
+
number(name) || raise(ConfigurationError,
|
43
|
+
"Did not recognize month #{name}, must be one of #{(NAMES + numbers).inspect}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Montrose
|
2
|
+
class MonthDay
|
3
|
+
class << self
|
4
|
+
MDAYS = (-31.upto(-1).to_a + 1.upto(31).to_a)
|
5
|
+
|
6
|
+
def parse(mdays)
|
7
|
+
return nil unless mdays.present?
|
8
|
+
|
9
|
+
case mdays
|
10
|
+
when String
|
11
|
+
parse(mdays.split(","))
|
12
|
+
else
|
13
|
+
Array(mdays).map { |d| assert(d.to_i) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def assert(number)
|
18
|
+
test = number.abs
|
19
|
+
raise ConfigurationError, "Out of range: #{MDAYS.inspect} does not include #{test}" unless MDAYS.include?(number.abs)
|
20
|
+
|
21
|
+
number
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|