vpim-rails-reinteractive 0.7

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.
Files changed (60) hide show
  1. data/CHANGES +504 -0
  2. data/COPYING +58 -0
  3. data/README +182 -0
  4. data/lib/atom.rb +728 -0
  5. data/lib/plist.rb +22 -0
  6. data/lib/vpim.rb +13 -0
  7. data/lib/vpim/address.rb +219 -0
  8. data/lib/vpim/attachment.rb +102 -0
  9. data/lib/vpim/date.rb +222 -0
  10. data/lib/vpim/dirinfo.rb +277 -0
  11. data/lib/vpim/duration.rb +119 -0
  12. data/lib/vpim/enumerator.rb +32 -0
  13. data/lib/vpim/field.rb +614 -0
  14. data/lib/vpim/icalendar.rb +386 -0
  15. data/lib/vpim/maker/vcard.rb +16 -0
  16. data/lib/vpim/property/base.rb +193 -0
  17. data/lib/vpim/property/common.rb +315 -0
  18. data/lib/vpim/property/location.rb +38 -0
  19. data/lib/vpim/property/priority.rb +43 -0
  20. data/lib/vpim/property/recurrence.rb +69 -0
  21. data/lib/vpim/property/resources.rb +24 -0
  22. data/lib/vpim/repo.rb +181 -0
  23. data/lib/vpim/rfc2425.rb +372 -0
  24. data/lib/vpim/rrule.rb +598 -0
  25. data/lib/vpim/vcard.rb +1429 -0
  26. data/lib/vpim/version.rb +18 -0
  27. data/lib/vpim/vevent.rb +187 -0
  28. data/lib/vpim/view.rb +90 -0
  29. data/lib/vpim/vjournal.rb +58 -0
  30. data/lib/vpim/vpim.rb +65 -0
  31. data/lib/vpim/vtodo.rb +103 -0
  32. data/samples/README.mutt +93 -0
  33. data/samples/ab-query.rb +57 -0
  34. data/samples/cmd-itip.rb +156 -0
  35. data/samples/ex_cpvcard.rb +55 -0
  36. data/samples/ex_get_vcard_photo.rb +22 -0
  37. data/samples/ex_mkv21vcard.rb +34 -0
  38. data/samples/ex_mkvcard.rb +64 -0
  39. data/samples/ex_mkyourown.rb +29 -0
  40. data/samples/ics-dump.rb +210 -0
  41. data/samples/ics-to-rss.rb +84 -0
  42. data/samples/mutt-aliases-to-vcf.rb +45 -0
  43. data/samples/osx-wrappers.rb +86 -0
  44. data/samples/reminder.rb +203 -0
  45. data/samples/rrule.rb +71 -0
  46. data/samples/tabbed-file-to-vcf.rb +390 -0
  47. data/samples/vcf-dump.rb +86 -0
  48. data/samples/vcf-lines.rb +61 -0
  49. data/samples/vcf-to-ics.rb +22 -0
  50. data/samples/vcf-to-mutt.rb +121 -0
  51. data/test/test_all.rb +17 -0
  52. data/test/test_date.rb +120 -0
  53. data/test/test_dur.rb +41 -0
  54. data/test/test_field.rb +156 -0
  55. data/test/test_ical.rb +415 -0
  56. data/test/test_repo.rb +158 -0
  57. data/test/test_rrule.rb +1030 -0
  58. data/test/test_vcard.rb +973 -0
  59. data/test/test_view.rb +79 -0
  60. metadata +135 -0
