Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add additional options for mday #172

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,12 @@ Montrose.every(:month, mday: [2, 15], total: 10)
# monthly on the first and last day of the month for 10 occurrences
Montrose.monthly(mday: [1, -1], total: 10)

# monthly on the 30th unless fewer days
Montrose.monthly(mday: { default: 30, fallback: -1 })

# monthly on the 25th except in december
Montrose.monthly(mday: { default: 25, december: 20 })

# every 18 months on the 10th thru 15th of the month for 10 occurrences
Montrose.every(18.months, total: 10, mday: 10..15)

Expand Down
37 changes: 35 additions & 2 deletions lib/montrose/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ def day=(days)
@day = Day.parse(days)
end

def mday=(mdays)
@mday = MonthDay.parse(mdays)
def mday=(mday_arg)
@mday = decompose_mday_arg(mday_arg)
end

def yday=(ydays)
Expand Down Expand Up @@ -370,5 +370,38 @@ def end_of_day
def beginning_of_day
@beginning_of_day ||= time_of_day_parse(Time.now.beginning_of_day)
end

def decompose_mday_arg(mday_arg)
case mday_arg
when Hash
return nil unless mday_arg[:default].present?
{
default: MonthDay.parse(mday_arg[:default]),
overrides: flatten_mday_arg(mday_arg),
fallback: single_day(mday_arg[:fallback])
}
else
{default: MonthDay.parse(mday_arg), overrides: {}, fallback: nil}
end
end

def flatten_mday_arg(mday_arg)
mday_arg.except(:default, :overrides, :fallback).each_with_object({}) do |(months, day), result|
case months
when Array
months.each { |month| result[month] = single_day(day) }
else
result[months] = single_day(day)
end
end
end

def single_day(day_number)
return nil unless day_number
raise ConfigurationError, "mday override #{day_number} must be an integer" unless day_number.is_a?(Integer)
MonthDay.assert(day_number)

day_number
end
end
end
48 changes: 41 additions & 7 deletions lib/montrose/rule/day_of_month.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,56 @@ def self.apply_options(opts)

# Initializes rule
#
# @param [Array<Fixnum>] days - valid days of month, i.e. [1, 2, -1]
# @param [Hash] opts `mday` valid days of month, and `skip_months` options
#
def initialize(days)
@days = days
def initialize(opts)
@days = opts.fetch(:default)
@overrides = opts.fetch(:overrides, {})
@fallback = opts.fetch(:fallback, nil)
end

def include?(time)
@days.include?(time.mday) || included_from_end_of_month?(time)
return override?(time) if has_override?(month_name(time))

@days.include?(time.mday) || included_from_end_of_month?(time) || fallback?(time)
end

private

# matches days specified at negative numbers
def included_from_end_of_month?(time)
month_days = ::Montrose::Utils.days_in_month(time.month, time.year) # given by activesupport
@days.any? { |d| month_days + d + 1 == time.mday }
def included_from_end_of_month?(time, days = @days)
days_in_month = month_days(time)
days.any? { |d| days_in_month + d + 1 == time.mday }
end

def has_override?(month)
@overrides.key?(month)
end

def override?(time)
return false if @overrides.blank?

time.day == @overrides[month_name(time)]
end

def month_name(time)
time.strftime("%B").downcase.to_sym
end

def fallback?(time)
days_in_month = month_days(time)

return false if @fallback.blank?
# If any negative days, we will always have a match
return false if @days.any?(&:negative?)
# If all days are < number of days in this month, we'll always have a match
return false if @days.all? { |d| d <= days_in_month }

time.day == @fallback || included_from_end_of_month?(time, [@fallback])
end

def month_days(time)
::Montrose::Utils.days_in_month(time.month, time.year) # given by activesupport
end
end
end
Expand Down
107 changes: 95 additions & 12 deletions spec/montrose/options_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -423,29 +423,57 @@
it "can be set" do
options[:mday] = [1, 20, 31]

_(options.mday).must_equal [1, 20, 31]
_(options[:mday]).must_equal [1, 20, 31]
_(options.mday).must_equal({default: [1, 20, 31], overrides: {}, fallback: nil})
_(options[:mday]).must_equal({default: [1, 20, 31], overrides: {}, fallback: nil})
end

it "can be set to a hash" do
options[:mday] = {default: [1, 20, 31]}

_(options.mday).must_equal({default: [1, 20, 31], overrides: {}, fallback: nil})
_(options[:mday]).must_equal({default: [1, 20, 31], overrides: {}, fallback: nil})
end

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

_(options.mday).must_equal [1]
_(options[:mday]).must_equal [1]
_(options.mday).must_equal({default: [1], overrides: {}, fallback: nil})
_(options[:mday]).must_equal({default: [1], overrides: {}, fallback: nil})
end

it "casts default element to array" do
options[:mday] = {default: 1}

_(options.mday).must_equal({default: [1], overrides: {}, fallback: nil})
_(options[:mday]).must_equal({default: [1], overrides: {}, fallback: nil})
end

it "allows negative numbers" do
options[:yday] = [-1]
options[:mday] = [-1]

_(options.yday).must_equal [-1]
_(options[:yday]).must_equal [-1]
_(options.mday).must_equal({default: [-1], overrides: {}, fallback: nil})
_(options[:mday]).must_equal({default: [-1], overrides: {}, fallback: nil})
end

