vpim 0.16 → 0.17

Sign up to get free protection for your applications and to get access to all the features.
data/lib/vpim/rfc2425.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  =begin
2
- $Id: rfc2425.rb,v 1.10 2005/01/01 17:17:01 sam Exp $
3
-
4
- Copyright (C) 2005 Sam Roberts
2
+ Copyright (C) 2006 Sam Roberts
5
3
 
6
4
  This library is free software; you can redistribute it and/or modify it
7
5
  under the same terms as the ruby language itself, see the file COPYING for
@@ -62,19 +60,18 @@ module Vpim
62
60
  # Split on \r\n or \n to get the lines, unfold continued lines (they
63
61
  # start with ' ' or \t), and return the array of unfolded lines.
64
62
  #
65
- # This also implements the (invalid) encoding convention of allowing empty
66
- # lines to be inserted for readability - it does this by dropping
67
- # zero-length lines.
63
+ # This also supports the (invalid) encoding convention of allowing empty
64
+ # lines to be inserted for readability - it does this by dropping zero-length
65
+ # lines.
68
66
  def Vpim.unfold(card) #:nodoc:
69
67
  unfolded = []
70
68
 
71
- card.split(/\r?\n/).each do
72
- |line|
73
-
69
+ card.each do |line|
70
+ line.chomp!
74
71
  # If it's a continuation line, add it to the last.
75
72
  # If it's an empty line, drop it from the input.
76
73
  if( line =~ /^[ \t]/ )
77
- unfolded << unfolded.pop + line[1, line.size-1]
74
+ unfolded[-1] << line[1, line.size-1]
78
75
  elsif( line =~ /^$/ )
79
76
  else
80
77
  unfolded << line
@@ -88,10 +85,10 @@ module Vpim
88
85
  def Vpim.decode_list(value, sep = ',') # :nodoc:
89
86
  list = []
90
87
 
