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

Update to BYSETPOS support #449

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
3 changes: 3 additions & 0 deletions lib/ice_cube.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ module Validations
autoload :YearlyInterval, "ice_cube/validations/yearly_interval"
autoload :HourlyInterval, "ice_cube/validations/hourly_interval"

autoload :MonthlyBySetPos, 'ice_cube/validations/monthly_by_set_pos'
autoload :YearlyBySetPos, 'ice_cube/validations/yearly_by_set_pos'

autoload :HourOfDay, "ice_cube/validations/hour_of_day"
autoload :MonthOfYear, "ice_cube/validations/month_of_year"
autoload :MinuteOfHour, "ice_cube/validations/minute_of_hour"
Expand Down
2 changes: 1 addition & 1 deletion lib/ice_cube/parsers/ical_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def self.rule_from_ical(ical)
when "BYYEARDAY"
validations[:day_of_year] = value.split(",").map(&:to_i)
when "BYSETPOS"
# noop
params[:validations][:by_set_pos] = value.split(',').collect(&:to_i)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps use the style validations[:by_set_pos] to match everything else in this case statement.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, existing code uses double quotes instead of single quotes for string in split and collect instead of map. Picky but just for consistency.

else
validations[name] = nil # invalid type
end
Expand Down
1 change: 1 addition & 0 deletions lib/ice_cube/rules/monthly_rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class MonthlyRule < ValidatedRule
# include Validations::DayOfYear # n/a

include Validations::MonthlyInterval
include Validations::MonthlyBySetPos

def initialize(interval = 1)
super
Expand Down
1 change: 1 addition & 0 deletions lib/ice_cube/rules/yearly_rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class YearlyRule < ValidatedRule
include Validations::DayOfYear

include Validations::YearlyInterval
include Validations::YearlyBySetPos

def initialize(interval = 1)
super
Expand Down
4 changes: 2 additions & 2 deletions lib/ice_cube/time_util.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
require "date"
require "time"
require 'date'
require 'time'
Comment on lines +1 to +2
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the type of quotes seems unnecessary. Double quotes are consistent with the existing style.


module IceCube
module TimeUtil
Expand Down
3 changes: 2 additions & 1 deletion lib/ice_cube/validated_rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class ValidatedRule < Rule
:base_sec, :base_min, :base_day, :base_hour, :base_month, :base_wday,
:day_of_year, :second_of_minute, :minute_of_hour, :day_of_month,
:hour_of_day, :month_of_year, :day_of_week,
:interval
:interval,
:by_set_pos
]

attr_reader :validations
Expand Down
76 changes: 76 additions & 0 deletions lib/ice_cube/validations/monthly_by_set_pos.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
module IceCube

module Validations::MonthlyBySetPos
def by_set_pos(*by_set_pos)
by_set_pos.flatten!
by_set_pos.each do |set_pos|
unless (-366..366).include?(set_pos) && set_pos != 0
raise ArgumentError, "Expecting number in [-366, -1] or [1, 366], got #{set_pos} (#{by_set_pos})"
nehresma marked this conversation as resolved.
Show resolved Hide resolved
end
end

@by_set_pos = by_set_pos
replace_validations_for(:by_set_pos, [Validation.new(by_set_pos, self)])
self
end

class Validation

attr_reader :rule, :by_set_pos

def initialize(by_set_pos, rule)
@by_set_pos = by_set_pos
@rule = rule
end

def type
:day
end

def dst_adjust?
true
end

def validate(step_time, start_time)
start_of_month = step_time.beginning_of_month
nehresma marked this conversation as resolved.
Show resolved Hide resolved
end_of_month = step_time.end_of_month
nehresma marked this conversation as resolved.
Show resolved Hide resolved

# Needs to start on the first day of the month
new_schedule = IceCube::Schedule.new(start_of_month.change(hour: step_time.hour, min: step_time.min, sec: step_time.sec)) do |s|
nehresma marked this conversation as resolved.
Show resolved Hide resolved
s.add_recurrence_rule(IceCube::Rule.from_hash(rule.to_hash.except(:by_set_pos, :count, :until)))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto my comment about except.

end

occurrences = new_schedule.occurrences_between(start_of_month, end_of_month)
index = occurrences.index(step_time)
if index == nil
1
else
positive_set_pos = index + 1
negative_set_pos = index - occurrences.length

