diff --git a/lib/rrule.rb b/lib/rrule.rb index cce6114..1de1d11 100644 --- a/lib/rrule.rb +++ b/lib/rrule.rb @@ -13,6 +13,9 @@ module RRule autoload :Humanizer, 'rrule/humanizer' autoload :Frequency, 'rrule/frequencies/frequency' + autoload :Secondly, 'rrule/frequencies/secondly' + autoload :Minutely, 'rrule/frequencies/minutely' + autoload :Hourly, 'rrule/frequencies/hourly' autoload :Daily, 'rrule/frequencies/daily' autoload :Weekly, 'rrule/frequencies/weekly' autoload :SimpleWeekly, 'rrule/frequencies/simple_weekly' diff --git a/lib/rrule/frequencies/frequency.rb b/lib/rrule/frequencies/frequency.rb index 310a2f6..ac2e08f 100644 --- a/lib/rrule/frequencies/frequency.rb +++ b/lib/rrule/frequencies/frequency.rb @@ -34,6 +34,12 @@ def next_occurrences def self.for_options(options) case options[:freq] + when 'SECONDLY' + Secondly + when 'MINUTELY' + Minutely + when 'HOURLY' + Hourly when 'DAILY' Daily when 'WEEKLY' diff --git a/lib/rrule/frequencies/hourly.rb b/lib/rrule/frequencies/hourly.rb new file mode 100644 index 0000000..95aa366 --- /dev/null +++ b/lib/rrule/frequencies/hourly.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module RRule + class Hourly < Frequency + def possible_days + [current_date.yday - 1] # convert to 0-indexed + end + + def timeset + super.map { |time| time.merge(hour: current_date.hour) } + end + + private + + def advance_by + { hours: context.options[:interval] } + end + end +end diff --git a/lib/rrule/frequencies/minutely.rb b/lib/rrule/frequencies/minutely.rb new file mode 100644 index 0000000..8359604 --- /dev/null +++ b/lib/rrule/frequencies/minutely.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module RRule + class Minutely < Frequency + def possible_days + [current_date.yday - 1] # convert to 0-indexed + end + + def timeset + super.map { |time| time.merge(hour: current_date.hour, minute: current_date.min) } + end + + private + + def advance_by + { minutes: context.options[:interval] } + end + end +end diff --git a/lib/rrule/frequencies/secondly.rb b/lib/rrule/frequencies/secondly.rb new file mode 100644 index 0000000..d1d2eed --- /dev/null +++ b/lib/rrule/frequencies/secondly.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module RRule + class Secondly < Frequency + def possible_days + [current_date.yday - 1] # convert to 0-indexed + end + + def timeset + super.map { |time| time.merge(hour: current_date.hour, minute: current_date.min, second: current_date.sec) } + end + + private + + def advance_by + { seconds: context.options[:interval] } + end + end +end diff --git a/lib/rrule/rule.rb b/lib/rrule/rule.rb index ed11428..108b444 100644 --- a/lib/rrule/rule.rb +++ b/lib/rrule/rule.rb @@ -187,7 +187,7 @@ def parse_options(rule) # when the associated "DTSTART" property has a DATE value type. # These rule parts MUST be ignored in RECUR value that violate the # above requirement - options[:timeset] = [{ hour: (options[:byhour].presence || dtstart.hour), minute: (options[:byminute].presence || dtstart.min), second: (options[:bysecond].presence || dtstart.sec) }] unless dtstart.is_a?(Date) + options[:timeset] = [{ hour: options[:byhour].presence || dtstart.hour, minute: options[:byminute].presence || dtstart.min, second: options[:bysecond].presence || dtstart.sec }] unless dtstart.is_a?(Date) options end diff --git a/spec/frequencies/hourly_spec.rb b/spec/frequencies/hourly_spec.rb new file mode 100644 index 0000000..5e3c64c --- /dev/null +++ b/spec/frequencies/hourly_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe RRule::Hourly do + let(:interval) { 1 } + let(:context) do + RRule::Context.new( + { interval: interval }, + date, + 'America/Los_Angeles' + ) + end + let(:filters) { [] } + let(:generator) { RRule::AllOccurrences.new(context) } + let(:timeset) { [{ hour: date.hour, minute: date.min, second: date.sec }] } + + before { context.rebuild(date.year, date.month) } + + describe '#next_occurrences' do + subject(:frequency) { described_class.new(context, filters, generator, timeset) } + + context 'with an interval of one' do + let(:date) { Time.zone.local(1997, 1, 1, 0, 0) } + + it 'returns sequential hours' do + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 1, 0)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 2, 0)] + end + end + + context 'with an interval of two' do + let(:interval) { 2 } + let(:date) { Time.zone.local(1997, 1, 1, 0, 0) } + + it 'returns every other hour' do + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 2, 0)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 4, 0)] + end + end + + context 'at the end of the day' do + let(:date) { Time.zone.local(1997, 1, 1, 23, 0) } + + it 'goes into the next day' do + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 23, 0)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 2, 0, 0)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 2, 1, 0)] + end + end + end +end diff --git a/spec/frequencies/minutely_spec.rb b/spec/frequencies/minutely_spec.rb new file mode 100644 index 0000000..aea2983 --- /dev/null +++ b/spec/frequencies/minutely_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe RRule::Minutely do + let(:interval) { 1 } + let(:context) do + RRule::Context.new( + { interval: interval }, + date, + 'America/Los_Angeles' + ) + end + let(:filters) { [] } + let(:generator) { RRule::AllOccurrences.new(context) } + let(:timeset) { [{ hour: date.hour, minute: date.min, second: date.sec }] } + + before { context.rebuild(date.year, date.month) } + + describe '#next_occurrences' do + subject(:frequency) { described_class.new(context, filters, generator, timeset) } + + context 'with an interval of one' do + let(:date) { Time.zone.local(1997, 1, 1, 0, 0) } + + it 'returns sequential minutes' do + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 1)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 2)] + end + end + + context 'with an interval of two' do + let(:interval) { 2 } + let(:date) { Time.zone.local(1997, 1, 1, 0, 0) } + + it 'returns every other minute' do + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 2)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 4)] + end + end + + context 'at the end of the hour' do + let(:date) { Time.zone.local(1997, 1, 1, 0, 59) } + + it 'goes into the next hour' do + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 59)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 1, 0)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 1, 1)] + end + end + end +end diff --git a/spec/frequencies/secondly_spec.rb b/spec/frequencies/secondly_spec.rb new file mode 100644 index 0000000..982b743 --- /dev/null +++ b/spec/frequencies/secondly_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe RRule::Secondly do + let(:interval) { 1 } + let(:context) do + RRule::Context.new( + { interval: interval }, + date, + 'America/Los_Angeles' + ) + end + let(:filters) { [] } + let(:generator) { RRule::AllOccurrences.new(context) } + let(:timeset) { [{ hour: date.hour, minute: date.min, second: date.sec }] } + + before { context.rebuild(date.year, date.month) } + + describe '#next_occurrences' do + subject(:frequency) { described_class.new(context, filters, generator, timeset) } + + context 'with an interval of one' do + let(:date) { Time.zone.local(1997, 1, 1, 0, 0, 0) } + + it 'returns sequential seconds' do + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0, 0)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0, 1)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0, 2)] + end + end + + context 'with an interval of two' do + let(:interval) { 2 } + let(:date) { Time.zone.local(1997, 1, 1, 0, 0, 0) } + + it 'returns every other second' do + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0, 0)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0, 2)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0, 4)] + end + end + + context 'at the end of the minute' do + let(:date) { Time.zone.local(1997, 1, 1, 0, 0, 59) } + + it 'goes into the next minute' do + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0, 59)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 1, 0)] + expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 1, 1)] + end + end + end +end diff --git a/spec/rule_spec.rb b/spec/rule_spec.rb index 46ce3b7..9a06970 100644 --- a/spec/rule_spec.rb +++ b/spec/rule_spec.rb @@ -127,6 +127,159 @@ end describe '#all' do + it 'returns the correct result with an rrule of FREQ=SECONDLY;COUNT=10' do + rrule = 'FREQ=SECONDLY;COUNT=10' + dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997') + timezone = 'America/New_York' + + rrule = RRule::Rule.new(rrule, dtstart: dtstart, tzid: timezone) + + expect(rrule.all).to match_array([ + Time.parse('Tue Sep 2 06:00:00 PDT 1997'), + Time.parse('Tue Sep 2 06:00:01 PDT 1997'), + Time.parse('Tue Sep 2 06:00:02 PDT 1997'), + Time.parse('Tue Sep 2 06:00:03 PDT 1997'), + Time.parse('Tue Sep 2 06:00:04 PDT 1997'), + Time.parse('Tue Sep 2 06:00:05 PDT 1997'), + Time.parse('Tue Sep 2 06:00:06 PDT 1997'), + Time.parse('Tue Sep 2 06:00:07 PDT 1997'), + Time.parse('Tue Sep 2 06:00:08 PDT 1997'), + Time.parse('Tue Sep 2 06:00:09 PDT 1997'), + ]) + end + + it 'returns the correct result with an rrule of FREQ=SECONDLY;INTERVAL=10;COUNT=5' do + rrule = 'FREQ=SECONDLY;INTERVAL=10;COUNT=5' + dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997') + timezone = 'America/New_York' + + rrule = RRule::Rule.new(rrule, dtstart: dtstart, tzid: timezone) + + expect(rrule.all).to match_array([ + Time.parse('Tue Sep 2 06:00:00 PDT 1997'), + Time.parse('Tue Sep 2 06:00:10 PDT 1997'), + Time.parse('Tue Sep 2 06:00:20 PDT 1997'), + Time.parse('Tue Sep 2 06:00:30 PDT 1997'), + Time.parse('Tue Sep 2 06:00:40 PDT 1997'), + ]) + end + + it 'returns the correct result with an rrule of FREQ=MINUTELY;COUNT=10' do + rrule = 'FREQ=MINUTELY;COUNT=10' + dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997') + timezone = 'America/New_York' + + rrule = RRule::Rule.new(rrule, dtstart: dtstart, tzid: timezone) + + expect(rrule.all).to match_array([ + Time.parse('Tue Sep 2 06:00:00 PDT 1997'), + Time.parse('Tue Sep 2 06:01:00 PDT 1997'), + Time.parse('Tue Sep 2 06:02:00 PDT 1997'), + Time.parse('Tue Sep 2 06:03:00 PDT 1997'), + Time.parse('Tue Sep 2 06:04:00 PDT 1997'), + Time.parse('Tue Sep 2 06:05:00 PDT 1997'), + Time.parse('Tue Sep 2 06:06:00 PDT 1997'), + Time.parse('Tue Sep 2 06:07:00 PDT 1997'), + Time.parse('Tue Sep 2 06:08:00 PDT 1997'), + Time.parse('Tue Sep 2 06:09:00 PDT 1997'), + ]) + end + + it 'returns the correct result with an rrule of FREQ=MINUTELY;INTERVAL=10;COUNT=5' do + rrule = 'FREQ=MINUTELY;INTERVAL=10;COUNT=5' + dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997') + timezone = 'America/New_York' + + rrule = RRule::Rule.new(rrule, dtstart: dtstart, tzid: timezone) + + expect(rrule.all).to match_array([ + Time.parse('Tue Sep 2 06:00:00 PDT 1997'), + Time.parse('Tue Sep 2 06:10:00 PDT 1997'), + Time.parse('Tue Sep 2 06:20:00 PDT 1997'), + Time.parse('Tue Sep 2 06:30:00 PDT 1997'), + Time.parse('Tue Sep 2 06:40:00 PDT 1997'), + ]) + end + + it 'returns the correct result with an rrule of FREQ=MINUTELY;BYSECOND=0,15,30,45;COUNT=10' do + rrule = 'FREQ=MINUTELY;BYSECOND=0,15,30,45;COUNT=10' + dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997') + timezone = 'America/New_York' + + rrule = RRule::Rule.new(rrule, dtstart: dtstart, tzid: timezone) + + expect(rrule.all).to match_array([ + Time.parse('Tue Sep 2 06:00:00 PDT 1997'), + Time.parse('Tue Sep 2 06:00:15 PDT 1997'), + Time.parse('Tue Sep 2 06:00:30 PDT 1997'), + Time.parse('Tue Sep 2 06:00:45 PDT 1997'), + Time.parse('Tue Sep 2 06:01:00 PDT 1997'), + Time.parse('Tue Sep 2 06:01:15 PDT 1997'), + Time.parse('Tue Sep 2 06:01:30 PDT 1997'), + Time.parse('Tue Sep 2 06:01:45 PDT 1997'), + Time.parse('Tue Sep 2 06:02:00 PDT 1997'), + Time.parse('Tue Sep 2 06:02:15 PDT 1997'), + ]) + end + + it 'returns the correct result with an rrule of FREQ=HOURLY;COUNT=10' do + rrule = 'FREQ=HOURLY;COUNT=10' + dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997') + timezone = 'America/New_York' + + rrule = RRule::Rule.new(rrule, dtstart: dtstart, tzid: timezone) + + expect(rrule.all).to match_array([ + Time.parse('Tue Sep 2 06:00:00 PDT 1997'), + Time.parse('Tue Sep 2 07:00:00 PDT 1997'), + Time.parse('Tue Sep 2 08:00:00 PDT 1997'), + Time.parse('Tue Sep 2 09:00:00 PDT 1997'), + Time.parse('Tue Sep 2 10:00:00 PDT 1997'), + Time.parse('Tue Sep 2 11:00:00 PDT 1997'), + Time.parse('Tue Sep 2 12:00:00 PDT 1997'), + Time.parse('Tue Sep 2 13:00:00 PDT 1997'), + Time.parse('Tue Sep 2 14:00:00 PDT 1997'), + Time.parse('Tue Sep 2 15:00:00 PDT 1997'), + ]) + end + + it 'returns the correct result with an rrule of FREQ=HOURLY;INTERVAL=10;COUNT=5' do + rrule = 'FREQ=HOURLY;INTERVAL=10;COUNT=5' + dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997') + timezone = 'America/New_York' + + rrule = RRule::Rule.new(rrule, dtstart: dtstart, tzid: timezone) + + expect(rrule.all).to match_array([ + Time.parse('Tue Sep 2 06:00:00 PDT 1997'), + Time.parse('Tue Sep 2 16:00:00 PDT 1997'), + Time.parse('Tue Sep 3 02:00:00 PDT 1997'), + Time.parse('Fri Sep 3 12:00:00 PDT 1997'), + Time.parse('Fri Sep 3 22:00:00 PDT 1997'), + ]) + end + + it 'returns the correct result with an rrule of FREQ=HOURLY;BYMINUTE=0,30;COUNT=10' do + rrule = 'FREQ=HOURLY;BYMINUTE=0,30;COUNT=10' + dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997') + timezone = 'America/New_York' + + rrule = RRule::Rule.new(rrule, dtstart: dtstart, tzid: timezone) + + expect(rrule.all).to match_array([ + Time.parse('Tue Sep 2 06:00:00 PDT 1997'), + Time.parse('Tue Sep 2 06:30:00 PDT 1997'), + Time.parse('Tue Sep 2 07:00:00 PDT 1997'), + Time.parse('Tue Sep 2 07:30:00 PDT 1997'), + Time.parse('Tue Sep 2 08:00:00 PDT 1997'), + Time.parse('Tue Sep 2 08:30:00 PDT 1997'), + Time.parse('Tue Sep 2 09:00:00 PDT 1997'), + Time.parse('Tue Sep 2 09:30:00 PDT 1997'), + Time.parse('Tue Sep 2 10:00:00 PDT 1997'), + Time.parse('Tue Sep 2 10:30:00 PDT 1997'), + ]) + end + it 'returns the correct result with an rrule of FREQ=DAILY;COUNT=10' do rrule = 'FREQ=DAILY;COUNT=10' dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997')