mumboe-vpim 0.7

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