rwdaddresses 1.01 → 1.02

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. data/Readme.txt +5 -0
  2. data/code/01rwdcore/01rwdcore.rb +15 -13
  3. data/code/01rwdcore/openhelpwindow.rb +26 -26
  4. data/code/01rwdcore/returntomain.rb +8 -8
  5. data/code/01rwdcore/rwdtinkerversion.rb +12 -13
  6. data/code/01rwdcore/rwdwindowreturn.rb +3 -5
  7. data/code/01rwdcore/test_cases.rb +115 -0
  8. data/code/01rwdcore/test_harness.rb +12 -0
  9. data/code/01rwdcore/uploadreturns.rb +60 -59
  10. data/code/superant.com.rwdaddresses/attachtmpcontactphoto.rb +23 -23
  11. data/code/superant.com.rwdaddresses/clearscreendisplay.rb +14 -15
  12. data/code/superant.com.rwdaddresses/createnewnamerecord.rb +19 -18
  13. data/code/superant.com.rwdaddresses/deletecontactrecord.rb +22 -21
  14. data/code/superant.com.rwdaddresses/deleterwdaddressesupdatefiles.rb +15 -16
  15. data/code/superant.com.rwdaddresses/downloadrwdaddressfiles.rb +34 -32
  16. data/code/superant.com.rwdaddresses/listnamerecord.rb +14 -14
  17. data/code/superant.com.rwdaddresses/listvcardrecord.rb +15 -0
  18. data/code/superant.com.rwdaddresses/loadconfigurationrecord.rb +45 -44
  19. data/code/superant.com.rwdaddresses/loadconfigurationvariables.rb +14 -13
  20. data/code/superant.com.rwdaddresses/loadnamerecord.rb +29 -29
  21. data/code/superant.com.rwdaddresses/openhelpwindowrwdaddresses.rb +25 -25
  22. data/code/superant.com.rwdaddresses/renamecontact.rb +14 -15
  23. data/code/superant.com.rwdaddresses/returntomain.rb +8 -8
  24. data/code/superant.com.rwdaddresses/runaddresseswindow.rb +8 -8
  25. data/code/superant.com.rwdaddresses/runrwdaddressesmenu1.rb +31 -31
  26. data/code/superant.com.rwdaddresses/runrwdaddresssyncbackwindow.rb +8 -8
  27. data/code/superant.com.rwdaddresses/rwdaddressesbackwindow.rb +8 -8
  28. data/code/superant.com.rwdaddresses/rwdaddresseshelpabout.rb +10 -11
  29. data/code/superant.com.rwdaddresses/saveconfigurationrecord.rb +20 -20
  30. data/code/superant.com.rwdaddresses/savevcardrecord.rb +76 -0
  31. data/code/superant.com.rwdaddresses/syncrwdaddress.rb +24 -26
  32. data/code/superant.com.rwdaddresses/uploadrwdaddressfiles.rb +28 -28
  33. data/code/superant.com.rwdaddresses/viewaddressconfiguration.rb +21 -21
  34. data/code/superant.com.rwdaddresses/viewnamedata.rb +27 -26
  35. data/code/superant.com.rwdaddresses/viewphoto.rb +4 -4
  36. data/code/superant.com.rwdaddresses/viewrwdaddressesconfiguration.rb +23 -21
  37. data/code/superant.com.rwdaddresses/viewtmpcontactphoto.rb +6 -6
  38. data/code/superant.com.rwdaddresses/viewvcarddata.rb +22 -0
  39. data/code/superant.com.rwdtinkerbackwindow/controlclient.rb +89 -92
  40. data/code/superant.com.rwdtinkerbackwindow/diagnostictab.rb +15 -20
  41. data/code/superant.com.rwdtinkerbackwindow/installapplet.rb +19 -18
  42. data/code/superant.com.rwdtinkerbackwindow/installgemapplet.rb +20 -19
  43. data/code/superant.com.rwdtinkerbackwindow/installremotegem.rb +19 -18
  44. data/code/superant.com.rwdtinkerbackwindow/listgemdirs.rb +7 -7
  45. data/code/superant.com.rwdtinkerbackwindow/listgemzips.rb +48 -49
  46. data/code/superant.com.rwdtinkerbackwindow/listinstalledfiles.rb +8 -7
  47. data/code/superant.com.rwdtinkerbackwindow/listzips.rb +22 -26
  48. data/code/superant.com.rwdtinkerbackwindow/loadconfigurationrecord.rb +32 -31
  49. data/code/superant.com.rwdtinkerbackwindow/loadconfigurationvariables.rb +14 -13
  50. data/code/superant.com.rwdtinkerbackwindow/network.rb +82 -82
  51. data/code/superant.com.rwdtinkerbackwindow/openappletname.rb +18 -17
  52. data/code/superant.com.rwdtinkerbackwindow/openhelpwindowtinkerwin2.rb +37 -37
  53. data/code/superant.com.rwdtinkerbackwindow/remotegemlist.rb +20 -20
  54. data/code/superant.com.rwdtinkerbackwindow/removeapplet.rb +33 -32
  55. data/code/superant.com.rwdtinkerbackwindow/runrwdtinkerbackwindow.rb +8 -9
  56. data/code/superant.com.rwdtinkerbackwindow/rwdtinkerwin2version.rb +10 -11
  57. data/code/superant.com.rwdtinkerbackwindow/saveconfigurationrecord.rb +19 -18
  58. data/code/superant.com.rwdtinkerbackwindow/viewappletcontents.rb +21 -20
  59. data/code/superant.com.rwdtinkerbackwindow/viewgemappletcontents.rb +23 -20
  60. data/configuration/language.dist +1 -1
  61. data/configuration/rwdaddresses.dist +2 -2
  62. data/configuration/rwdtinker.dist +2 -2
  63. data/configuration/tinkerwin2variables.dist +1 -1
  64. data/extras/vpim/date.rb +198 -0
  65. data/extras/vpim/dirinfo.rb +229 -0
  66. data/extras/vpim/enumerator.rb +29 -0
  67. data/extras/vpim/field.rb +497 -0
  68. data/extras/vpim/maker/vcard.rb +312 -0
  69. data/extras/vpim/rfc2425.rb +244 -0
  70. data/extras/vpim/rrule.rb +482 -0
  71. data/extras/vpim/time.rb +42 -0
  72. data/extras/vpim/vcard.rb +151 -0
  73. data/extras/vpim/vpim.rb +94 -0
  74. data/gui/frontwindow0/superant.com.rwdaddresses/16editrecord.rwd +5 -0
  75. data/gui/frontwindow0/superant.com.rwdaddresses/17viewvcardrecord.rwd +32 -0
  76. data/gui/helpaboutinstalled/superant.com.tinkerhelpabout/3copyright.rwd +1 -1
  77. data/gui/tinkerbackwindows/superant.com.rwdaddressessyncbackwindow/70rwddiagnostics.rwd +1 -11
  78. data/init.rb +231 -228
  79. data/names/Angelina Jolie.vcf +8 -0
  80. data/rwd_files/HowTo_Addresses.txt +5 -0
  81. data/rwd_files/HowTo_Tinker.txt +14 -0
  82. data/rwdconfig.dist +6 -2
  83. data/tests/makedist.rb +36 -7
  84. metadata +23 -21
  85. data/extras/cmdline_parse +0 -47
  86. data/extras/config_file +0 -69
  87. data/extras/errorMsg +0 -19
  88. data/extras/makePlaylist +0 -34
  89. data/extras/mp3controld +0 -289
  90. data/extras/playlist +0 -186
  91. data/extras/plugins/Network +0 -237
  92. data/extras/showHelp +0 -18
  93. data/gui/frontwindowselections/superant.com.rwdaddressesselectiontab/rwdaddressessyncselectiontab.rwd +0 -12
  94. data/gui/helpaboutinstalled/superant.com.rwdwin2helpabout/1appname.rwd +0 -4
  95. data/gui/helpaboutinstalled/superant.com.rwdwin2helpabout/3copyright.rwd +0 -3
  96. data/gui/helpaboutinstalled/superant.com.rwdwin2helpabout/5version.rwd +0 -10
  97. data/installed/rwdaddressesdata2.inf +0 -8
  98. data/rwd_files/tinker.png +0 -0