it "allows default negative numbers" do
options[:mday] = {default: [-1]}

_(options.mday).must_equal({default: [-1], overrides: {}, fallback: nil})
_(options[:mday]).must_equal({default: [-1], overrides: {}, fallback: nil})
end

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

_(options.mday).must_equal [6, 7, 8]
_(options[:mday]).must_equal [6, 7, 8]
_(options.mday).must_equal({default: [6, 7, 8], overrides: {}, fallback: nil})
_(options[:mday]).must_equal({default: [6, 7, 8], overrides: {}, fallback: nil})
end

it "casts default range to array" do
options[:mday] = {default: 6..8}

_(options.mday).must_equal({default: [6, 7, 8], overrides: {}, fallback: nil})
_(options[:mday]).must_equal({default: [6, 7, 8], overrides: {}, fallback: nil})
end

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

it "casts default nil to empty array" do
options[:mday] = {default: nil}

_(options.mday).must_be_nil
_(options[:mday]).must_be_nil
end

it "raises for out of range" do
_(-> { options[:mday] = [1, 100] }).must_raise
end

it "raises for default out of range" do
_(-> { options[:mday] = {default: [1, 100]} }).must_raise
end

it "raises for array override" do
_(-> { options[:mday] = {default: 31, february: [28, 29]} }).must_raise
end

it "raises for array fallback" do
_(-> { options[:mday] = {default: 31, fallback: [28, 29]} }).must_raise
end

it "allows negative overrides" do
options[:mday] = {default: 31, february: -1}

_(options.mday).must_equal({default: [31], overrides: {february: -1}, fallback: nil})
_(options[:mday]).must_equal({default: [31], overrides: {february: -1}, fallback: nil})
end

it "allows negative fallback" do
options[:mday] = {default: 31, fallback: -1}

_(options.mday).must_equal({default: [31], overrides: {}, fallback: -1})
_(options[:mday]).must_equal({default: [31], overrides: {}, fallback: -1})
end

it "raises for override out of range" do
_(-> { options[:mday] = {default: 31, february: 100} }).must_raise
end

it "raises for fallback out of range" do
_(-> { options[:mday] = {default: 31, fallback: 100} }).must_raise
end

it "collects overrides" do
options[:mday] = {default: 31, september: 30, february: 28}

_(options.mday).must_equal({default: [31], overrides: {september: 30, february: 28}, fallback: nil})
_(options[:mday]).must_equal({default: [31], overrides: {september: 30, february: 28}, fallback: nil})
end

it "flattens overrides" do
options[:mday] = {:default => 31, [:september, :april, :june, :november] => 30, :february => 28}

_(options.mday).must_equal({default: [31], overrides: {september: 30, april: 30, june: 30, november: 30, february: 28}, fallback: nil})
_(options[:mday]).must_equal({default: [31], overrides: {september: 30, april: 30, june: 30, november: 30, february: 28}, fallback: nil})
end
end

describe "#yday" do
Expand Down Expand Up @@ -767,7 +850,7 @@
options[:on] = {friday: 13}

_(options[:day]).must_equal [5]
_(options[:mday]).must_equal [13]
_(options[:mday]).must_equal({default: [13], overrides: {}, fallback: nil})
_(options[:on]).must_equal(friday: 13)
end

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

_(options[:day]).must_equal [2]
_(options[:mday]).must_equal((2..8).to_a)
_(options[:mday]).must_equal({default: (2..8).to_a, overrides: {}, fallback: nil})
_(options[:month]).must_equal [11]
end

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

_(options[:month]).must_equal [1]
_(options[:mday]).must_equal [31]
_(options[:mday]).must_equal({default: [31], overrides: {}, fallback: nil})
end

it { _(-> { options[:on] = -3 }).must_raise Montrose::ConfigurationError }
Expand Down
23 changes: 22 additions & 1 deletion spec/montrose/rule/day_of_month_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
require "spec_helper"

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

describe "#include?" do
it { assert rule.include?(Time.local(2016, 1, 1)) }
Expand All @@ -14,6 +17,24 @@
it { refute rule.include?(Time.local(2015, 1, 2)) }
it { refute rule.include?(Time.local(2015, 1, 30)) }
it { refute rule.include?(Time.local(2015, 2, 27)) }

it { assert fallback_rule.include?(Time.local(2016, 1, 31)) }
it { assert fallback_rule.include?(Time.local(2015, 2, 28)) }
it { assert fallback_rule.include?(Time.local(2016, 2, 29)) }
it { assert fallback_rule.include?(Time.local(2016, 4, 30)) }
it { refute fallback_rule.include?(Time.local(2016, 1, 30)) }

it { assert override_rule.include?(Time.local(2016, 1, 15)) }
it { assert override_rule.include?(Time.local(2016, 2, 28)) }
it { assert override_rule.include?(Time.local(2016, 9, 30)) }
it { assert override_rule.include?(Time.local(2016, 11, 30)) }
it { assert override_rule.include?(Time.local(2016, 4, 30)) }
it { refute override_rule.include?(Time.local(2016, 9, 15)) }
it { refute override_rule.include?(Time.local(2016, 11, 15)) }
it { refute override_rule.include?(Time.local(2016, 4, 15)) }
it { refute override_rule.include?(Time.local(2016, 2, 15)) }

it { refute fallback_rule_2.include?(Time.local(2016, 1, 31)) }
end

describe "#continue?" do
Expand Down