montrose 0.11.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +66 -0
  3. data/Appraisals +8 -12
  4. data/CHANGELOG.md +24 -0
  5. data/Guardfile +2 -2
  6. data/README.md +27 -15
  7. data/Rakefile +2 -4
  8. data/bin/setup +1 -0
  9. data/bin/standardrb +29 -0
  10. data/gemfiles/activesupport_5.2.gemfile +5 -1
  11. data/gemfiles/activesupport_6.0.gemfile +5 -1
  12. data/gemfiles/{activesupport_4.2.gemfile → activesupport_6.1.gemfile} +5 -1
  13. data/gemfiles/{activesupport_5.0.gemfile → activesupport_7.0.gemfile} +5 -1
  14. data/lib/montrose/chainable.rb +26 -10
  15. data/lib/montrose/clock.rb +4 -4
  16. data/lib/montrose/day.rb +83 -0
  17. data/lib/montrose/frequency.rb +58 -25
  18. data/lib/montrose/hour.rb +22 -0
  19. data/lib/montrose/ical.rb +128 -0
  20. data/lib/montrose/minute.rb +22 -0
  21. data/lib/montrose/month.rb +47 -0
  22. data/lib/montrose/month_day.rb +25 -0
  23. data/lib/montrose/options.rb +73 -79
  24. data/lib/montrose/recurrence.rb +40 -13
  25. data/lib/montrose/rule/between.rb +1 -1
  26. data/lib/montrose/rule/covering.rb +40 -0
  27. data/lib/montrose/rule/during.rb +7 -15
  28. data/lib/montrose/rule/minute_of_hour.rb +25 -0
  29. data/lib/montrose/rule/nth_day_of_month.rb +0 -2
  30. data/lib/montrose/rule/nth_day_of_year.rb +0 -2
  31. data/lib/montrose/rule/time_of_day.rb +1 -1
  32. data/lib/montrose/rule/until.rb +1 -1
  33. data/lib/montrose/rule.rb +18 -16
  34. data/lib/montrose/schedule.rb +6 -8
  35. data/lib/montrose/stack.rb +3 -4
  36. data/lib/montrose/time_of_day.rb +48 -0
  37. data/lib/montrose/utils.rb +2 -40
  38. data/lib/montrose/version.rb +1 -1
  39. data/lib/montrose/week.rb +20 -0
  40. data/lib/montrose/year_day.rb +25 -0
  41. data/lib/montrose.rb +43 -11
  42. data/montrose.gemspec +17 -17
  43. metadata +43 -36
  44. data/.rubocop.yml +0 -136
  45. data/.travis.yml +0 -33
  46. data/bin/rubocop +0 -16
  47. data/gemfiles/activesupport_4.1.gemfile +0 -12
  48. data/gemfiles/activesupport_5.1.gemfile +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 01fee4660a2a1e95d60de269d491675460289ae52f6c582aee5ea6be39a883cc
4
- data.tar.gz: ece2fbd3f36909337abd6712025053385f6ceedf95695a7852b6e05b6c804906
3
+ metadata.gz: c4b7914b70270912f8acf6757a8f130112d9137c50c34b933714f0edf97ac3c1
4
+ data.tar.gz: d70713d758bf62b45a7a74196f3d2c5bcc0a4bcf48b899164d7b5d4e2afe6b4a
5
5
  SHA512:
