Skip to content

Commit 136faf0

Browse files
Nicolas Marliernehresma
authored andcommitted
Support BYSETPOS for MONTHLY AND YEARLY freq
1 parent 10ae8dc commit 136faf0

File tree

11 files changed

+256
-5
lines changed

11 files changed

+256
-5
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@
88
/spec/reports/
99
/tmp/
1010

11+
# rubymine
12+
.idea
13+
1114
# rspec failure tracking
1215
.rspec_status

lib/ice_cube.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ module Validations
4949
autoload :YearlyInterval, "ice_cube/validations/yearly_interval"
5050
autoload :HourlyInterval, "ice_cube/validations/hourly_interval"
5151

52+
autoload :MonthlyBySetPos, 'ice_cube/validations/monthly_by_set_pos'
53+
autoload :YearlyBySetPos, 'ice_cube/validations/yearly_by_set_pos'
54+
5255
autoload :HourOfDay, "ice_cube/validations/hour_of_day"
5356
autoload :MonthOfYear, "ice_cube/validations/month_of_year"
5457
autoload :MinuteOfHour, "ice_cube/validations/minute_of_hour"

lib/ice_cube/parsers/ical_parser.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def self.rule_from_ical(ical)
7575
when "BYYEARDAY"
7676
validations[:day_of_year] = value.split(",").map(&:to_i)
7777
when "BYSETPOS"
78-
# noop
78+
params[:validations][:by_set_pos] = value.split(',').collect(&:to_i)
7979
else
8080
validations[name] = nil # invalid type
8181
end

lib/ice_cube/rules/monthly_rule.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class MonthlyRule < ValidatedRule
1010
# include Validations::DayOfYear # n/a
1111

1212
include Validations::MonthlyInterval
13+
include Validations::MonthlyBySetPos
1314

1415
def initialize(interval = 1)
1516
super

lib/ice_cube/rules/yearly_rule.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class YearlyRule < ValidatedRule
1010
include Validations::DayOfYear
1111

1212
include Validations::YearlyInterval
13+
include Validations::YearlyBySetPos
1314

1415
def initialize(interval = 1)
1516
super

lib/ice_cube/time_util.rb

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
require "date"
2-
require "time"
1+
require 'date'
2+
require 'time'
3+
require 'active_support'
4+
require 'active_support/core_ext'
35

46
module IceCube
57
module TimeUtil
@@ -193,6 +195,36 @@ def self.which_occurrence_in_month(time, wday)
193195
[nth_occurrence_of_weekday, this_weekday_in_month_count]
194196
end
195197

198+
# Use Activesupport CoreExt functions to manipulate time
199+
def self.start_of_month time
200+
time.beginning_of_month
201+
end
202+
203+
# Use Activesupport CoreExt functions to manipulate time
204+
def self.end_of_month time
205+
time.end_of_month
206+
end
207+
208+
# Use Activesupport CoreExt functions to manipulate time
209+
def self.start_of_year time
210+
time.beginning_of_year
211+
end
212+
213+
# Use Activesupport CoreExt functions to manipulate time
214+
def self.end_of_year time
215+
time.end_of_year
216+
end
217+
218+
# Use Activesupport CoreExt functions to manipulate time
219+
def self.previous_month time
220+
time - 1.month
221+
end
222+
223+
# Use Activesupport CoreExt functions to manipulate time
224+
def self.previous_year time
225+
time - 1.year
226+
end
227+
196228
# Get the days in the month for +time
197229
def self.days_in_month(time)
198230
date = Date.new(time.year, time.month, 1)

lib/ice_cube/validated_rule.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ class ValidatedRule < Rule
1818
:base_sec, :base_min, :base_day, :base_hour, :base_month, :base_wday,
1919
:day_of_year, :second_of_minute, :minute_of_hour, :day_of_month,
2020
:hour_of_day, :month_of_year, :day_of_week,
21-
:interval
21+
:interval,
22+
:by_set_pos
2223
]
2324

