Skip to content

Commit 4e5b014

Browse files
author
Adrian Hooper
committed
Add additional options for mday
Adds new options that can be supplied when creating an `mday` recurrence. This is backwards compatible with the existing API, but now allows for overriding specific months, or specifying a fallback if a month does not meet the criteria. ``` Montrose.every(:month, mday: { default: 30, fallback: -1 }) Montrose.every(:month, mday: { default: 30, february: 28, march: 29 }) Montrose.every(:month, mday: { default: 30, [:february, :march] => 29, fallback: -1) ```
1 parent af7a3fd commit 4e5b014

File tree

5 files changed

+185
-21
lines changed

5 files changed

+185
-21
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,12 @@ Montrose.every(:month, mday: [2, 15], total: 10)
231231
# monthly on the first and last day of the month for 10 occurrences
232232
Montrose.monthly(mday: [1, -1], total: 10)
233233

234+
# monthly on the 30th unless fewer days
235+
Montrose.monthly(mday: { default: 30, fallback: -1 })
236+
237+
# monthly on the 25th except in december
238+
Montrose.monthly(mday: { default: 25, december: 20 })
239+
234240
# every 18 months on the 10th thru 15th of the month for 10 occurrences
235241
Montrose.every(18.months, total: 10, mday: 10..15)
236242

lib/montrose/options.rb

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ def day=(days)
206206
@day = Day.parse(days)
207207
end
208208

209-
def mday=(mdays)
210-
@mday = MonthDay.parse(mdays)
209+
def mday=(mday_arg)
210+
@mday = decompose_mday_arg(mday_arg)
211211
end
212212

213213
def yday=(ydays)
@@ -370,5 +370,38 @@ def end_of_day
370370
def beginning_of_day
371371
@beginning_of_day ||= time_of_day_parse(Time.now.beginning_of_day)
372372
end
373+
374+
def decompose_mday_arg(mday_arg)
375+
case mday_arg
376+
when Hash
377+
return nil unless mday_arg[:default].present?
378+
{
379+
default: MonthDay.parse(mday_arg[:default]),
380+
overrides: flatten_mday_arg(mday_arg),
381+
fallback: single_day(mday_arg[:fallback])
382+
}
383+
else
384+
{default: MonthDay.parse(mday_arg), overrides: {}, fallback: nil}
385+
end
386+
end
387+
388+
def flatten_mday_arg(mday_arg)
389+
mday_arg.except(:default, :overrides, :fallback).each_with_object({}) do |(months, day), result|
390+
case months
391+
when Array
392+
months.each { |month| result[month] = single_day(day) }
393+
else
394+
result[months] = single_day(day)
395+
end
396+
end
397+
end
398+
399+
def single_day(day_number)
400+
return nil unless day_number
401+
raise ConfigurationError, "mday override #{day_number} must be an integer" unless day_number.is_a?(Integer)
402+
MonthDay.assert(day_number)
403+
404+
day_number
405+
end
373406
end
374407
end

lib/montrose/rule/day_of_month.rb

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,46 @@ def self.apply_options(opts)
1111

1212
# Initializes rule
1313
#
14-
# @param [Array<Fixnum>] days - valid days of month, i.e. [1, 2, -1]
14+
# @param [Hash] opts `mday` valid days of month, and `skip_months` options
1515
#
16-
def initialize(days)
17-
@days = days
16+
def initialize(opts)
17+
@days = opts.fetch(:default)
18+
@overrides = opts.fetch(:overrides, {})
19+
@fallback = opts.fetch(:fallback, nil)
1820
end
1921

2022
def include?(time)
21-
@days.include?(time.mday) || included_from_end_of_month?(time)
23+
return override?(time) if has_override?(month_name(time))
24+
25+
@days.include?(time.mday) || included_from_end_of_month?(time) || fallback?(time)
2226
end
2327

2428
private
2529

2630
# matches days specified at negative numbers
27-
def included_from_end_of_month?(time)
31+
def included_from_end_of_month?(time, days = @days)
2832
month_days = ::Montrose::Utils.days_in_month(time.month, time.year) # given by activesupport
29-
@days.any? { |d| month_days + d + 1 == time.mday }
33+
days.any? { |d| month_days + d + 1 == time.mday }
34+
end
35+
36+
def has_override?(month)
37+
@overrides.key?(month)
38+
end
39+
40+
def override?(time)
41+
return false if @overrides.blank?
42+
43+
time.day == @overrides[month_name(time)]
44+
end
45+
46+
def month_name(time)
47+
time.strftime("%B").downcase.to_sym
48+
end
49+
50+
def fallback?(time)
51+
return false unless @fallback.present?
52+
53+
time.day == @fallback || included_from_end_of_month?(time, [@fallback])
3054
end
3155
end
3256
end

