Skip to content
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@
/spec/reports/
/tmp/

# rubymine
.idea

# rspec failure tracking
.rspec_status
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Indonesian translations. ([#505](https://github.com/seejohnrun/ice_cube/pull/505)) by [@achmiral](https://github.com/achmiral)
- Support for BYSETPOS

### Changed
- Removed use of `delegate` method added in [66f1d797](https://github.com/ice-cube-ruby/ice_cube/commit/66f1d797092734563bfabd2132c024c7d087f683) , reverting to previous implementation. ([#522](https://github.com/ice-cube-ruby/ice_cube/pull/522)) by [@pacso](https://github.com/pacso)
Expand Down
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)
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
36 changes: 34 additions & 2 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 'date'
require 'time'
require 'active_support'
require 'active_support/core_ext'

module IceCube
module TimeUtil
Expand Down Expand Up @@ -193,6 +195,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 @@ -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
85 changes: 85 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,85 @@
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?(Integer)

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

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
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?(Integer)

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
42 changes: 42 additions & 0 deletions spec/examples/by_set_pos_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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

context 'when billing occurs at the end of the month' do
it 'should return the correct end of the months dates' do
schedule = IceCube::Schedule.from_ical "RRULE:FREQ=MONTHLY;COUNT=4;BYMONTHDAY=28,29,30,31;BYSETPOS=-1"
schedule.start_time = Time.new(2022, 11, 1, 12, 0, 0)
expect(schedule.occurrences_between(Time.new(2022, 10, 01), Time.new(2023, 10, 31))).to eq([
Time.new(2022,11,30,12,0,0),
Time.new(2022,12,31,12,0,0),
Time.new(2023,01,31,12,0,0),
Time.new(2023,02,28,12,0,0)
])
end
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 @@ -84,6 +84,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
2 changes: 1 addition & 1 deletion spec/examples/occurrence_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
time_now = Time.current
occurrence = Occurrence.new(time_now)

expect(occurrence.to_s(:short)).to eq time_now.to_s(:short)
expect(occurrence.to_fs(:short)).to eq time_now.to_fs(:short)
end
end

Expand Down
6 changes: 6 additions & 0 deletions spec/examples/time_util_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ module IceCube
end
end

describe :serialize_time do
it "supports ISO8601 time strings" do
expect(TimeUtil.serialize_time(Time.utc(2014, 4, 4, 10, 30, 0))).to eq("2014-04-04T18:30:00+08:00")
end
end

describe :match_zone do
let(:date) { Date.new(2014, 1, 1) }

Expand Down
Loading