rrule 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +8 -0
- data/lib/rrule/rule.rb +53 -53
- data/lib/rrule.rb +3 -0
- data/rrule.gemspec +1 -1
- data/spec/rule_spec.rb +94 -5
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f4e67330d4bebaf8408d3125386565754c269ef
|
4
|
+
data.tar.gz: e1bfbeadff4cd2cf622360d38357d954bd9f5984
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 55b57448971a919141f491bc141d017eb2bd98421ed0f7cf6bf79d51859fb5cf5e6647d1c28b9738e4465258108fa436be006466b25428823a49b6220808216f
|
7
|
+
data.tar.gz: 72792b23c6a0e483a37d16501d1a79a2efc2303e1e0113bf03dc83bcd6775933f0a946bbf7c3f52be211d17a5e87560977874f57b90223af567a2f2a302a8bc0
|
data/README.md
CHANGED
@@ -30,6 +30,14 @@ rrule.between(Time.new(2016, 6, 23), Time.new(2016, 6, 24))
|
|
30
30
|
=> [2016-06-23 16:45:32 -0700]
|
31
31
|
```
|
32
32
|
|
33
|
+
You can limit the number of instances that are returned with the `limit` option:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
rrule = RRule::Rule.new('FREQ=DAILY;COUNT=3')
|
37
|
+
rrule.all(limit: 2)
|
38
|
+
=> [2016-06-23 16:45:32 -0700, 2016-06-24 16:45:32 -0700]
|
39
|
+
```
|
40
|
+
|
33
41
|
By default the DTSTART of the recurrence is the current time, but this can be overriden with the `dtstart` option:
|
34
42
|
|
35
43
|
```ruby
|
data/lib/rrule/rule.rb
CHANGED
@@ -1,44 +1,33 @@
|
|
1
1
|
module RRule
|
2
2
|
class Rule
|
3
|
-
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :dtstart, :tz, :exdate
|
4
6
|
|
5
7
|
def initialize(rrule, dtstart: Time.now, tzid: 'UTC', exdate: [])
|
6
|
-
@
|
7
|
-
# This removes all sub-second and floors it to the second level.
|
8
|
-
# Sub-second level calculations breaks a lot of assumptions in this
|
9
|
-
# library and rounding it may also cause unexpected inequalities.
|
10
|
-
@dtstart = Time.at(dtstart.to_i).in_time_zone(tzid)
|
8
|
+
@dtstart = floor_to_seconds(dtstart).in_time_zone(tzid)
|
11
9
|
@tz = tzid
|
12
10
|
@exdate = exdate
|
11
|
+
@options = parse_options(rrule)
|
13
12
|
end
|
14
13
|
|
15
|
-
def all
|
16
|
-
|
17
|
-
end
|
18
|
-
|
19
|
-
def between(start_date, end_date)
|
20
|
-
# This removes all sub-second and floors it to the second level.
|
21
|
-
# Sub-second level calculations breaks a lot of assumptions in this
|
22
|
-
# library and rounding it may also cause unexpected inequalities.
|
23
|
-
floored_start_date = Time.at(start_date.to_i)
|
24
|
-
floored_end_date = Time.at(end_date.to_i)
|
25
|
-
reject_exdates(all_until(floored_end_date).reject { |instance| instance < floored_start_date })
|
14
|
+
def all(limit: nil)
|
15
|
+
all_until(limit: limit)
|
26
16
|
end
|
27
17
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
18
|
+
def between(start_date, end_date, limit: nil)
|
19
|
+
floored_start_date = floor_to_seconds(start_date)
|
20
|
+
floored_end_date = floor_to_seconds(end_date)
|
21
|
+
all_until(end_date: floored_end_date, limit: limit).reject { |instance| instance < floored_start_date }
|
32
22
|
end
|
33
23
|
|
34
|
-
def
|
35
|
-
|
24
|
+
def each
|
25
|
+
return enum_for(:each) unless block_given?
|
36
26
|
|
37
27
|
context = Context.new(options, dtstart, tz)
|
38
28
|
context.rebuild(dtstart.year, dtstart.month)
|
39
29
|
|
40
30
|
timeset = options[:timeset]
|
41
|
-
total = 0
|
42
31
|
count = options[:count]
|
43
32
|
|
44
33
|
filters = []
|
@@ -81,7 +70,7 @@ module RRule
|
|
81
70
|
end
|
82
71
|
|
83
72
|
loop do
|
84
|
-
return
|
73
|
+
return if frequency.current_date.year > MAX_YEAR
|
85
74
|
|
86
75
|
possible_days_of_year = frequency.possible_days
|
87
76
|
|
@@ -90,42 +79,45 @@ module RRule
|
|
90
79
|
end
|
91
80
|
|
92
81
|
results_with_time = generator.combine_dates_and_times(possible_days_of_year, timeset)
|
93
|
-
results_with_time.
|
94
|
-
if
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
end
|
99
|
-
|
100
|
-
if options[:until]
|
101
|
-
if this_result > options[:until]
|
102
|
-
return result
|
103
|
-
end
|
104
|
-
result.push(this_result)
|
105
|
-
elsif this_result >= dtstart
|
106
|
-
total += 1
|
107
|
-
if options[:count]
|
108
|
-
count -= 1
|
109
|
-
result.push(this_result)
|
110
|
-
return result if count == 0
|
111
|
-
else
|
112
|
-
result.push(this_result)
|
113
|
-
end
|
114
|
-
end
|
82
|
+
results_with_time.each do |this_result|
|
83
|
+
next if this_result < dtstart
|
84
|
+
return if options[:until] && this_result > options[:until]
|
85
|
+
return if count && (count -= 1) < 0
|
86
|
+
yield this_result unless exdate.include?(this_result)
|
115
87
|
end
|
116
88
|
|
117
89
|
frequency.advance
|
118
90
|
end
|
119
91
|
end
|
120
92
|
|
121
|
-
def
|
122
|
-
|
93
|
+
def next
|
94
|
+
enumerator.next
|
123
95
|
end
|
124
96
|
|
125
|
-
|
97
|
+
private
|
98
|
+
|
99
|
+
attr_reader :options
|
100
|
+
|
101
|
+
def floor_to_seconds(date)
|
102
|
+
return date
|
103
|
+
# This removes all sub-second and floors it to the second level.
|
104
|
+
# Sub-second level calculations breaks a lot of assumptions in this
|
105
|
+
# library and rounding it may also cause unexpected inequalities.
|
106
|
+
Time.at(date.to_i)
|
107
|
+
end
|
108
|
+
|
109
|
+
def enumerator
|
110
|
+
@enumerator ||= to_enum
|
111
|
+
end
|
112
|
+
|
113
|
+
def all_until(end_date: MAX_DATE, limit: nil)
|
114
|
+
limit ? take(limit) : take_while { |date| date <= end_date }
|
115
|
+
end
|
116
|
+
|
117
|
+
def parse_options(rule)
|
126
118
|
options = { interval: 1, wkst: 1 }
|
127
119
|
|
128
|
-
params =
|
120
|
+
params = rule.split(';')
|
129
121
|
params.each do |param|
|
130
122
|
option, value = param.split('=')
|
131
123
|
|
@@ -133,11 +125,19 @@ module RRule
|
|
133
125
|
when 'FREQ'
|
134
126
|
options[:freq] = value
|
135
127
|
when 'COUNT'
|
136
|
-
|
128
|
+
i = begin
|
129
|
+
Integer(value)
|
130
|
+
rescue ArgumentError
|
131
|
+
raise InvalidRRule, "COUNT must be a non-negative integer"
|
132
|
+
end
|
133
|
+
raise InvalidRRule, "COUNT must be a non-negative integer" if i < 0
|
134
|
+
options[:count] = i
|
137
135
|
when 'UNTIL'
|
138
136
|
options[:until] = Time.parse(value)
|
139
137
|
when 'INTERVAL'
|
140
|
-
|
138
|
+
i = Integer(value) rescue 0
|
139
|
+
raise InvalidRRule, "INTERVAL must be a positive integer" unless i > 0
|
140
|
+
options[:interval] = i
|
141
141
|
when 'BYDAY'
|
142
142
|
options[:byweekday] = value.split(',').map { |day| Weekday.parse(day) }
|
143
143
|
when 'BYSETPOS'
|
data/lib/rrule.rb
CHANGED
data/rrule.gemspec
CHANGED
data/spec/rule_spec.rb
CHANGED
@@ -1,6 +1,35 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe RRule::Rule do
|
4
|
+
describe '#next' do
|
5
|
+
it 'can sequentially return values' do
|
6
|
+
rrule = 'FREQ=DAILY;COUNT=10'
|
7
|
+
dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997')
|
8
|
+
timezone = 'America/New_York'
|
9
|
+
|
10
|
+
rrule = RRule::Rule.new(rrule, dtstart: dtstart, tzid: timezone)
|
11
|
+
|
12
|
+
expect(rrule.next).to eql Time.parse('Tue Sep 2 06:00:00 PDT 1997')
|
13
|
+
expect(rrule.next).to eql Time.parse('Wed Sep 3 06:00:00 PDT 1997')
|
14
|
+
expect(rrule.next).to eql Time.parse('Thu Sep 4 06:00:00 PDT 1997')
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#take' do
|
19
|
+
it 'can return the next N instances' do
|
20
|
+
rrule = 'FREQ=DAILY;COUNT=10'
|
21
|
+
dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997')
|
22
|
+
timezone = 'America/New_York'
|
23
|
+
|
24
|
+
rrule = RRule::Rule.new(rrule, dtstart: dtstart, tzid: timezone)
|
25
|
+
expect(rrule.take(3)).to match_array([
|
26
|
+
Time.parse('Tue Sep 2 06:00:00 PDT 1997'),
|
27
|
+
Time.parse('Wed Sep 3 06:00:00 PDT 1997'),
|
28
|
+
Time.parse('Thu Sep 4 06:00:00 PDT 1997')
|
29
|
+
])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
4
33
|
describe '#all' do
|
5
34
|
it 'returns the correct result with an rrule of FREQ=DAILY;COUNT=10' do
|
6
35
|
rrule = 'FREQ=DAILY;COUNT=10'
|
@@ -22,6 +51,21 @@ describe RRule::Rule do
|
|
22
51
|
])
|
23
52
|
end
|
24
53
|
|
54
|
+
it 'returns the correct result with an rrule of FREQ=DAILY;COUNT=10 and a limit' do
|
55
|
+
rrule = 'FREQ=DAILY;COUNT=10'
|
56
|
+
dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997')
|
57
|
+
timezone = 'America/New_York'
|
58
|
+
|
59
|
+
rrule = RRule::Rule.new(rrule, dtstart: dtstart, tzid: timezone)
|
60
|
+
expect(rrule.all(limit: 5)).to match_array([
|
61
|
+
Time.parse('Tue Sep 2 06:00:00 PDT 1997'),
|
62
|
+
Time.parse('Wed Sep 3 06:00:00 PDT 1997'),
|
63
|
+
Time.parse('Thu Sep 4 06:00:00 PDT 1997'),
|
64
|
+
Time.parse('Fri Sep 5 06:00:00 PDT 1997'),
|
65
|
+
Time.parse('Sat Sep 6 06:00:00 PDT 1997')
|
66
|
+
])
|
67
|
+
end
|
68
|
+
|
25
69
|
it 'returns the correct result with an rrule of FREQ=DAILY;UNTIL=19971224T000000Z' do
|
26
70
|
rrule = 'FREQ=DAILY;UNTIL=19971224T000000Z'
|
27
71
|
dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997')
|
@@ -1668,8 +1712,8 @@ describe RRule::Rule do
|
|
1668
1712
|
Time.parse('Mon Aug 29 19:00:00 PDT 2016')
|
1669
1713
|
])
|
1670
1714
|
end
|
1671
|
-
|
1672
1715
|
end
|
1716
|
+
|
1673
1717
|
describe '#between' do
|
1674
1718
|
it 'returns the correct result with an rrule of FREQ=DAILY;INTERVAL=2' do
|
1675
1719
|
rrule = 'FREQ=DAILY;INTERVAL=2'
|
@@ -1707,6 +1751,21 @@ describe RRule::Rule do
|
|
1707
1751
|
])
|
1708
1752
|
end
|
1709
1753
|
|
1754
|
+
it 'returns the correct result with an rrule of FREQ=DAILY;INTERVAL=2 and a limit' do
|
1755
|
+
rrule = 'FREQ=DAILY;INTERVAL=2'
|
1756
|
+
dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997')
|
1757
|
+
timezone = 'America/New_York'
|
1758
|
+
|
1759
|
+
rrule = RRule::Rule.new(rrule, dtstart: dtstart, tzid: timezone)
|
1760
|
+
expect(rrule.between(Time.parse('Tue Sep 2 06:00:00 PDT 1997'), Time.parse('Wed Oct 22 06:00:00 PDT 1997'), limit: 5)).to match_array([
|
1761
|
+
Time.parse('Tue Sep 2 06:00:00 PDT 1997'),
|
1762
|
+
Time.parse('Thu Sep 4 06:00:00 PDT 1997'),
|
1763
|
+
Time.parse('Sat Sep 6 06:00:00 PDT 1997'),
|
1764
|
+
Time.parse('Mon Sep 8 06:00:00 PDT 1997'),
|
1765
|
+
Time.parse('Wed Sep 10 06:00:00 PDT 1997')
|
1766
|
+
])
|
1767
|
+
end
|
1768
|
+
|
1710
1769
|
it 'returns the correct result with an rrule of FREQ=WEEKLY;INTERVAL=2;WKST=SU' do
|
1711
1770
|
rrule = 'FREQ=WEEKLY;INTERVAL=2;WKST=SU'
|
1712
1771
|
dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997')
|
@@ -1958,10 +2017,13 @@ describe RRule::Rule do
|
|
1958
2017
|
dtstart = Time.parse('Thu Jan 28 17:00:00 PST 2016').in_time_zone(timezone)
|
1959
2018
|
|
1960
2019
|
rrule = RRule.parse(rrule, dtstart: dtstart, tzid: timezone)
|
1961
|
-
expect(rrule.between(
|
1962
|
-
|
1963
|
-
|
1964
|
-
|
2020
|
+
expect(rrule.between(
|
2021
|
+
Time.parse('Tue May 24 14:34:59 PDT 2016'),
|
2022
|
+
Time.parse('Sun Jul 24 14:35:09 PDT 2016')
|
2023
|
+
)).to match_array([
|
2024
|
+
Time.parse('Thu Jun 16 17:00:00 PDT 2016'),
|
2025
|
+
Time.parse('Thu Jul 14 17:00:00 PDT 2016')
|
2026
|
+
])
|
1965
2027
|
end
|
1966
2028
|
|
1967
2029
|
it 'returns the correct result with a date start right on the year border' do
|
@@ -1985,4 +2047,31 @@ describe RRule::Rule do
|
|
1985
2047
|
expect(rule.between(start_time, end_time)).to eql([expected_instance])
|
1986
2048
|
end
|
1987
2049
|
end
|
2050
|
+
|
2051
|
+
describe 'validation' do
|
2052
|
+
it 'raises RRule::InvalidRRule if INTERVAL is not a positive integer' do
|
2053
|
+
dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997')
|
2054
|
+
timezone = 'America/New_York'
|
2055
|
+
|
2056
|
+
expect { RRule::Rule.new('FREQ=DAILY;INTERVAL=0', dtstart: dtstart, tzid: timezone) }.to raise_error(RRule::InvalidRRule)
|
2057
|
+
expect { RRule::Rule.new('FREQ=DAILY;INTERVAL=-1', dtstart: dtstart, tzid: timezone) }.to raise_error(RRule::InvalidRRule)
|
2058
|
+
expect { RRule::Rule.new('FREQ=DAILY;INTERVAL=1.1', dtstart: dtstart, tzid: timezone) }.to raise_error(RRule::InvalidRRule)
|
2059
|
+
expect { RRule::Rule.new('FREQ=DAILY;INTERVAL=BOOM', dtstart: dtstart, tzid: timezone) }.to raise_error(RRule::InvalidRRule)
|
2060
|
+
end
|
2061
|
+
|
2062
|
+
it 'raises RRule::InvalidRRule if COUNT is not an integer' do
|
2063
|
+
dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997')
|
2064
|
+
timezone = 'America/New_York'
|
2065
|
+
|
2066
|
+
expect { RRule::Rule.new('FREQ=DAILY;COUNT=BOOM', dtstart: dtstart, tzid: timezone) }.to raise_error(RRule::InvalidRRule)
|
2067
|
+
expect { RRule::Rule.new('FREQ=DAILY;COUNT=1.5', dtstart: dtstart, tzid: timezone) }.to raise_error(RRule::InvalidRRule)
|
2068
|
+
end
|
2069
|
+
|
2070
|
+
it 'raises RRule::InvalidRRule if COUNT is negative' do
|
2071
|
+
dtstart = Time.parse('Tue Sep 2 06:00:00 PDT 1997')
|
2072
|
+
timezone = 'America/New_York'
|
2073
|
+
|
2074
|
+
expect { RRule::Rule.new('FREQ=DAILY;COUNT=-1', dtstart: dtstart, tzid: timezone) }.to raise_error(RRule::InvalidRRule)
|
2075
|
+
end
|
2076
|
+
end
|
1988
2077
|
end
|