icalendar 1.0.2 → 1.1.0

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.
data/README CHANGED
@@ -75,6 +75,131 @@ Now for some examples:
75
75
  # network port, database etc.
76
76
  cal_string = cal.to_ical
77
77
  puts cal_string
78
+
79
+ ## ALARMS
80
+
81
+ ## Within an event, you can create e-mail notification alarms like this...
82
+
83
+ cal.event.do
84
+ # ...other event properties
85
+ alarm do
86
+ action "EMAIL"
87
+ description "This is an event reminder" # email body (required)
88
+ summary "Alarm notification" # email subject (required)
89
+ attendees %w(mailto:me@my-domain.com mailto:me-too@my-domain.com) # one or more email recipients (required)
90
+ add_attendee "mailto:me-three@my-domain.com"
91
+ remove_attendee "mailto:me@my-domain.com"
92
+ trigger "-PT15M" # 15 minutes before
93
+ add_attach "ftp://host.com/novo-procs/felizano.exe", {"FMTTYPE" => "application/binary"} # email attachments (optional)
94
+ end
95
+
96
+ alarm do
97
+ action "DISPLAY" # This line isn't necessary, it's the default
98
+ summary "Alarm notification"
99
+ trigger "-P1DT0H0M0S" # 1 day before
100
+ end
101
+
102
+ alarm do
103
+ action "AUDIO"
104
+ trigger "-PT15M"
105
+ add_attach "Basso", {"VALUE" => ["URI"]} # only one attach allowed (optional)
106
+ end
107
+ end
108
+
109
+ # Output
110
+
111
+ # BEGIN:VALARM
112
+ # ACTION:EMAIL
113
+ # ATTACH;FMTTYPE=application/binary:ftp://host.com/novo-procs/felizano.exe
114
+ # TRIGGER:-PT15M
115
+ # SUMMARY:Alarm notification
116
+ # DESCRIPTION:This is an event reminder
117
+ # ATTENDEE:mailto:me-too@my-domain.com
118
+ # ATTENDEE:mailto:me-three@my-domain.com
119
+ # END:VALARM
120
+ #
121
+ # BEGIN:VALARM
122
+ # ACTION:DISPLAY
123
+ # TRIGGER:-P1DT0H0M0S
124
+ # SUMMARY:Alarm notification
125
+ # END:VALARM
126
+ #
127
+ # BEGIN:VALARM
128
+ # ACTION:AUDIO
129
+ # ATTACH;VALUE=URI:Basso
130
+ # TRIGGER:-PT15M
131
+ # END:VALARM
132
+
133
+ ## Timezones
134
+
135
+ # Create a timezone definition (previous convention)
136
+ cal = Calendar.new
137
+ timezone = Icalendar::Timezone.new
138
+ daylight = Icalendar::Daylight.new
139
+ standard = Icalendar::Standard.new
140
+
141
+ timezone.timezone_id = "America/Chicago"
142
+
143
+ daylight.timezone_offset_from = "-0600"
144
+ daylight.timezone_offset_to = "-0500"
145
+ daylight.timezone_name = "CDT"
146
+ daylight.dtstart = "19700308TO20000"
147
+ daylight.recurrence_rules = ["FREQ=YEARLY;BYMONTH=3;BYDAY=2SU"]
148
+
149
+ standard.timezone_offset_from = "-0500"
150
+ standard.timezone_offset_to = "-0600"
151
+ standard.timezone_name = "CST"
152
+ standard.dtstart = "19701101T020000"
153
+ standard.recurrence_rules = ["YEARLY;BYMONTH=11;BYDAY=1SU"]
154
+
155
+ timezone.add(daylight)
156
+ timezone.add(standard)
157
+ cal.add(timezone)
158
+
159
+ # Now, you can make timezones like this
160
+ cal = Calendar.new
161
+ cal.timezone do
162
+ timezone_id "America/Chicago"
163
+
164
+ daylight do
165
+ timezone_offset_from "-0600"
166
+ timezone_offset_to "-0500"
167
+ timezone_name "CDT"
168
+ dtstart "19700308TO20000"
169
+ add_recurrence_rule "FREQ=YEARLY;BYMONTH=3;BYDAY=2SU"
170
+ end
171
+
172
+ standard do
173
+ timezone_offset_from "-0500"
174
+ timezone_offset_to "-0600"
175
+ timezone_name "CST"
176
+ dtstart "19701101T020000"
177
+ add_recurrence_rule "YEARLY;BYMONTH=11;BYDAY=1SU"
178
+ end
179
+ end
180
+
181
+ # Both conventions output
182
+
183
+ # BEGIN:VTIMEZONE
184
+ # TZID:America/Chicago
185
+ # BEGIN:DAYLIGHT
186
+ # TZOFFSETFROM:-0600
187
+ # TZOFFSETTO:-0500
188
+ # TZNAME:CDT
189
+ # DTSTART:19700308T020000
190
+ # RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
191
+ # END:DAYLIGHT
192
+ # BEGIN:STANDARD
193
+ # TZOFFSETFROM:-0500
194
+ # TZOFFSETTO:-0600
195
+ # TZNAME:CST
196
+ # DTSTART:19701101T020000
197
+ # RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
198
+ # END:STANDARD
199
+ # END:VTIMEZONE
200
+
201
+ == Unicode
202
+ Add `$KCODE = 'u'` to make icalender work correctly with Utf8 texts
78
203
 