6
- metadata.gz: bb823d11097cd9228ec7631809bece2b1028adf728c7a4919284dfc7a6fcafa3d65c9d1ed446f60f1a16335143830ae36b4725c6d980b5cb6a152b80961f4e0b
7
- data.tar.gz: deb0db72983faf47192095aef16e7512f928c4ff4f1f6fd20f913394850b354fc2e004dcda70455d08abe4471a2647d8b3a31832814f737cf68a09d6d9ffbaa8
6
+ metadata.gz: 5ea9a0877d09b1d360c3de7dc9b68a4fd9b6f64047728991d0398333777c18afa06f12fba1b2e178f53bd412f054314b125a174099a896c554cc53a337672fb5
7
+ data.tar.gz: f0b7431307846f0fa969352cfb98101ae2542ad37dd443e8324dd1480b01edfa81f8c06f92b6dc96f4278410f7cb221b7a1c1a487241a0faf8f5ee07fd763db3
@@ -0,0 +1,66 @@
1
+ version: 2.1
2
+ orbs:
3
+ ruby: circleci/ruby@1.3.0
4
+ commands:
5
+ run-tests:
6
+ description: Run tests
7
+ steps:
8
+ - run: bundle exec rake test
9
+ restore:
10
+ description: Restore cache
11
+ steps:
12
+ - restore_cache:
13
+ keys:
14
+ - v1_bundler_deps-
15
+ save:
16
+ description: Save cache
17
+ steps:
18
+ - save_cache:
19
+ paths:
20
+ - ./vendor/bundle
21
+ key: v1-bundler-deps-{{ .Environment.CIRCLE_JOB }}
22
+ bundle:
23
+ description: Install dependencies
24
+ steps:
25
+ - run:
26
+ echo "export BUNDLE_JOBS=4" >> $BASH_ENV
27
+ echo "export BUNDLE_RETRY=3" >> $BASH_ENV
28
+ echo "export BUNDLE_PATH=$(pwd)/vendor/bundle" >> $BASH_ENV
29
+ echo "export BUNDLE_GEMFILE=$(pwd)/${GEMFILE_NAME}" >> $BASH_ENV
30
+ - run: bundle install
31
+
32
+ jobs:
33
+ test:
34
+ parameters:
35
+ ruby_version:
36
+ type: string
37
+ gemfile:
38
+ type: string
39
+ docker:
40
+ - image: 'cimg/ruby:<< parameters.ruby_version >>'
41
+ environment:
42
+ GEMFILE_NAME: <<parameters.gemfile>>
43
+ steps:
44
+ - checkout
45
+ - restore
46
+ - bundle
47
+ - save
48
+ - run-tests
49
+
50
+ workflows:
51
+ all-tests:
52
+ jobs:
53
+ - test:
54
+ matrix:
55
+ parameters:
56
+ ruby_version: ['2.6', '2.7', '3.0', '3.1']
57
+ gemfile:
58
+ [
59
+ 'gemfiles/activesupport_5.2.gemfile',
60
+ 'gemfiles/activesupport_6.0.gemfile',
61
+ 'gemfiles/activesupport_6.1.gemfile',
62
+ 'gemfiles/activesupport_7.0.gemfile',
63
+ ]
64
+ exclude:
65
+ - ruby_version: '2.6'
66
+ gemfile: gemfiles/activesupport_7.0.gemfile
data/Appraisals CHANGED
@@ -1,21 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- appraise "activesupport-5.2" do
4
- gem "activesupport", "~> 5.2.0"
5
- end
6
-
7
- appraise "activesupport-5.1" do
8
- gem "activesupport", "~> 5.1.0"
3
+ appraise "activesupport-7.0" do
4
+ gem "activesupport", "~> 7.0"
9
5
  end
10
6
 
11
- appraise "activesupport-5.0" do
12
- gem "activesupport", "~> 5.0.0"
7
+ appraise "activesupport-6.1" do
8
+ gem "activesupport", "~> 6.1"
13
9
  end
14
10
 
15
- appraise "activesupport-4.2" do
16
- gem "activesupport", "~> 4.2.0"
11
+ appraise "activesupport-6.0" do
12
+ gem "activesupport", "~> 6.0"
17
13
  end
18
14
 
19
- appraise "activesupport-4.1" do
20
- gem "activesupport", "~> 4.1.0"
15
+ appraise "activesupport-5.2" do
16
+ gem "activesupport", "~> 5.2"
21
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/Guardfile CHANGED
@@ -25,8 +25,8 @@ guard :minitest do
25
25
 
26
26
  # with Minitest::Spec
27
27
  watch(%r{^spec/(.*)_spec\.rb$})
28
- watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
29
- watch(%r{^lib/(.+)\.rb$}) { "spec/rfc_spec.rb" }
28
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
29
+ watch(%r{^lib/(.+)\.rb$}) { "spec/rfc_spec.rb" }
30
30
  watch(%r{^spec/spec_helper\.rb$}) { "spec" }
