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 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