if @by_set_pos.include?(positive_set_pos) || @by_set_pos.include?(negative_set_pos)
0
else
1
end
end
end

def build_s(builder)
builder.piece(:by_set_pos) << by_set_pos
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @nehresma

Looks like this should be improved:

> IceCube::Rule.monthly(2).day(1,2,3,4,5).by_set_pos(2).to_s
"Every 2 months on Weekdays [2]"

end

def build_hash(builder)
builder[:by_set_pos] = by_set_pos
end

def build_ical(builder)
builder['BYSETPOS'] << by_set_pos
end

nil
end

end

end
77 changes: 77 additions & 0 deletions lib/ice_cube/validations/yearly_by_set_pos.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
module IceCube

module Validations::YearlyBySetPos
nehresma marked this conversation as resolved.
Show resolved Hide resolved

def by_set_pos(*by_set_pos)
by_set_pos.flatten!
by_set_pos.each do |set_pos|
unless (-366..366).include?(set_pos) && set_pos != 0
raise ArgumentError, "Expecting number in [-366, -1] or [1, 366], got #{set_pos} (#{by_set_pos})"
end
end

@by_set_pos = by_set_pos
replace_validations_for(:by_set_pos, [Validation.new(by_set_pos, self)])
self
end

class Validation

attr_reader :rule, :by_set_pos

def initialize(by_set_pos, rule)

@by_set_pos = by_set_pos
@rule = rule
end

def type
:day
end

def dst_adjust?
true
end

def validate(step_time, start_time)
start_of_year = step_time.beginning_of_year
nehresma marked this conversation as resolved.
Show resolved Hide resolved
end_of_year = step_time.end_of_year
nehresma marked this conversation as resolved.
Show resolved Hide resolved

# Needs to start on the first day of the year
new_schedule = IceCube::Schedule.new(start_of_year.change(hour: step_time.hour, min: step_time.min, sec: step_time.sec)) do |s|
s.add_recurrence_rule(IceCube::Rule.from_hash(rule.to_hash.except(:by_set_pos, :count, :until)))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto my comment about except.

end

occurrences = new_schedule.occurrences_between(start_of_year, end_of_year)

index = occurrences.index(step_time)
if index == nil
1
else
positive_set_pos = index + 1
negative_set_pos = index - occurrences.length

if @by_set_pos.include?(positive_set_pos) || @by_set_pos.include?(negative_set_pos)
0
else
1
end
end
end

def build_s(builder)
builder.piece(:by_set_pos) << by_set_pos
end

def build_hash(builder)
builder[:by_set_pos] = by_set_pos
end

def build_ical(builder)
builder['BYSETPOS'] << by_set_pos
end

nil
end
end
end
75 changes: 75 additions & 0 deletions spec/examples/by_set_pos_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
require File.dirname(__FILE__) + '/../spec_helper'

module IceCube
describe MonthlyRule, 'BYSETPOS' do
it 'should behave correctly' do
schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=WE;BYSETPOS=4"
schedule.start_time = Time.new(2015, 5, 28, 12, 0, 0)
expect(schedule.occurrences_between(Time.new(2015, 01, 01), Time.new(2017, 01, 01))).
to eq([
Time.new(2015,6,24,12,0,0),
Time.new(2015,7,22,12,0,0),
Time.new(2015,8,26,12,0,0),
Time.new(2015,9,23,12,0,0)
])
end

it 'should work with intervals' do
schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=WE;BYSETPOS=4;INTERVAL=2"
schedule.start_time = Time.new(2015, 5, 28, 12, 0, 0)
expect(schedule.occurrences_between(Time.new(2015, 01, 01), Time.new(2017, 01, 01))).
to eq([
Time.new(2015,7,22,12,0,0),
Time.new(2015,9,23,12,0,0),
Time.new(2015,11,25,12,0,0),
Time.new(2016,1,27,12,0,0),
])
end
end

describe YearlyRule, 'BYSETPOS' do
it 'should behave correctly' do
schedule = IceCube::Schedule.from_ical "RRULE:FREQ=YEARLY;BYMONTH=7;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1"
schedule.start_time = Time.new(1966,7,5)
expect(schedule.occurrences_between(Time.new(2015, 01, 01), Time.new(2017, 01, 01))).
to eq([
Time.new(2015, 7, 31),
Time.new(2016, 7, 31)
])
end

