Skip to content
Merged
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
2 changes: 2 additions & 0 deletions lib/ice_cube.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ module Validations
autoload :YearlyInterval, 'ice_cube/validations/yearly_interval'
autoload :HourlyInterval, 'ice_cube/validations/hourly_interval'

autoload :BySetPos, 'ice_cube/validations/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
1 change: 1 addition & 0 deletions lib/ice_cube/parsers/ical_parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def self.rule_from_ical(ical)
when 'BYYEARDAY'
validations[:day_of_year] = value.split(',').map(&:to_i)
when 'BYSETPOS'
params[:validations][:by_set_pos] = value.split(',').collect(&:to_i)
else
validations[name] = nil # invalid type
end
Expand Down
8 changes: 8 additions & 0 deletions lib/ice_cube/rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ def on?(time, schedule)
next_time(time, schedule, time).to_i == time.to_i
end

def interval_type
self.class.interval_type
end

class << self

# Convert from a hash and create a rule
Expand Down Expand Up @@ -90,6 +94,10 @@ def from_hash(original_hash)
rule
end

def interval_type
@_interval_type ||= self.name.split('::').last.sub(/Rule$/, '').downcase
end

private

def apply_validation(rule, name, args)
Expand Down
4 changes: 3 additions & 1 deletion lib/ice_cube/validated_rule.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class ValidatedRule < Rule

include Validations::Count
include Validations::Until
include Validations::BySetPos

# Validations ordered for efficiency in sequence of:
# * descending intervals
Expand All @@ -20,7 +21,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
138 changes: 138 additions & 0 deletions lib/ice_cube/validations/by_set_pos.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
require "active_support/core_ext/date/calculations"
require "active_support/core_ext/date_time/calculations"
require "active_support/core_ext/time/calculations"

require "active_support/core_ext/hash/except"

module IceCube

module Validations::BySetPos

def by_set_pos(*bysplist)
bysplist.flatten.each do |set_pos_day|
unless set_pos_day.is_a?(Integer) && (-366..366).include?(set_pos_day) && set_pos_day != 0
raise ArgumentError, "expecting Integer value in [-366, -1] or [1, 366] for setposday, got #{set_pos_day} (#{bysplist})"
end

validations_for(:by_set_pos) << Validation.new(set_pos_day, self)
end

self
end

class Validation

attr_reader :source_rule, :set_pos_day

def initialize(set_pos_day, source_rule)
@set_pos_day = set_pos_day
@source_rule = source_rule
end

def type
:day
end

def dst_adjust?
true
end

def validate(step_time, _start_time)
@step_time = step_time

if step_time == occurrences_this_period[zero_indexed_position]
0
else
1
end
end

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

def build_hash(builder)
builder.validations_array(:by_set_pos) << set_pos_day
end

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

private

attr_reader :step_time

def zero_indexed_position
if set_pos_day > 0
set_pos_day - 1
else
set_pos_day
end
end

def occurrences_this_period
schedule_for_rule.occurrences_between(
beginning_of_period,
end_of_period
)
end

def interval_type
if source_rule.interval_type == 'daily'
'day'
else
source_rule.interval_type.sub(/ly$/, '')
end
end

def beginning_of_period
if interval_type == 'second'
fail 'boo'
else
step_time.public_send("beginning_of_#{interval_type}")
end
end

def end_of_period
if interval_type == 'second'
fail 'boo'
else
step_time.public_send("end_of_#{interval_type}")
end
end

def last_period
case interval_type
when 'second'
fail 'boo'
when 'minute'
step_time - ONE_MINUTE
when 'hour'
step_time - ONE_HOUR
when 'day'
step_time.yesterday
when 'week', 'month', 'year'
step_time.public_send("last_#{interval_type}")
end
end

def schedule_for_rule
IceCube::Schedule.new(last_period) do |s|
s.add_recurrence_rule Rule.from_hash(rule_hash_for_all_occurrences)
end
end

def rule_hash_for_all_occurrences
source_rule.to_hash.except(:count, :until).tap do |hash|
if hash[:validations]
hash[:validations].delete(:by_set_pos)
end
end
end

end

end