31
31
  end
32
32
 
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
 
@@ -446,6 +451,13 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
446
451
 
447
452
  You can also run `bin/console` for an interactive prompt that will allow you to experiment.
448
453
 
454
+ To run tests against multiple versions of activesupport, use the Appraisals:
455
+
456
+ ```sh
457
+ bin/appraisal install
458
+ bin/appraisal rake test
459
+ ```
460
+
449
461
  ## Contributing
450
462
 
451
463
  Bug reports and pull requests are welcome on GitHub at https://github.com/rossta/montrose. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
data/Rakefile CHANGED
@@ -3,7 +3,7 @@
3
3
  require "bundler/setup"
4
4
  require "bundler/gem_tasks"
5
5
  require "rake/testtask"
6
- require "rubocop/rake_task"
6
+ require "standard/rake"
7
7
  require "yard"
8
8
 
9
9
  Rake::TestTask.new(:spec) do |t|
@@ -15,9 +15,7 @@ end
15
15
 
16
16
  task test: :spec
17
17
 
18
- RuboCop::RakeTask.new
19
-
20
- task default: %i[spec rubocop]
18
+ task default: %i[spec standard]
21
19
 
22
20
  namespace :doc do
23
21
  desc "Generate docs and publish to gh-pages"
data/bin/setup CHANGED
@@ -3,5 +3,6 @@ set -euo pipefail
3
3
  IFS=$'\n\t'
4
4
 
5
5
  bundle install
6
+ bundle update
6
7
 
7
8
  # Do any other automated setup that you need to do here
data/bin/standardrb ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'standardrb' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("standard", "standardrb")
@@ -2,11 +2,15 @@
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"
9
9
  gem "yard"
10
+ gem "guard"
11
+ gem "guard-minitest"
12
+ gem "guard-rubocop"
13
+ gem "pry-byebug"
10
14
  end
11
15
 
12
16
  gemspec path: "../"
@@ -2,11 +2,15 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activesupport", "~> 6.0.0.beta2"
5
+ gem "activesupport", "~> 6.0"
6
6
 
7
7
  group :development do
8
8
  gem "coveralls"
9
9
  gem "yard"
10
+ gem "guard"
11
+ gem "guard-minitest"
12
+ gem "guard-rubocop"
13
+ gem "pry-byebug"
10
14
  end
11
15
 
12
16
  gemspec path: "../"
@@ -2,11 +2,15 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activesupport", "~> 4.2.0"
5
+ gem "activesupport", "~> 6.1"
6
6
 
7
7
  group :development do
8
8
  gem "coveralls"
9
9
  gem "yard"
10
+ gem "guard"
11
+ gem "guard-minitest"
12
+ gem "guard-rubocop"
13
+ gem "pry-byebug"
10
14
  end
11
15
 
12
16
  gemspec path: "../"
@@ -2,11 +2,15 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activesupport", "~> 5.0.0"
5
+ gem "activesupport", "~> 7.0"
6
6
 
7
7
  group :development do
8
8
  gem "coveralls"
9
9
  gem "yard"
10
+ gem "guard"
11
+ gem "guard-minitest"
12
+ gem "guard-rubocop"
13
+ gem "pry-byebug"
10
14
  end
11
15
 
12
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
@@ -139,7 +138,7 @@ module Montrose
139
138
  def starts(starts_at)
140
139
  merge(starts: starts_at)
141
140
  end
142
- alias starting starts
141
+ alias_method :starting, :starts
143
142
 
144
143
  # Create a recurrence ending at given timestamp.
145
144
  #
@@ -153,9 +152,12 @@ module Montrose
153
152
  def until(ends_at)
154
153
  merge(until: ends_at)
155
154
  end
156
- alias ending until
155
+ alias_method :ending, :until
157
156
 
158
- # Create a recurrence occurring during date range.
157
+ # Create a recurrence occurring between the start and end
158
+ # of a given date range; :between is shorthand for separate
159
+ # :starts and :until options. When used with explicit :start
160
+ # and/or :until options, those will take precedence.
159
161
  #
160
162
  # @param [Range<Date>] date_range