it 'should work with intervals' do
schedule = IceCube::Schedule.from_ical "RRULE:FREQ=YEARLY;BYMONTH=7;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1;INTERVAL=2"
schedule.start_time = Time.new(1966,7,5)
expect(schedule.occurrences_between(Time.new(2015, 01, 01), Time.new(2023, 01, 01))).
to eq([
Time.new(2016, 7, 31),
Time.new(2018, 7, 31),
Time.new(2020, 7, 31),
Time.new(2022, 7, 31),
])
end

it 'should work with counts' do
schedule = IceCube::Schedule.from_ical "RRULE:FREQ=YEARLY;BYMONTH=7;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1;COUNT=3"
schedule.start_time = Time.new(2016,1,1)
expect(schedule.occurrences_between(Time.new(2016, 01, 01), Time.new(2050, 01, 01))).
to eq([
Time.new(2016, 7, 31),
Time.new(2017, 7, 31),
Time.new(2018, 7, 31),
])
end

it 'should work with counts and intervals' do
schedule = IceCube::Schedule.from_ical "RRULE:FREQ=YEARLY;BYMONTH=7;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1;COUNT=3;INTERVAL=2"
schedule.start_time = Time.new(2016,1,1)
expect(schedule.occurrences_between(Time.new(2016, 01, 01), Time.new(2050, 01, 01))).
to eq([
Time.new(2016, 7, 31),
Time.new(2018, 7, 31),
Time.new(2020, 7, 31),
])
end
end
end
21 changes: 20 additions & 1 deletion spec/examples/from_ical_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,26 @@ module IceCube
expect(rule).to eq(IceCube::Rule.weekly(2, :monday))
end

it "should return no occurrences after daily interval with count is over" do
it 'should be able to parse by_set_pos start (BYSETPOS)' do
rule = IceCube::Rule.from_ical("FREQ=MONTHLY;BYDAY=MO,WE;BYSETPOS=-1,1")
expect(rule).to eq(IceCube::Rule.monthly.day(:monday, :wednesday).by_set_pos([-1, 1]))
end

it 'should raise when by_set_pos is out of range (BYSETPOS)' do
expect {
IceCube::Rule.from_ical("FREQ=MONTHLY;BYDAY=MO,WE;BYSETPOS=-367")
}.to raise_error(/Expecting number in \[-366, -1\] or \[1, 366\]/)

expect {
IceCube::Rule.from_ical("FREQ=MONTHLY;BYDAY=MO,WE;BYSETPOS=367")
}.to raise_error(/Expecting number in \[-366, -1\] or \[1, 366\]/)

expect {
IceCube::Rule.from_ical("FREQ=MONTHLY;BYDAY=MO,WE;BYSETPOS=0")
}.to raise_error(/Expecting number in \[-366, -1\] or \[1, 366\]/)
end

it 'should return no occurrences after daily interval with count is over' do
schedule = IceCube::Schedule.new(Time.now)
schedule.add_recurrence_rule(IceCube::Rule.from_ical("FREQ=DAILY;COUNT=5"))
expect(schedule.occurrences_between(Time.now + (IceCube::ONE_DAY * 7), Time.now + (IceCube::ONE_DAY * 14)).count).to eq(0)
Expand Down
4 changes: 2 additions & 2 deletions spec/examples/to_yaml_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ module IceCube
end

it "should be able to make a round-trip to YAML with .day_of_year" do
schedule1 = Schedule.new(Time.now)
schedule1 = Schedule.new(Time.zone.now)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Time.zone is another active support extension. The stock Time class in Ruby has no zone method.

schedule1.add_recurrence_rule Rule.yearly.day_of_year(100, 200)

yaml_string = schedule1.to_yaml
Expand Down Expand Up @@ -112,7 +112,7 @@ module IceCube
end

it "should be able to make a round-trip to YAML with .month_of_year" do
schedule = Schedule.new(Time.now)
schedule = Schedule.new(Time.zone.now)
schedule.add_recurrence_rule Rule.yearly.month_of_year(:april, :may)

yaml_string = schedule.to_yaml
Expand Down