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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d44082eaa05a068714f05fe6116f8442b0507b3c7612174d93716ce6fd4af4ba
4
- data.tar.gz: 691d88452db689b1555772b6749ae12e72bf675fbc55d4082b2d7f4cd41dd795
3
+ metadata.gz: c4b7914b70270912f8acf6757a8f130112d9137c50c34b933714f0edf97ac3c1
4
+ data.tar.gz: d70713d758bf62b45a7a74196f3d2c5bcc0a4bcf48b899164d7b5d4e2afe6b4a
5
5
  SHA512:
6
- metadata.gz: 389adad3b13244fdb642204fa158bde6a204de4c752cab7a11dc1fe2a237a98f7b73c905477fd335d47164b795921a92090b68a528327b115ff2721b8c2945f7
7
- data.tar.gz: 9ce4b11ae2d6c0fd06b06ca9fe56aa05767bca879be8c5cd3a24dae1c7fed442ca50d4195cf5225ff42f1bb2f2704a9ab8fb308b0a9167865317a245dd782441
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.1.2
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: 'circleci/ruby:<< parameters.ruby_version >>'
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.5', '2.6', '2.7']
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
- # exclude:
64
- # - ruby_version: '3.0'
65
- # - gemfile: rails_5_2.gemfile
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.0"
8
+ gem "activesupport", "~> 6.1"
5
9
  end
6
10
 
7
11
  appraise "activesupport-6.0" do
8
- gem "activesupport", "~> 6.0.0"
12
+ gem "activesupport", "~> 6.0"
9
13
  end
10
14
 
11
15
  appraise "activesupport-5.2" do
12
- gem "activesupport", "~> 5.2.0"
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
- * [Introductory blog post](http://bit.ly/1PA68Zb)
10
- * [NYC.rb
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
- * 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
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
- * 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)
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
- * [ice_cube](https://github.com/seejohnrun/ice_cube)
439
- * [recurrence](https://github.com/fnando/recurrence)
440
- * [runt](https://github.com/mlipper/runt)
441
- * [http.rb](https://github.com/httprb/http) - not a recurrence project, but inspirational to design, implementation, and interface of `Montrose`
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
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activesupport", "~> 5.2.0"
5
+ gem "activesupport", "~> 5.2"
6
6
 
7
7
  group :development do
8
8
  gem "coveralls"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activesupport", "~> 6.0.0"
5
+ gem "activesupport", "~> 6.0"
6
6
 
7
7
  group :development do
8
8
  gem "coveralls"
@@ -2,7 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activesupport", "~> 6.1.0"
5
+ gem "activesupport", "~> 6.1"
6
6
 
7
7
  group :development do
8
8
  gem "coveralls"
@@ -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: "../"
@@ -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
@@ -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 = {})
@@ -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