@@ -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 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 bday_in_monthly. Current day is already specified,
237
+ # so this will just eliminate the current day if its no 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
+
@@ -0,0 +1,42 @@
1
+ =begin
2
+ $Id: time.rb,v 1.5 2005/02/02 02:55:59 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 'date'
12
+
13
+ # Extensions to builtin Time allowing addition to Time by multiples of other
14
+ # intervals than a second.
15
+
16
+ class Time
17
+ # Returns a new Time, +years+ later than this time. Feb 29 of a
18
+ # leap year will be rounded up to Mar 1 if the target date is not a leap
19
+ # year.
20
+ def plus_year(years)
21
+ Time.local(year + years, month, day, hour, min, sec, usec)
22
+ end
23
+
24
+ # Returns a new Time, +months+ later than this time. The day will be
25
+ # rounded down if it is not valid for that month.
26
+ # 31 plus 1 month will be on Feb 28!
27
+ def plus_month(months)
28
+ d = Date.new(year, month, day)
29
+ d >>= months
30
+ Time.local(d.year, d.month, d.day, hour, min, sec, usec)
31
+ end
32
+
33
+ # Returns a new Time, +days+ later than this time.
34
+ # Does this do as I expect over DST? What if the hour doesn't exist
35
+ # in the next day, due to DST changes?
36
+ def plus_day(days)
37
+ d = Date.new(year, month, day)
38
+ d += days
39
+ Time.local(d.year, d.month, d.day, hour, min, sec, usec)
40
+ end
41
+ end
42
+
@@ -0,0 +1,151 @@
1
+ =begin
2
+ $Id: vcard.rb,v 1.13 2004/12/05 03:16:33 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 'extras/vpim/dirinfo'
12
+ require 'extras/vpim/vpim'
13
+
14
+ module Vpim
15
+ # A vCard, a specialization of a directory info object.
16
+ #
17
+ # The vCard format is specified by:
18
+ # - RFC2426: vCard MIME Directory Profile (vCard 3.0)
19
+ # - RFC2425: A MIME Content-Type for Directory Information
20
+ #
21
+ # This implements vCard 3.0, but it is also capable of decoding vCard 2.1.
22
+ #
23
+ # For information about:
24
+ # - link:rfc2426.txt: vCard MIME Directory Profile (vCard 3.0)
25
+ # - link:rfc2425.txt: A MIME Content-Type for Directory Information
26
+ # - http://www.imc.org/pdi/pdiproddev.html: vCard 2.1 Specifications
27
+ #
28
+ # vCards are usually transmitted in files with <code>.vcf</code>
29
+ # extensions.
30
+ #
31
+ # TODO - an open question is what exactly "vcard 2.1" support means. While I
32
+ # decode vCard 2.1 correctly, I don't encode it. Should I implement a
33
+ # transcoder, to vCards can be decoded from either version, and then written
34
+ # to either version? Maybe an option to Field#encode()?
35
+ #
36
+ # TODO - there are very few methods that Vcard has that DirectoryInfo
37
+ # doesn't. I could probably just do away with it entirely, but I suspect
38
+ # that there are methods that could be usefully added to Vcard, perhaps to
39
+ # get the email addresses, or the name, or perhaps to set fields, like
40
+ # email=. What would be useful?
41
+ #
42
+ # = Example
43
+ #
44
+ # Here's an example of encoding a simple vCard using the low-level API:
45
+ #
46
+ # card = Vpim::Vcard.create
47
+ # card << Vpim::DirectoryInfo::Field.create('email', user.name@example.com, 'type' => "internet" )
48
+ # card << Vpim::DirectoryInfo::Field.create('url', "http://www.example.com/user" )
49
+ # card << Vpim::DirectoryInfo::Field.create('fn', "User Name" )
50
+ # puts card.to_s
51
+ #
52
+ # New! Use the Vpim::Maker::Vcard to make vCards!
53
+ class Vcard < DirectoryInfo
54
+
55
+ private_class_method :new
56
+
57
+ # Create a vCard 3.0 object with the minimum required fields, plus any
58
+ # +fields+ you want in the card (they can also be added later).
59
+ def Vcard.create(fields = [] )
60
+ fields.unshift Field.create('VERSION', "3.0")
61
+ super(fields, 'VCARD')
62
+ end
63
+
64
+ # Decode a collection of vCards into an array of Vcard objects.
65
+ #
66
+ # +card+ can be either a String or an IO object.
67
+ #
68
+ # Since vCards are self-delimited (by a BEGIN:vCard and an END:vCard),
69
+ # multiple vCards can be concatenated into a single directory info object.
70
+ # They may or may not be related. For example, AddressBook.app (the OS X
71
+ # contact manager) will export multiple selected cards in this format.
72
+ #
73
+ # The card data must be UTF-8 (ASCII is valid UTF-8) or UCS-2 (2 byte
74
+ # Unicode). All valid vCards must begin with "BEGIN:VCARD", and the UCS-2
75
+ # encoding of 'B' is a the nul character (0x00) followed by the ASCII
76
+ # value of 'B', so the input is considered to be UCS-2 if the first
77
+ # character in the string is nul, and converted to UTF-8.
78
+ #
79
+ # If you know that you have only one vCard, then you can decode that
80
+ # single vCard by doing something like:
81
+ #
82
+ # vcard = Vcard.decode(card_data).first
83
+ def Vcard.decode(card)
84
+ if card.respond_to? :to_str
85
+ string = card.to_str
86
+ elsif card.kind_of? IO
87
+ string = card.read(nil)
88
+ else
89
+ raise ArgumentError, "Vcard.decode cannot be called with a #{card.type}"
90
+ end
91
+
92
+ # The card representation can be either UTF-8, or UCS-2. If its
93
+ # UCS-2, then the first byte will be 0, so check for this, and convert
94
+ # if necessary.
95
+ #
96
+ # We know it's 0, because the first character in a vCard must be the 'B'
97
+ # of "BEGIN:VCARD", and in UCS-2 all ascii are encoded as a 0 byte
98
+ # followed by the ASCII byte, UNICODE is great.
99
+ if string[0] == 0
100
+ string = string.unpack('n*').pack('U*')
101
+ end
102
+
103
+ entities = Vpim.expand(Vpim.decode(string))
104
+
105
+ # Since all vCards must have a begin/end, the top-level should consist
106
+ # entirely of entities/arrays, even if its a single vCard.
107
+ if entities.detect { |e| ! e.kind_of? Array }
108
+ raise "Not a valid vCard"
109
+ end
110
+
111
+ vcards = []
112
+
113
+ for e in entities
114
+ vcards.push(new(e.flatten, 'VCARD'))
115
+ end
116
+
117
+ vcards
118
+ end
119
+
120
+ # The vCard version multiplied by 10 as an Integer. If no VERSION field
121
+ # is present (which is non-conformant), nil is returned. For example, a
122
+ # version 2.1 vCard would have a version of 21, and a version 3.0 vCard
123
+ # would have a version of 30.
124
+ def version
125
+ v = self["version"]
126
+ unless v
127
+ raise Vpim::InvalidEncodingError, 'Invalid vCard - it has no version field!'
128
+ end
129
+ v = v.to_f * 10
130
+ v = v.to_i
131
+ end
132
+
133
+ # The value of the field named +name+, optionally limited to fields of
134
+ # type +type+. If no match is found, nil is returned, if multiple matches
135
+ # are found, the first match to have one of its type values be 'pref'
136
+ # (preferred) is returned, otherwise the first match is returned.
137
+ def [](name, type=nil)
138
+ fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }
139
+
140
+ # limit to preferred, if possible
141
+ pref = fields.find_all { |f| f.pref? }
142
+
143
+ if(pref.first)
144
+ fields = pref
145
+ end
146
+
147
+ fields.first ? fields.first.value : nil
148
+ end
149
+ end
150
+ end
151
+