spec/montrose/options_spec.rb

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -423,29 +423,57 @@
423423
it "can be set" do
424424
options[:mday] = [1, 20, 31]
425425

426-
_(options.mday).must_equal [1, 20, 31]
427-
_(options[:mday]).must_equal [1, 20, 31]
426+
_(options.mday).must_equal({default: [1, 20, 31], overrides: {}, fallback: nil})
427+
_(options[:mday]).must_equal({default: [1, 20, 31], overrides: {}, fallback: nil})
428+
end
429+
430+
it "can be set to a hash" do
431+
options[:mday] = {default: [1, 20, 31]}
432+
433+
_(options.mday).must_equal({default: [1, 20, 31], overrides: {}, fallback: nil})
434+
_(options[:mday]).must_equal({default: [1, 20, 31], overrides: {}, fallback: nil})
428435
end
429436

430437
it "casts to element to array" do
431438
options[:mday] = 1
432439

433-
_(options.mday).must_equal [1]
434-
_(options[:mday]).must_equal [1]
440+
_(options.mday).must_equal({default: [1], overrides: {}, fallback: nil})
441+
_(options[:mday]).must_equal({default: [1], overrides: {}, fallback: nil})
442+
end
443+
444+
it "casts default element to array" do
445+
options[:mday] = {default: 1}
446+
447+
_(options.mday).must_equal({default: [1], overrides: {}, fallback: nil})
448+
_(options[:mday]).must_equal({default: [1], overrides: {}, fallback: nil})
435449
end
436450

437451
it "allows negative numbers" do
438-
options[:yday] = [-1]
452+
options[:mday] = [-1]
439453

440-
_(options.yday).must_equal [-1]
441-
_(options[:yday]).must_equal [-1]
454+
_(options.mday).must_equal({default: [-1], overrides: {}, fallback: nil})
455+
_(options[:mday]).must_equal({default: [-1], overrides: {}, fallback: nil})
456+
end
457+
458+
it "allows default negative numbers" do
459+
options[:mday] = {default: [-1]}
460+
461+
_(options.mday).must_equal({default: [-1], overrides: {}, fallback: nil})
462+
_(options[:mday]).must_equal({default: [-1], overrides: {}, fallback: nil})
442463
end
443464

444465
it "casts range to array" do
445466
options[:mday] = 6..8
446467

447-
_(options.mday).must_equal [6, 7, 8]
448-
_(options[:mday]).must_equal [6, 7, 8]
468+
_(options.mday).must_equal({default: [6, 7, 8], overrides: {}, fallback: nil})
469+
_(options[:mday]).must_equal({default: [6, 7, 8], overrides: {}, fallback: nil})
470+
end
471+
472+
it "casts default range to array" do
473+
options[:mday] = {default: 6..8}
474+
475+
_(options.mday).must_equal({default: [6, 7, 8], overrides: {}, fallback: nil})
476+
_(options[:mday]).must_equal({default: [6, 7, 8], overrides: {}, fallback: nil})
449477
end
450478

451479
it "casts nil to empty array" do
@@ -455,9 +483,64 @@
455483
_(options[:day]).must_be_nil
456484
end
457485

486+
it "casts default nil to empty array" do
487+
options[:mday] = {default: nil}
488+
489+
_(options.mday).must_be_nil
490+
_(options[:mday]).must_be_nil
491+
end
492+
458493
it "raises for out of range" do
459494
_(-> { options[:mday] = [1, 100] }).must_raise
460495
end
496+
497+
it "raises for default out of range" do
498+
_(-> { options[:mday] = {default: [1, 100]} }).must_raise
499+
end
500+
501+
it "raises for array override" do
502+
_(-> { options[:mday] = {default: 31, february: [28, 29]} }).must_raise
503+
end
504+
505+
it "raises for array fallback" do
506+
_(-> { options[:mday] = {default: 31, fallback: [28, 29]} }).must_raise
507+
end
508+
509+
it "allows negative overrides" do
510+
options[:mday] = {default: 31, february: -1}
511+
512+
_(options.mday).must_equal({default: [31], overrides: {february: -1}, fallback: nil})
513+
_(options[:mday]).must_equal({default: [31], overrides: {february: -1}, fallback: nil})
514+
end
515+
516+
it "allows negative fallback" do
517+
options[:mday] = {default: 31, fallback: -1}
518+
519+
_(options.mday).must_equal({default: [31], overrides: {}, fallback: -1})
520+
_(options[:mday]).must_equal({default: [31], overrides: {}, fallback: -1})
521+
end
522+
523+
it "raises for override out of range" do
524+
_(-> { options[:mday] = {default: 31, february: 100} }).must_raise
525+
end
526+
527+
it "raises for fallback out of range" do
528+
_(-> { options[:mday] = {default: 31, fallback: 100} }).must_raise
529+
end
530+
531+
it "collects overrides" do
532+
options[:mday] = {default: 31, september: 30, february: 28}
533+
534+
_(options.mday).must_equal({default: [31], overrides: {september: 30, february: 28}, fallback: nil})
535+
_(options[:mday]).must_equal({default: [31], overrides: {september: 30, february: 28}, fallback: nil})
536+
end
537+
538+
it "flattens overrides" do
539+
options[:mday] = {:default => 31, [:september, :april, :june, :november] => 30, :february => 28}
540+
541+
_(options.mday).must_equal({default: [31], overrides: {september: 30, april: 30, june: 30, november: 30, february: 28}, fallback: nil})
542+
_(options[:mday]).must_equal({default: [31], overrides: {september: 30, april: 30, june: 30, november: 30, february: 28}, fallback: nil})
543+
end
461544
end
462545

