Skip to content

Commit 49bc0da

Browse files
author
Ryan Mitchell
authored
Merge pull request #66 from userlist/granular-freq
Add support for secondly, minutely, and hourly frequencies
2 parents d4b0723 + f300918 commit 49bc0da

10 files changed

Lines changed: 382 additions & 1 deletion

File tree

lib/rrule.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ module RRule
1313
autoload :Humanizer, 'rrule/humanizer'
1414

1515
autoload :Frequency, 'rrule/frequencies/frequency'
16+
autoload :Secondly, 'rrule/frequencies/secondly'
17+
autoload :Minutely, 'rrule/frequencies/minutely'
18+
autoload :Hourly, 'rrule/frequencies/hourly'
1619
autoload :Daily, 'rrule/frequencies/daily'
1720
autoload :Weekly, 'rrule/frequencies/weekly'
1821
autoload :SimpleWeekly, 'rrule/frequencies/simple_weekly'

lib/rrule/frequencies/frequency.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ def next_occurrences
3434

3535
def self.for_options(options)
3636
case options[:freq]
37+
when 'SECONDLY'
38+
Secondly
39+
when 'MINUTELY'
40+
Minutely
41+
when 'HOURLY'
42+
Hourly
3743
when 'DAILY'
3844
Daily
3945
when 'WEEKLY'

lib/rrule/frequencies/hourly.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module RRule
4+
class Hourly < Frequency
5+
def possible_days
6+
[current_date.yday - 1] # convert to 0-indexed
7+
end
8+
9+
def timeset
10+
super.map { |time| time.merge(hour: current_date.hour) }
11+
end
12+
13+
private
14+
15+
def advance_by
16+
{ hours: context.options[:interval] }
17+
end
18+
end
19+
end

lib/rrule/frequencies/minutely.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module RRule
4+
class Minutely < Frequency
5+
def possible_days
6+
[current_date.yday - 1] # convert to 0-indexed
7+
end
8+
9+
def timeset
10+
super.map { |time| time.merge(hour: current_date.hour, minute: current_date.min) }
11+
end
12+
13+
private
14+
15+
def advance_by
16+
{ minutes: context.options[:interval] }
17+
end
18+
end
19+
end

lib/rrule/frequencies/secondly.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module RRule
4+
class Secondly < Frequency
5+
def possible_days
6+
[current_date.yday - 1] # convert to 0-indexed
7+
end
8+
9+
def timeset
10+
super.map { |time| time.merge(hour: current_date.hour, minute: current_date.min, second: current_date.sec) }
11+
end
12+
13+
private
14+
15+
def advance_by
16+
{ seconds: context.options[:interval] }
17+
end
18+
end
19+
end

lib/rrule/rule.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def parse_options(rule)
187187
# when the associated "DTSTART" property has a DATE value type.
188188
# These rule parts MUST be ignored in RECUR value that violate the
189189
# above requirement
190-
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)
190+
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)
191191

192192
options
193193
end

spec/frequencies/hourly_spec.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe RRule::Hourly do
6+
let(:interval) { 1 }
7+
let(:context) do
8+
RRule::Context.new(
9+
{ interval: interval },
10+
date,
11+
'America/Los_Angeles'
12+
)
13+
end
14+
let(:filters) { [] }
15+
let(:generator) { RRule::AllOccurrences.new(context) }
16+
let(:timeset) { [{ hour: date.hour, minute: date.min, second: date.sec }] }
17+
18+
before { context.rebuild(date.year, date.month) }
19+
20+
describe '#next_occurrences' do
21+
subject(:frequency) { described_class.new(context, filters, generator, timeset) }
22+
23+
context 'with an interval of one' do
24+
let(:date) { Time.zone.local(1997, 1, 1, 0, 0) }
25+
26+
it 'returns sequential hours' do
27+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0)]
28+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 1, 0)]
29+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 2, 0)]
30+
end
31+
end
32+
33+
context 'with an interval of two' do
34+
let(:interval) { 2 }
35+
let(:date) { Time.zone.local(1997, 1, 1, 0, 0) }
36+
37+
it 'returns every other hour' do
38+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0)]
39+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 2, 0)]
40+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 4, 0)]
41+
end
42+
end
43+
44+
context 'at the end of the day' do
45+
let(:date) { Time.zone.local(1997, 1, 1, 23, 0) }
46+
47+
it 'goes into the next day' do
48+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 23, 0)]
49+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 2, 0, 0)]
50+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 2, 1, 0)]
51+
end
52+
end
53+
end
54+
end

