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 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