data/lib/vpim/rrule.rb ADDED
@@ -0,0 +1,598 @@
1
+ =begin
2
+ Copyright (C) 2008 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ require 'vpim/rfc2425'
10
+ require 'vpim/date'
11
+ require 'vpim/vpim'
12
+
13
+ =begin
14
+ require 'pp'
15
+
16
+ $debug = ENV['DEBUG']
17
+
18
+ class Date
19
+ def inspect
20
+ self.to_s
21
+ end
22
+ end
23
+
24
+ def debug(*objs)
25
+ if $debug
26
+ pp(*objs)
27
+ print ' (', caller(1)[0], ')', "\n"
28
+ end
29
+ end
30
+ =end
31
+
32
+ module Vpim
33
+
34
+ # Implements the iCalendar recurrence rule syntax. See etc/rrule.txt for the
35
+ # syntax description and examples from RFC 2445. The description is pretty
36
+ # hard to understand, but the examples are more helpful.
37
+ #
38
+ # The implementation is reasonably complete, but still lacks support for:
39
+ #
40
+ # Recurrence by date (RDATE) and exclusions (EXDATE, EXRULE).
41
+ #
42
+ # TODO - BYWEEKNO: rules that are limited to particular weeks in a year.
43
+ #
44
+ # TODO - BYHOUR, BYMINUTE, BYSECOND: trivial to do, but I don't have an
45
+ # immediate need for them.
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 = dtstart.in_time_zone
60
+
61
+ @rrule = rrule
62
+
63
+ # Freq is mandatory, but must occur only once.
64
+ @freq = nil
65
+
66
+ # Both Until and Count must not occur, neither is OK.
67
+ @until = nil
68
+ @count = nil
69
+
70
+ # Interval is optional, but defaults to 1.
71
+ @interval = 1
72
+
73
+ # WKST defines what day a week begins on, the default is monday.
74
+ @wkst = 'MO'
75
+
76
+ # Recurrence can modified by these.
77
+ @by = {}
78
+
79
+ if @rrule
80
+ @rrule.scan(/([^;=]+)=([^;=]+)/) do |key,value|
81
+ key.upcase!
82
+ value.upcase!
83
+
84
+ case key
85
+ when 'FREQ'
86
+ @freq = value
87
+
88
+ when 'UNTIL'
89
+ if @count
90
+ raise "found UNTIL, but COUNT already specified"
91
+ end
92
+ @until = Rrule.time_from_rfc2425(value)
93
+
94
+ when 'COUNT'
95
+ if @until
96
+ raise "found COUNT, but UNTIL already specified"
97
+ end
98
+ @count = value.to_i
99
+
100
+ when 'INTERVAL'
101
+ @interval = value.to_i
102
+ if @interval < 1
103
+ raise "interval must be a positive integer"
104
+ end
105
+
106
+ when 'WKST'
107
+ # TODO - check value is MO-SU
108
+ @wkst = value
109
+
110
+ else
111
+ @by[key] = value
112
+ end
113
+ end
114
+
115
+ if !@freq
116
+ # TODO - this shouldn't be an arg error, but a FormatError, its not the
117
+ # caller's fault!
118
+ raise ArgumentError, "recurrence rule lacks a frequency"
119
+ end
120
+ end
121
+ end
122
+
123
+ # Return an Enumerable, it's #each() will yield over all occurrences up to
124
+ # (and not including) time +dountil+.
125
+ def each_until(dountil)
126
+ Vpim::Enumerator.new(self, dountil)
127
+ end
128
+
129
+ # Yields for each +ytime+ in the recurring set of events.
130
+ #
131
+ # Warning: the set may be infinite! If you need an upper bound on the
132
+ # number of occurrences, you need to implement a count, or pass a time,
133
+ # +dountil+, which will not be iterated past (i.e. all times yielded will be
134
+ # less than +dountil+).
135
+ #
136
+ # Also, iteration will not currently continue past the limit of a Time
137
+ # object, which is some time in 2037 with the 32-bit time_t common on
138
+ # most systems.
139
+ def each(dountil = nil) #:yield: ytime
140
+ t = @dtstart.clone
141
+
142
+ # Time.to_a => [ sec, min, hour, day, month, year, wday, yday, isdst, zone ]
143
+
144
+ # Every event occurs at its start time, but only if the start time is
145
+ # earlier than DOUNTIL...
146
+ if !dountil || t < dountil
147
+ yield t
148
+ end
149
+ count = 1
150
+
151
+ # With no recurrence, DTSTART is the only occurrence.
152
+ if !@rrule
153
+ return self
154
+ end
155
+
156
+ loop do
157
+ # Build the set of times to yield within this interval (and after
158
+ # DTSTART)
159
+
160
+ days = DaySet.new(t)
161
+ hour = nil
162
+ min = nil
163
+ sec = nil
164
+
165
+ # Need to make a Dates class, and make month an instance of it, and add
166
+ # the "intersect" operator.
167
+
168
+ case @freq
169
+ #when 'YEARLY' then
170
+ # Don't need to keep track of year, all occurrences are within t's
171
+ # year.
172
+ when 'MONTHLY' then days.month = t.month
173
+ when 'WEEKLY' then #days.month = t.month
174
+ # TODO - WEEKLY
175
+ when 'DAILY' then days.mday = t.month, t.mday
176
+ when 'HOURLY' then hour = [t.hour]
177
+ when 'MINUTELY' then min = [t.min]
178
+ when 'SECONDLY' then sec = [t.sec]
179
+ end
180
+
181
+ # debug [t, days]
182
+ # Process the BY* modifiers in RFC defined order:
183
+ # BYMONTH,
184
+ # BYWEEKNO,
185
+ # BYYEARDAY,
186
+ # BYMONTHDAY,
187
+ # BYDAY,
188
+ # BYHOUR,
189
+ # BYMINUTE,
190
+ # BYSECOND,
191
+ # BYSETPOS
192
+
193
+ bymon = [nil]
194
+
195
+ if @by['BYMONTH']
196
+ bymon = @by['BYMONTH'].split(',')
197
+ bymon = bymon.map { |m| m.to_i }
198
+ # debug bymon
199
+
200
+ # In yearly, at this point, month will always be nil. At other
201
+ # frequencies, it will not.
202
+ days.intersect_bymon(bymon)
203
+
204
+ # debug days
205
+ end
206
+
207
+ # TODO - BYWEEKNO
208
+
209
+ if @by['BYYEARDAY']
210
+ byyday = @by['BYYEARDAY'].scan(/,?([+-]?[1-9]\d*)/)
211
+ # debug byyday
212
+ dates = byyearday(t.year, byyday)
213
+ days.intersect_dates(dates)
214
+ end
215
+
216
+ if @by['BYMONTHDAY']
217
+ bymday = @by['BYMONTHDAY'].scan(/,?([+-]?[1-9]\d*)/)
218
+ # debug bymday
219
+ # Generate all days matching this for all months. For yearly, this
220
+ # is what we want, for anything of monthly or higher frequency, it
221
+ # is too many days, but that's OK, since the month will already
222
+ # be specified and intersection will eliminate the out-of-range
223
+ # dates.
224
+ dates = bymonthday(t.year, bymday)
225
+ # debug dates
226
+ days.intersect_dates(dates)
227
+ # debug days
228
+ end
229
+
230
+ if @by['BYDAY']
231
+ byday = @by['BYDAY'].scan(/,?([+-]?[1-9]?\d*)?(SU|MO|TU|WE|TH|FR|SA)/i)
232
+
233
+ # BYDAY means different things in different frequencies. The +n+
234
+ # is only meaningful when freq is yearly or monthly.
235
+
236
+ case @freq
237
+ when 'YEARLY'
238
+ dates = bymon.map { |m| byday_in_monthly(t.year, m, byday) }.flatten
239
+ when 'MONTHLY'
240
+ dates = byday_in_monthly(t.year, t.month, byday)
241
+ when 'WEEKLY'
242
+ dates = byday_in_weekly(t.year, t.month, t.mday, @wkst, byday)
243
+ when 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY'
244
+ # Reuse the byday_in_monthly. Current day is already specified,
245
+ # so this will just eliminate the current day if its not allowed
246
+ # in BYDAY.
247
+ dates = byday_in_monthly(t.year, t.month, byday)
248
+ end
249
+
250
+ # debug dates
251
+ days.intersect_dates(dates)
252
+ # debug days
253
+ end
254
+
255
+ # TODO - BYHOUR, BYMINUTE, BYSECOND
256
+
257
+ hour = [@dtstart.hour] if !hour
258
+ min = [@dtstart.min] if !min
259
+ sec = [@dtstart.sec] if !sec
260
+
261
+ # debug days
262
+
263
+ # Generate the yield set so BYSETPOS can be evaluated.
264
+ yset = []
265
+
266
+ days.each do |m,d|
267
+ hour.each do |h|
268
+ min.each do |n|
269
+ sec.each do |s|
270
+ y = Time.zone.local(t.year, m, d, h, n, s, 0)
271
+
272
+ next if y.hour != h
273
+
274
+ yset << y
275
+ end
276
+ end
277
+ end
278
+ end
279
+
280
+ if @by['BYSETPOS']
281
+ bysetpos = @by['BYSETPOS'].split(',')
282
+ yset = bysetpos.map do |i|
283
+ i = i.to_i
284
+ case
285
+ when i < 0
286
+ # yset[-1] is last
287
+ yset[i]
288
+ when i > 0
289
+ # yset[1] is first
290
+ yset[i-1]
291
+ else
292
+ # ignore invalid syntax
293
+ end
294
+ end.compact # set positions out of scope will be nil, RFC says ignore them
295
+ end
296
+
297
+ # Yield the occurrence, if we haven't gone over COUNT, or past UNTIL, or
298
+ # past the end of representable time.
299
+
300
+ yset.each do |y|
301
+ # The generated set can sometimes generate results earlier
302
+ # than the DTSTART, skip them. Also, we already yielded
303
+ # DTSTART, skip it.
304
+ next if y <= @dtstart
305
+
306
+ count += 1
307
+
308
+ # We are done if current count is past @count.
309
+ if(@count && (count > @count))
310
+ return self
311
+ end
312
+
313
+ # We are done if current time is past @until.
314
+ if @until && (y > @until)
315
+ return self
316
+ end
317
+ # We are also done if current time is past the
318
+ # caller-requested until.
319
+ if dountil && (y >= dountil)
320
+ return self
321
+ end
322
+ yield y
323
+ end
324
+
325
+ # Add @interval to @freq component
326
+
327
+ # Note - when we got past representable time, the error is:
328
+ # time out of range (ArgumentError)
329
+ # Finish when we see this.
330
+ begin
331
+ case @freq
332
+ when 'YEARLY' then
333
+ t += @interval.years
334
+
335
+ when 'MONTHLY' then
336
+ t += @interval.months
337
+
338
+ when 'WEEKLY' then
339
+ t += (@interval * 7).days
340
+
341
+ when 'DAILY' then
342
+ t += @interval.days
343
+
344
+ when 'HOURLY' then
345
+ t += @interval.hours
346
+
347
+ when 'MINUTELY' then
348
+ t += @interval.minutes
349
+
350
+ when 'SECONDLY' then
351
+ t += @interval.seconds
352
+
353
+ when nil
354
+ return self
355
+ end
356
+ rescue ArgumentError
357
+ return self if $!.message =~ /^time out of range$/
358
+
359
+ raise ArgumentError, "#{$!.message} while adding interval to #{t.inspect}"
360
+ end
361
+
362
+ return self if dountil && (t > dountil)
363
+ end
364
+ end
365
+
366
+
367
+
368
+ class DaySet #:nodoc:
369
+
370
+ def initialize(ref)
371
+ @ref = ref # Need to know because leap years have an extra day, and to get
372
+ # our defaults.
373
+ @month = nil
374
+ @week = nil
375
+ end
376
+
377
+ def month=(mon)
378
+ @month = { mon => nil }
379
+ end
380
+
381
+ def week=(week)
382
+ @week = week
383
+ end
384
+
385
+ def mday=(pair)
386
+ @month = { pair[0] => [ pair[1] ] }
387
+ end
388
+
389
+ def intersect_bymon(bymon) #:nodoc:
390
+ if !@month
391
+ @month = {}
392
+ bymon.each do |m|
393
+ @month[m] = nil
394
+ end
395
+ else
396
+ @month.delete_if { |m, days| ! bymon.include? m }
397
+ end
398
+ end
399
+
400
+ def intersect_dates(dates) #:nodoc:
401
+ return unless dates
402
+
403
+ # If no months are in the dayset, add all the ones in dates
404
+ if !@month
405
+ @month = {}
406
+
407
+ dates.each do |d|
408
+ @month[d.mon] = nil
409
+ end
410
+ end
411
+
412
+ # In each month,
413
+ # if there are days,
414
+ # eliminate those not in dates
415
+ # otherwise
416
+ # add all those in dates
417
+ @month.each do |mon, days|
418
+ days_in_mon = dates.find_all { |d| d.mon == mon }
419
+ days_in_mon = days_in_mon.map { |d| d.day }
420
+
421
+ if days
422
+ days_in_mon = days_in_mon & days
423
+ end
424
+ @month[mon] = days_in_mon
425
+ end
426
+ end
427
+
428
+ def each
429
+ @month = { @ref.month => [ @ref.mday ] } if !@month
430
+ @month.each_key do |m|
431
+ @month[m] = [@ref.day] if !@month[m]
432
+ # FIXME - if @ref.day is 31, and the month doesn't have 32 days, we'll
433
+ # generate invalid dates here, check for that, and eliminate them
434
+ end
435
+
436
+ @month.keys.sort.each do |m|
437
+ @month[m].sort.each do |d|
438
+ yield m, d
439
+ end
440
+ end
441
+ end
442
+ end
443
+
444
+ def self.time_from_rfc2425(str) #:nodoc:
445
+ # With ruby1.8 we can use DateTime to do this quick-n-easy:
446
+ # dt = DateTime.parse(str)
447
+ # Time.zone.local(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec, 0)
448
+
449
+ # The time can be a DATE or a DATE-TIME, the latter always has a 'T' in it.
450
+
451
+ if str =~ /T/
452
+ d = Vpim.decode_date_time(str)
453
+ # We get [ year, month, day, hour, min, sec, usec, tz ]
454
+ if(d.pop == "Z")
455
+ t = Time.use_zone("UTC") { Time.zone.local(*d) }
456
+ else
457
+ t = Time.zone.local(*d)
458
+ end
459
+ else
460
+ d = Vpim.decode_date(str)
461
+ # We get [ year, month, day ]
462
+ # FIXME - I have to choose gm or local, though neither makes much
463
+ # sense. This is a bit of a hack - what we should really do is return
464
+ # an instance of Date, and Time should allow itself to be compared to
465
+ # Date... This hack will give odd results when comparing times, because
466
+ # it will create a Time on the right date but whos time is 00:00:00.
467
+ t = Time.zone.local(*d)
468
+ end
469
+ if t.month != d[1] || t.day != d[2] || (d[3] && t.hour != d[3])
470
+ raise Vpim::InvalidEncodingError, "Error - datetime does not exist"
471
+ end
472
+ t
473
+ end
474
+
475
+ def bymonthday(year, bymday) #:nodoc:
476
+ dates = []
477
+
478
+ bymday.each do |mday|
479
+ dates |= DateGen.bymonthday(year, nil, mday[0].to_i)
480
+ end
481
+ dates.sort!
482
+ dates
483
+ end
484
+
485
+ def byyearday(year, byyday) #:nodoc:
486
+ dates = []
487
+
488
+ byyday.each do |yday|
489
+ dates << Date.ordinal(year, yday[0].to_i)
490
+ end
491
+ dates.sort!
492
+ dates
493
+ end
494
+
495
+ def byday_in_monthly(year, mon, byday) #:nodoc:
496
+ dates = []
497
+
498
+ byday.each do |rule|
499
+ if rule[0].empty?
500
+ n = nil
501
+ else
502
+ n = rule[0].to_i
503
+ end
504
+ begin
505
+ dates |= DateGen.bywday(year, mon, Date.str2wday(rule[1]), n)
506
+ rescue ArgumentError => e
507
+ # Catch and ignore a very specific error type:
508
+ # Those caused by rules like FREQ=MONTHLY;BYDAY=5SA, which as far as
509
+ # I can tell are valid, but some months don't e.g., have a 5th Saturday
510
+ unless e.message == 'n is out of bounds of month' && n == 5
511
+ raise
512
+ end
513
+ end
514
+
515
+ end
516
+ dates.sort!
517
+ dates
518
+ end
519
+
520
+ def byday_in_weekly(year, mon, day, wkst, byday) #:nodoc:
521
+ # debug ["day", year,mon,day,wkst,byday]
522
+ days = byday.map{ |_, byday| Date.str2wday(byday) }
523
+ week = DateGen.weekofdate(year, mon, day, wkst)
524
+ # debug [ "week", dates ]
525
+ week.delete_if do |d|
526
+ !days.include?(d.wday)
527
+ end
528
+ week
529
+ end
530
+
531
+ # Help encode an RRULE value.
532
+ #
533
+ # TODO - the Maker is both incomplete, and its a bit cheesy, I'd like to do
534
+ # something that is a kind of programmatic version of the UI that iCal has.
535
+ class Maker
536
+ def initialize(&block) #:yield: self
537
+ @freq = nil
538
+ @until = nil
539
+ @count = nil
540
+ @interval = nil
541
+ @wkst = nil
542
+ @by = {}
543
+
544
+ if block
545
+ yield self
546
+ end
547
+ end
548
+
549
+ FREQ = %w{ YEARLY WEEKLY MONTHLY DAILY } #:nodoc: incomplete!
550
+
551
+ def frequency=(freq)
552
+ freq = freq.to_str.upcase
553
+ unless FREQ.include? freq
554
+ raise ArgumentError, "Frequency #{freq} is not valid"
555
+ end
556
+ @freq = freq
557
+ end
558
+
559
+ # +runtil+ is Time, Date, or DateTime
560
+ def until=(runtil)
561
+ if @count
562
+ raise ArgumentError, "Cannot specify UNTIL if COUNT was specified"
563
+ end
564
+ @until = runtil
565
+ end
566
+
567
+ # +count+ is integral
568
+ def count=(rcount)
569
+ if @until
570
+ raise ArgumentError, "Cannot specify COUNT if UNTIL was specified"
571
+ end
572
+ @count = rcount.to_int
573
+ end
574
+
575
+ # TODO - BY....
576
+
577
+ def encode
578
+ unless @freq
579
+ raise ArgumentError, "Must specify FREQUENCY"
580
+ end
581
+
582
+ rrule = "FREQ=#{@freq}"
583
+
584
+ [
585
+ ["COUNT", @count],
586
+ ["UNTIL", @until],
587
+ # TODO...
588
+ ].each do |k,v|
589
+ if v
590
+ rrule += ";#{k}=#{v}"
591
+ end
592
+ end
593
+ rrule
594
+ end
595
+ end
596
+ end
597
+ end
598
+