spec/frequencies/minutely_spec.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe RRule::Minutely do
6+
let(:interval) { 1 }
7+
let(:context) do
8+
RRule::Context.new(
9+
{ interval: interval },
10+
date,
11+
'America/Los_Angeles'
12+
)
13+
end
14+
let(:filters) { [] }
15+
let(:generator) { RRule::AllOccurrences.new(context) }
16+
let(:timeset) { [{ hour: date.hour, minute: date.min, second: date.sec }] }
17+
18+
before { context.rebuild(date.year, date.month) }
19+
20+
describe '#next_occurrences' do
21+
subject(:frequency) { described_class.new(context, filters, generator, timeset) }
22+
23+
context 'with an interval of one' do
24+
let(:date) { Time.zone.local(1997, 1, 1, 0, 0) }
25+
26+
it 'returns sequential minutes' do
27+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0)]
28+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 1)]
29+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 2)]
30+
end
31+
end
32+
33+
context 'with an interval of two' do
34+
let(:interval) { 2 }
35+
let(:date) { Time.zone.local(1997, 1, 1, 0, 0) }
36+
37+
it 'returns every other minute' do
38+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0)]
39+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 2)]
40+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 4)]
41+
end
42+
end
43+
44+
context 'at the end of the hour' do
45+
let(:date) { Time.zone.local(1997, 1, 1, 0, 59) }
46+
47+
it 'goes into the next hour' do
48+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 59)]
49+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 1, 0)]
50+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 1, 1)]
51+
end
52+
end
53+
end
54+
end

spec/frequencies/secondly_spec.rb

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
describe RRule::Secondly do
6+
let(:interval) { 1 }
7+
let(:context) do
8+
RRule::Context.new(
9+
{ interval: interval },
10+
date,
11+
'America/Los_Angeles'
12+
)
13+
end
14+
let(:filters) { [] }
15+
let(:generator) { RRule::AllOccurrences.new(context) }
16+
let(:timeset) { [{ hour: date.hour, minute: date.min, second: date.sec }] }
17+
18+
before { context.rebuild(date.year, date.month) }
19+
20+
describe '#next_occurrences' do
21+
subject(:frequency) { described_class.new(context, filters, generator, timeset) }
22+
23+
context 'with an interval of one' do
24+
let(:date) { Time.zone.local(1997, 1, 1, 0, 0, 0) }
25+
26+
it 'returns sequential seconds' do
27+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0, 0)]
28+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0, 1)]
29+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0, 2)]
30+
end
31+
end
32+
33+
context 'with an interval of two' do
34+
let(:interval) { 2 }
35+
let(:date) { Time.zone.local(1997, 1, 1, 0, 0, 0) }
36+
37+
it 'returns every other second' do
38+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0, 0)]
39+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0, 2)]
40+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0, 4)]
41+
end
42+
end
43+
44+
context 'at the end of the minute' do
45+
let(:date) { Time.zone.local(1997, 1, 1, 0, 0, 59) }
46+
47+
it 'goes into the next minute' do
48+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 0, 59)]
49+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 1, 0)]
50+
expect(frequency.next_occurrences).to eql [Time.zone.local(1997, 1, 1, 0, 1, 1)]
51+
end
52+
end
53+
end
54+
end

0 commit comments

Comments
 (0)