Skip to content

Commit 423faef

Browse files
committed
Add support for BYSETPOS (monthly and yearly frequency)
In August 2016, @NicolasMarlier added BYSETPOS support (ice-cube-ruby#349). Then, in July 2018, @nehresma added a few small changes to run in modern Ruby and a more modern rspec (ice-cube-ruby#449). Then, in January 2019, @davidstosik and @k3rni suggested changes to reduce complexity. This incorporates all the above into a single diff.
1 parent fb6c657 commit 423faef

File tree

10 files changed

+193
-1
lines changed

10 files changed

+193
-1
lines changed

lib/ice_cube.rb

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

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

lib/ice_cube/parsers/ical_parser.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +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+
params[:validations][:by_set_pos] = value.split(',').collect(&:to_i)
7879
else
7980
validations[name] = nil # invalid type
8081
end

lib/ice_cube/rules/monthly_rule.rb

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

1414
include Validations::MonthlyInterval
15+
include Validations::MonthlyBySetPos
1516

1617
def initialize(interval = 1)
1718
super

lib/ice_cube/rules/yearly_rule.rb

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

1414
include Validations::YearlyInterval
15+
include Validations::YearlyBySetPos
1516

1617
def initialize(interval = 1)
1718
super

lib/ice_cube/time_util.rb

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

46
module IceCube
57
module TimeUtil

lib/ice_cube/validated_rule.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ class ValidatedRule < Rule
2020
:base_sec, :base_min, :base_day, :base_hour, :base_month, :base_wday,
2121
:day_of_year, :second_of_minute, :minute_of_hour, :day_of_month,
2222
:hour_of_day, :month_of_year, :day_of_week,
23-
:interval
23+
:interval,
24+
:by_set_pos
2425
]
2526

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

spec/examples/by_set_pos_spec.rb

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

spec/examples/from_ical_spec.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ module IceCube
8686
expect(rule).to eq(IceCube::Rule.weekly(2, :monday))
8787
end
8888

89+
it 'should be able to parse by_set_pos start (BYSETPOS)' do
90+
rule = IceCube::Rule.from_ical("FREQ=MONTHLY;BYDAY=MO,WE;BYSETPOS=-1,1")
91+
expect(rule).to eq(IceCube::Rule.monthly.day(:monday, :wednesday).by_set_pos([-1, 1]))
92+
end
93+
8994
it 'should return no occurrences after daily interval with count is over' do
9095
schedule = IceCube::Schedule.new(Time.now)
9196
schedule.add_recurrence_rule(IceCube::Rule.from_ical("FREQ=DAILY;COUNT=5"))

0 commit comments

Comments
 (0)