end
31 changes: 31 additions & 0 deletions spec/examples/by_set_pos_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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)
expectations = [
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)
]
expect(schedule.occurrences(Time.new(2017, 01, 01))).to eq(expectations)
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)
expectations = [
Time.new(2015, 7, 31),
Time.new(2016, 7, 31)
]
expect(schedule.occurrences_between(Time.new(2015, 01, 01), Time.new(2017, 01, 01))).to eq(expectations)
end
end
end
45 changes: 45 additions & 0 deletions spec/examples/from_ical_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ module IceCube
expect(rule).to eq(IceCube::Rule.weekly(2, :monday))
end

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 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"))
Expand Down Expand Up @@ -185,6 +190,16 @@ def sorted_ical(ical)
expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
end

it 'handles bysetpos' do
start_time = Time.now

schedule = IceCube::Schedule.new(start_time)
schedule.add_recurrence_rule(IceCube::Rule.weekly.day(:monday).by_set_pos(1, -1))

ical = schedule.to_ical
expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
end

end

describe 'weekly frequency' do
Expand Down Expand Up @@ -237,6 +252,16 @@ def sorted_ical(ical)
ical = schedule.to_ical
expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
end

it 'handles bysetpos' do
start_time = Time.now

schedule = IceCube::Schedule.new(start_time)
schedule.add_recurrence_rule(IceCube::Rule.weekly.day(:monday).by_set_pos(1, -1))

ical = schedule.to_ical
expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
end
end

describe 'monthly frequency' do
Expand Down Expand Up @@ -279,6 +304,16 @@ def sorted_ical(ical)
ical = schedule.to_ical
expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
end

it 'handles bysetpos' do
start_time = Time.now

schedule = IceCube::Schedule.new(start_time)
schedule.add_recurrence_rule(IceCube::Rule.monthly.day(:monday).by_set_pos(1, -1))

ical = schedule.to_ical
expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
end
end

describe 'yearly frequency' do
Expand Down Expand Up @@ -332,6 +367,16 @@ def sorted_ical(ical)
expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
end

it 'handles bysetpos' do
start_time = Time.now

schedule = IceCube::Schedule.new(start_time)
schedule.add_recurrence_rule(IceCube::Rule.yearly.month_of_year(:february).day(:monday).by_set_pos(1, -1))

ical = schedule.to_ical
expect(sorted_ical(IceCube::Schedule.from_ical(ical).to_ical)).to eq(sorted_ical(ical))
end

it 'handles specific months' do
start_time = Time.now

Expand Down
17 changes: 17 additions & 0 deletions spec/examples/rfc_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,23 @@
expect(dates).to eq(expecation)
end

it 'should ~ third instance into the month of one of Tuesday, Wednesday, or Thursday, for the next 3 months' do
start_time = Time.utc(1997, 9, 4, 9, 0, 0)
schedule = IceCube::Schedule.new(start_time)
schedule.add_recurrence_rule IceCube::Rule.monthly.count(3).day(:tuesday , :wednesday , :thursday).by_set_pos(3)
dates = schedule.all_occurrences
expectation = [Time.utc(1997, 9, 4, 9), Time.utc(1997, 10, 7, 9), Time.utc(1997, 11, 6, 9)]
expect(dates).to eq(expectation)
end

it 'should ~ second-to-last weekday of the month' do
start_time = Time.utc(1997, 9, 29, 9, 0, 0)
schedule = IceCube::Schedule.new(start_time)
schedule.add_recurrence_rule IceCube::Rule.monthly.day(:monday, :tuesday , :wednesday , :thursday, :friday).by_set_pos(-2)
next_dates = schedule.occurrences(Time.utc(1997, 12, 31))
expectation = [Time.utc(1997, 9, 29, 9), Time.utc(1997, 10, 30, 9), Time.utc(1997, 11, 27, 9), Time.utc(1997, 12, 30, 9)]
expect(next_dates).to eq(expectation)
end
end

def test_expectations(schedule, dates_array)
Expand Down
8 changes: 8 additions & 0 deletions spec/examples/yearly_rule_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@
expect(schedule.occurrences(Time.utc(2010, 12, 31))).to eq days_of_year
end

it 'should produce the correct days for @interval = 1 when you specify week days' do
start_time = Time.utc(2010, 1, 1)
schedule = IceCube::Schedule.new(start_time)
schedule.add_recurrence_rule IceCube::Rule.yearly.day(:monday)

expect(schedule.occurrences(Time.utc(2010, 12, 31)).count).to eq 52
end

it 'should produce the correct days for @interval = 1 when you specify negative days' do
schedule = IceCube::Schedule.new(Time.utc(2010, 1, 1))
schedule.add_recurrence_rule IceCube::Rule.yearly.day_of_year(100, -1)
Expand Down