161
163
  #
@@ -168,6 +170,20 @@ module Montrose
168
170
  merge(between: date_range)
169
171
  end
170
172
 
173
+ # Create a recurrence which will only emit values within the
174
+ # date range, also called "masking."
175
+ #
176
+ # @param [Range<Date>] date_range
177
+ #
178
+ # @example
179
+ # Montrose.weekly.covering(Date.tomorrow..Date.new(2016, 3, 15))
180
+ #
181
+ # @return [Montrose::Recurrence]
182
+ #
183
+ def covering(date_range)
184
+ merge(covering: date_range)
185
+ end
186
+
171
187
  # Create a recurrence occurring within a time-of-day range or ranges.
172
188
  # Given time ranges will parse as times-of-day and ignore given dates.
173
189
  #
@@ -241,7 +257,7 @@ module Montrose
241
257
  def day_of_month(days, *extras)
242
258
  merge(mday: days.array_concat(extras))
243
259
  end
244
- alias mday day_of_month
260
+ alias_method :mday, :day_of_month
245
261
 
246
262
  # Create a recurrence for given days of week
247
263
  #
@@ -257,7 +273,7 @@ module Montrose
257
273
  def day_of_week(weekdays, *extras)
258
274
  merge(day: weekdays.array_concat(extras))
259
275
  end
260
- alias day day_of_week
276
+ alias_method :day, :day_of_week
261
277
 
262
278
  # Create a recurrence for given days of year
263
279
  #
@@ -273,7 +289,7 @@ module Montrose
273
289
  def day_of_year(days, *extras)
274
290
  merge(yday: days.array_concat(extras))
275
291
  end
276
- alias yday day_of_year
292
+ alias_method :yday, :day_of_year
277
293
 
278
294
  # Create a recurrence for given hours of day
279
295
  #
@@ -289,7 +305,7 @@ module Montrose
289
305
  def hour_of_day(hours, *extras)
290
306
  merge(hour: hours.array_concat(extras))
291
307
  end
292
- alias hour hour_of_day
308
+ alias_method :hour, :hour_of_day
293
309
 
294
310
  # Create a recurrence for given months of year
295
311
  #
@@ -305,7 +321,7 @@ module Montrose
305
321
  def month_of_year(months, *extras)
306
322
  merge(month: months.array_concat(extras))
307
323
  end
308
- alias month month_of_year
324
+ alias_method :month, :month_of_year
309
325
 
310
326
  # Create a recurrence for given weeks of year
311
327
  #
@@ -335,7 +351,7 @@ module Montrose
335
351
  def total(total)
336
352
  merge(total: total)
337
353
  end
338
- alias repeat total
354
+ alias_method :repeat, :total
339
355
 
340
356
  # Create a new recurrence combining options of self
341
357
  # and other. The value of entries with duplicate
@@ -26,7 +26,7 @@ module Montrose
26
26
  if @at
27
27
  times = @at.map { |hour, min, sec = 0| @time.change(hour: hour, min: min, sec: sec) }
28
28
 
29
- min_next = times.select { |t| t > @time }.min and return min_next
29
+ (min_next = times.select { |t| t > @time }.min) && (return min_next)
30
30
 
31
31
  advance_step(times.min || @time)
32
32
  else
@@ -41,7 +41,7 @@ module Montrose
41
41
  end
42
42
 
43
43
  def step
44
- @step ||= smallest_step or fail ConfigurationError, "No step for #{@options.inspect}"
44
+ (@step ||= smallest_step) || fail(ConfigurationError, "No step for #{@options.inspect}")
45
45
  end
46
46
 
47
47
  def smallest_step
@@ -76,9 +76,9 @@ module Montrose
76
76
  is_frequency = @every == unit
77
77
  if ([unit] + alternates).any? { |u| @options.key?(u) } && !is_frequency
78
78
  # smallest unit, increment by 1
79
- { step_key(unit) => 1 }
79
+ {step_key(unit) => 1}
80
80
  elsif is_frequency
81
- { step_key(unit) => @interval }
81
+ {step_key(unit) => @interval}
82
82
  end
83
83
  end
84
84
 
@@ -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