79
204
  == Parsing iCalendars:
80
205
 
data/Rakefile CHANGED
@@ -5,7 +5,7 @@ require 'rake/rdoctask'
5
5
  require 'rake/clean'
6
6
  require 'rake/contrib/sshpublisher'
7
7
 
8
- PKG_VERSION = "1.0.2"
8
+ PKG_VERSION = "1.1.0"
9
9
 
10
10
  $VERBOSE = nil
11
11
  TEST_CHANGES_SINCE = Time.now - 600 # Recent tests = changed in last 10 minutes
@@ -74,8 +74,8 @@ spec = Gem::Specification.new do |s|
74
74
  s.extra_rdoc_files = ["README", "COPYING", "GPL"]
75
75
  s.rdoc_options.concat ['--main', 'README']
76
76
 
77
- s.author = "Jeff Rose"
78
- s.email = "rosejn@gmail.com"
77
+ s.author = "Sean Dague"
78
+ s.email = "sean@dague.net"
79
79
  end
80
80
 
81
81
  Rake::GemPackageTask.new(spec) do |pkg|
data/docs/api/STUB ADDED
File without changes
data/lib/icalendar.rb CHANGED
@@ -34,3 +34,5 @@ require 'icalendar/component/alarm'
34
34
  # Calendar parser
35
35
  require 'icalendar/parser'
36
36
 
37
+ # TZINFO support
38
+ # require 'icalendar/tzinfo'
@@ -27,9 +27,18 @@ module Icalendar
27
27
 
28
28
  def event(&block)
29
29
  e = Event.new
30
+ # Note: I'm not sure this is the best way to pass this down, but it works
31
+ e.tzid = self.timezones[0].tzid if self.timezones.length > 0
32
+
30
33
  self.add_component e
31
34
 
32
- e.instance_eval &block if block
35
+ if block
36
+ e.instance_eval &block
37
+ if e.tzid
38
+ e.dtstart.ical_params = { "TZID" => e.tzid }
39
+ e.dtend.ical_params = { "TZID" => e.tzid }
40
+ end
41
+ end
33
42
 
34
43
  e
35
44
  end
@@ -145,25 +145,29 @@ module Icalendar
145
145
 
146
146
  # Property value
147
147
  value = ":#{val.to_ical}"
148
- escaped = prelude + value.gsub("\\", "\\\\").gsub("\n", "\\n").gsub(",", "\\,").gsub(";", "\\;")
149
- s << escaped.slice!(0, MAX_LINE_LENGTH) << "\r\n " while escaped.size > MAX_LINE_LENGTH
150
- s << escaped << "\r\n"
151
- s.gsub!(/ *$/, '')
148
+ add_sliced_text(s,prelude+escape_chars(value))
152
149
  else
153
150
  prelude = "#{key.gsub(/_/, '-').upcase}"
154
151
  val.each do |v|
155
152
  params = print_parameters(v)
156
153
  value = ":#{v.to_ical}"
157
- escaped = prelude + params + value.gsub("\\", "\\\\").gsub("\n", "\\n").gsub(",", "\\,").gsub(";", "\\;")
158
- s << escaped.slice!(0, MAX_LINE_LENGTH) << "\r\n " while escaped.size > MAX_LINE_LENGTH
159
- s << escaped << "\r\n"
160
- s.gsub!(/ *$/, '')
154
+ add_sliced_text(s,prelude + params + escape_chars(value))
161
155
  end
162
156
  end
163
157
  end
164
158
  s
165
159
  end
166
160
 
161
+ def escape_chars(value)
162
+ value.gsub("\\", "\\\\").gsub("\n", "\\n").gsub(",", "\\,").gsub(";", "\\;")
163
+ end
164
+
165
+ def add_sliced_text(add_to,escaped)
166
+ escaped = escaped.split('') # split is unicdoe-aware when `$KCODE = 'u'`
167
+ add_to << escaped.slice!(0,MAX_LINE_LENGTH).to_s << "\r\n " while escaped.length != 0 # shift(MAX_LINE_LENGTH) does not work with ruby 1.8.6
168
+ add_to.gsub!(/ *$/, '')
169
+ end
170
+
167
171
  # Print the parameters for a specific property
168
172
  def print_parameters(val)
169
173
  s = ""
@@ -31,7 +31,7 @@ module Icalendar
31
31
 
32
32
  # Multi properties
33
33
  ical_multiline_property :attendee, :attendee, :attendees
34
- ical_multi_property :attach, :attachment, :attachments
34
+ ical_multiline_property :attach, :attachment, :attachments
35
35
 
36
36
  def initialize()
37
37
  super("VALARM")
@@ -27,6 +27,9 @@ module Icalendar
27
27
  # Complete description of the calendar component
28
28
  ical_property :description
29
29
 
30
+ # Specifies the timezone for the event
31
+ attr_accessor :tzid
32
+
30
33
  # Specifies date-time when calendar component begins
31
34
  ical_property :dtstart, :start
32
35
 
@@ -117,7 +120,10 @@ module Icalendar
117
120
 
118
121
  a
119
122
  end
120
-
123
+
124
+ def occurrences_starting(time)
125
+ recurrence_rules.first.occurrences_of_event_starting(self, time)
126
+ end
121
127
 
122
128
  end
123
129
  end
@@ -50,19 +50,37 @@ module Icalendar
50
50
  # of components.
51
51
  def to_ical
52
52
  print_component do
