icalendar 2.3.0 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +5 -2
- data/History.txt +5 -0
- data/README.md +201 -125
- data/icalendar.gemspec +1 -1
- data/lib/icalendar.rb +1 -0
- data/lib/icalendar/calendar.rb +1 -0
- data/lib/icalendar/component.rb +6 -0
- data/lib/icalendar/has_properties.rb +6 -1
- data/lib/icalendar/parser.rb +20 -10
- data/lib/icalendar/value.rb +3 -2
- data/lib/icalendar/values/array.rb +4 -0
- data/lib/icalendar/values/date.rb +9 -1
- data/lib/icalendar/values/date_or_date_time.rb +29 -0
- data/lib/icalendar/version.rb +1 -1
- data/spec/calendar_spec.rb +1 -1
- data/spec/event_spec.rb +12 -0
- data/spec/fixtures/event.ics +17 -0
- data/spec/parser_spec.rb +11 -0
- data/spec/roundtrip_spec.rb +10 -8
- data/spec/spec_helper.rb +0 -1
- data/spec/timezone_spec.rb +7 -7
- data/spec/tzinfo_spec.rb +6 -6
- data/spec/values/date_or_date_time_spec.rb +41 -0
- data/spec/values/text_spec.rb +6 -0
- metadata +10 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 851d909893eaa853f0196567cf367c7062e725f8
|
4
|
+
data.tar.gz: d218b26b7f7b9b8f004308fab26290538c542e0b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7e47ce5836cf569bc8a8dfa79d642093302587ae6cff544162ad9f26197c20df73d3062e1bb74a52e21c9a765a0fa8ce7469f9c3342520528bbd660647249616
|
7
|
+
data.tar.gz: fd89f85a3cb86ebb50dbfff5db479bfcf7232c7f7ef061ae2d60fcb5d8cb48bb876aff4604b3ec50e2bcecfa25b2d86a0374de5c9d473d09ba616a52377f2421
|
data/.travis.yml
CHANGED
data/History.txt
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
=== 2.4.0 2015-07-04
|
2
|
+
* Enable parsing individual ICalendar components - Patrick Schnetger
|
3
|
+
* many bug fixes. Thanks to Quan Sun, Garry Shutler, Ryan Bigg, Patrick Schnetger and others
|
4
|
+
* README/documentation updates. Thanks to JonMidhir and Hendrik Sollich
|
5
|
+
|
1
6
|
=== 2.3.0 2015-04-26
|
2
7
|
* fix value parameter for properties with multiple values
|
3
8
|
* fix error when assigning Icalendar::Values::Array to a component
|
data/README.md
CHANGED
@@ -38,51 +38,79 @@ EXAMPLES
|
|
38
38
|
|
39
39
|
### Creating calendars and events ###
|
40
40
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
41
|
+
```ruby
|
42
|
+
require 'icalendar'
|
43
|
+
|
44
|
+
# Create a calendar with an event (standard method)
|
45
|
+
cal = Icalendar::Calendar.new
|
46
|
+
cal.event do |e|
|
47
|
+
e.dtstart = Icalendar::Values::Date.new('20050428')
|
48
|
+
e.dtend = Icalendar::Values::Date.new('20050429')
|
49
|
+
e.summary = "Meeting with the man."
|
50
|
+
e.description = "Have a long lunch meeting and decide nothing..."
|
51
|
+
e.ip_class = "PRIVATE"
|
52
|
+
end
|
53
|
+
|
54
|
+
cal.publish
|
55
|
+
```
|
54
56
|
|
55
57
|
#### Or you can make events like this ####
|
56
58
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
59
|
+
```ruby
|
60
|
+
event = Icalendar::Event.new
|
61
|
+
event.dtstart = DateTime.civil(2006, 6, 23, 8, 30)
|
62
|
+
event.summary = "A great event!"
|
63
|
+
cal.add_event(event)
|
61
64
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
+
event2 = cal.event # This automatically adds the event to the calendar
|
66
|
+
event2.dtstart = DateTime.civil(2006, 6, 24, 8, 30)
|
67
|
+
event2.summary = "Another great event!"
|
68
|
+
```
|
65
69
|
|
66
70
|
#### Support for property parameters ####
|
67
71
|
|
68
|
-
|
72
|
+
```ruby
|
73
|
+
params = {"altrep" => "http://my.language.net", "language" => "SPANISH"}
|
74
|
+
|
75
|
+
event = cal.event do |e|
|
76
|
+
e.dtstart = Icalendar::Values::Date.new('20050428')
|
77
|
+
e.dtend = Icalendar::Values::Date.new('20050429')
|
78
|
+
e.summary = Icalendar::Values::Text.new "This is a summary with params.", params
|
79
|
+
end
|
80
|
+
event.summary.ical_params #=> {'altrep' => 'http://my.language.net', 'language' => 'SPANISH'}
|
81
|
+
|
82
|
+
# or
|
83
|
+
|
84
|
+
event = cal.event do |e|
|
85
|
+
e.dtstart = Icalendar::Values::Date.new('20050428')
|
86
|
+
e.dtend = Icalendar::Values::Date.new('20050429')
|
87
|
+
e.summary = "This is a summary with params."
|
88
|
+
e.summary.ical_params = params
|
89
|
+
end
|
90
|
+
event.summary.ical_params #=> {'altrep' => 'http://my.language.net', 'language' => 'SPANISH'}
|
91
|
+
```
|
92
|
+
|
93
|
+
#### Support for Dates or DateTimes
|
69
94
|
|
70
|
-
|
71
|
-
e.dtstart = Icalendar::Values::Date.new('20050428')
|
72
|
-
e.dtend = Icalendar::Values::Date.new('20050429')
|
73
|
-
e.summary = Icalendar::Values::Text.new "This is a summary with params.", params
|
74
|
-
end
|
75
|
-
event.summary.ical_params #=> {'altrep' => 'http://my.language.net', 'language' => 'SPANISH'}
|
95
|
+
Sometimes we don't care if an event's start or end are `Date` or `DateTime` objects. For this, we can use `DateOrDateTime.new(value).call`
|
76
96
|
|
77
|
-
|
97
|
+
```ruby
|
98
|
+
event = cal.event do |e|
|
99
|
+
e.dtstart = Icalendar::Values::DateOrDateTime.new('20140924').call
|
100
|
+
e.dtend = Icalendar::Values::DateOrDateTime.new('20140924').call
|
101
|
+
e.summary = 'This is an all-day event, because DateOrDateTime will return Dates'
|
102
|
+
end
|
103
|
+
```
|
78
104
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
105
|
+
#### Support for URLs
|
106
|
+
|
107
|
+
For clients that can parse and display a URL associated with an event, it's possible to assign one.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
event = cal.event do |e|
|
111
|
+
e.url = 'https://example.com'
|
112
|
+
end
|
113
|
+
```
|
86
114
|
|
87
115
|
#### We can output the calendar as a string ####
|
88
116
|
|
@@ -94,30 +122,32 @@ ALARMS
|
|
94
122
|
|
95
123
|
### Within an event ###
|
96
124
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
125
|
+
```ruby
|
126
|
+
cal.event do |e|
|
127
|
+
# ...other event properties
|
128
|
+
e.alarm do |a|
|
129
|
+
a.action = "EMAIL"
|
130
|
+
a.description = "This is an event reminder" # email body (required)
|
131
|
+
a.summary = "Alarm notification" # email subject (required)
|
132
|
+
a.attendee = %w(mailto:me@my-domain.com mailto:me-too@my-domain.com) # one or more email recipients (required)
|
133
|
+
a.append_attendee "mailto:me-three@my-domain.com"
|
134
|
+
a.trigger = "-PT15M" # 15 minutes before
|
135
|
+
a.append_attach Icalendar::Values::Uri.new "ftp://host.com/novo-procs/felizano.exe", "fmttype" => "application/binary" # email attachments (optional)
|
136
|
+
end
|
137
|
+
|
138
|
+
e.alarm do |a|
|
139
|
+
a.action = "DISPLAY" # This line isn't necessary, it's the default
|
140
|
+
a.summary = "Alarm notification"
|
141
|
+
a.trigger = "-P1DT0H0M0S" # 1 day before
|
142
|
+
end
|
143
|
+
|
144
|
+
e.alarm do |a|
|
145
|
+
a.action = "AUDIO"
|
146
|
+
a.trigger = "-PT15M"
|
147
|
+
a.append_attach "Basso"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
```
|
121
151
|
|
122
152
|
#### Output ####
|
123
153
|
|
@@ -147,38 +177,42 @@ ALARMS
|
|
147
177
|
|
148
178
|
Calling the `event.alarm` method will create an alarm if one doesn't exist. To check if an event has an alarm use the `has_alarm?` method.
|
149
179
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
180
|
+
```ruby
|
181
|
+
event.has_alarm?
|
182
|
+
# => false
|
183
|
+
|
184
|
+
event.alarm
|
185
|
+
# => #<Icalendar::Alarm ... >
|
186
|
+
|
187
|
+
event.has_alarm?
|
188
|
+
#=> true
|
189
|
+
```
|
158
190
|
|
159
191
|
TIMEZONES
|
160
192
|
---
|
161
193
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
194
|
+
```ruby
|
195
|
+
cal = Icalendar::Calendar.new
|
196
|
+
cal.timezone do |t|
|
197
|
+
t.tzid = "America/Chicago"
|
198
|
+
|
199
|
+
t.daylight do |d|
|
200
|
+
d.tzoffsetfrom = "-0600"
|
201
|
+
d.tzoffsetto = "-0500"
|
202
|
+
d.tzname = "CDT"
|
203
|
+
d.dtstart = "19700308T020000"
|
204
|
+
d.rrule = "FREQ=YEARLY;BYMONTH=3;BYDAY=2SU"
|
205
|
+
end
|
206
|
+
|
207
|
+
t.standard do |s|
|
208
|
+
s.tzoffsetfrom = "-0500"
|
209
|
+
s.tzoffsetto = "-0600"
|
210
|
+
s.tzname = "CST"
|
211
|
+
s.dtstart = "19701101T020000"
|
212
|
+
s.rrule = "FREQ=YEARLY;BYMONTH=11;BYDAY=1SU"
|
213
|
+
end
|
214
|
+
end
|
215
|
+
```
|
182
216
|
|
183
217
|
#### Output ####
|
184
218
|
|
@@ -207,53 +241,76 @@ iCalendar has been tested and works with `tzinfo` versions 0.3 and 1.1
|
|
207
241
|
|
208
242
|
#### Example ####
|
209
243
|
|
210
|
-
|
244
|
+
```ruby
|
245
|
+
require 'icalendar/tzinfo'
|
211
246
|
|
212
|
-
|
247
|
+
cal = Icalendar::Calendar.new
|
213
248
|
|
214
|
-
|
215
|
-
|
249
|
+
event_start = DateTime.new 2008, 12, 29, 8, 0, 0
|
250
|
+
event_end = DateTime.new 2008, 12, 29, 11, 0, 0
|
216
251
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
252
|
+
tzid = "America/Chicago"
|
253
|
+
tz = TZInfo::Timezone.get tzid
|
254
|
+
timezone = tz.ical_timezone event_start
|
255
|
+
cal.add_timezone timezone
|
221
256
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
257
|
+
cal.event do |e|
|
258
|
+
e.dtstart = Icalendar::Values::DateTime.new event_start, 'tzid' => tzid
|
259
|
+
e.dtend = Icalendar::Values::DateTime.new event_end, 'tzid' => tzid
|
260
|
+
e.summary = "Meeting with the man."
|
261
|
+
e.description = "Have a long lunch meeting and decide nothing..."
|
262
|
+
e.organizer = "mailto:jsmith@example.com"
|
263
|
+
e.organizer = Icalendar::Values::CalAddress.new("mailto:jsmith@example.com", cn: 'John Smith')
|
264
|
+
end
|
265
|
+
```
|
230
266
|
|
231
267
|
|
232
268
|
Parsing iCalendars
|
233
269
|
---
|
234
270
|
|
235
|
-
|
236
|
-
|
271
|
+
```ruby
|
272
|
+
# Open a file or pass a string to the parser
|
273
|
+
cal_file = File.open("single_event.ics")
|
237
274
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
275
|
+
# Parser returns an array of calendars because a single file
|
276
|
+
# can have multiple calendars.
|
277
|
+
cals = Icalendar::Calendar.parse(cal_file)
|
278
|
+
cal = cals.first
|
242
279
|
|
243
|
-
|
244
|
-
|
280
|
+
# Now you can access the cal object in just the same way I created it
|
281
|
+
event = cal.events.first
|
245
282
|
|
246
|
-
|
247
|
-
|
248
|
-
|
283
|
+
puts "start date-time: #{event.dtstart}"
|
284
|
+
puts "start date-time timezone: #{event.dtstart.ical_params['tzid']}"
|
285
|
+
puts "summary: #{event.summary}"
|
286
|
+
```
|
249
287
|
|
250
288
|
You can also create a `Parser` instance directly, this can be used to enable
|
251
289
|
strict parsing:
|
252
290
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
291
|
+
```ruby
|
292
|
+
# Sometimes you want to strongly verify only rfc-approved properties are
|
293
|
+
# used
|
294
|
+
strict_parser = Icalendar::Parser.new(cal_file, true)
|
295
|
+
cal = strict_parser.parse
|
296
|
+
```
|
297
|
+
|
298
|
+
Parsing Components (e.g. Events)
|
299
|
+
---
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
# Open a file or pass a string to the parser
|
303
|
+
event_file = File.open("event.ics")
|
304
|
+
|
305
|
+
# Parser returns an array of events because a single file
|
306
|
+
# can have multiple events.
|
307
|
+
events = Icalendar::Event.parse(event_file)
|
308
|
+
event = events.first
|
309
|
+
|
310
|
+
puts "start date-time: #{event.dtstart}"
|
311
|
+
puts "start date-time timezone: #{event.dtstart.ical_params['tzid']}"
|
312
|
+
puts "summary: #{event.summary}"
|
313
|
+
```
|
257
314
|
|
258
315
|
Finders
|
259
316
|
---
|
@@ -262,15 +319,17 @@ Often times in web apps and other interactive applications you'll need to
|
|
262
319
|
lookup items in a calendar to make changes or get details. Now you can find
|
263
320
|
everything by the unique id automatically associated with all components.
|
264
321
|
|
265
|
-
|
266
|
-
|
267
|
-
|
322
|
+
```ruby
|
323
|
+
cal = Calendar.new
|
324
|
+
10.times { cal.event } # Create 10 events with only default data.
|
325
|
+
some_event = cal.events[5] # Grab it from the array of events
|
268
326
|
|
269
|
-
|
270
|
-
|
327
|
+
# Use the uid as the key in your app
|
328
|
+
key = some_event.uid
|
271
329
|
|
272
|
-
|
273
|
-
|
330
|
+
# so later you can find it.
|
331
|
+
same_event = cal.find_event(key)
|
332
|
+
```
|
274
333
|
|
275
334
|
Examples
|
276
335
|
---
|
@@ -311,3 +370,20 @@ Support & Contributions
|
|
311
370
|
|
312
371
|
Please submit pull requests from a rebased topic branch and
|
313
372
|
include tests for all bugs and features.
|
373
|
+
|
374
|
+
Contributor Code of Conduct
|
375
|
+
---
|
376
|
+
|
377
|
+
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
378
|
+
|
379
|
+
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
|
380
|
+
|
381
|
+
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
382
|
+
|
383
|
+
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
384
|
+
|
385
|
+
This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
|
386
|
+
|
387
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
388
|
+
|
389
|
+
This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/)
|
data/icalendar.gemspec
CHANGED
@@ -47,6 +47,6 @@ ActiveSupport is required for TimeWithZone support, but not required for general
|
|
47
47
|
s.add_development_dependency 'i18n', '< 0.7.0'
|
48
48
|
|
49
49
|
s.add_development_dependency 'timecop', '~> 0.7.0'
|
50
|
-
s.add_development_dependency 'rspec', '~>
|
50
|
+
s.add_development_dependency 'rspec', '~> 3.0'
|
51
51
|
s.add_development_dependency 'simplecov', '~> 0.8'
|
52
52
|
end
|
data/lib/icalendar.rb
CHANGED
@@ -13,6 +13,7 @@ module Icalendar
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def self.parse(source, single = false)
|
16
|
+
warn "**** DEPRECATION WARNING ****\nIcalender.parse will be removed. Please switch to Icalendar::Calendar.parse."
|
16
17
|
calendars = Parser.new(source).parse
|
17
18
|
single ? calendars.first : calendars
|
18
19
|
end
|
data/lib/icalendar/calendar.rb
CHANGED
data/lib/icalendar/component.rb
CHANGED
@@ -10,6 +10,12 @@ module Icalendar
|
|
10
10
|
attr_reader :ical_name
|
11
11
|
attr_accessor :parent
|
12
12
|
|
13
|
+
def self.parse(source)
|
14
|
+
parser = Parser.new(source)
|
15
|
+
parser.component = self.new
|
16
|
+
parser.parse
|
17
|
+
end
|
18
|
+
|
13
19
|
def initialize(name, ical_name = nil)
|
14
20
|
@name = name
|
15
21
|
@ical_name = ical_name || "V#{name.upcase}"
|
@@ -141,7 +141,12 @@ module Icalendar
|
|
141
141
|
property_var = "@#{prop}"
|
142
142
|
|
143
143
|
define_method "#{prop}=" do |value|
|
144
|
-
|
144
|
+
mapped = map_property_value value, klass, true
|
145
|
+
if mapped.is_a? Icalendar::Values::Array
|
146
|
+
instance_variable_set property_var, mapped.to_a.compact
|
147
|
+
else
|
148
|
+
instance_variable_set property_var, [mapped].compact
|
149
|
+
end
|
145
150
|
end
|
146
151
|
|
147
152
|
define_method prop do
|
data/lib/icalendar/parser.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Icalendar
|
2
2
|
|
3
3
|
class Parser
|
4
|
-
|
4
|
+
attr_writer :component
|
5
5
|
attr_reader :source, :strict
|
6
6
|
|
7
7
|
def initialize(source, strict = false)
|
@@ -21,21 +21,22 @@ module Icalendar
|
|
21
21
|
def parse
|
22
22
|
source.rewind
|
23
23
|
read_in_data
|
24
|
-
|
24
|
+
components = []
|
25
25
|
while (fields = next_fields)
|
26
|
-
if fields[:name] == 'begin' && fields[:value].downcase ==
|
27
|
-
|
26
|
+
if fields[:name] == 'begin' && fields[:value].downcase == component.ical_name.downcase
|
27
|
+
components << parse_component(component)
|
28
28
|
end
|
29
29
|
end
|
30
|
-
|
30
|
+
components
|
31
31
|
end
|
32
32
|
|
33
33
|
def parse_property(component, fields = nil)
|
34
34
|
fields = next_fields if fields.nil?
|
35
|
-
prop_value = wrap_property_value component, fields
|
36
35
|
prop_name = %w(class method).include?(fields[:name]) ? "ip_#{fields[:name]}" : fields[:name]
|
36
|
+
multi_property = component.class.multiple_properties.include? prop_name
|
37
|
+
prop_value = wrap_property_value component, fields, multi_property
|
37
38
|
begin
|
38
|
-
method_name = if
|
39
|
+
method_name = if multi_property
|
39
40
|
"append_#{prop_name}"
|
40
41
|
else
|
41
42
|
"#{prop_name}="
|
@@ -52,10 +53,10 @@ module Icalendar
|
|
52
53
|
end
|
53
54
|
end
|
54
55
|
|
55
|
-
def wrap_property_value(component, fields)
|
56
|
+
def wrap_property_value(component, fields, multi_property)
|
56
57
|
klass = get_wrapper_class component, fields
|
57
|
-
if klass
|
58
|
-
delimiter =
|
58
|
+
if wrap_in_array? klass, fields[:value], multi_property
|
59
|
+
delimiter = fields[:value].match(/(?<!\\)([,;])/)[1]
|
59
60
|
Icalendar::Values::Array.new fields[:value].split(/(?<!\\)[;,]/),
|
60
61
|
klass,
|
61
62
|
fields[:params],
|
@@ -69,6 +70,11 @@ module Icalendar
|
|
69
70
|
retry
|
70
71
|
end
|
71
72
|
|
73
|
+
def wrap_in_array?(klass, value, multi_property)
|
74
|
+
klass.value_type != 'RECUR' &&
|
75
|
+
((multi_property && value =~ /(?<!\\)[,;]/) || value =~ /(?<!\\);/)
|
76
|
+
end
|
77
|
+
|
72
78
|
def get_wrapper_class(component, fields)
|
73
79
|
klass = component.class.default_property_types[fields[:name]]
|
74
80
|
if !fields[:params]['value'].nil?
|
@@ -87,6 +93,10 @@ module Icalendar
|
|
87
93
|
|
88
94
|
private
|
89
95
|
|
96
|
+
def component
|
97
|
+
@component ||= Icalendar::Calendar.new
|
98
|
+
end
|
99
|
+
|
90
100
|
def parse_component(component)
|
91
101
|
while (fields = next_fields)
|
92
102
|
if fields[:name] == 'end'
|
data/lib/icalendar/value.rb
CHANGED
@@ -55,7 +55,7 @@ module Icalendar
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def escape_param_value(value)
|
58
|
-
v = value.gsub
|
58
|
+
v = value.to_s.gsub('"', "'")
|
59
59
|
v =~ /[;:,]/ ? %("#{v}") : v
|
60
60
|
end
|
61
61
|
|
@@ -63,8 +63,9 @@ module Icalendar
|
|
63
63
|
|
64
64
|
end
|
65
65
|
|
66
|
-
#
|
66
|
+
# helpers; not actual iCalendar value type
|
67
67
|
require_relative 'values/array'
|
68
|
+
require_relative 'values/date_or_date_time'
|
68
69
|
|
69
70
|
# iCalendar value types
|
70
71
|
require_relative 'values/binary'
|
@@ -11,6 +11,10 @@ module Icalendar
|
|
11
11
|
value.map do |v|
|
12
12
|
if v.is_a? Icalendar::Values::Array
|
13
13
|
Icalendar::Values::Array.new v.value, klass, v.ical_params, delimiter: v.value_delimiter
|
14
|
+
elsif v.is_a? ::Array
|
15
|
+
Icalendar::Values::Array.new v, klass, params, delimiter: value_delimiter
|
16
|
+
elsif v.is_a? Icalendar::Value
|
17
|
+
v
|
14
18
|
else
|
15
19
|
klass.new v, params
|
16
20
|
end
|
@@ -8,7 +8,13 @@ module Icalendar
|
|
8
8
|
|
9
9
|
def initialize(value, params = {})
|
10
10
|
if value.is_a? String
|
11
|
-
|
11
|
+
begin
|
12
|
+
parsed_date = ::Date.strptime(value, FORMAT)
|
13
|
+
rescue ArgumentError => e
|
14
|
+
raise FormatError.new("Failed to parse \"#{value}\" - #{e.message}")
|
15
|
+
end
|
16
|
+
|
17
|
+
super parsed_date, params
|
12
18
|
elsif value.respond_to? :to_date
|
13
19
|
super value.to_date, params
|
14
20
|
else
|
@@ -28,6 +34,8 @@ module Icalendar
|
|
28
34
|
end
|
29
35
|
end
|
30
36
|
|
37
|
+
class FormatError < ArgumentError
|
38
|
+
end
|
31
39
|
end
|
32
40
|
|
33
41
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Icalendar
|
2
|
+
module Values
|
3
|
+
|
4
|
+
# DateOrDateTime can be used to set an attribute to either a Date or a DateTime value.
|
5
|
+
# It should not be used wihtout also invoking the `call` method.
|
6
|
+
class DateOrDateTime
|
7
|
+
|
8
|
+
attr_reader :value, :params, :parsed
|
9
|
+
def initialize(value, params = {})
|
10
|
+
@value = value
|
11
|
+
@params = params
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
@parsed ||= begin
|
16
|
+
Icalendar::Values::DateTime.new value, params
|
17
|
+
rescue Icalendar::Values::DateTime::FormatError
|
18
|
+
Icalendar::Values::Date.new value, params
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_ical
|
23
|
+
fail NoMethodError, 'You cannot use DateOrDateTime directly. Invoke `call` before `to_ical`'
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
data/lib/icalendar/version.rb
CHANGED
data/spec/calendar_spec.rb
CHANGED
@@ -71,7 +71,7 @@ describe Icalendar::Calendar do
|
|
71
71
|
|
72
72
|
describe '#add_event' do
|
73
73
|
it 'delegates to non add_ version' do
|
74
|
-
subject.
|
74
|
+
expect(subject).to receive(:event).with(ical_component)
|
75
75
|
subject.add_event ical_component
|
76
76
|
end
|
77
77
|
end
|
data/spec/event_spec.rb
CHANGED
@@ -96,6 +96,18 @@ describe Icalendar::Event do
|
|
96
96
|
end
|
97
97
|
end
|
98
98
|
|
99
|
+
describe '.parse' do
|
100
|
+
let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', fn) }
|
101
|
+
let(:fn) { 'event.ics' }
|
102
|
+
|
103
|
+
it 'should return an events array' do
|
104
|
+
events = Icalendar::Event.parse(source)
|
105
|
+
expect(events).to be_instance_of Array
|
106
|
+
expect(events.count).to be 1
|
107
|
+
expect(events.first).to be_instance_of Icalendar::Event
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
99
111
|
describe '#find_alarm' do
|
100
112
|
it 'should not respond_to find_alarm' do
|
101
113
|
expect(subject.respond_to?(:find_alarm)).to be false
|
@@ -0,0 +1,17 @@
|
|
1
|
+
BEGIN:VEVENT
|
2
|
+
DTSTAMP:20050118T211523Z
|
3
|
+
UID:bsuidfortestabc123
|
4
|
+
DTSTART;TZID=US-Mountain:20050120T170000
|
5
|
+
DTEND;TZID=US-Mountain:20050120T184500
|
6
|
+
CLASS:PRIVATE
|
7
|
+
GEO:37.386013;-122.0829322
|
8
|
+
ORGANIZER:mailto:joebob@random.net
|
9
|
+
PRIORITY:2
|
10
|
+
SUMMARY:This is a really long summary to test the method of unfolding lines
|
11
|
+
\, so I'm just going to make it a whole bunch of lines.
|
12
|
+
ATTACH:http://bush.sucks.org/impeach/him.rhtml
|
13
|
+
ATTACH:http://corporations-dominate.existence.net/why.rhtml
|
14
|
+
RDATE;TZID=US-Mountain:20050121T170000,20050122T170000
|
15
|
+
X-TEST-COMPONENT;QTEST="Hello, World":Shouldn't double double quotes
|
16
|
+
END:VEVENT
|
17
|
+
|
data/spec/parser_spec.rb
CHANGED
@@ -32,6 +32,17 @@ describe Icalendar::Parser do
|
|
32
32
|
expect(ics).to match 'EXDATE;VALUE=DATE:20120323,20130323'
|
33
33
|
end
|
34
34
|
end
|
35
|
+
context 'event.ics' do
|
36
|
+
let(:fn) { 'event.ics' }
|
37
|
+
|
38
|
+
before { subject.component = Icalendar::Event.new }
|
39
|
+
|
40
|
+
it 'returns an array of events' do
|
41
|
+
expect(subject.parse).to be_instance_of Array
|
42
|
+
expect(subject.parse.count).to be 1
|
43
|
+
expect(subject.parse[0]).to be_instance_of Icalendar::Event
|
44
|
+
end
|
45
|
+
end
|
35
46
|
end
|
36
47
|
|
37
48
|
describe '#parse with bad line' do
|
data/spec/roundtrip_spec.rb
CHANGED
@@ -6,12 +6,13 @@ describe Icalendar do
|
|
6
6
|
let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'single_event.ics') }
|
7
7
|
|
8
8
|
it 'will generate the same file as is parsed' do
|
9
|
-
|
9
|
+
ical = Icalendar::Calendar.parse(source).first.to_ical
|
10
|
+
expect(ical).to eq source
|
10
11
|
end
|
11
12
|
|
12
13
|
it 'array properties can be assigned to a new event' do
|
13
14
|
event = Icalendar::Event.new
|
14
|
-
parsed = Icalendar.parse
|
15
|
+
parsed = Icalendar::Calendar.parse(source).first
|
15
16
|
event.rdate = parsed.events.first.rdate
|
16
17
|
expect(event.rdate.first).to be_kind_of Icalendar::Values::Array
|
17
18
|
expect(event.rdate.first.ical_params).to eq 'tzid' => ['US-Mountain']
|
@@ -21,13 +22,14 @@ describe Icalendar do
|
|
21
22
|
describe 'timezone round trip' do
|
22
23
|
let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'timezone.ics') }
|
23
24
|
it 'will generate the same file as it parsed' do
|
24
|
-
|
25
|
+
ical = Icalendar::Calendar.parse(source).first.to_ical
|
26
|
+
expect(ical).to eq source
|
25
27
|
end
|
26
28
|
end
|
27
29
|
|
28
30
|
describe 'non-default values' do
|
29
31
|
let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'nondefault_values.ics') }
|
30
|
-
subject { Icalendar.parse(source
|
32
|
+
subject { Icalendar::Calendar.parse(source).first.events.first }
|
31
33
|
|
32
34
|
it 'will set dtstart to Date' do
|
33
35
|
expect(subject.dtstart.value).to eq ::Date.new(2006, 12, 15)
|
@@ -48,7 +50,7 @@ describe Icalendar do
|
|
48
50
|
|
49
51
|
describe 'sorting daily events' do
|
50
52
|
let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'two_day_events.ics') }
|
51
|
-
subject { Icalendar.parse(source
|
53
|
+
subject { Icalendar::Calendar.parse(source).first.events }
|
52
54
|
|
53
55
|
it 'sorts day events' do
|
54
56
|
events = subject.sort_by(&:dtstart)
|
@@ -60,7 +62,7 @@ describe Icalendar do
|
|
60
62
|
|
61
63
|
describe 'sorting time events' do
|
62
64
|
let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'two_time_events.ics') }
|
63
|
-
subject { Icalendar.parse(source
|
65
|
+
subject { Icalendar::Calendar.parse(source).first.events }
|
64
66
|
|
65
67
|
it 'sorts time events by start time' do
|
66
68
|
events = subject.sort_by(&:dtstart)
|
@@ -82,7 +84,7 @@ describe Icalendar do
|
|
82
84
|
|
83
85
|
describe 'sorting date / time events' do
|
84
86
|
let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'two_date_time_events.ics') }
|
85
|
-
subject { Icalendar.parse(source
|
87
|
+
subject { Icalendar::Calendar.parse(source).first.events }
|
86
88
|
|
87
89
|
it 'sorts time events' do
|
88
90
|
events = subject.sort_by(&:dtstart)
|
@@ -102,7 +104,7 @@ describe Icalendar do
|
|
102
104
|
|
103
105
|
context 'strict parser' do
|
104
106
|
let(:strict) { true }
|
105
|
-
specify { expect { subject.parse }.to raise_error }
|
107
|
+
specify { expect { subject.parse }.to raise_error(NoMethodError) }
|
106
108
|
end
|
107
109
|
|
108
110
|
context 'lenient parser' do
|
data/spec/spec_helper.rb
CHANGED
data/spec/timezone_spec.rb
CHANGED
@@ -7,25 +7,25 @@ describe Icalendar::Timezone do
|
|
7
7
|
|
8
8
|
context 'with both standard and daylight components' do
|
9
9
|
before(:each) do
|
10
|
-
subject.daylight { |d| d.
|
11
|
-
subject.standard { |s| s.
|
10
|
+
subject.daylight { |d| allow(d).to receive(:valid?).and_return true }
|
11
|
+
subject.standard { |s| allow(s).to receive(:valid?).and_return true }
|
12
12
|
end
|
13
13
|
|
14
14
|
it { should be_valid }
|
15
15
|
end
|
16
16
|
|
17
17
|
context 'with only standard' do
|
18
|
-
before(:each) { subject.standard { |s| s.
|
19
|
-
it {
|
18
|
+
before(:each) { subject.standard { |s| allow(s).to receive(:valid?).and_return true } }
|
19
|
+
it { expect(subject).to be_valid }
|
20
20
|
end
|
21
21
|
|
22
22
|
context 'with only daylight' do
|
23
|
-
before(:each) { subject.daylight { |d| d.
|
24
|
-
it {
|
23
|
+
before(:each) { subject.daylight { |d| allow(d).to receive(:valid?).and_return true } }
|
24
|
+
it { expect(subject).to be_valid }
|
25
25
|
end
|
26
26
|
|
27
27
|
context 'with neither standard or daylight' do
|
28
28
|
it { should_not be_valid }
|
29
29
|
end
|
30
30
|
end
|
31
|
-
end
|
31
|
+
end
|
data/spec/tzinfo_spec.rb
CHANGED
@@ -26,18 +26,18 @@ describe 'TZInfo::Timezone' do
|
|
26
26
|
end
|
27
27
|
|
28
28
|
describe 'no end transition' do
|
29
|
-
let(:tz) { TZInfo::Timezone.get '
|
29
|
+
let(:tz) { TZInfo::Timezone.get 'Asia/Shanghai' }
|
30
30
|
let(:date) { DateTime.now }
|
31
31
|
|
32
32
|
it 'only creates a standard component' do
|
33
33
|
expect(subject.to_ical).to eq <<-EXPECTED.gsub "\n", "\r\n"
|
34
34
|
BEGIN:VTIMEZONE
|
35
|
-
TZID:
|
35
|
+
TZID:Asia/Shanghai
|
36
36
|
BEGIN:STANDARD
|
37
|
-
DTSTART:
|
38
|
-
TZOFFSETFROM
|
39
|
-
TZOFFSETTO
|
40
|
-
TZNAME:
|
37
|
+
DTSTART:19910914T230000
|
38
|
+
TZOFFSETFROM:+0900
|
39
|
+
TZOFFSETTO:+0800
|
40
|
+
TZNAME:CST
|
41
41
|
END:STANDARD
|
42
42
|
END:VTIMEZONE
|
43
43
|
EXPECTED
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Icalendar::Values::DateOrDateTime do
|
4
|
+
|
5
|
+
subject { described_class.new value, params }
|
6
|
+
let(:params) { {} }
|
7
|
+
|
8
|
+
describe '#call' do
|
9
|
+
context 'DateTime value' do
|
10
|
+
let(:value) { '20140209T194355Z' }
|
11
|
+
|
12
|
+
it 'returns a DateTime object' do
|
13
|
+
expect(subject.call).to be_a_kind_of(Icalendar::Values::DateTime)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'has the proper value' do
|
17
|
+
expect(subject.call.value).to eq DateTime.new(2014, 2, 9, 19, 43, 55)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'Date value' do
|
22
|
+
let(:value) { '20140209' }
|
23
|
+
|
24
|
+
it 'returns a Date object' do
|
25
|
+
expect(subject.call).to be_a_kind_of(Icalendar::Values::Date)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'has the proper value' do
|
29
|
+
expect(subject.call.value).to eq Date.new(2014, 2, 9)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'unparseable date' do
|
34
|
+
let(:value) { '99999999' }
|
35
|
+
|
36
|
+
it 'raises an error including the unparseable time' do
|
37
|
+
expect { subject.call }.to raise_error(ArgumentError, %r{Failed to parse \"#{value}\"})
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/spec/values/text_spec.rb
CHANGED
@@ -68,5 +68,11 @@ describe Icalendar::Values::Text do
|
|
68
68
|
expect(subject.params_ical).to eq %(;PARAM="Hello, 'World'",GoodbyeMoon)
|
69
69
|
end
|
70
70
|
end
|
71
|
+
context 'nil value' do
|
72
|
+
let(:param_value) { nil }
|
73
|
+
it 'trats nil as blank' do
|
74
|
+
expect(subject.params_ical).to eq %(;PARAM=)
|
75
|
+
end
|
76
|
+
end
|
71
77
|
end
|
72
78
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: icalendar
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ryan Ahearn
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-07-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -114,14 +114,14 @@ dependencies:
|
|
114
114
|
requirements:
|
115
115
|
- - "~>"
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: '
|
117
|
+
version: '3.0'
|
118
118
|
type: :development
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version: '
|
124
|
+
version: '3.0'
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
126
|
name: simplecov
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -177,6 +177,7 @@ files:
|
|
177
177
|
- lib/icalendar/values/boolean.rb
|
178
178
|
- lib/icalendar/values/cal_address.rb
|
179
179
|
- lib/icalendar/values/date.rb
|
180
|
+
- lib/icalendar/values/date_or_date_time.rb
|
180
181
|
- lib/icalendar/values/date_time.rb
|
181
182
|
- lib/icalendar/values/duration.rb
|
182
183
|
- lib/icalendar/values/float.rb
|
@@ -193,6 +194,7 @@ files:
|
|
193
194
|
- spec/calendar_spec.rb
|
194
195
|
- spec/downcased_hash_spec.rb
|
195
196
|
- spec/event_spec.rb
|
197
|
+
- spec/fixtures/event.ics
|
196
198
|
- spec/fixtures/nondefault_values.ics
|
197
199
|
- spec/fixtures/nonstandard.ics
|
198
200
|
- spec/fixtures/recurrence.ics
|
@@ -211,6 +213,7 @@ files:
|
|
211
213
|
- spec/timezone_spec.rb
|
212
214
|
- spec/todo_spec.rb
|
213
215
|
- spec/tzinfo_spec.rb
|
216
|
+
- spec/values/date_or_date_time_spec.rb
|
214
217
|
- spec/values/date_time_spec.rb
|
215
218
|
- spec/values/duration_spec.rb
|
216
219
|
- spec/values/period_spec.rb
|
@@ -242,7 +245,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
242
245
|
version: '0'
|
243
246
|
requirements: []
|
244
247
|
rubyforge_project:
|
245
|
-
rubygems_version: 2.
|
248
|
+
rubygems_version: 2.5.1
|
246
249
|
signing_key:
|
247
250
|
specification_version: 4
|
248
251
|
summary: A ruby implementation of the iCalendar specification (RFC-5545).
|
@@ -251,6 +254,7 @@ test_files:
|
|
251
254
|
- spec/calendar_spec.rb
|
252
255
|
- spec/downcased_hash_spec.rb
|
253
256
|
- spec/event_spec.rb
|
257
|
+
- spec/fixtures/event.ics
|
254
258
|
- spec/fixtures/nondefault_values.ics
|
255
259
|
- spec/fixtures/nonstandard.ics
|
256
260
|
- spec/fixtures/recurrence.ics
|
@@ -269,6 +273,7 @@ test_files:
|
|
269
273
|
- spec/timezone_spec.rb
|
270
274
|
- spec/todo_spec.rb
|
271
275
|
- spec/tzinfo_spec.rb
|
276
|
+
- spec/values/date_or_date_time_spec.rb
|
272
277
|
- spec/values/date_time_spec.rb
|
273
278
|
- spec/values/duration_spec.rb
|
274
279
|
- spec/values/period_spec.rb
|