91
- value.each(sep) {
92
- |item|
93
- list << yield(item) unless item =~ %r{^\s*#{sep}?$}
94
- }
88
+ value.each(sep) do |item|
89
+ item.chomp!(sep)
90
+ list << yield(item)
91
+ end
95
92
  list
96
93
  end
97
94
 
@@ -160,17 +157,32 @@ module Vpim
160
157
 
161
158
  # Convert a RFC 2425 date-list into an array of dates.
162
159
  def Vpim.decode_date_list(v) # :nodoc:
163
- dates = Vpim.decode_list(v) { |date| Vpim.decode_date(date) }
160
+ Vpim.decode_list(v) do |date|
161
+ date.strip!
162
+ if date.length > 0
163
+ Vpim.decode_date(date)
164
+ end
165
+ end.compact
164
166
  end
165
167
 
166
168
  # Convert a RFC 2425 time-list into an array of times.
167
169
  def Vpim.decode_time_list(v) # :nodoc:
168
- times = Vpim.decode_list(v) { |time| Vpim.decode_time(time) }
170
+ Vpim.decode_list(v) do |time|
171
+ time.strip!
172
+ if time.length > 0
173
+ Vpim.decode_time(time)
174
+ end
175
+ end.compact
169
176
  end
170
177
 
171
178
  # Convert a RFC 2425 date-time-list into an array of date-times.
172
179
  def Vpim.decode_date_time_list(v) # :nodoc:
173
- datetimes = Vpim.decode_list(v) { |datetime| Vpim.decode_date_time(datetime) }
180
+ Vpim.decode_list(v) do |datetime|
181
+ datetime.strip!
182
+ if datetime.length > 0
183
+ Vpim.decode_date_time(datetime)
184
+ end
185
+ end.compact
174
186
  end
175
187
 
176
188
  # Convert RFC 2425 text into a String.
@@ -179,6 +191,7 @@ module Vpim
179
191
  # \N -> NL
180
192
  # \, -> ,
181
193
  def Vpim.decode_text(v) # :nodoc:
194
+ # FIXME - this will fail for "\\,"!
182
195
  v.gsub(/\\[nN]/, "\n").gsub(/\\,/, ",").gsub(/\\\\/) { |m| "\\" }
183
196
  end
184
197
 
@@ -0,0 +1,246 @@
1
+ =begin
2
+ $Id: rfc2425.rb,v 1.10 2005/01/01 17:17:01 sam Exp $
3
+
4
+ Copyright (C) 2005 Sam Roberts
5
+
6
+ This library is free software; you can redistribute it and/or modify it
7
+ under the same terms as the ruby language itself, see the file COPYING for
8
+ details.
9
+ =end
10
+
11
+ require 'vpim/vpim'
12
+
13
+ module Vpim
14
+ # Contains regular expression strings for the EBNF of RFC 2425.
15
+ module Bnf #:nodoc:
16
+
17
+ # 1*(ALPHA / DIGIT / "-")
18
+ # Note: I think I can add A-Z here, and get rid of the "i" matches elsewhere.
19
+ # Note: added '_' to allowed because its produced by Notes - X-LOTUS-CHILD_UID
20
+ NAME = '[-a-z0-9_]+'
21
+
22
+ # <"> <Any character except CTLs, DQUOTE> <">
23
+ QSTR = '"([^"]*)"'
24
+
25
+ # *<Any character except CTLs, DQUOTE, ";", ":", ",">
26
+ PTEXT = '([^";:,]+)'
27
+
28
+ # param-value = ptext / quoted-string
29
+ PVALUE = "(?:#{QSTR}|#{PTEXT})"
30
+
31
+ # param = name "=" param-value *("," param-value)
32
+ # Note: v2.1 allows a type or encoding param-value to appear without the type=
33
+ # or the encoding=. This is hideous, but we try and support it, if there
34
+ # is no "=", then $2 will be "", and we will treat it as a v2.1 param.
35
+ PARAM = ";(#{NAME})(=?)((?:#{PVALUE})?(?:,#{PVALUE})*)"
36
+
37
+ # V3.0: contentline = [group "."] name *(";" param) ":" value
38
+ # V2.1: contentline = *( group "." ) name *(";" param) ":" value
39
+ #
40
+ # We accept the V2.1 syntax for backwards compatibility.
41
+ #LINE = "((?:#{NAME}\\.)*)?(#{NAME})([^:]*)\:(.*)"
42
+ LINE = "^((?:#{NAME}\\.)*)?(#{NAME})((?:#{PARAM})*):(.*)$"
43
+
44
+ # date = date-fullyear ["-"] date-month ["-"] date-mday
45
+ # date-fullyear = 4 DIGIT
46
+ # date-month = 2 DIGIT
47
+ # date-mday = 2 DIGIT
48
+ DATE = '(\d\d\d\d)-?(\d\d)-?(\d\d)'
49
+
50
+ # time = time-hour [":"] time-minute [":"] time-second [time-secfrac] [time-zone]
51
+ # time-hour = 2 DIGIT
52
+ # time-minute = 2 DIGIT
53
+ # time-second = 2 DIGIT
54
+ # time-secfrac = "," 1*DIGIT
55
+ # time-zone = "Z" / time-numzone
56
+ # time-numzome = sign time-hour [":"] time-minute
57
+ TIME = '(\d\d):?(\d\d):?(\d\d)(\.\d+)?(Z|[-+]\d\d:?\d\d)?'
58
+ end
59
+ end
60
+
61
+ module Vpim
62
+ # Split on \r\n or \n to get the lines, unfold continued lines (they
63
+ # start with ' ' or \t), and return the array of unfolded lines.
64
+ #
65
+ # This also supports the (invalid) encoding convention of allowing empty
66
+ # lines to be inserted for readability - it does this by dropping zero-length
67
+ # lines.
68
+ def Vpim.unfold(card) #:nodoc:
69
+ unfolded = []
70
+
71
+ card.each do |line|
72
+ line.chomp!
73
+ # If it's a continuation line, add it to the last.
74
+ # If it's an empty line, drop it from the input.
75
+ if( line =~ /^[ \t]/ )
76
+ unfolded[-1] << line[1, line.size-1]
77
+ elsif( line =~ /^$/ )
78
+ else
79
+ unfolded << line
80
+ end
81
+ end
82
+
83
+ unfolded
84
+ end
85
+
86
+ # Convert a +sep+-seperated list of values into an array of values.
87
+ def Vpim.decode_list(value, sep = ',') # :nodoc:
88
+ list = []
89
+
90
+ value.each(sep) {
91
+ |item|
92
+ list << yield(item) unless item =~ %r{^\s*#{sep}?$}
93
+ }
94
+ list
95
+ end
96
+
97
+ # Convert a RFC 2425 date into an array of [year, month, day].
98
+ def Vpim.decode_date(v) # :nodoc:
99
+ unless v =~ %r{\s*#{Bnf::DATE}\s*}
100
+ raise Vpim::InvalidEncodingError, "date not valid (#{v})"
101
+ end
102
+ [$1.to_i, $2.to_i, $3.to_i]
103
+ end
104
+
105
+ # Note in the following the RFC2425 allows yyyy-mm-ddThh:mm:ss, but RFC2445
106
+ # does not. I choose to encode to the subset that is valid for both.
107
+
108
+ # Encode a Date object as "yyyymmdd".
109
+ def Vpim.encode_date(d) # :nodoc:
110
+ "%0.4d%0.2d%0.2d" % [ d.year, d.mon, d.day ]
111
+ end
112
+
113
+ # Encode a Date object as "yyyymmdd".
114
+ def Vpim.encode_time(d) # :nodoc:
115
+ "%0.4d%0.2d%0.2d" % [ d.year, d.mon, d.day ]
116
+ end
117
+
118
+ # Encode a Time or DateTime object as "yyyymmddThhmmss"
119
+ def Vpim.encode_date_time(d) # :nodoc:
120
+ "%0.4d%0.2d%0.2dT%0.2d%0.2d%0.2d" % [ d.year, d.mon, d.day, d.hour, d.min, d.sec ]
121
+ end
122
+
123
+ # Convert a RFC 2425 time into an array of [hour,min,sec,secfrac,timezone]
124
+ def Vpim.decode_time(v) # :nodoc:
125
+ unless match = %r{\s*#{Bnf::TIME}\s*}.match(v)
126
+ raise Vpim::InvalidEncodingError, "time not valid (#{v})"
127
+ end
128
+ hour, min, sec, secfrac, tz = match.to_a[1..5]
129
+
130
+ [hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz]
131
+ end
132
+
133
+ # Convert a RFC 2425 date-time into an array of [hour,min,sec,secfrac,timezone]
134
+ def Vpim.decode_date_time(v) # :nodoc:
135
+ unless match = %r{\s*#{Bnf::DATE}T#{Bnf::TIME}\s*}.match(v)
136
+ raise Vpim::InvalidEncodingError, "date-time '#{v}' not valid"
137
+ end
138
+ year, month, day, hour, min, sec, secfrac, tz = match.to_a[1..8]
139
+
140
+ [
141
+ # date
142
+ year.to_i, month.to_i, day.to_i,
143
+ # time
144
+ hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz
145
+ ]
146
+ end
147
+
148
+ # Vpim.decode_boolean
149
+ #
150
+ # float
151
+ #
152
+ # float_list
153
+ #
154
+ # integer
155
+ #
156
+ # integer_list
157
+ #
158
+ # text_list
159
+
160
+ # Convert a RFC 2425 date-list into an array of dates.
161
+ def Vpim.decode_date_list(v) # :nodoc:
162
+ dates = Vpim.decode_list(v) { |date| Vpim.decode_date(date) }
163
+ end
164
+
165
+ # Convert a RFC 2425 time-list into an array of times.
166
+ def Vpim.decode_time_list(v) # :nodoc:
167
+ times = Vpim.decode_list(v) { |time| Vpim.decode_time(time) }
168
+ end
169
+
170
+ # Convert a RFC 2425 date-time-list into an array of date-times.
171
+ def Vpim.decode_date_time_list(v) # :nodoc:
172
+ datetimes = Vpim.decode_list(v) { |datetime| Vpim.decode_date_time(datetime) }
173
+ end
174
+
175
+ # Convert RFC 2425 text into a String.
176
+ # \\ -> \
177
+ # \n -> NL
178
+ # \N -> NL
179
+ # \, -> ,
180
+ def Vpim.decode_text(v) # :nodoc:
181
+ v.gsub(/\\[nN]/, "\n").gsub(/\\,/, ",").gsub(/\\\\/) { |m| "\\" }
182
+ end
183
+
184
+
185
+ # Unfold the lines in +card+, then return an array of one Field object per
186
+ # line.
187
+ def Vpim.decode(card) #:nodoc:
188
+ content = Vpim.unfold(card).collect { |line| DirectoryInfo::Field.decode(line) }
189
+ end
190
+
191
+
192
+ # Expand an array of fields into its syntactic entities. Each entity is a sequence
193
+ # of fields where the sequences is delimited by a BEGIN/END field. Since
194
+ # BEGIN/END delimited entities can be nested, we build a tree. Each entry in
195
+ # the array is either a Field or an array of entries (where each entry is
196
+ # either a Field, or an array of entries...).
197
+ def Vpim.expand(src) #:nodoc:
198
+ # output array to expand the src to
199
+ dst = []
200
+ # stack used to track our nesting level, as we see begin/end we start a
201
+ # new/finish the current entity, and push/pop that entity from the stack
202
+ current = [ dst ]
203
+
204
+ for f in src
205
+ if f.name? 'begin'
206
+ e = [ f ]
207
+
208
+ current.last.push(e)
209
+ current.push(e)
210
+
211
+ elsif f.name? 'end'
212
+ current.last.push(f)
213
+
214
+ unless current.last.first.value? current.last.last.value
215
+ raise "BEGIN/END mismatch (#{current.last.first.value} != #{current.last.last.value})"
216
+ end
217
+
218
+ current.pop
219
+
220
+ else
221
+ current.last.push(f)
222
+ end
223
+ end
224
+
225
+ dst
226
+ end
227
+
228
+ # Split an array into an array of all the fields at the outer level, and
229
+ # an array of all the inner arrays of fields. Return the array [outer,
230
+ # inner].
231
+ def Vpim.outer_inner(fields) #:nodoc:
232
+ # seperate into the outer-level fields, and the arrays of component
233
+ # fields
234
+ outer = []
235
+ inner = []
236
+ fields.each do |line|
237
+ case line
238
+ when Array; inner << line
239
+ else; outer << line
240
+ end
241
+ end
242
+ return outer, inner
243
+ end
244
+
245
+ end
246
+
data/lib/vpim/rrule.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  =begin
2
- $Id: rrule.rb,v 1.18 2005/01/23 22:19:48 sam Exp $
3
-
4
- Copyright (C) 2005 Sam Roberts
2
+ Copyright (C) 2006 Sam Roberts
5
3
 
6
4
  This library is free software; you can redistribute it and/or modify it
7
5
  under the same terms as the ruby language itself, see the file COPYING for
@@ -86,7 +84,7 @@ module Vpim
86
84
 
87
85
  case key
88
86
  when 'FREQ'
89
- @freq = value.upcase
87
+ @freq = value
90
88
 
91
89
  when 'UNTIL'
92
90
  if @count
@@ -0,0 +1,482 @@
1
+ =begin
2
+ $Id: rrule.rb,v 1.18 2005/01/23 22:19:48 sam Exp $
3
+
4
+ Copyright (C) 2005 Sam Roberts
5
+
6
+ This library is free software; you can redistribute it and/or modify it
7
+ under the same terms as the ruby language itself, see the file COPYING for
8
+ details.
9
+ =end
10
+
11
+ require 'vpim/rfc2425'
12
+ require 'vpim/date'
13
+ require 'vpim/time'
14
+ require 'vpim/vpim'
15
+
16
+ =begin
17
+ require 'pp'
18
+
19
+ $debug = ENV['DEBUG']
20
+
21
+ def debug(*objs)
22
+ if $debug
23
+ pp(*objs)
24
+ print ' (', caller(1)[0], ')', "\n"
25
+ end
26
+ end
27
+ =end
28
+
29
+ module Vpim
30
+
31
+ # Implements the iCalendar recurence rule syntax. See etc/rrule.txt for the
32
+ # syntax description and examples from RFC 2445. The description is pretty
33
+ # hard to understand, but the examples are more helpful.
34
+ #
35
+ # The implementation is pretty complete, but still lacks support for:
36
+ #
37
+ # TODO - BYWEEKLY, BYWEEKNO, WKST: rules that recur by the week, or are
38
+ # limited to particular weeks, not hard, but not trivial, I'll do it for the
39
+ # next release
40
+ #
41
+ # TODO - BYHOUR, BYMINUTE, BYSECOND: trivial to do, but I don't have an
42
+ # immediate need for them, I'll do it for the next release
43
+ #
44
+ # TODO - BYSETPOS: limiting to only certain recurrences in a set (what does
45
+ # -1, last occurence, mean for an infinitely occuring rule?)
46
+ #
47
+ # TODO - new API? -> Rrule#infinite?
48
+ #
49
+ # == Examples
50
+ #
51
+ # - link:rrule.txt: utility for printing recurrence rules
52
+ class Rrule
53
+ include Enumerable
54
+
55
+ # The recurrence rule, +rrule+, specifies how to generate a set of times
56
+ # from a start time, +dtstart+ (which must the first of the set of
57
+ # recurring times). If +rrule+ is nil, the set contains only +dtstart+.
58
+ def initialize(dtstart, rrule = nil)
59
+ # dtstart must be in local time, they say, but I think that really
60
+ # means must be in a particular timezone
61
+
62
+ # Note: DTSTART is always in the recurrence set
63
+ @dtstart = dtstart
64
+ @rrule = rrule
65
+
66
+ # Freq is mandatory, but must occur only once.
67
+ @freq = nil
68
+
69
+ # Both Until and Count must not occur, neither is OK.
70
+ @until = nil
71
+ @count = nil
72
+
73
+ # Interval is optional, but defaults to 1.
74
+ @interval = 1
75
+
76
+ # WKST defines what day a week begins on, the default is monday.
77
+ @wkst = 'MO'
78
+
79
+ # Recurrence can modified by these.
80
+ @by = {}
81
+
82
+ if @rrule
83
+ @rrule.scan(/([^;=]+)=([^;=]+)/) do |key,value|
84
+ key.upcase!
85
+ value.upcase!
86
+
87
+ case key
88
+ when 'FREQ'
89
+ @freq = value.upcase
90
+
91
+ when 'UNTIL'
92
+ if @count
93
+ raise "found UNTIL, but COUNT already specified"
94
+ end
95
+ @until = Rrule.time_from_rfc2425(value)
96
+
97
+ when 'COUNT'
98
+ if @until
99
+ raise "found COUNT, but UNTIL already specified"
100
+ end
101
+ @count = value.to_i
102
+
103
+ when 'INTERVAL'
104
+ @interval = value.to_i
105
+ if @interval < 1
106
+ raise "interval must be a positive integer"
107
+ end
108
+
109
+ when 'WKST'
110
+ # TODO - check value is MO-SU
111
+ @wkst = value
112
+
113
+ else
114
+ @by[key] = value
115
+ end
116
+ end
117
+
118
+ if !@freq
119
+ # TODO - this shouldn't be an arg error, but a FormatError, its not the
120
+ # caller's fault!
121
+ raise ArgumentError, "recurrence rule lacks a frequency"
122
+ end
123
+ end
124
+ end
125
+
126
+ # Return an Enumerable, it's #each() will yield over all occurences up to
127
+ # (and not including) time +dountil+.
128
+ def each_until(dountil)
129
+ Vpim::Enumerator.new(self, dountil)
130
+ end
131
+
132
+ # Yields for each +ytime+ in the recurring set of events.
133
+ #
134
+ # Warning: the set may be infinite! If you need an upper bound on the
135
+ # number of occurences, you need to implement a count, or pass a time,
136
+ # +dountil+, which will not be iterated past (i.e. all times yielded will be
137
+ # less than +dountil+).
138
+ #
139
+ # Also, iteration will not currently continue past the limit of a Time
140
+ # object, which is some time in 2037 with the 32-bit time_t's common on
141
+ # most systems.
142
+ def each(dountil = nil) #:yield: ytime
143
+ t = @dtstart.clone
144
+ count = 1
145
+
146
+ # Time.to_a => [ sec, min, hour, day, month, year, wday, yday, isdst, zone ]
147
+
148
+ # Every event occurs at least once, at its start time, but only if the start
149
+ # time is earlier than 'dountil'...
150
+ if !@rrule
151
+ if !dountil || t < dountil
152
+ yield t
153
+ end
154
+ return self
155
+ end
156
+
157
+ loop do
158
+ # Build the set of times to yield within this interval (and after
159
+ # DTSTART)
160
+
161
+ days = DaySet.new(t)
162
+ hour = nil
163
+ min = nil
164
+ sec = nil
165
+
166
+ # Need to make a Dates class, and make month an instance of it, and add
167
+ # the "intersect" operator.
168
+
169
+ case @freq
170
+ #when 'YEARLY' then
171
+ # Don't need to keep track of year, all occurences are within t's
172
+ # year.
173
+ when 'MONTHLY' then days.month = t.month #month = { t.month => nil }
174
+ # when 'WEEKLY' then days.mday = t.month, t.mday
175
+ # TODO - WEEKLY
176
+ when 'DAILY' then days.mday = t.month, t.mday #month = { t.month => [ t.mday ] }
177
+ when 'HOURLY' then hour = [t.hour]
178
+ when 'MINUTELY' then min = [t.min]
179
+ when 'SECONDLY' then sec = [t.sec]
180
+ end
181
+
182
+ # Process the BY* modifiers in RFC defined order:
183
+ # BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY,
184
+ # BYHOUR, BYMINUTE, BYSECOND and BYSETPOS
185
+
186
+ if @by['BYMONTH']
187
+ bymon = @by['BYMONTH'].split(',')
188
+ bymon = bymon.collect { |m| m.to_i }
189
+ # debug bymon
190
+
191
+ # In yearly, at this point, month will always be nil. At other
192
+ # frequencies, it will not.
193
+ days.intersect_bymon(bymon)
194
+
195
+ # debug days
196
+ end
197
+
198
+ # TODO - BYWEEKNO
199
+
200
+ if @by['BYYEARDAY']
201
+ byyday = @by['BYYEARDAY'].scan(/,?([+-]?[1-9]\d*)/)
202
+ # debug byyday
203
+ dates = byyearday(t.year, byyday)
204
+ days.intersect_dates(dates)
205
+ end
206
+
207
+ if @by['BYMONTHDAY']
208
+ bymday = @by['BYMONTHDAY'].scan(/,?([+-]?[1-9]\d*)/)
209
+ # debug bymday
210
+ # Generate all days matching this for all months. For yearly, this
211
+ # is what we want, for anything of monthly or higher frequency, it
212
+ # is too many days, but that's OK, since the month will already
213
+ # be specified and intersection will eliminate the out-of-range
214
+ # dates.
215
+ dates = bymonthday(t.year, bymday)
216
+ # debug dates
217
+ days.intersect_dates(dates)
218
+ # debug days
219
+ end
220
+
221
+ if @by['BYDAY']
222
+ byday = @by['BYDAY'].scan(/,?([+-]?[1-9]?\d*)?(SU|MO|TU|WE|TH|FR|SA)/i)
223
+ # debug byday
224
+
225
+ # BYDAY means different things in different frequencies. The +n+
226
+ # is only meaningful when freq is yearly or monthly.
227
+
228
+ case @freq
229
+ when 'YEARLY'
230
+ dates = byday_in_yearly(t.year, byday)
231
+ when 'MONTHLY'
232
+ dates = byday_in_monthly(t.year, t.month, byday)
233
+ when 'WEEKLY'
234
+ # dates = byday_in_weekly(t.year, wkstart, t.month, t.day, byday)
235
+ when 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY'
236
+ # Reuse the byday_in_monthly. Current day is already specified,
237
+ # so this will just eliminate the current day if its not allowed
238
+ # in BYDAY.
239
+ dates = byday_in_monthly(t.year, t.month, byday)
240
+ end
241
+
242
+ # debug dates
243
+ days.intersect_dates(dates)
244
+ # debug days
245
+ end
246
+
247
+ # TODO - BYHOUR, BYMINUTE, BYSECOND
248
+
249
+ # TODO - BYSETPOS
250
+
251
+ # Yield the time, if we haven't gone over COUNT, or past UNTIL, or past
252
+ # the end of representable time.
253
+
254
+ hour = [@dtstart.hour] if !hour
255
+ min = [@dtstart.min] if !min
256
+ sec = [@dtstart.sec] if !sec
257
+
258
+ # debug days
259
+
260
+ days.each do |m,d|
261
+ hour.each do |h|
262
+ min.each do |n|
263
+ sec.each do |s|
264
+ if(@count && (count > @count))
265
+ return self
266
+ end
267
+ y = Time.local(t.year, m, d, h, n, s, 0)
268
+
269
+ next if y.hour != h
270
+
271
+ # The generated set can sometimes generate results earlier
272
+ # than the DTSTART, skip them.
273
+ next if y < @dtstart
274
+
275
+ # We are done if current time is past @until.
276
+ if @until && (y > @until)
277
+ return self
278
+ end
279
+ # We are also done if current time is past the
280
+ # caller-requested until.
281
+ if dountil && (y >= dountil)
282
+ return self
283
+ end
284
+ yield y
285
+ count += 1
286
+ end
287
+ end
288
+ end
289
+ end
290
+
291
+
292
+ # Add @interval to @freq component
293
+
294
+ # Note - when we got past representable time, the error is:
295
+ # time out of range (ArgumentError)
296
+ # Finish when we see this.
297
+ begin
298
+ case @freq
299
+ when 'YEARLY' then
300
+ t = t.plus_year(@interval)
301
+
302
+ when 'MONTHLY' then
303
+ t = t.plus_month(@interval)
304
+
305
+ when 'WEEKLY' then
306
+ t = t.plus_day(@interval * 7)
307
+
308
+ when 'DAILY' then
309
+ t = t.plus_day(@interval)
310
+
311
+ when 'HOURLY' then
312
+ t += @interval * 60 * 60
313
+
314
+ when 'MINUTELY' then
315
+ t += @interval * 60
316
+
317
+ when 'SECONDLY' then
318
+ t += @interval
319
+
320
+ when nil
321
+ return self
322
+ end
323
+ rescue ArgumentError
324
+ return self if $!.message =~ /^time out of range$/
325
+
326
+ raise ArgumentError, "#{$!.message} while adding interval to #{t.inspect}"
327
+ end
328
+
329
+ return self if dountil && (t > dountil)
330
+ end
331
+ end
332
+
333
+ class DaySet #:nodoc:
334
+
335
+ def initialize(ref)
336
+ @ref = ref # Need to know because leap years have an extra day, and to get
337
+ # our defaults.
338
+ @month = nil
339
+ @week = nil
340
+ end
341
+
342
+ def month=(mon)
343
+ @month = { mon => nil }
344
+ end
345
+
346
+ def week=(week)
347
+ @week = week
348
+ end
349
+
350
+ def mday=(pair)
351
+ @month = { pair[0] => [ pair[1] ] }
352
+ end
353
+
354
+ def intersect_bymon(bymon) #:nodoc:
355
+ if !@month
356
+ @month = {}
357
+ bymon.each do |m|
358
+ @month[m] = nil
359
+ end
360
+ else
361
+ @month.delete_if { |m, days| ! bymon.include? m }
362
+ end
363
+ end
364
+
365
+ def intersect_dates(dates) #:nodoc:
366
+ if dates
367
+ # If no months are in the dayset, add all the ones in dates
368
+ if !@month
369
+ @month = {}
370
+
371
+ dates.each do |d|
372
+ @month[d.mon] = nil
373
+ end
374
+ end
375
+
376
+ # In each month,
377
+ # if there are days,
378
+ # eliminate those not in dates
379
+ # otherwise
380
+ # add all those in dates
381
+ @month.each do |mon, days|
382
+ days_in_mon = dates.find_all { |d| d.mon == mon }
383
+ days_in_mon = days_in_mon.collect { |d| d.day }
384
+
385
+ if days
386
+ days_in_mon = days_in_mon & days
387
+ end
388
+ @month[mon] = days_in_mon
389
+ end
390
+ end
391
+ end
392
+
393
+ def each
394
+ @month = { @ref.month => [ @ref.mday ] } if !@month
395
+ @month.each_key do |m|
396
+ @month[m] = [@ref.day] if !@month[m]
397
+ # FIXME - if @ref.day is 31, and the month doesn't have 32 days, we'll
398
+ # generate invalid dates here, check for that, and eliminate them
399
+ end
400
+
401
+ @month.keys.sort.each do |m|
402
+ @month[m].sort.each do |d|
403
+ yield m, d
404
+ end
405
+ end
406
+ end
407
+ end
408
+
409
+ def self.time_from_rfc2425(str) #:nodoc:
410
+ # With ruby1.8 we can use DateTime to do this quick-n-easy:
411
+ # dt = DateTime.parse(str)
412
+ # Time.local(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec, 0)
413
+
414
+ # The time can be a DATE or a DATE-TIME, the latter always has a 'T' in it.
415
+
416
+ if str =~ /T/
417
+ d = Vpim.decode_date_time(str)
418
+ # We get [ year, month, day, hour, min, sec, usec, tz ]
419
+ if(d.pop == "Z")
420
+ t = Time.gm(*d)
421
+ else
422
+ t = Time.local(*d)
423
+ end
424
+ else
425
+ d = Vpim.decode_date(str)
426
+ # We get [ year, month, day ]
427
+ # FIXME - I have to choose gm or local, though neither makes much
428
+ # sense. This is a bit of a hack - what we should really do is return
429
+ # an instance of Date, and Time should allow itself to be compared to
430
+ # Date... This hack will give odd results when comparing times, because
431
+ # it will create a Time on the right date but whos time is 00:00:00.
432
+ t = Time.local(*d)
433
+ end
434
+ if t.month != d[1] || t.day != d[2] || (d[3] && t.hour != d[3])
435
+ raise Vpim::InvalidEncodingError, "Error - datetime does not exist"
436
+ end
437
+ t
438
+ end
439
+
440
+ def bymonthday(year, bymday) #:nodoc:
441
+ dates = []
442
+
443
+ bymday.each do |mday|
444
+ dates |= DateGen.bymonthday(year, nil, mday[0].to_i)
445
+ end
446
+ dates.sort!
447
+ dates
448
+ end
449
+
450
+ def byyearday(year, byyday) #:nodoc:
451
+ dates = []
452
+
453
+ byyday.each do |yday|
454
+ dates << Date.new2(year, yday[0].to_i)
455
+ end
456
+ dates.sort!
457
+ dates
458
+ end
459
+
460
+ def byday_in_yearly(year, byday) #:nodoc:
461
+ byday_in_monthly(year, nil, byday)
462
+ end
463
+
464
+ def byday_in_monthly(year, mon, byday) #:nodoc:
465
+ dates = []
466
+
467
+ byday.each do |rule|
468
+ if rule[0].empty?
469
+ n = nil
470
+ else
471
+ n = rule[0].to_i
472
+ end
473
+ dates |= DateGen.bywday(year, mon, Date.str2wday(rule[1]), n)
474
+ end
475
+ dates.sort!
476
+ dates
477
+ end
478
+
479
+ end
480
+
481
+ end
482
+