2425
attr_reader :validations
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
module IceCube
2+
3+
module Validations::MonthlyBySetPos
4+
5+
def by_set_pos(*by_set_pos)
6+
return by_set_pos([by_set_pos]) if by_set_pos.is_a?(Fixnum)
7+
8+
unless by_set_pos.nil? || by_set_pos.is_a?(Array)
9+
raise ArgumentError, "Expecting Array or nil value for count, got #{by_set_pos.inspect}"
10+
end
11+
by_set_pos.flatten!
12+
by_set_pos.each do |set_pos|
13+
unless (set_pos >= -366 && set_pos <= -1) ||
14+
(set_pos <= 366 && set_pos >= 1)
15+
raise ArgumentError, "Expecting number in [-366, -1] or [1, 366], got #{set_pos} (#{by_set_pos})"
16+
end
17+
end
18+
19+
@by_set_pos = by_set_pos
20+
replace_validations_for(:by_set_pos, by_set_pos && [Validation.new(by_set_pos, self)])
21+
self
22+
end
23+
24+
class Validation
25+
26+
attr_reader :rule, :by_set_pos
27+
28+
def initialize(by_set_pos, rule)
29+
30+
@by_set_pos = by_set_pos
31+
@rule = rule
32+
end
33+
34+
def type
35+
:day
36+
end
37+
38+
def dst_adjust?
39+
true
40+
end
41+
42+
def validate(step_time, schedule)
43+
start_of_month = TimeUtil.start_of_month step_time
44+
end_of_month = TimeUtil.end_of_month step_time
45+
46+
47+
new_schedule = IceCube::Schedule.new(TimeUtil.previous_month(step_time)) do |s|
48+
s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.reject{|k, v| [:by_set_pos, :count, :until].include? k})
49+
end
50+
51+
puts step_time
52+
occurrences = new_schedule.occurrences_between(start_of_month, end_of_month)
53+
p occurrences
54+
index = occurrences.index(step_time)
55+
if index == nil
56+
1
57+
else
58+
positive_set_pos = index + 1
59+
negative_set_pos = index - occurrences.length
60+
61+
if @by_set_pos.include?(positive_set_pos) || @by_set_pos.include?(negative_set_pos)
62+
0
63+
else
64+
1
65+
end
66+
end
67+
end
68+
69+
70+
def build_s(builder)
71+
builder.piece(:by_set_pos) << by_set_pos
72+
end
73+
74+
def build_hash(builder)
75+
builder[:by_set_pos] = by_set_pos
76+
end
77+
78+
def build_ical(builder)
79+
builder['BYSETPOS'] << by_set_pos
80+
end
81+
82+
nil
83+
end
84+
85+
end
86+
87+
end
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
module IceCube
2+
3+
module Validations::YearlyBySetPos
4+
5+
def by_set_pos(*by_set_pos)
6+
return by_set_pos([by_set_pos]) if by_set_pos.is_a?(Fixnum)
7+
8+
unless by_set_pos.nil? || by_set_pos.is_a?(Array)
9+
raise ArgumentError, "Expecting Array or nil value for count, got #{by_set_pos.inspect}"
10+
end
11+
by_set_pos.flatten!
12+
by_set_pos.each do |set_pos|
13+
unless (set_pos >= -366 && set_pos <= -1) ||
14+
(set_pos <= 366 && set_pos >= 1)
15+
raise ArgumentError, "Expecting number in [-366, -1] or [1, 366], got #{set_pos} (#{by_set_pos})"
16+
end
17+
end
18+
19+
@by_set_pos = by_set_pos
20+
replace_validations_for(:by_set_pos, by_set_pos && [Validation.new(by_set_pos, self)])
21+
self
22+
end
23+
24+
class Validation
25+
26+
attr_reader :rule, :by_set_pos
27+
28+
def initialize(by_set_pos, rule)
29+
30+
@by_set_pos = by_set_pos
31+
@rule = rule
32+
end
33+
34+
def type
35+
:day
36+
end
37+
38+
def dst_adjust?
39+
true
40+
end
41+
42+
def validate(step_time, schedule)
43+
start_of_year = TimeUtil.start_of_year step_time
44+
end_of_year = TimeUtil.end_of_year step_time
45+
46+
47+
new_schedule = IceCube::Schedule.new(TimeUtil.previous_year(step_time)) do |s|
48+
s.add_recurrence_rule IceCube::Rule.from_hash(rule.to_hash.reject{|k, v| [:by_set_pos, :count, :until].include? k})
49+
end
50+
51+
occurrences = new_schedule.occurrences_between(start_of_year, end_of_year)
52+
53+
index = occurrences.index(step_time)
54+
if index == nil
55+
1
56+
else
57+
positive_set_pos = index + 1
58+
negative_set_pos = index - occurrences.length
59+
60+
if @by_set_pos.include?(positive_set_pos) || @by_set_pos.include?(negative_set_pos)
61+
0
62+
else
63+
1
64+
end
65+
end
66+
67+
68+
69+
end
70+
71+
72+
def build_s(builder)
73+
builder.piece(:by_set_pos) << by_set_pos
74+
end
75+
76+
def build_hash(builder)
77+
builder[:by_set_pos] = by_set_pos
78+
end
79+
80+
def build_ical(builder)
81+
builder['BYSETPOS'] << by_set_pos
82+
end
83+
84+
nil
85+
end
86+
87+
end
88+
89+
end

spec/examples/by_set_pos_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
require File.dirname(__FILE__) + '/../spec_helper'
2+
3+
module IceCube
4+
5+
describe MonthlyRule, 'BYSETPOS' do
6+
it 'should behave correctly' do
7+
schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYDAY=WE;BYSETPOS=4"
8+
schedule.start_time = Time.new(2015, 5, 28, 12, 0, 0)
9+
expect(schedule.occurrences_between(Time.new(2015, 01, 01), Time.new(2017, 01, 01))).to eq([
10+
Time.new(2015,6,24,12,0,0),
11+
Time.new(2015,7,22,12,0,0),
12+
Time.new(2015,8,26,12,0,0),
13+
Time.new(2015,9,23,12,0,0)
14+
])
15+
end
16+
17+
end
18+
19+
describe YearlyRule, 'BYSETPOS' do
20+
it 'should behave correctly' do
21+
schedule = IceCube::Schedule.from_ical "RRULE:FREQ=YEARLY;BYMONTH=7;BYDAY=SU,MO,TU,WE,TH,FR,SA;BYSETPOS=-1"
22+
schedule.start_time = Time.new(1966,7,5)
23+
expect(schedule.occurrences_between(Time.new(2015, 01, 01), Time.new(2017, 01, 01))).to eq([
24+
Time.new(2015, 7, 31),
25+
Time.new(2016, 7, 31)
26+
])
27+
end
28+
end
29+
end

0 commit comments

Comments
 (0)