53
- s = ""
53
+ s = ""
54
54
  @components.each_value do |comp|
55
55
  s << comp.to_ical
56
56
  end
57
- s
57
+ s
58
58
  end
59
59
  end
60
-
60
+
61
61
 
62
62
  def initialize(name = "VTIMEZONE")
63
63
  super(name)
64
64
  end
65
+
66
+ # Allow block syntax for declaration of standard and daylight components of timezone
67
+ def standard(&block)
68
+ e = Standard.new
69
+ self.add_component e
65
70
 
71
+ e.instance_eval &block if block
72
+
73
+ e
74
+ end
75
+
76
+ def daylight(&block)
77
+ e = Daylight.new
78
+ self.add_component e
79
+
80
+ e.instance_eval &block if block
81
+
82
+ e
83
+ end
66
84
  end
67
85
 
68
86
  # A Standard component is a sub-component of the Timezone component which
@@ -16,6 +16,12 @@ require 'date'
16
16
  # end
17
17
  # end
18
18
 
19
+ module Icalendar
20
+ module TzidSupport
21
+ attr_accessor :icalendar_tzid
22
+ end
23
+ end
24
+
19
25
  require 'uri/generic'
20
26
 
21
27
  class String
@@ -53,7 +59,10 @@ module URI
53
59
  end
54
60
 
55
61
  class DateTime < Date
56
- def to_ical(utc = false)
62
+ attr_accessor :ical_params
63
+ include Icalendar::TzidSupport
64
+
65
+ def to_ical
57
66
  s = ""
58
67
 
59
68
  # 4 digit year
@@ -82,7 +91,7 @@ class DateTime < Date
82
91
  s << self.sec.to_s
83
92
 
84
93
  # UTC time gets a Z suffix
85
- if utc
94
+ if icalendar_tzid == "UTC"
86
95
  s << "Z"
87
96
  end
88
97
 
@@ -91,6 +100,7 @@ class DateTime < Date
91
100
  end
92
101
 
93
102
  class Date
103
+ attr_accessor :ical_params
94
104
  def to_ical(utc = false)
95
105
  s = ""
96
106
 
@@ -108,6 +118,7 @@ class Date
108
118
  end
109
119
 
110
120
  class Time
121
+ attr_accessor :ical_params
111
122
  def to_ical(utc = false)
112
123
  s = ""
113
124
 
@@ -12,6 +12,101 @@ require 'uri'
12
12
  require 'stringio'
13
13
 
14
14
  module Icalendar
