rrule 0.1.1 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 38233f8772b83457ca170a1e6eaa20d8f8455c02
4
- data.tar.gz: d0a65079756ddb7faf5f4615145ce7b99814c8b7
3
+ metadata.gz: 2f4e67330d4bebaf8408d3125386565754c269ef
4
+ data.tar.gz: e1bfbeadff4cd2cf622360d38357d954bd9f5984
5
5
  SHA512:
6
- metadata.gz: 57f3d09db8034288534d465af4af7011e3fa39418d36372fa44e834464c970eaaa29dacd7f649a98a3c39909ad5112516ed873f3dfacd8966b27eb8270442979
7
- data.tar.gz: a79d86f15deb399905eeebb6a9d5274839614bc772580361f5cf4f43ccb11e1d1584f19fb86ad6e58d16cbae3c5f43c89ef57adba6126626a4701d177e28f15d
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
- attr_reader :rrule, :dtstart, :tz, :exdate
3
+ include Enumerable
4
+
5
+ attr_reader :dtstart, :tz, :exdate
4
6
 
5
7
  def initialize(rrule, dtstart: Time.now, tzid: 'UTC', exdate: [])
6
- @rrule = rrule
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
- reject_exdates(all_until(nil))
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
- private
29
-
30
- def reject_exdates(results)
31
- results.reject { |date| exdate.include?(date) }
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 all_until(end_date)
35
- result = []
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 result if frequency.current_date.year > MAX_YEAR
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.sort.each do |this_result|
94
- if end_date
95
- if this_result > end_date
96
- return result
97
- end
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 options
122
- @options ||= parse_options
93
+ def next
94
+ enumerator.next
123
95
  end
124
96
 
125
- def parse_options
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 = @rrule.split(';')
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
- options[:count] = value.to_i
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
- options[:interval] = value.to_i
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
@@ -25,4 +25,7 @@ module RRule
25
25
  end
26
26
 
27
27
  MAX_YEAR = 9999
28
+ MAX_DATE = DateTime.new(MAX_YEAR)
29
+
30
+ class InvalidRRule < StandardError; end
28
31
  end
data/rrule.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'rrule'
3
- s.version = '0.1.1'
3
+ s.version = '0.2.1'
4
4
  s.date = '2016-06-06'
5
5
  s.summary = 'RRule expansion'
6
6
  s.description = 'A gem for expanding dates according to the RRule specification'
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(Time.parse('Tue May 24 14:34:59 PDT 2016'), Time.parse('Sun Jul 24 14:35:09 PDT 2016'))).to match_array([
1962
- Time.parse('Thu Jun 16 17:00:00 PDT 2016'),
1963
- Time.parse('Thu Jul 14 17:00:00 PDT 2016')
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rrule
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Mitchell