463546
describe "#yday" do
@@ -767,7 +850,7 @@
767850
options[:on] = {friday: 13}
768851

769852
_(options[:day]).must_equal [5]
770-
_(options[:mday]).must_equal [13]
853+
_(options[:mday]).must_equal({default: [13], overrides: {}, fallback: nil})
771854
_(options[:on]).must_equal(friday: 13)
772855
end
773856

@@ -776,15 +859,15 @@
776859
options[:on] = {tuesday: 2..8}
777860

778861
_(options[:day]).must_equal [2]
779-
_(options[:mday]).must_equal((2..8).to_a)
862+
_(options[:mday]).must_equal({default: (2..8).to_a, overrides: {}, fallback: nil})
780863
_(options[:month]).must_equal [11]
781864
end
782865

783866
it "decompose month name => month day to month and mday" do
784867
options[:on] = {january: 31}
785868

786869
_(options[:month]).must_equal [1]
787-
_(options[:mday]).must_equal [31]
870+
_(options[:mday]).must_equal({default: [31], overrides: {}, fallback: nil})
788871
end
789872

790873
it { _(-> { options[:on] = -3 }).must_raise Montrose::ConfigurationError }

spec/montrose/rule/day_of_month_spec.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
require "spec_helper"
44

55
describe Montrose::Rule::DayOfMonth do
6-
let(:rule) { Montrose::Rule::DayOfMonth.new([1, 10, -1]) }
6+
let(:rule) { Montrose::Rule::DayOfMonth.new(default: [1, 10, -1], overrides: {}, fallback: nil) }
7+
let(:fallback_rule) { Montrose::Rule::DayOfMonth.new(default: [31], overrides: {}, fallback: -1) }
8+
let(:override_rule) { Montrose::Rule::DayOfMonth.new(default: [15], overrides: {february: 28, september: 30, november: 30, april: 30}, fallback: nil) }
79

810
describe "#include?" do
911
it { assert rule.include?(Time.local(2016, 1, 1)) }
@@ -14,6 +16,22 @@
1416
it { refute rule.include?(Time.local(2015, 1, 2)) }
1517
it { refute rule.include?(Time.local(2015, 1, 30)) }
1618
it { refute rule.include?(Time.local(2015, 2, 27)) }
19+
20+
it { assert fallback_rule.include?(Time.local(2016, 1, 31)) }
21+
it { assert fallback_rule.include?(Time.local(2015, 2, 28)) }
22+
it { assert fallback_rule.include?(Time.local(2016, 2, 29)) }
23+
it { assert fallback_rule.include?(Time.local(2016, 4, 30)) }
24+
it { refute fallback_rule.include?(Time.local(2016, 1, 30)) }
25+
26+
it { assert override_rule.include?(Time.local(2016, 1, 15)) }
27+
it { assert override_rule.include?(Time.local(2016, 2, 28)) }
28+
it { assert override_rule.include?(Time.local(2016, 9, 30)) }
29+
it { assert override_rule.include?(Time.local(2016, 11, 30)) }
30+
it { assert override_rule.include?(Time.local(2016, 4, 30)) }
31+
it { refute override_rule.include?(Time.local(2016, 9, 15)) }
32+
it { refute override_rule.include?(Time.local(2016, 11, 15)) }
33+
it { refute override_rule.include?(Time.local(2016, 4, 15)) }
34+
it { refute override_rule.include?(Time.local(2016, 2, 15)) }
1735
end
1836

1937
describe "#continue?" do

0 commit comments

Comments
 (0)