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

Support BYSETPOS for MONTHLY AND YEARLY freq #349

Open
wants to merge 1 commit into
base: master
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ coverage
coverage.data
*.gem
.bundle
.idea
3 changes: 3 additions & 0 deletions lib/ice_cube.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,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
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 @@ -68,6 +68,7 @@ def self.rule_from_ical(ical)
when 'BYYEARDAY'
params[:validations][:day_of_year] = value.split(',').collect(&:to_i)
when 'BYSETPOS'
params[:validations][:by_set_pos] = value.split(',').collect(&:to_i)
else
raise "Invalid or unsupported rrule command: #{name}"
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 @@ -3,6 +3,7 @@ module IceCube
class MonthlyRule < ValidatedRule

include Validations::MonthlyInterval
include Validations::MonthlyBySetPos

def initialize(interval = 1, week_start = :sunday)
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 @@ -3,6 +3,7 @@ module IceCube
class YearlyRule < ValidatedRule

include Validations::YearlyInterval
include Validations::YearlyBySetPos

def initialize(interval = 1, week_start = :sunday)
super
Expand Down
32 changes: 32 additions & 0 deletions lib/ice_cube/time_util.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require 'date'
require 'time'
require 'active_support'
require 'active_support/core_ext'

module IceCube
module TimeUtil
Expand Down Expand Up @@ -180,6 +182,36 @@ def self.which_occurrence_in_month(time, wday)
[nth_occurrence_of_weekday, this_weekday_in_month_count]
end

# Use Activesupport CoreExt functions to manipulate time
def self.start_of_month time
time.beginning_of_month
end

# Use Activesupport CoreExt functions to manipulate time
def self.end_of_month time
time.end_of_month
end

# Use Activesupport CoreExt functions to manipulate time
def self.start_of_year time
time.beginning_of_year
end

# Use Activesupport CoreExt functions to manipulate time
def self.end_of_year time
time.end_of_year
end

# Use Activesupport CoreExt functions to manipulate time
def self.previous_month time
time - 1.month
end

# Use Activesupport CoreExt functions to manipulate time
def self.previous_year time
time - 1.year
end

# Get the days in the month for +time
def self.days_in_month(time)
date = Date.new(time.year, time.month, 1)
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 @@ -27,7 +27,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
87 changes: 87 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,87 @@
module IceCube

module Validations::MonthlyBySetPos

def by_set_pos(*by_set_pos)
return by_set_pos([by_set_pos]) if by_set_pos.is_a?(Fixnum)

unless by_set_pos.nil? || by_set_pos.is_a?(Array)
raise ArgumentError, "Expecting Array or nil value for count, got #{by_set_pos.inspect}"
end
by_set_pos.flatten!
by_set_pos.each do |set_pos|
unless (set_pos >= -366 && set_pos <= -1) ||
(set_pos <= 366 && set_pos >= 1)
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, 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, schedule)
start_of_month = TimeUtil.start_of_month step_time
end_of_month = TimeUtil.end_of_month step_time


new_schedule = IceCube::Schedule.new(TimeUtil.previous_month(step_time)) do |s|
s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.reject{|k, v| [:by_set_pos, :count, :until].include? k})
end

puts step_time
occurrences = new_schedule.occurrences_between(start_of_month, end_of_month)
p occurrences
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
89 changes: 89 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,89 @@
module IceCube

module Validations::YearlyBySetPos

def by_set_pos(*by_set_pos)
return by_set_pos([by_set_pos]) if by_set_pos.is_a?(Fixnum)

unless by_set_pos.nil? || by_set_pos.is_a?(Array)
raise ArgumentError, "Expecting Array or nil value for count, got #{by_set_pos.inspect}"
end
by_set_pos.flatten!
by_set_pos.each do |set_pos|
unless (set_pos >= -366 && set_pos <= -1) ||
(set_pos <= 366 && set_pos >= 1)
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, 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, schedule)
start_of_year = TimeUtil.start_of_year step_time
end_of_year = TimeUtil.end_of_year step_time


new_schedule = IceCube::Schedule.new(TimeUtil.previous_year(step_time)) do |s|
s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.reject{|k, v| [:by_set_pos, :count, :until].include? k})
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
29 changes: 29 additions & 0 deletions spec/examples/by_set_pos_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
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

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
end
end
5 changes: 5 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
rule.should == 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")
rule.should == 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