15
+ class RRule
16
+
17
+ class Weekday
18
+ def initialize(day, position)
19
+ @day, @position = day, position
20
+ end
21
+
22
+ def to_s
23
+ "#{@position}#{@day}"
24
+ end
25
+ end
26
+
27
+ def initialize(name, params, value, parser)
28
+ frequency_match = value.match(/FREQ=(SECONDLY|MINUTELY|HOURLY|DAILY|WEEKLY|MONTHLY|YEARLY)/)
29
+ raise Icalendar::InvalidPropertyValue.new("FREQ must be specified for RRULE values") unless frequency_match
30
+ @frequency = frequency_match[1]
31
+ @until = parse_date_val("UNTIL", value)
32
+ @count = parse_int_val("COUNT", value)
33
+ raise Icalendar::InvalidPropertyValue.new("UNTIL and COUNT must not both be specified for RRULE values") if [@until, @count].compact.length > 1
34
+ @interval = parse_int_val("INTERVAL", value)
35
+ @by_list = {:bysecond => parse_int_list("BYSECOND", value)}
36
+ @by_list[:byminute] = parse_int_list("BYMINUTE",value)
37
+ @by_list[:byhour] = parse_int_list("BYHOUR", value)
38
+ @by_list[:byday] = parse_weekday_list("BYDAY", value)
39
+ @by_list[:bymonthday] = parse_int_list("BYMONTHDAY", value)
40
+ @by_list[:byyearday] = parse_int_list("BYYEARDAY", value)
41
+ @by_list[:byweekno] = parse_int_list("BYWEEKNO", value)
42
+ @by_list[:bymonth] = parse_int_list("BYMONTH", value)
43
+ @by_list[:bysetpos] = parse_int_list("BYSETPOS", value)
44
+ @wkst = parse_wkstart(value)
45
+ end
46
+
47
+ def to_ical
48
+ result = ["FREQ=#{@frequency}"]
49
+ result << ";UNTIL=#{@until.to_ical}" if @until
50
+ result << ";COUNT=#{@count}" if @count
51
+ result << ";INTERVAL=#{@interval}" if @interval
52
+ @by_list.each do |key, value|
53
+ result << ";#{key.to_s.upcase}=#{value}" if value
54
+ end
55
+ result << ";WKST=#{@wkst}" if @wkst
56
+ result.join
57
+ end
58
+
59
+ def parse_date_val(name, string)
60
+ match = string.match(/;#{name}=(.*?)(;|$)/)
61
+ match ? DateTime.parse(match[1]) : nil
62
+ end
63
+
64
+ def parse_int_val(name, string)
65
+ match = string.match(/;#{name}=(\d+)(;|$)/)
66
+ match ? match[1].to_i : nil
67
+ end
68
+
69
+ def parse_int_list(name, string)
70
+ match = string.match(/;#{name}=([+-]?.*?)(;|$)/)
71
+ if match
72
+ match[1].split(",").map {|int| int.to_i}
73
+ else
74
+ nil
75
+ end
76
+ end
77
+
78
+ def parse_weekday_list(name, string)
79
+ match = string.match(/;#{name}=(.*?)(;|$)/)
80
+ if match
81
+ match[1].split(",").map {|weekday|
82
+ wd_match = weekday.match(/([+-]?\d*)(SU|MO|TU|WE|TH|FR|SA)/)
83
+ Weekday.new(wd_match[2], wd_match[1])
84
+ }
85
+ else
86
+ nil
87
+ end
88
+ end
89
+
90
+ def parse_wkstart(string)
91
+ match = string.match(/;WKSTART=(SU|MO|TU|WE|TH|FR|SA)(;|$)/)
92
+ if match
93
+ %w{SU MO TU WE TH FR SA}.index(match[1])
94
+ else
95
+ nil
96
+ end
97
+ end
98
+
99
+ # TODO: Incomplete
100
+ def occurrences_of_event_starting(event, datetime)
101
+ initial_start = event.dtstart
102
+ (0...@count).map {|day_offset|
103
+ occurrence = event.clone
104
+ occurrence.dtstart = initial_start + day_offset
105
+ occurrence.clone
106
+ }
107
+ end
108
+ end
109
+
15
110
  def Icalendar.parse(src, single = false)
16
111
  cals = Icalendar::Parser.new(src).parse
17
112
 
@@ -153,6 +248,7 @@ module Icalendar
153
248
 
154
249
  # Lookup the property name to see if we have a string to
155
250
  # object parser for this property type.
251
+ orig_value = value
156
252
  if @parsers.has_key?(name)
157
253
  value = @parsers[name].call(name, params, value)
158
254
  end
@@ -223,7 +319,7 @@ module Icalendar
223
319
  pname = $1
224
320
  pvals = $3
225
321
 
226
- # If their isn't an '=' sign then we need to do some custom
322
+ # If there isn't an '=' sign then we need to do some custom
227
323
  # business. Defaults to 'type'
228
324
  if $2 == ""
229
325
  pvals = $1
@@ -298,6 +394,11 @@ module Icalendar
298
394
  # GEO
299
395
  m = self.method(:parse_geo)
300
396
  @parsers["GEO"] = m
397
+
398
+ #RECUR
399
+ m = self.method(:parse_recur)
400
+ @parsers["RRULE"] = m
401
+ @parsers["EXRULE"] = m
301
402
 
302
403
  end
303
404
 
@@ -317,11 +418,25 @@ module Icalendar
317
418
  # NOTE: invalid dates & times will be returned as strings...
318
419
  def parse_datetime(name, params, value)
319
420
  begin
320
- DateTime.parse(value)
421
+ if params["VALUE"] && params["VALUE"].first == "DATE"
422
+ result = Date.parse(value)
423
+ else
424
+ result = DateTime.parse(value)
425
+ if /Z$/ =~ value
426
+ timezone = "UTC"
427
+ else
428
+ timezone = params["TZID"].first if params["TZID"]
429
+ end
430
+ result.icalendar_tzid = timezone
431
+ end
432
+ result
321
433
  rescue Exception
322
434
  value
323
435
  end
324
-
436
+ end
437
+
438
+ def parse_recur(name, params, value)
439
+ ::Icalendar::RRule.new(name, params, value, self)
325
440
  end
326
441
 
327
442
  # Durations
@@ -0,0 +1,121 @@
1
+ =begin
2
+ Copyright (C) 2008 Sean Dague
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
+ # The following adds a bunch of mixins to the tzinfo class, with the
10
+ # intent on making it very easy to load in tzinfo data for generating
11
+ # ical events. With this you can do the following:
12
+ #
13
+ # require "icalendar/tzinfo"
14
+ #
15
+ # estart = DateTime.new(2008, 12, 29, 8, 0, 0)
16
+ # eend = DateTime.new(2008, 12, 29, 11, 0, 0)
17
+ # tstring = "America/Chicago"
18
+ #
19
+ # tz = TZInfo::Timezone.get(tstring)
20
+ # cal = Calendar.new
21
+ # # the mixins now generate all the timezone info for the date in question
22
+ # timezone = tz.ical_timezone(estart)
23
+ # cal.add(timezone)
24
+ #
25
+ # cal.event do
26
+ # dtstart estart
27
+ # dtend eend
28
+ # summary "Meeting with the man."
29
+ # description "Have a long lunch meeting and decide nothing..."
30
+ # klass "PRIVATE"
31
+ # end
32
+ #
33
+ # puts cal.to_ical
34
+ #
35
+ # The recurance rule calculations are hacky, and only start at the
36
+ # beginning of the current dst transition. I doubt this works for non
37
+ # dst areas yet. However, for a standard dst flipping zone, this
38
+ # seems to work fine (tested in Mozilla Thunderbird + Lightning).
39
+ # Future goal would be making this better.
40
+
41
+ # require "rubygems"
42
+ # require "tzinfo"
43
+
44
+ module TZInfo
45
+ class Timezone
46
+ def ical_timezone(date)
47
+ period = period_for_local(date)
48
+ timezone = Icalendar::Timezone.new
49
+ timezone.timezone_id = identifier
50
+ timezone.add(period.daylight)
51
+ timezone.add(period.standard)
52
+ return timezone
53
+ end
54
+ end
55
+
56
+ class TimezoneTransitionInfo
57
+ def offset_from
58
+ a = previous_offset.utc_total_offset
59
+ sprintf("%2.2d%2.2d", (a / 3600).to_i, ((a / 60) % 60).to_i)
60
+ end
61
+
62
+ def offset_to
63
+ a = offset.utc_total_offset
64
+ sprintf("%2.2d%2.2d", (a / 3600).to_i, ((a / 60) % 60).to_i)
65
+ end
66
+
67
+ def rrule
68
+ start = local_start.to_datetime
69
+ # this is somewhat of a hack, but seems to work ok
70
+ [sprintf(
71
+ "FREQ=YEARLY;BYMONTH=%d;BYDAY=%d%s",
72
+ start.month,
73
+ ((start.day - 1)/ 7).to_i + 1,
74
+ start.strftime("%a").upcase[0,2]
75
+ )]
76
+ end
77
+
78
+ def dtstart
79
+ local_start.to_datetime.strftime("%Y%m%dT%H%M%S")
80
+ end
81
+
82
+ end
83
+
84
+ class TimezonePeriod
85
+ def daylight
86
+ day = Icalendar::Daylight.new
87
+ if dst?
88
+ day.timezone_name = abbreviation.to_s
89
+ day.timezone_offset_from = start_transition.offset_from
90
+ day.timezone_offset_to = start_transition.offset_to
91
+ day.dtstart = start_transition.dtstart
92
+ day.recurrence_rules = start_transition.rrule
93
+ else
94
+ day.timezone_name = abbreviation.to_s.sub("ST","DT")
95
+ day.timezone_offset_from = end_transition.offset_from
96
+ day.timezone_offset_to = end_transition.offset_to
97
+ day.dtstart = end_transition.dtstart
98
+ day.recurrence_rules = end_transition.rrule
99
+ end
100
+ return day
101
+ end
102
+
103
+ def standard
104
+ std = Icalendar::Standard.new
105
+ if dst?
106
+ std.timezone_name = abbreviation.to_s.sub("DT","ST")
107
+ std.timezone_offset_from = end_transition.offset_from
108
+ std.timezone_offset_to = end_transition.offset_to
109
+ std.dtstart = end_transition.dtstart
110
+ std.recurrence_rules = end_transition.rrule
111
+ else
112
+ std.timezone_name = abbreviation.to_s
113
+ std.timezone_offset_from = start_transition.offset_from
114
+ std.timezone_offset_to = start_transition.offset_to
115
+ std.dtstart = start_transition.dtstart
116
+ std.recurrence_rules = start_transition.rrule
117
+ end
118
+ return std
119
+ end
120
+ end
121
+ end
@@ -3,6 +3,14 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
3
3
  require 'test/unit'
4
4
  require 'icalendar'
5
5
 
6
+ unless defined? 1.days
7
+ class Integer
8
+ def days
9
+ self * 60 * 60 * 24
10
+ end
11
+ end
12
+ end
13
+
6
14
  # Define a test event
7
15
  testEvent = <<EOS
8
16
  BEGIN:VEVENT
@@ -40,5 +48,209 @@ class TestEvent < Test::Unit::TestCase
40
48
 
41
49
  assert_equal("PRIVATE", event2.ip_class)
42
50
  end
51
+ end
52
+
53
+ class TestEventWithSpecifiedTimezone < Test::Unit::TestCase
54
+
55
+ def setup
56
+ src = <<EOS
57
+ BEGIN:VCALENDAR
58
+ METHOD:PUBLISH
59
+ CALSCALE:GREGORIAN
60
+ VERSION:2.0
61
+ BEGIN:VEVENT
62
+ UID:19970901T130000Z-123401@host.com
63
+ DTSTAMP:19970901T1300Z
64
+ DTSTART;TZID=America/Chicago:19970903T163000
65
+ DTEND;TZID=America/Chicago:19970903T190000
66
+ SUMMARY:Annual Employee Review
67
+ CLASS:PRIVATE
68
+ CATEGORIES:BUSINESS,HUMAN RESOURCES
69
+ END:VEVENT
70
+ END:VCALENDAR
71
+ EOS
72
+ @calendar = Icalendar.parse(src).first
73
+ @event = @calendar.events.first
74
+ end
75
+
76
+ def test_event_is_parsed
77
+ assert_not_nil(@event)
78
+ end
79
+
80
+ def test_dtstart_should_understand_icalendar_tzid
81
+ assert_respond_to(@event.dtstart, :icalendar_tzid)
82
+ end
83
+
84
+ def test_dtstart_tzid_should_be_correct
85
+ puts "#{@event.dtstart.icalendar_tzid} #{@event.dtstart}"
86
+ assert_equal("America/Chicago",@event.dtstart.icalendar_tzid)
87
+ end
88
+
89
+ def test_dtend_tzid_should_be_correct
90
+ assert_equal("America/Chicago",@event.dtend.icalendar_tzid)
91
+ end
92
+
93
+ end
94
+
95
+ class TestEventWithZuluTimezone < Test::Unit::TestCase
96
+
97
+ def setup
98
+ src = <<EOS
99
+ BEGIN:VCALENDAR
100
+ METHOD:PUBLISH
101
+ CALSCALE:GREGORIAN
102
+ VERSION:2.0
103
+ BEGIN:VEVENT
104
+ UID:19970901T130000Z-123401@host.com
105
+ DTSTAMP:19970901T1300Z
106
+ DTSTART:19970903T163000Z
107
+ DTEND:19970903T190000Z
108
+ SUMMARY:Annual Employee Review
109
+ CLASS:PRIVATE
110
+ CATEGORIES:BUSINESS,HUMAN RESOURCES
111
+ END:VEVENT
112
+ END:VCALENDAR
113
+ EOS
114
+ @calendar = Icalendar.parse(src).first
115
+ @event = @calendar.events.first
116
+ end
117
+
118
+ def test_event_is_parsed
119
+ assert_not_nil(@event)
120
+ end
121
+
122
+ def test_dtstart_tzid_should_be_correct
123
+ puts "#{@event.dtstart.icalendar_tzid} #{@event.dtstart}"
124
+ assert_equal("UTC",@event.dtstart.icalendar_tzid)
125
+ end
126
+
127
+ def test_dtend_tzid_should_be_correct
128
+ assert_equal("UTC",@event.dtend.icalendar_tzid)
129
+ end
43
130
 
44
131
  end
132
+
133
+ class TestEventWithFloatingTimezone < Test::Unit::TestCase
134
+
135
+ def setup
136
+ src = <<EOS
137
+ BEGIN:VCALENDAR
138
+ METHOD:PUBLISH
139
+ CALSCALE:GREGORIAN
140
+ VERSION:2.0
141
+ BEGIN:VEVENT
142
+ UID:19970901T130000Z-123401@host.com
143
+ DTSTAMP:19970901T1300Z
144
+ DTSTART:19970903T163000
145
+ DTEND:19970903T190000
146
+ SUMMARY:Annual Employee Review
147
+ CLASS:PRIVATE
148
+ CATEGORIES:BUSINESS,HUMAN RESOURCES
149
+ END:VEVENT
150
+ END:VCALENDAR
151
+ EOS
152
+ @calendar = Icalendar.parse(src).first
153
+ @event = @calendar.events.first
154
+ end
155
+
156
+ def test_event_is_parsed
157
+ assert_not_nil(@event)
158
+ end
159
+
160
+ def test_dtstart_tzid_should_be_nil
161
+ puts "#{@event.dtstart.icalendar_tzid.inspect} #{@event.dtstart}"
162
+ assert_nil(@event.dtstart.icalendar_tzid)
163
+ end
164
+
165
+ def test_dtend_tzid_should_be_nil
166
+ assert_nil(@event.dtend.icalendar_tzid)
167
+ end
168
+
169
+ end
170
+
171
+ class TestAllDayEventWithoutTime < Test::Unit::TestCase
172
+
173
+ def setup
174
+ src = <<EOS
175
+ BEGIN:VCALENDAR
176
+ VERSION:2.0
177
+ X-WR-CALNAME:New Event
178
+ PRODID:-//Apple Computer\, Inc//iCal 2.0//EN
179
+ X-WR-RELCALID:3A016BE7-8932-4456-8ABD-C8F7EEC5963A
180
+ X-WR-TIMEZONE:Europe/London
181
+ CALSCALE:GREGORIAN
182
+ METHOD:PUBLISH
183
+ BEGIN:VEVENT
184
+ DTSTART;VALUE=DATE:20090110
185
+ DTEND;VALUE=DATE:20090111
186
+ SUMMARY:New Event
187
+ UID:3829F33C-F601-49AC-A3A5-C3AC4A6A3483
188
+ SEQUENCE:4
189
+ DTSTAMP:20090109T184719Z
190
+ END:VEVENT
191
+ END:VCALENDAR
192
+ EOS
193
+ @calendar = Icalendar.parse(src).first
194
+ @event = @calendar.events.first
195
+ end
196
+
197
+ def test_event_is_parsed
198
+ assert_not_nil(@event)
199
+ end
200
+
201
+ def test_dtstart_set_correctly
202
+ assert_equal("20090110", @event.dtstart.to_ical)
203
+ end
204
+
205
+ end
206
+
207
+ class TestRecurringEventWithCount < Test::Unit::TestCase
208
+ # DTSTART;TZID=US-Eastern:19970902T090000
209
+ # RRULE:FREQ=DAILY;COUNT=10
210
+ # ==> (1997 9:00 AM EDT)September 2-11
211
+
212
+ def setup
213
+ src = <<EOS
214
+ BEGIN:VCALENDAR
215
+ METHOD:PUBLISH
216
+ CALSCALE:GREGORIAN
217
+ VERSION:2.0
218
+ BEGIN:VEVENT
219
+ UID:19970901T130000Z-123401@host.com
220
+ DTSTAMP:19970901T1300Z
221
+ DTSTART:19970902T090000Z
222
+ DTEND:19970902T100000Z
223
+ RRULE:FREQ=DAILY;COUNT=10
224
+ SUMMARY:Annual Employee Review
225
+ CLASS:PRIVATE
226
+ CATEGORIES:BUSINESS,HUMAN RESOURCES
227
+ END:VEVENT
228
+ END:VCALENDAR
229
+ EOS
230
+ @calendar = Icalendar.parse(src).first
231
+ @event = @calendar.events.first
232
+ end
233
+
234
+ def test_event_is_parsed
235
+ assert_not_nil(@event)
236
+ end
237
+
238
+ def test_recurrence_rules_should_return_a_recurrence_rule_array
239
+ assert_equal 1, @event.recurrence_rules.length
240
+ assert_kind_of(Icalendar::RRule, @event.recurrence_rules.first)
241
+ end
242
+
243
+ def test_occurrences_after_with_start_before_start_at_should_return_count_occurrences
244
+ assert_equal 10, @event.occurrences_starting(Time.utc(1997, 9, 2, 8, 30, 0, 0)).length
245
+ end
246
+
247
+ # def test_occurrences_after_with_start_before_start_at_should_return_an_event_with_the_dtstart_as_the_first_event
248
+ # assert_equal @event.dtstart.to_s, @event.occurrences_starting(Time.utc(1997, 9, 2, 8, 30, 0, 0)).first.dtstart.to_s
249
+ # end
250
+ #
251
+ # def test_occurrences_after_with_start_before_start_at_should_return_events_with_the_correct_dtstart_values
252
+ # expected = (0..9).map {|delta| (@event.dtstart + delta).to_s}
253
+ # assert_equal expected, @event.occurrences_starting(Time.utc(1997, 9, 2, 8, 30, 0, 0)).map {|occurence| occurence.dtstart.to_s}
254
+ # end
255
+ end
256
+
@@ -0,0 +1,67 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
2
+
3
+ require 'test/unit'
4
+ require 'icalendar'
5
+
6
+ class TestTimezone < Test::Unit::TestCase
7
+
8
+ # Create a calendar with an event for each test.
9
+ def setup
10
+ @cal = Icalendar::Calendar.new
11
+ # Define a test timezone
12
+ @testTimezone = %Q(BEGIN:VTIMEZONE\r\nTZID:America/Chicago\r\nBEGIN:STANDARD\r\nTZOFFSETTO:-0600\r\nRRULE:FREQ=YEARLY\\;BYMONTH=11\\;BYDAY=1SU\r\nTZOFFSETFROM:-0500\r\nDTSTART:19701101T020000\r\nTZNAME:CST\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZOFFSETTO:-0500\r\nRRULE:FREQ=YEARLY\\;BYMONTH=3\\;BYDAY=2SU\r\nTZOFFSETFROM:-0600\r\nDTSTART:19700308TO20000\r\nTZNAME:CDT\r\nEND:DAYLIGHT\r\nEND:VTIMEZONE\r\n)
13
+ end
14
+
15
+ def test_new
16
+ @tz = Icalendar::Timezone.new
17
+ assert(@tz)
18
+ end
19
+
20
+ def test_raw_generation
21
+ timezone = Icalendar::Timezone.new
22
+ daylight = Icalendar::Daylight.new
23
+ standard = Icalendar::Standard.new
24
+
25
+ timezone.timezone_id = "America/Chicago"
26
+
27
+ daylight.timezone_offset_from = "-0600"
28
+ daylight.timezone_offset_to = "-0500"
29
+ daylight.timezone_name = "CDT"
30
+ daylight.dtstart = "19700308TO20000"
31
+ daylight.recurrence_rules = ["FREQ=YEARLY;BYMONTH=3;BYDAY=2SU"]
32
+
33
+ standard.timezone_offset_from = "-0500"
34
+ standard.timezone_offset_to = "-0600"
35
+ standard.timezone_name = "CST"
36
+ standard.dtstart = "19701101T020000"
37
+ standard.recurrence_rules = ["FREQ=YEARLY;BYMONTH=11;BYDAY=1SU"]
38
+
39
+ timezone.add(standard)
40
+ timezone.add(daylight)
41
+ @cal.add(timezone)
42
+ assert_equal(@testTimezone, @cal.timezones.first.to_ical)
43
+ end
44
+
45
+ def test_block_creation
46
+ @cal.timezone do
47
+ timezone_id "America/Chicago"
48
+
49
+ daylight do
50
+ timezone_offset_from "-0600"
51
+ timezone_offset_to "-0500"
52
+ timezone_name "CDT"
53
+ dtstart "19700308TO20000"
54
+ add_recurrence_rule "FREQ=YEARLY;BYMONTH=3;BYDAY=2SU"
55
+ end
56
+
57
+ standard do
58
+ timezone_offset_from "-0500"
59
+ timezone_offset_to "-0600"
60
+ timezone_name "CST"
61
+ dtstart "19701101T020000"
62
+ add_recurrence_rule "FREQ=YEARLY;BYMONTH=11;BYDAY=1SU"
63
+ end
64
+ end
65
+ assert_equal(@testTimezone, @cal.timezones.first.to_ical)
66
+ end
67
+ end
@@ -18,7 +18,7 @@ LAST-MODIFIED:19960817T133000
18
18
  SEQUENCE:2
19
19
  ORGANIZER:mailto:joe@example.com?subject=Ruby
20
20
  UID:foobar
21
- X-TIME-OF-DAY:111736
21
+ X-TIME-OF-DAY:101736
22
22
  CATEGORIES:foo\\,bar\\,baz
23
23
  GEO:46.01\\;8.57
24
24
  DESCRIPTION:desc
@@ -60,7 +60,7 @@ EOS
60
60
  timestamp DateTime.parse("2006-07-20T17:40:52+0200")
61
61
 
62
62
  # Time
63
- x_time_of_day Time.at(123456)
63
+ x_time_of_day Time.at(123456).utc
64
64
 
65
65
  uid "foobar"
66
66
  end
File without changes
metadata CHANGED
@@ -1,98 +1,109 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.9.4
3
- specification_version: 1
4
2
  name: icalendar
5
3
  version: !ruby/object:Gem::Version
6
- version: 1.0.2
7
- date: 2007-11-25 00:00:00 +01:00
8
- summary: A ruby implementation of the iCalendar specification (RFC-2445).
9
- require_paths:
10
- - lib
11
- email: rosejn@gmail.com
12
- homepage: http://icalendar.rubyforge.org/
13
- rubyforge_project:
14
- description: Implements the iCalendar specification (RFC-2445) in Ruby. This allows for the generation and parsing of .ics files, which are used by a variety of calendaring applications.
15
- autorequire: icalendar
16
- default_executable:
17
- bindir: bin
18
- has_rdoc: true
19
- required_ruby_version: !ruby/object:Gem::Version::Requirement
20
- requirements:
21
- - - ">"
22
- - !ruby/object:Gem::Version
23
- version: 0.0.0
24
- version:
4
+ version: 1.1.0
25
5
  platform: ruby
26
- signing_key:
27
- cert_chain:
28
- post_install_message:
29
6
  authors:
30
- - Jeff Rose
7
+ - Sean Dague
8
+ autorequire: icalendar
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-29 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Implements the iCalendar specification (RFC-2445) in Ruby. This allows for the generation and parsing of .ics files, which are used by a variety of calendaring applications.
17
+ email: sean@dague.net
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ - COPYING
25
+ - GPL
31
26
  files:
32
- - test/component
33
- - test/component/event_test.rb
34
- - test/component/todo_test.rb
27
+ - test/calendar_test.rb
35
28
  - test/parameter_test.rb
36
29
  - test/interactive.rb
37
- - test/coverage
30
+ - test/conversions_test.rb
38
31
  - test/component_test.rb
32
+ - test/parser_test.rb
33
+ - test/read_write.rb
39
34
  - test/fixtures
40
- - test/fixtures/simplecal.ics
41
- - test/fixtures/folding.ics
42
35
  - test/fixtures/single_event.ics
36
+ - test/fixtures/folding.ics
37
+ - test/fixtures/simplecal.ics
43
38
  - test/fixtures/life.ics
44
- - test/read_write.rb
45
- - test/calendar_test.rb
46
- - test/parser_test.rb
47
- - test/conversions_test.rb
39
+ - test/component
40
+ - test/component/timezone_test.rb
41
+ - test/component/todo_test.rb
42
+ - test/component/event_test.rb
43
+ - test/coverage
44
+ - test/coverage/STUB
48
45
  - lib/icalendar
46
+ - lib/icalendar/parameter.rb
47
+ - lib/icalendar/component.rb
48
+ - lib/icalendar/base.rb
49
+ - lib/icalendar/parser.rb
50
+ - lib/icalendar/tzinfo.rb
51
+ - lib/icalendar/calendar.rb
49
52
  - lib/icalendar/component
50
53
  - lib/icalendar/component/alarm.rb
54
+ - lib/icalendar/component/todo.rb
51
55
  - lib/icalendar/component/event.rb
52
- - lib/icalendar/component/freebusy.rb
53
56
  - lib/icalendar/component/journal.rb
54
57
  - lib/icalendar/component/timezone.rb
55
- - lib/icalendar/component/todo.rb
58
+ - lib/icalendar/component/freebusy.rb
56
59
  - lib/icalendar/conversions.rb
57
- - lib/icalendar/parameter.rb
58
- - lib/icalendar/component.rb
59
60
  - lib/icalendar/helpers.rb
60
- - lib/icalendar/parser.rb
61
- - lib/icalendar/calendar.rb
62
- - lib/icalendar/base.rb
63
- - lib/hash_attrs.rb
64
- - lib/icalendar.rb
65
61
  - lib/meta.rb
66
- - docs/api
62
+ - lib/icalendar.rb
63
+ - lib/hash_attrs.rb
67
64
  - docs/rfcs
65
+ - docs/rfcs/rfc2446.pdf
66
+ - docs/rfcs/rfc2426.pdf
68
67
  - docs/rfcs/itip_notes.txt
68
+ - docs/rfcs/rfc2447.pdf
69
69
  - docs/rfcs/rfc2425.pdf
70
- - docs/rfcs/rfc2426.pdf
71
70
  - docs/rfcs/rfc2445.pdf
72
- - docs/rfcs/rfc2446.pdf
73
- - docs/rfcs/rfc2447.pdf
74
71
  - docs/rfcs/rfc3283.txt
72
+ - docs/api
73
+ - docs/api/STUB
75
74
  - examples/single_event.ics
76
- - examples/create_cal.rb
77
75
  - examples/parse_cal.rb
76
+ - examples/create_cal.rb
78
77
  - Rakefile
79
78
  - README
80
79
  - COPYING
81
80
  - GPL
82
- test_files: []
83
-
81
+ has_rdoc: true
82
+ homepage: http://icalendar.rubyforge.org/
83
+ post_install_message:
84
84
  rdoc_options:
85
85
  - --main
86
86
  - README
87
- extra_rdoc_files:
88
- - README
89
- - COPYING
90
- - GPL
91
- executables: []
92
-
93
- extensions: []
94
-
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: "0"
94
+ version:
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: "0"
100
+ version:
95
101
  requirements: []
96
102
 
97
- dependencies: []
103
+ rubyforge_project:
104
+ rubygems_version: 1.2.0
105
+ signing_key:
106
+ specification_version: 2
107
+ summary: A ruby implementation of the iCalendar specification (RFC-2445).
108
+ test_files: []
98
109