montrose 0.11.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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