montrose 0.12.0 → 0.13.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Code Climate](https://codeclimate.com/github/rossta/montrose/badges/gpa.svg)](https://codeclimate.com/github/rossta/montrose)
|
5
5
|
[![Coverage Status](https://coveralls.io/repos/rossta/montrose/badge.svg?branch=master&service=github)](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
|