Merge from emacs-24
[emacs.git] / lisp / calendar / icalendar.el
blobb024a38f8095c7c8c62f4086add967f9740fd06f
1 ;;; icalendar.el --- iCalendar implementation -*-coding: utf-8 -*-
3 ;; Copyright (C) 2002-2014 Free Software Foundation, Inc.
5 ;; Author: Ulf Jasper <ulf.jasper@web.de>
6 ;; Created: August 2002
7 ;; Keywords: calendar
8 ;; Human-Keywords: calendar, diary, iCalendar, vCalendar
9 ;; Version: 0.19
11 ;; This file is part of GNU Emacs.
13 ;; GNU Emacs is free software: you can redistribute it and/or modify
14 ;; it under the terms of the GNU General Public License as published by
15 ;; the Free Software Foundation, either version 3 of the License, or
16 ;; (at your option) any later version.
18 ;; GNU Emacs is distributed in the hope that it will be useful,
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;; GNU General Public License for more details.
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
26 ;;; Commentary:
28 ;; This package is documented in the Emacs Manual.
30 ;; Please note:
31 ;; - Diary entries which have a start time but no end time are assumed to
32 ;; last for one hour when they are exported.
33 ;; - Weekly diary entries are assumed to occur the first time in the first
34 ;; week of the year 2000 when they are exported.
35 ;; - Yearly diary entries are assumed to occur the first time in the year
36 ;; 1900 when they are exported.
37 ;; - Float diary entries are assumed to occur the first time on the
38 ;; day when they are exported.
40 ;;; History:
42 ;; 0.07 onwards: see lisp/ChangeLog
44 ;; 0.06: (2004-10-06)
45 ;; - Bugfixes regarding icalendar-import-format-*.
46 ;; - Fix in icalendar-convert-diary-to-ical -- thanks to Philipp Grau.
48 ;; 0.05: (2003-06-19)
49 ;; - New import format scheme: Replaced icalendar-import-prefix-*,
50 ;; icalendar-import-ignored-properties, and
51 ;; icalendar-import-separator with icalendar-import-format(-*).
52 ;; - icalendar-import-file and icalendar-convert-diary-to-ical
53 ;; have an extra parameter which should prevent them from
54 ;; erasing their target files (untested!).
55 ;; - Tested with Emacs 21.3.2
57 ;; 0.04:
58 ;; - Bugfix: import: double quoted param values did not work
59 ;; - Read DURATION property when importing.
60 ;; - Added parameter icalendar-duration-correction.
62 ;; 0.03: (2003-05-07)
63 ;; - Export takes care of european-calendar-style.
64 ;; - Tested with Emacs 21.3.2 and XEmacs 21.4.12
66 ;; 0.02:
67 ;; - Should work in XEmacs now. Thanks to Len Trigg for the XEmacs patches!
68 ;; - Added exporting from Emacs diary to ical.
69 ;; - Some bugfixes, after testing with calendars from http://icalshare.com.
70 ;; - Tested with Emacs 21.3.2 and XEmacs 21.4.12
72 ;; 0.01: (2003-03-21)
73 ;; - First published version. Trial version. Alpha version.
75 ;; ======================================================================
76 ;; To Do:
78 ;; * Import from ical to diary:
79 ;; + Need more properties for icalendar-import-format
80 ;; (added all that Mozilla Calendar uses)
81 ;; From iCal specifications (RFC2445: 4.8.1), icalendar.el lacks
82 ;; ATTACH, CATEGORIES, COMMENT, GEO, PERCENT-COMPLETE (VTODO),
83 ;; PRIORITY, RESOURCES) not considering date/time and time-zone
84 ;; + check vcalendar version
85 ;; + check (unknown) elements
86 ;; + recurring events!
87 ;; + works for european style calendars only! Does it?
88 ;; + alarm
89 ;; + exceptions in recurring events
90 ;; + the parser is too soft
91 ;; + error log is incomplete
92 ;; + nice to have: #include "webcal://foo.com/some-calendar.ics"
93 ;; + timezones probably still need some improvements.
95 ;; * Export from diary to ical
96 ;; + diary-date, diary-float, and self-made sexp entries are not
97 ;; understood
99 ;; * Other things
100 ;; + clean up all those date/time parsing functions
101 ;; + Handle todo items?
102 ;; + Check iso 8601 for datetime and period
103 ;; + Which chars to (un)escape?
106 ;;; Code:
108 (defconst icalendar-version "0.19"
109 "Version number of icalendar.el.")
111 ;; ======================================================================
112 ;; Customizables
113 ;; ======================================================================
114 (defgroup icalendar nil
115 "iCalendar support."
116 :prefix "icalendar-"
117 :group 'calendar)
119 (defcustom icalendar-import-format
120 "%s%d%l%o"
121 "Format for importing events from iCalendar into Emacs diary.
122 It defines how iCalendar events are inserted into diary file.
123 This may either be a string or a function.
125 In case of a formatting STRING the following specifiers can be used:
126 %c Class, see `icalendar-import-format-class'
127 %d Description, see `icalendar-import-format-description'
128 %l Location, see `icalendar-import-format-location'
129 %o Organizer, see `icalendar-import-format-organizer'
130 %s Summary, see `icalendar-import-format-summary'
131 %t Status, see `icalendar-import-format-status'
132 %u URL, see `icalendar-import-format-url'
133 %U UID, see `icalendar-import-format-uid'
135 A formatting FUNCTION will be called with a VEVENT as its only
136 argument. It must return a string. See
137 `icalendar-import-format-sample' for an example."
138 :type '(choice
139 (string :tag "String")
140 (function :tag "Function"))
141 :group 'icalendar)
143 (defcustom icalendar-import-format-summary
144 "%s"
145 "Format string defining how the summary element is formatted.
146 This applies only if the summary is not empty! `%s' is replaced
147 by the summary."
148 :type 'string
149 :group 'icalendar)
151 (defcustom icalendar-import-format-description
152 "\n Desc: %s"
153 "Format string defining how the description element is formatted.
154 This applies only if the description is not empty! `%s' is
155 replaced by the description."
156 :type 'string
157 :group 'icalendar)
159 (defcustom icalendar-import-format-location
160 "\n Location: %s"
161 "Format string defining how the location element is formatted.
162 This applies only if the location is not empty! `%s' is replaced
163 by the location."
164 :type 'string
165 :group 'icalendar)
167 (defcustom icalendar-import-format-organizer
168 "\n Organizer: %s"
169 "Format string defining how the organizer element is formatted.
170 This applies only if the organizer is not empty! `%s' is
171 replaced by the organizer."
172 :type 'string
173 :group 'icalendar)
175 (defcustom icalendar-import-format-url
176 "\n URL: %s"
177 "Format string defining how the URL element is formatted.
178 This applies only if the URL is not empty! `%s' is replaced by
179 the URL."
180 :type 'string
181 :group 'icalendar)
183 (defcustom icalendar-import-format-uid
184 "\n UID: %s"
185 "Format string defining how the UID element is formatted.
186 This applies only if the UID is not empty! `%s' is replaced by
187 the UID."
188 :type 'string
189 :version "24.3"
190 :group 'icalendar)
192 (defcustom icalendar-import-format-status
193 "\n Status: %s"
194 "Format string defining how the status element is formatted.
195 This applies only if the status is not empty! `%s' is replaced by
196 the status."
197 :type 'string
198 :group 'icalendar)
200 (defcustom icalendar-import-format-class
201 "\n Class: %s"
202 "Format string defining how the class element is formatted.
203 This applies only if the class is not empty! `%s' is replaced by
204 the class."
205 :type 'string
206 :group 'icalendar)
208 (defcustom icalendar-recurring-start-year
209 2005
210 "Start year for recurring events.
211 Some calendar browsers only propagate recurring events for
212 several years beyond the start time. Set this string to a year
213 just before the start of your personal calendar."
214 :type 'integer
215 :group 'icalendar)
217 (defcustom icalendar-export-hidden-diary-entries
219 "Determines whether hidden diary entries are exported.
220 If non-nil hidden diary entries (starting with `&') get exported,
221 if nil they are ignored."
222 :type 'boolean
223 :group 'icalendar)
225 (defcustom icalendar-uid-format
226 "emacs%t%c"
227 "Format of unique ID code (UID) for each iCalendar object.
228 The following specifiers are available:
229 %c COUNTER, an integer value that is increased each time a uid is
230 generated. This may be necessary for systems which do not
231 provide time-resolution finer than a second.
232 %h HASH, a hash value of the diary entry,
233 %s DTSTART, the start date (excluding time) of the diary entry,
234 %t TIMESTAMP, a unique creation timestamp,
235 %u USERNAME, the variable `user-login-name'.
237 For example, a value of \"%s_%h@mydomain.com\" will generate a
238 UID code for each entry composed of the time of the event, a hash
239 code for the event, and your personal domain name."
240 :type 'string
241 :group 'icalendar)
243 (defcustom icalendar-export-sexp-enumeration-days
245 "Number of days over which a sexp diary entry is enumerated.
246 In general sexp entries cannot be translated to icalendar format.
247 They are therefore enumerated, i.e. explicitly evaluated for a
248 certain number of days, and then exported. The enumeration starts
249 on the current day and continues for the number of days given here.
251 See `icalendar-export-sexp-enumerate-all' for a list of sexp
252 entries which by default are NOT enumerated."
253 :version "25.1"
254 :type 'integer
255 :group 'icalendar)
257 (defcustom icalendar-export-sexp-enumerate-all
259 "Determines whether ALL sexp diary entries are enumerated.
260 If non-nil all sexp diary entries are enumerated for
261 `icalendar-export-sexp-enumeration-days' days instead of
262 translating into an icalendar equivalent. This affects the
263 following sexp diary entries: `diary-anniversary',
264 `diary-cyclic', `diary-date', `diary-float',`diary-block'. All
265 other sexp entries are enumerated in any case."
266 :version "25.1"
267 :type 'boolean
268 :group 'icalendar)
270 (defvar icalendar-debug nil
271 "Enable icalendar debug messages.")
273 ;; ======================================================================
274 ;; NO USER SERVICEABLE PARTS BELOW THIS LINE
275 ;; ======================================================================
277 (defconst icalendar--weekday-array ["SU" "MO" "TU" "WE" "TH" "FR" "SA"])
279 ;; ======================================================================
280 ;; all the other libs we need
281 ;; ======================================================================
282 (require 'calendar)
283 (require 'diary-lib)
285 ;; ======================================================================
286 ;; misc
287 ;; ======================================================================
288 (defun icalendar--dmsg (&rest args)
289 "Print message ARGS if `icalendar-debug' is non-nil."
290 (if icalendar-debug
291 (apply 'message args)))
293 ;; ======================================================================
294 ;; Core functionality
295 ;; Functions for parsing icalendars, importing and so on
296 ;; ======================================================================
298 (defun icalendar--get-unfolded-buffer (folded-ical-buffer)
299 "Return a new buffer containing the unfolded contents of a buffer.
300 Folding is the iCalendar way of wrapping long lines. In the
301 created buffer all occurrences of CR LF BLANK are replaced by the
302 empty string. Argument FOLDED-ICAL-BUFFER is the unfolded input
303 buffer."
304 (let ((unfolded-buffer (get-buffer-create " *icalendar-work*")))
305 (save-current-buffer
306 (set-buffer unfolded-buffer)
307 (erase-buffer)
308 (insert-buffer-substring folded-ical-buffer)
309 (goto-char (point-min))
310 (while (re-search-forward "\r?\n[ \t]" nil t)
311 (replace-match "" nil nil)))
312 unfolded-buffer))
314 (defsubst icalendar--rris (regexp rep string &optional fixedcase literal)
315 "Replace regular expression in string.
316 Pass arguments REGEXP REP STRING FIXEDCASE LITERAL to
317 `replace-regexp-in-string' (Emacs) or to `replace-in-string' (XEmacs)."
318 (cond ((fboundp 'replace-regexp-in-string)
319 ;; Emacs:
320 (replace-regexp-in-string regexp rep string fixedcase literal))
321 ((fboundp 'replace-in-string)
322 ;; XEmacs:
323 (save-match-data ;; apparently XEmacs needs save-match-data
324 (replace-in-string string regexp rep literal)))))
326 (defun icalendar--read-element (invalue inparams)
327 "Recursively read the next iCalendar element in the current buffer.
328 INVALUE gives the current iCalendar element we are reading.
329 INPARAMS gives the current parameters.....
330 This function calls itself recursively for each nested calendar element
331 it finds."
332 (let (element children line name params param param-name param-value
333 value
334 (continue t))
335 (setq children '())
336 (while (and continue
337 (re-search-forward "^\\([A-Za-z0-9-]+\\)[;:]" nil t))
338 (setq name (intern (match-string 1)))
339 (backward-char 1)
340 (setq params '())
341 (setq line '())
342 (while (looking-at ";")
343 (re-search-forward ";\\([A-Za-z0-9-]+\\)=" nil nil)
344 (setq param-name (intern (match-string 1)))
345 (re-search-forward "\\(\\([^;,:\"]+\\)\\|\"\\([^\"]+\\)\"\\)[;:]"
346 nil t)
347 (backward-char 1)
348 (setq param-value (or (match-string 2) (match-string 3)))
349 (setq param (list param-name param-value))
350 (while (looking-at ",")
351 (re-search-forward "\\(\\([^;,:]+\\)\\|\"\\([^\"]+\\)\"\\)"
352 nil t)
353 (if (match-string 2)
354 (setq param-value (match-string 2))
355 (setq param-value (match-string 3)))
356 (setq param (append param param-value)))
357 (setq params (append params param)))
358 (unless (looking-at ":")
359 (error "Oops"))
360 (forward-char 1)
361 (re-search-forward "\\(.*\\)\\(\r?\n[ \t].*\\)*" nil t)
362 (setq value (icalendar--rris "\r?\n[ \t]" "" (match-string 0)))
363 (setq line (list name params value))
364 (cond ((eq name 'BEGIN)
365 (setq children
366 (append children
367 (list (icalendar--read-element (intern value)
368 params)))))
369 ((eq name 'END)
370 (setq continue nil))
372 (setq element (append element (list line))))))
373 (if invalue
374 (list invalue inparams element children)
375 children)))
377 ;; ======================================================================
378 ;; helper functions for examining events
379 ;; ======================================================================
381 ;;(defsubst icalendar--get-all-event-properties (event)
382 ;; "Return the list of properties in this EVENT."
383 ;; (car (cddr event)))
385 (defun icalendar--get-event-property (event prop)
386 "For the given EVENT return the value of the first occurrence of PROP."
387 (catch 'found
388 (let ((props (car (cddr event))) pp)
389 (while props
390 (setq pp (car props))
391 (if (eq (car pp) prop)
392 (throw 'found (car (cddr pp))))
393 (setq props (cdr props))))
394 nil))
396 (defun icalendar--get-event-property-attributes (event prop)
397 "For the given EVENT return attributes of the first occurrence of PROP."
398 (catch 'found
399 (let ((props (car (cddr event))) pp)
400 (while props
401 (setq pp (car props))
402 (if (eq (car pp) prop)
403 (throw 'found (cadr pp)))
404 (setq props (cdr props))))
405 nil))
407 (defun icalendar--get-event-properties (event prop)
408 "For the given EVENT return a list of all values of the property PROP."
409 (let ((props (car (cddr event))) pp result)
410 (while props
411 (setq pp (car props))
412 (if (eq (car pp) prop)
413 (setq result (append (split-string (car (cddr pp)) ",") result)))
414 (setq props (cdr props)))
415 result))
417 ;; (defun icalendar--set-event-property (event prop new-value)
418 ;; "For the given EVENT set the property PROP to the value NEW-VALUE."
419 ;; (catch 'found
420 ;; (let ((props (car (cddr event))) pp)
421 ;; (while props
422 ;; (setq pp (car props))
423 ;; (when (eq (car pp) prop)
424 ;; (setcdr (cdr pp) new-value)
425 ;; (throw 'found (car (cddr pp))))
426 ;; (setq props (cdr props)))
427 ;; (setq props (car (cddr event)))
428 ;; (setcar (cddr event)
429 ;; (append props (list (list prop nil new-value)))))))
431 (defun icalendar--get-children (node name)
432 "Return all children of the given NODE which have a name NAME.
433 For instance the VCALENDAR node can have VEVENT children as well as VTODO
434 children."
435 (let ((result nil)
436 (children (cadr (cddr node))))
437 (when (eq (car node) name)
438 (setq result node))
439 ;;(message "%s" node)
440 (when children
441 (let ((subresult
442 (delq nil
443 (mapcar (lambda (n)
444 (icalendar--get-children n name))
445 children))))
446 (if subresult
447 (if result
448 (setq result (append result subresult))
449 (setq result subresult)))))
450 result))
452 ;; private
453 (defun icalendar--all-events (icalendar)
454 "Return the list of all existing events in the given ICALENDAR."
455 (let ((result '()))
456 (mapc (lambda (elt)
457 (setq result (append (icalendar--get-children elt 'VEVENT)
458 result)))
459 (nreverse icalendar))
460 result))
462 (defun icalendar--split-value (value-string)
463 "Split VALUE-STRING at ';='."
464 (let ((result '())
465 param-name param-value)
466 (when value-string
467 (save-current-buffer
468 (set-buffer (get-buffer-create " *icalendar-work*"))
469 (set-buffer-modified-p nil)
470 (erase-buffer)
471 (insert value-string)
472 (goto-char (point-min))
473 (while
474 (re-search-forward
475 "\\([A-Za-z0-9-]+\\)=\\(\\([^;:]+\\)\\|\"\\([^\"]+\\)\"\\);?"
476 nil t)
477 (setq param-name (intern (match-string 1)))
478 (setq param-value (match-string 2))
479 (setq result
480 (append result (list (list param-name param-value)))))))
481 result))
483 (defun icalendar--convert-tz-offset (alist dst-p)
484 "Return a cons of two strings representing a timezone start.
485 ALIST is an alist entry from a VTIMEZONE, like STANDARD.
486 DST-P is non-nil if this is for daylight savings time.
487 The strings are suitable for assembling into a TZ variable."
488 (let* ((offsetto (car (cddr (assq 'TZOFFSETTO alist))))
489 (offsetfrom (car (cddr (assq 'TZOFFSETFROM alist))))
490 (rrule-value (car (cddr (assq 'RRULE alist))))
491 (dtstart (car (cddr (assq 'DTSTART alist))))
492 (no-dst (equal offsetto offsetfrom)))
493 ;; FIXME: for now we only handle RRULE and not RDATE here.
494 (when (and offsetto dtstart (or rrule-value no-dst))
495 (let* ((rrule (icalendar--split-value rrule-value))
496 (freq (cadr (assq 'FREQ rrule)))
497 (bymonth (cadr (assq 'BYMONTH rrule)))
498 (byday (cadr (assq 'BYDAY rrule))))
499 ;; FIXME: we don't correctly handle WKST here.
500 (if (or no-dst (and (string= freq "YEARLY") bymonth))
501 (cons
502 (concat
503 ;; Fake a name.
504 (if dst-p "DST" "STD")
505 ;; For TZ, OFFSET is added to the local time. So,
506 ;; invert the values.
507 (if (eq (aref offsetto 0) ?-) "+" "-")
508 (substring offsetto 1 3)
510 (substring offsetto 3 5))
511 ;; The start time.
512 (unless no-dst
513 (let* ((day (icalendar--get-weekday-number (substring byday -2)))
514 (week (if (eq day -1)
515 byday
516 (substring byday 0 -2))))
517 ;; "Translate" the iCalendar way to specify the last
518 ;; (sun|mon|...)day in month to the tzset way.
519 (if (string= week "-1") ; last day as iCalendar calls it
520 (setq week "5")) ; last day as tzset calls it
521 (concat "M" bymonth "." week "." (if (eq day -1) "0"
522 (int-to-string day))
523 ;; Start time.
525 (substring dtstart -6 -4)
527 (substring dtstart -4 -2)
529 (substring dtstart -2))))))))))
531 (defun icalendar--parse-vtimezone (alist)
532 "Turn a VTIMEZONE ALIST into a cons (ID . TZ-STRING).
533 Return nil if timezone cannot be parsed."
534 (let* ((tz-id (icalendar--convert-string-for-import
535 (icalendar--get-event-property alist 'TZID)))
536 (daylight (cadr (cdar (icalendar--get-children alist 'DAYLIGHT))))
537 (day (and daylight (icalendar--convert-tz-offset daylight t)))
538 (standard (cadr (cdar (icalendar--get-children alist 'STANDARD))))
539 (std (and standard (icalendar--convert-tz-offset standard nil))))
540 (if (and tz-id std)
541 (cons tz-id
542 (if day
543 (concat (car std) (car day)
544 "," (cdr day) "," (cdr std))
545 (car std))))))
547 (defun icalendar--convert-all-timezones (icalendar)
548 "Convert all timezones in the ICALENDAR into an alist.
549 Each element of the alist is a cons (ID . TZ-STRING),
550 like `icalendar--parse-vtimezone'."
551 (let (result)
552 (dolist (zone (icalendar--get-children (car icalendar) 'VTIMEZONE))
553 (setq zone (icalendar--parse-vtimezone zone))
554 (if zone
555 (setq result (cons zone result))))
556 result))
558 (defun icalendar--find-time-zone (prop-list zone-map)
559 "Return a timezone string for the time zone in PROP-LIST, or nil if none.
560 ZONE-MAP is a timezone alist as returned by `icalendar--convert-all-timezones'."
561 (let ((id (plist-get prop-list 'TZID)))
562 (if id
563 (cdr (assoc id zone-map)))))
565 (defun icalendar--decode-isodatetime (isodatetimestring &optional day-shift
566 zone)
567 "Return ISODATETIMESTRING in format like `decode-time'.
568 Converts from ISO-8601 to Emacs representation. If
569 ISODATETIMESTRING specifies UTC time (trailing letter Z) the
570 decoded time is given in the local time zone! If optional
571 parameter DAY-SHIFT is non-nil the result is shifted by DAY-SHIFT
572 days.
573 ZONE, if provided, is the timezone, in any format understood by `encode-time'.
575 FIXME: multiple comma-separated values should be allowed!"
576 (icalendar--dmsg isodatetimestring)
577 (if isodatetimestring
578 ;; day/month/year must be present
579 (let ((year (read (substring isodatetimestring 0 4)))
580 (month (read (substring isodatetimestring 4 6)))
581 (day (read (substring isodatetimestring 6 8)))
582 (hour 0)
583 (minute 0)
584 (second 0))
585 (when (> (length isodatetimestring) 12)
586 ;; hour/minute present
587 (setq hour (read (substring isodatetimestring 9 11)))
588 (setq minute (read (substring isodatetimestring 11 13))))
589 (when (> (length isodatetimestring) 14)
590 ;; seconds present
591 (setq second (read (substring isodatetimestring 13 15))))
592 (when (and (> (length isodatetimestring) 15)
593 ;; UTC specifier present
594 (char-equal ?Z (aref isodatetimestring 15)))
595 ;; if not UTC add current-time-zone offset
596 ;; current-time-zone should be called with actual UTC time
597 ;; (daylight saving at that time may differ to current one)
598 (setq second (+ (car (current-time-zone
599 (encode-time second minute hour day month year
600 0)))
601 second)))
602 ;; shift if necessary
603 (if day-shift
604 (let ((mdy (calendar-gregorian-from-absolute
605 (+ (calendar-absolute-from-gregorian
606 (list month day year))
607 day-shift))))
608 (setq month (nth 0 mdy))
609 (setq day (nth 1 mdy))
610 (setq year (nth 2 mdy))))
611 ;; create the decoded date-time
612 ;; FIXME!?!
613 (condition-case nil
614 (decode-time (encode-time second minute hour day month year zone))
615 (error
616 (message "Cannot decode \"%s\"" isodatetimestring)
617 ;; hope for the best...
618 (list second minute hour day month year 0 nil 0))))
619 ;; isodatetimestring == nil
620 nil))
622 (defun icalendar--decode-isoduration (isodurationstring
623 &optional duration-correction)
624 "Convert ISODURATIONSTRING into format provided by `decode-time'.
625 Converts from ISO-8601 to Emacs representation. If ISODURATIONSTRING
626 specifies UTC time (trailing letter Z) the decoded time is given in
627 the local time zone!
629 Optional argument DURATION-CORRECTION shortens result by one day.
631 FIXME: TZID-attributes are ignored....!
632 FIXME: multiple comma-separated values should be allowed!"
633 (if isodurationstring
634 (save-match-data
635 (string-match
636 (concat
637 "^P[+-]?\\("
638 "\\(\\([0-9]+\\)D\\)" ; days only
639 "\\|"
640 "\\(\\(\\([0-9]+\\)D\\)?T\\(\\([0-9]+\\)H\\)?" ; opt days
641 "\\(\\([0-9]+\\)M\\)?\\(\\([0-9]+\\)S\\)?\\)" ; mand. time
642 "\\|"
643 "\\(\\([0-9]+\\)W\\)" ; weeks only
644 "\\)$") isodurationstring)
645 (let ((seconds 0)
646 (minutes 0)
647 (hours 0)
648 (days 0)
649 (months 0)
650 (years 0))
651 (cond
652 ((match-beginning 2) ;days only
653 (setq days (read (substring isodurationstring
654 (match-beginning 3)
655 (match-end 3))))
656 (when duration-correction
657 (setq days (1- days))))
658 ((match-beginning 4) ;days and time
659 (if (match-beginning 5)
660 (setq days (* 7 (read (substring isodurationstring
661 (match-beginning 6)
662 (match-end 6))))))
663 (if (match-beginning 7)
664 (setq hours (read (substring isodurationstring
665 (match-beginning 8)
666 (match-end 8)))))
667 (if (match-beginning 9)
668 (setq minutes (read (substring isodurationstring
669 (match-beginning 10)
670 (match-end 10)))))
671 (if (match-beginning 11)
672 (setq seconds (read (substring isodurationstring
673 (match-beginning 12)
674 (match-end 12))))))
675 ((match-beginning 13) ;weeks only
676 (setq days (* 7 (read (substring isodurationstring
677 (match-beginning 14)
678 (match-end 14)))))))
679 (list seconds minutes hours days months years)))
680 ;; isodatetimestring == nil
681 nil))
683 (defun icalendar--add-decoded-times (time1 time2)
684 "Add TIME1 to TIME2.
685 Both times must be given in decoded form. One of these times must be
686 valid (year > 1900 or something)."
687 ;; FIXME: does this function exist already?
688 (decode-time (encode-time
689 (+ (nth 0 time1) (nth 0 time2))
690 (+ (nth 1 time1) (nth 1 time2))
691 (+ (nth 2 time1) (nth 2 time2))
692 (+ (nth 3 time1) (nth 3 time2))
693 (+ (nth 4 time1) (nth 4 time2))
694 (+ (nth 5 time1) (nth 5 time2))
697 ;;(or (nth 6 time1) (nth 6 time2)) ;; FIXME?
700 (defun icalendar--datetime-to-american-date (datetime &optional separator)
701 "Convert the decoded DATETIME to American-style format.
702 Optional argument SEPARATOR gives the separator between month,
703 day, and year. If nil a blank character is used as separator.
704 American format: \"month day year\"."
705 (if datetime
706 (format "%d%s%d%s%d" (nth 4 datetime) ;month
707 (or separator " ")
708 (nth 3 datetime) ;day
709 (or separator " ")
710 (nth 5 datetime)) ;year
711 ;; datetime == nil
712 nil))
714 (define-obsolete-function-alias 'icalendar--datetime-to-noneuropean-date
715 'icalendar--datetime-to-american-date "icalendar 0.19")
717 (defun icalendar--datetime-to-european-date (datetime &optional separator)
718 "Convert the decoded DATETIME to European format.
719 Optional argument SEPARATOR gives the separator between month,
720 day, and year. If nil a blank character is used as separator.
721 European format: (day month year).
722 FIXME"
723 (if datetime
724 (format "%d%s%d%s%d" (nth 3 datetime) ;day
725 (or separator " ")
726 (nth 4 datetime) ;month
727 (or separator " ")
728 (nth 5 datetime)) ;year
729 ;; datetime == nil
730 nil))
732 (defun icalendar--datetime-to-iso-date (datetime &optional separator)
733 "Convert the decoded DATETIME to ISO format.
734 Optional argument SEPARATOR gives the separator between month,
735 day, and year. If nil a blank character is used as separator.
736 ISO format: (year month day)."
737 (if datetime
738 (format "%d%s%d%s%d" (nth 5 datetime) ;year
739 (or separator " ")
740 (nth 4 datetime) ;month
741 (or separator " ")
742 (nth 3 datetime)) ;day
743 ;; datetime == nil
744 nil))
746 (defun icalendar--datetime-to-diary-date (datetime &optional separator)
747 "Convert the decoded DATETIME to diary format.
748 Optional argument SEPARATOR gives the separator between month,
749 day, and year. If nil a blank character is used as separator.
750 Call icalendar--datetime-to-*-date according to the current
751 calendar date style."
752 (funcall (intern-soft (format "icalendar--datetime-to-%s-date"
753 calendar-date-style))
754 datetime separator))
756 (defun icalendar--datetime-to-colontime (datetime)
757 "Extract the time part of a decoded DATETIME into 24-hour format.
758 Note that this silently ignores seconds."
759 (format "%02d:%02d" (nth 2 datetime) (nth 1 datetime)))
761 (defun icalendar--get-month-number (monthname)
762 "Return the month number for the given MONTHNAME."
763 (catch 'found
764 (let ((num 1)
765 (m (downcase monthname)))
766 (mapc (lambda (month)
767 (let ((mm (downcase month)))
768 (if (or (string-equal mm m)
769 (string-equal (substring mm 0 3) m))
770 (throw 'found num))
771 (setq num (1+ num))))
772 calendar-month-name-array))
773 ;; Error:
774 -1))
776 (defun icalendar--get-weekday-number (abbrevweekday)
777 "Return the number for the ABBREVWEEKDAY."
778 (if abbrevweekday
779 (catch 'found
780 (let ((num 0)
781 (aw (downcase abbrevweekday)))
782 (mapc (lambda (day)
783 (let ((d (downcase day)))
784 (if (string-equal d aw)
785 (throw 'found num))
786 (setq num (1+ num))))
787 icalendar--weekday-array)))
788 ;; Error:
789 -1))
791 (defun icalendar--get-weekday-numbers (abbrevweekdays)
792 "Return the list of numbers for the comma-separated ABBREVWEEKDAYS."
793 (when abbrevweekdays
794 (let* ((num -1)
795 (weekday-alist (mapcar (lambda (day)
796 (progn
797 (setq num (1+ num))
798 (cons (downcase day) num)))
799 icalendar--weekday-array)))
800 (delq nil
801 (mapcar (lambda (abbrevday)
802 (cdr (assoc abbrevday weekday-alist)))
803 (split-string (downcase abbrevweekdays) ","))))))
805 (defun icalendar--get-weekday-abbrev (weekday)
806 "Return the abbreviated WEEKDAY."
807 (catch 'found
808 (let ((num 0)
809 (w (downcase weekday)))
810 (mapc (lambda (day)
811 (let ((d (downcase day)))
812 (if (or (string-equal d w)
813 (string-equal (substring d 0 3) w))
814 (throw 'found (aref icalendar--weekday-array num)))
815 (setq num (1+ num))))
816 calendar-day-name-array))
817 ;; Error:
818 nil))
820 (defun icalendar--date-to-isodate (date &optional day-shift)
821 "Convert DATE to iso-style date.
822 DATE must be a list of the form (month day year).
823 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days."
824 (let ((mdy (calendar-gregorian-from-absolute
825 (+ (calendar-absolute-from-gregorian date)
826 (or day-shift 0)))))
827 (format "%04d%02d%02d" (nth 2 mdy) (nth 0 mdy) (nth 1 mdy))))
830 (defun icalendar--datestring-to-isodate (datestring &optional day-shift)
831 "Convert diary-style DATESTRING to iso-style date.
832 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days
833 -- DAY-SHIFT must be either nil or an integer. This function
834 tries to figure the date style from DATESTRING itself. If that
835 is not possible it uses the current calendar date style."
836 (let ((day -1) month year)
837 (save-match-data
838 (cond ( ;; iso-style numeric date
839 (string-match (concat "\\s-*"
840 "\\([0-9]\\{4\\}\\)[ \t/]\\s-*"
841 "0?\\([1-9][0-9]?\\)[ \t/]\\s-*"
842 "0?\\([1-9][0-9]?\\)")
843 datestring)
844 (setq year (read (substring datestring (match-beginning 1)
845 (match-end 1))))
846 (setq month (read (substring datestring (match-beginning 2)
847 (match-end 2))))
848 (setq day (read (substring datestring (match-beginning 3)
849 (match-end 3)))))
850 ( ;; non-iso numeric date -- must rely on configured
851 ;; calendar style
852 (string-match (concat "\\s-*"
853 "0?\\([1-9][0-9]?\\)[ \t/]\\s-*"
854 "0?\\([1-9][0-9]?\\),?[ \t/]\\s-*"
855 "\\([0-9]\\{4\\}\\)")
856 datestring)
857 (setq day (read (substring datestring (match-beginning 1)
858 (match-end 1))))
859 (setq month (read (substring datestring (match-beginning 2)
860 (match-end 2))))
861 (setq year (read (substring datestring (match-beginning 3)
862 (match-end 3))))
863 (if (eq calendar-date-style 'american)
864 (let ((x month))
865 (setq month day)
866 (setq day x))))
867 ( ;; date contains month names -- iso style
868 (string-match (concat "\\s-*"
869 "\\([0-9]\\{4\\}\\)[ \t/]\\s-*"
870 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
871 "0?\\([123]?[0-9]\\)")
872 datestring)
873 (setq year (read (substring datestring (match-beginning 1)
874 (match-end 1))))
875 (setq month (icalendar--get-month-number
876 (substring datestring (match-beginning 2)
877 (match-end 2))))
878 (setq day (read (substring datestring (match-beginning 3)
879 (match-end 3)))))
880 ( ;; date contains month names -- european style
881 (string-match (concat "\\s-*"
882 "0?\\([123]?[0-9]\\)[ \t/]\\s-*"
883 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
884 "\\([0-9]\\{4\\}\\)")
885 datestring)
886 (setq day (read (substring datestring (match-beginning 1)
887 (match-end 1))))
888 (setq month (icalendar--get-month-number
889 (substring datestring (match-beginning 2)
890 (match-end 2))))
891 (setq year (read (substring datestring (match-beginning 3)
892 (match-end 3)))))
893 ( ;; date contains month names -- american style
894 (string-match (concat "\\s-*"
895 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
896 "0?\\([123]?[0-9]\\),?[ \t/]\\s-*"
897 "\\([0-9]\\{4\\}\\)")
898 datestring)
899 (setq day (read (substring datestring (match-beginning 2)
900 (match-end 2))))
901 (setq month (icalendar--get-month-number
902 (substring datestring (match-beginning 1)
903 (match-end 1))))
904 (setq year (read (substring datestring (match-beginning 3)
905 (match-end 3)))))
907 nil)))
908 (if (> day 0)
909 (let ((mdy (calendar-gregorian-from-absolute
910 (+ (calendar-absolute-from-gregorian (list month day
911 year))
912 (or day-shift 0)))))
913 (icalendar--dmsg (format "%04d%02d%02d" (nth 2 mdy) (nth 0 mdy) (nth 1 mdy)))
914 (format "%04d%02d%02d" (nth 2 mdy) (nth 0 mdy) (nth 1 mdy)))
915 nil)))
917 (defun icalendar--diarytime-to-isotime (timestring ampmstring)
918 "Convert a time like 9:30pm to an iso-conform string like T213000.
919 In this example the TIMESTRING would be \"9:30\" and the
920 AMPMSTRING would be \"pm\". The minutes may be missing as long
921 as the colon is missing as well, i.e. \"9\" is allowed as
922 TIMESTRING and has the same result as \"9:00\"."
923 (if timestring
924 (let* ((parts (save-match-data (split-string timestring ":")))
925 (h (car parts))
926 (m (if (cdr parts) (cadr parts)
927 (if (> (length h) 2) "" "00")))
928 (starttimenum (read (concat h m))))
929 ;; take care of am/pm style
930 ;; Be sure *not* to convert 12:00pm - 12:59pm to 2400-2459
931 (if (and ampmstring (string= "pm" ampmstring) (< starttimenum 1200))
932 (setq starttimenum (+ starttimenum 1200)))
933 ;; Similar effect with 12:00am - 12:59am (need to convert to 0000-0059)
934 (if (and ampmstring (string= "am" ampmstring) (>= starttimenum 1200))
935 (setq starttimenum (- starttimenum 1200)))
936 (format "T%04d00" starttimenum))
937 nil))
939 (defun icalendar--convert-string-for-export (string)
940 "Escape comma and other critical characters in STRING."
941 (icalendar--rris "," "\\\\," string))
943 (defun icalendar--convert-string-for-import (string)
944 "Remove escape chars for comma, semicolon etc. from STRING."
945 (icalendar--rris
946 "\\\\n" "\n " (icalendar--rris
947 "\\\\\"" "\"" (icalendar--rris
948 "\\\\;" ";" (icalendar--rris
949 "\\\\," "," string)))))
951 ;; ======================================================================
952 ;; Export -- convert emacs-diary to iCalendar
953 ;; ======================================================================
955 ;;;###autoload
956 (defun icalendar-export-file (diary-filename ical-filename)
957 "Export diary file to iCalendar format.
958 All diary entries in the file DIARY-FILENAME are converted to iCalendar
959 format. The result is appended to the file ICAL-FILENAME."
960 (interactive "FExport diary data from file: \n\
961 Finto iCalendar file: ")
962 (save-current-buffer
963 (set-buffer (find-file diary-filename))
964 (icalendar-export-region (point-min) (point-max) ical-filename)))
966 (define-obsolete-function-alias 'icalendar-convert-diary-to-ical
967 'icalendar-export-file "22.1")
969 (defvar icalendar--uid-count 0
970 "Auxiliary counter for creating unique ids.")
972 (defun icalendar--create-uid (entry-full contents)
973 "Construct a unique iCalendar UID for a diary entry.
974 ENTRY-FULL is the full diary entry string. CONTENTS is the
975 current iCalendar object, as a string. Increase
976 `icalendar--uid-count'. Returns the UID string."
977 (let ((uid icalendar-uid-format))
979 ;; Allow other apps (such as org-mode) to create its own uid
980 (get-text-property 0 'uid entry-full)
981 (setq uid (get-text-property 0 'uid entry-full))
982 (setq uid (replace-regexp-in-string
983 "%c"
984 (format "%d" icalendar--uid-count)
985 uid t t))
986 (setq icalendar--uid-count (1+ icalendar--uid-count))
987 (setq uid (replace-regexp-in-string
988 "%t"
989 (format "%d%d%d" (car (current-time))
990 (cadr (current-time))
991 (car (cddr (current-time))))
992 uid t t))
993 (setq uid (replace-regexp-in-string
994 "%h"
995 (format "%d" (abs (sxhash entry-full))) uid t t))
996 (setq uid (replace-regexp-in-string
997 "%u" (or user-login-name "UNKNOWN_USER") uid t t))
998 (let ((dtstart (if (string-match "^DTSTART[^:]*:\\([0-9]*\\)" contents)
999 (substring contents (match-beginning 1) (match-end 1))
1000 "DTSTART")))
1001 (setq uid (replace-regexp-in-string "%s" dtstart uid t t))))
1003 ;; Return the UID string
1004 uid))
1006 ;;;###autoload
1007 (defun icalendar-export-region (min max ical-filename)
1008 "Export region in diary file to iCalendar format.
1009 All diary entries in the region from MIN to MAX in the current buffer are
1010 converted to iCalendar format. The result is appended to the file
1011 ICAL-FILENAME.
1012 This function attempts to return t if something goes wrong. In this
1013 case an error string which describes all the errors and problems is
1014 written into the buffer `*icalendar-errors*'."
1015 (interactive "r
1016 FExport diary data into iCalendar file: ")
1017 (let ((result "")
1018 (start 0)
1019 (entry-main "")
1020 (entry-rest "")
1021 (entry-full "")
1022 (header "")
1023 (contents-n-summary)
1024 (contents)
1025 (found-error nil)
1026 (nonmarker (concat "^" (regexp-quote diary-nonmarking-symbol)
1027 "?"))
1028 (other-elements nil))
1029 ;; prepare buffer with error messages
1030 (save-current-buffer
1031 (set-buffer (get-buffer-create "*icalendar-errors*"))
1032 (erase-buffer))
1034 ;; here we go
1035 (save-excursion
1036 (goto-char min)
1037 (while (re-search-forward
1038 ;; possibly ignore hidden entries beginning with "&"
1039 (if icalendar-export-hidden-diary-entries
1040 "^\\([^ \t\n#].+\\)\\(\\(\n[ \t].*\\)*\\)"
1041 "^\\([^ \t\n&#].+\\)\\(\\(\n[ \t].*\\)*\\)") max t)
1042 (setq entry-main (match-string 1))
1043 (if (match-beginning 2)
1044 (setq entry-rest (match-string 2))
1045 (setq entry-rest ""))
1046 (setq entry-full (concat entry-main entry-rest))
1048 (condition-case error-val
1049 (progn
1050 (setq cns-cons-or-list
1051 (icalendar--convert-to-ical nonmarker entry-main))
1052 (setq other-elements (icalendar--parse-summary-and-rest
1053 entry-full))
1054 (mapc (lambda (contents-n-summary)
1055 (setq contents (concat (car contents-n-summary)
1056 "\nSUMMARY:"
1057 (cdr contents-n-summary)))
1058 (let ((cla (cdr (assoc 'cla other-elements)))
1059 (des (cdr (assoc 'des other-elements)))
1060 (loc (cdr (assoc 'loc other-elements)))
1061 (org (cdr (assoc 'org other-elements)))
1062 (sta (cdr (assoc 'sta other-elements)))
1063 (sum (cdr (assoc 'sum other-elements)))
1064 (url (cdr (assoc 'url other-elements)))
1065 (uid (cdr (assoc 'uid other-elements))))
1066 (if cla
1067 (setq contents (concat contents "\nCLASS:" cla)))
1068 (if des
1069 (setq contents (concat contents "\nDESCRIPTION:"
1070 des)))
1071 (if loc
1072 (setq contents (concat contents "\nLOCATION:" loc)))
1073 (if org
1074 (setq contents (concat contents "\nORGANIZER:"
1075 org)))
1076 (if sta
1077 (setq contents (concat contents "\nSTATUS:" sta)))
1078 ;;(if sum
1079 ;; (setq contents (concat contents "\nSUMMARY:" sum)))
1080 (if url
1081 (setq contents (concat contents "\nURL:" url)))
1083 (setq header (concat "\nBEGIN:VEVENT\nUID:"
1084 (or uid
1085 (icalendar--create-uid
1086 entry-full contents)))))
1087 (setq result (concat result header contents
1088 "\nEND:VEVENT")))
1089 (if (consp cns-cons-or-list)
1090 (list cns-cons-or-list)
1091 cns-cons-or-list)))
1092 ;; handle errors
1093 (error
1094 (setq found-error t)
1095 (save-current-buffer
1096 (set-buffer (get-buffer-create "*icalendar-errors*"))
1097 (insert (format "Error in line %d -- %s: `%s'\n"
1098 (count-lines (point-min) (point))
1099 error-val
1100 entry-main))))))
1102 ;; we're done, insert everything into the file
1103 (save-current-buffer
1104 (let ((coding-system-for-write 'utf-8))
1105 (set-buffer (find-file ical-filename))
1106 (goto-char (point-max))
1107 (insert "BEGIN:VCALENDAR")
1108 (insert "\nPRODID:-//Emacs//NONSGML icalendar.el//EN")
1109 (insert "\nVERSION:2.0")
1110 (insert result)
1111 (insert "\nEND:VCALENDAR\n")
1112 ;; save the diary file
1113 (save-buffer)
1114 (unless found-error
1115 (bury-buffer)))))
1116 found-error))
1118 (defun icalendar--convert-to-ical (nonmarker entry-main)
1119 "Convert a diary entry to iCalendar format.
1120 NONMARKER is a regular expression matching the start of non-marking
1121 entries. ENTRY-MAIN is the first line of the diary entry."
1123 (unless icalendar-export-sexp-enumerate-all
1125 ;; anniversaries -- %%(diary-anniversary ...)
1126 (icalendar--convert-anniversary-to-ical nonmarker entry-main)
1127 ;; cyclic events -- %%(diary-cyclic ...)
1128 (icalendar--convert-cyclic-to-ical nonmarker entry-main)
1129 ;; diary-date -- %%(diary-date ...)
1130 (icalendar--convert-date-to-ical nonmarker entry-main)
1131 ;; float events -- %%(diary-float ...)
1132 (icalendar--convert-float-to-ical nonmarker entry-main)
1133 ;; block events -- %%(diary-block ...)
1134 (icalendar--convert-block-to-ical nonmarker entry-main)))
1135 ;; other sexp diary entries
1136 (icalendar--convert-sexp-to-ical nonmarker entry-main)
1137 ;; weekly by day -- Monday 8:30 Team meeting
1138 (icalendar--convert-weekly-to-ical nonmarker entry-main)
1139 ;; yearly by day -- 1 May Tag der Arbeit
1140 (icalendar--convert-yearly-to-ical nonmarker entry-main)
1141 ;; "ordinary" events, start and end time given
1142 ;; 1 Feb 2003 blah
1143 (icalendar--convert-ordinary-to-ical nonmarker entry-main)
1144 ;; everything else
1145 ;; Oops! what's that?
1146 (error "Could not parse entry")))
1148 (defun icalendar--parse-summary-and-rest (summary-and-rest)
1149 "Parse SUMMARY-AND-REST from a diary to fill iCalendar properties.
1150 Returns an alist."
1151 (save-match-data
1152 (if (functionp icalendar-import-format)
1153 ;; can't do anything
1155 ;; split summary-and-rest
1156 (let* ((case-fold-search nil)
1157 (s icalendar-import-format)
1158 (p-cla (or (string-match "%c" icalendar-import-format) -1))
1159 (p-des (or (string-match "%d" icalendar-import-format) -1))
1160 (p-loc (or (string-match "%l" icalendar-import-format) -1))
1161 (p-org (or (string-match "%o" icalendar-import-format) -1))
1162 (p-sum (or (string-match "%s" icalendar-import-format) -1))
1163 (p-sta (or (string-match "%t" icalendar-import-format) -1))
1164 (p-url (or (string-match "%u" icalendar-import-format) -1))
1165 (p-uid (or (string-match "%U" icalendar-import-format) -1))
1166 (p-list (sort (list p-cla p-des p-loc p-org p-sta p-sum p-url p-uid) '<))
1167 (ct 0)
1168 pos-cla pos-des pos-loc pos-org pos-sta pos-sum pos-url pos-uid)
1169 (dotimes (i (length p-list))
1170 ;; Use 'ct' to keep track of current position in list
1171 (cond ((and (>= p-cla 0) (= (nth i p-list) p-cla))
1172 (setq ct (+ ct 1))
1173 (setq pos-cla (* 2 ct)))
1174 ((and (>= p-des 0) (= (nth i p-list) p-des))
1175 (setq ct (+ ct 1))
1176 (setq pos-des (* 2 ct)))
1177 ((and (>= p-loc 0) (= (nth i p-list) p-loc))
1178 (setq ct (+ ct 1))
1179 (setq pos-loc (* 2 ct)))
1180 ((and (>= p-org 0) (= (nth i p-list) p-org))
1181 (setq ct (+ ct 1))
1182 (setq pos-org (* 2 ct)))
1183 ((and (>= p-sta 0) (= (nth i p-list) p-sta))
1184 (setq ct (+ ct 1))
1185 (setq pos-sta (* 2 ct)))
1186 ((and (>= p-sum 0) (= (nth i p-list) p-sum))
1187 (setq ct (+ ct 1))
1188 (setq pos-sum (* 2 ct)))
1189 ((and (>= p-url 0) (= (nth i p-list) p-url))
1190 (setq ct (+ ct 1))
1191 (setq pos-url (* 2 ct)))
1192 ((and (>= p-uid 0) (= (nth i p-list) p-uid))
1193 (setq ct (+ ct 1))
1194 (setq pos-uid (* 2 ct)))) )
1195 (mapc (lambda (ij)
1196 (setq s (icalendar--rris (car ij) (cadr ij) s t t)))
1197 (list
1198 ;; summary must be first! because of %s
1199 (list "%s"
1200 (concat "\\(" icalendar-import-format-summary "\\)??"))
1201 (list "%c"
1202 (concat "\\(" icalendar-import-format-class "\\)??"))
1203 (list "%d"
1204 (concat "\\(" icalendar-import-format-description "\\)??"))
1205 (list "%l"
1206 (concat "\\(" icalendar-import-format-location "\\)??"))
1207 (list "%o"
1208 (concat "\\(" icalendar-import-format-organizer "\\)??"))
1209 (list "%t"
1210 (concat "\\(" icalendar-import-format-status "\\)??"))
1211 (list "%u"
1212 (concat "\\(" icalendar-import-format-url "\\)??"))
1213 (list "%U"
1214 (concat "\\(" icalendar-import-format-uid "\\)??"))))
1215 ;; Need the \' regexp in order to detect multi-line items
1216 (setq s (concat "\\`"
1217 (icalendar--rris "%s" "\\(.*?\\)" s nil t)
1218 "\\'"))
1219 (if (string-match s summary-and-rest)
1220 (let (cla des loc org sta sum url uid)
1221 (if (and pos-sum (match-beginning pos-sum))
1222 (setq sum (substring summary-and-rest
1223 (match-beginning pos-sum)
1224 (match-end pos-sum))))
1225 (if (and pos-cla (match-beginning pos-cla))
1226 (setq cla (substring summary-and-rest
1227 (match-beginning pos-cla)
1228 (match-end pos-cla))))
1229 (if (and pos-des (match-beginning pos-des))
1230 (setq des (substring summary-and-rest
1231 (match-beginning pos-des)
1232 (match-end pos-des))))
1233 (if (and pos-loc (match-beginning pos-loc))
1234 (setq loc (substring summary-and-rest
1235 (match-beginning pos-loc)
1236 (match-end pos-loc))))
1237 (if (and pos-org (match-beginning pos-org))
1238 (setq org (substring summary-and-rest
1239 (match-beginning pos-org)
1240 (match-end pos-org))))
1241 (if (and pos-sta (match-beginning pos-sta))
1242 (setq sta (substring summary-and-rest
1243 (match-beginning pos-sta)
1244 (match-end pos-sta))))
1245 (if (and pos-url (match-beginning pos-url))
1246 (setq url (substring summary-and-rest
1247 (match-beginning pos-url)
1248 (match-end pos-url))))
1249 (if (and pos-uid (match-beginning pos-uid))
1250 (setq uid (substring summary-and-rest
1251 (match-beginning pos-uid)
1252 (match-end pos-uid))))
1253 (list (if cla (cons 'cla cla) nil)
1254 (if des (cons 'des des) nil)
1255 (if loc (cons 'loc loc) nil)
1256 (if org (cons 'org org) nil)
1257 (if sta (cons 'sta sta) nil)
1258 ;;(if sum (cons 'sum sum) nil)
1259 (if url (cons 'url url) nil)
1260 (if uid (cons 'uid uid) nil))))))))
1262 ;; subroutines for icalendar-export-region
1263 (defun icalendar--convert-ordinary-to-ical (nonmarker entry-main)
1264 "Convert \"ordinary\" diary entry to iCalendar format.
1265 NONMARKER is a regular expression matching the start of non-marking
1266 entries. ENTRY-MAIN is the first line of the diary entry."
1267 (if (string-match
1268 (concat nonmarker
1269 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)\\s-*" ; date
1270 "\\(\\([0-9][0-9]?\\(:[0-9][0-9]\\)?\\)\\([ap]m\\)?" ; start time
1271 "\\("
1272 "-\\([0-9][0-9]?\\(:[0-9][0-9]\\)?\\)\\([ap]m\\)?\\)?" ; end time
1273 "\\)?"
1274 "\\s-*\\(.*?\\) ?$")
1275 entry-main)
1276 (let* ((datetime (substring entry-main (match-beginning 1)
1277 (match-end 1)))
1278 (startisostring (icalendar--datestring-to-isodate
1279 datetime))
1280 (endisostring (icalendar--datestring-to-isodate
1281 datetime 1))
1282 (endisostring1)
1283 (starttimestring (icalendar--diarytime-to-isotime
1284 (if (match-beginning 3)
1285 (substring entry-main
1286 (match-beginning 3)
1287 (match-end 3))
1288 nil)
1289 (if (match-beginning 5)
1290 (substring entry-main
1291 (match-beginning 5)
1292 (match-end 5))
1293 nil)))
1294 (endtimestring (icalendar--diarytime-to-isotime
1295 (if (match-beginning 7)
1296 (substring entry-main
1297 (match-beginning 7)
1298 (match-end 7))
1299 nil)
1300 (if (match-beginning 9)
1301 (substring entry-main
1302 (match-beginning 9)
1303 (match-end 9))
1304 nil)))
1305 (summary (icalendar--convert-string-for-export
1306 (substring entry-main (match-beginning 10)
1307 (match-end 10)))))
1308 (icalendar--dmsg "ordinary %s" entry-main)
1310 (unless startisostring
1311 (error "Could not parse date"))
1313 ;; If only start-date is specified, then end-date is next day,
1314 ;; otherwise it is same day.
1315 (setq endisostring1 (if starttimestring
1316 startisostring
1317 endisostring))
1319 (when starttimestring
1320 (unless endtimestring
1321 (let ((time
1322 (read (icalendar--rris "^T0?" ""
1323 starttimestring))))
1324 (if (< time 230000)
1325 ;; Case: ends on same day
1326 (setq endtimestring (format "T%06d"
1327 (+ 10000 time)))
1328 ;; Case: ends on next day
1329 (setq endtimestring (format "T%06d"
1330 (- time 230000)))
1331 (setq endisostring1 endisostring)) )))
1333 (cons (concat "\nDTSTART;"
1334 (if starttimestring "VALUE=DATE-TIME:"
1335 "VALUE=DATE:")
1336 startisostring
1337 (or starttimestring "")
1338 "\nDTEND;"
1339 (if endtimestring "VALUE=DATE-TIME:"
1340 "VALUE=DATE:")
1341 endisostring1
1342 (or endtimestring ""))
1343 summary))
1344 ;; no match
1345 nil))
1347 (defun icalendar-first-weekday-of-year (abbrevweekday year)
1348 "Find the first ABBREVWEEKDAY in a given YEAR.
1349 Returns day number."
1350 (let* ((day-of-week-jan01 (calendar-day-of-week (list 1 1 year)))
1351 (result (+ 1
1352 (- (icalendar--get-weekday-number abbrevweekday)
1353 day-of-week-jan01))))
1354 (cond ((<= result 0)
1355 (setq result (+ result 7)))
1356 ((> result 7)
1357 (setq result (- result 7))))
1358 result))
1360 (defun icalendar--convert-weekly-to-ical (nonmarker entry-main)
1361 "Convert weekly diary entry to iCalendar format.
1362 NONMARKER is a regular expression matching the start of non-marking
1363 entries. ENTRY-MAIN is the first line of the diary entry."
1364 (if (and (string-match (concat nonmarker
1365 "\\([a-z]+\\)\\s-+"
1366 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)"
1367 "\\([ap]m\\)?"
1368 "\\(-"
1369 "\\([0-9][0-9]?:[0-9][0-9]\\)"
1370 "\\([ap]m\\)?\\)?"
1371 "\\)?"
1372 "\\s-*\\(.*?\\) ?$")
1373 entry-main)
1374 (icalendar--get-weekday-abbrev
1375 (substring entry-main (match-beginning 1)
1376 (match-end 1))))
1377 (let* ((day (icalendar--get-weekday-abbrev
1378 (substring entry-main (match-beginning 1)
1379 (match-end 1))))
1380 (starttimestring (icalendar--diarytime-to-isotime
1381 (if (match-beginning 3)
1382 (substring entry-main
1383 (match-beginning 3)
1384 (match-end 3))
1385 nil)
1386 (if (match-beginning 4)
1387 (substring entry-main
1388 (match-beginning 4)
1389 (match-end 4))
1390 nil)))
1391 (endtimestring (icalendar--diarytime-to-isotime
1392 (if (match-beginning 6)
1393 (substring entry-main
1394 (match-beginning 6)
1395 (match-end 6))
1396 nil)
1397 (if (match-beginning 7)
1398 (substring entry-main
1399 (match-beginning 7)
1400 (match-end 7))
1401 nil)))
1402 (summary (icalendar--convert-string-for-export
1403 (substring entry-main (match-beginning 8)
1404 (match-end 8)))))
1405 (icalendar--dmsg "weekly %s" entry-main)
1407 (when starttimestring
1408 (unless endtimestring
1409 (let ((time (read
1410 (icalendar--rris "^T0?" ""
1411 starttimestring))))
1412 (setq endtimestring (format "T%06d"
1413 (+ 10000 time))))))
1414 (cons (concat "\nDTSTART;"
1415 (if starttimestring
1416 "VALUE=DATE-TIME:"
1417 "VALUE=DATE:")
1418 ;; Find the first requested weekday of the
1419 ;; start year
1420 (funcall 'format "%04d%02d%02d"
1421 icalendar-recurring-start-year 1
1422 (icalendar-first-weekday-of-year
1423 day icalendar-recurring-start-year))
1424 (or starttimestring "")
1425 "\nDTEND;"
1426 (if endtimestring
1427 "VALUE=DATE-TIME:"
1428 "VALUE=DATE:")
1429 (funcall 'format "%04d%02d%02d"
1430 ;; end is non-inclusive!
1431 icalendar-recurring-start-year 1
1432 (+ (icalendar-first-weekday-of-year
1433 day icalendar-recurring-start-year)
1434 (if endtimestring 0 1)))
1435 (or endtimestring "")
1436 "\nRRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY="
1437 day)
1438 summary))
1439 ;; no match
1440 nil))
1442 (defun icalendar--convert-yearly-to-ical (nonmarker entry-main)
1443 "Convert yearly diary entry to iCalendar format.
1444 NONMARKER is a regular expression matching the start of non-marking
1445 entries. ENTRY-MAIN is the first line of the diary entry."
1446 (if (string-match (concat nonmarker
1447 (if (eq calendar-date-style 'european)
1448 "\\([0-9]+[0-9]?\\)\\s-+\\([a-z]+\\)\\s-+"
1449 "\\([a-z]+\\)\\s-+\\([0-9]+[0-9]?\\)\\s-+")
1450 "\\*?\\s-*"
1451 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1452 "\\("
1453 "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1454 "\\)?"
1455 "\\s-*\\([^0-9]+.*?\\) ?$" ; must not match years
1457 entry-main)
1458 (let* ((daypos (if (eq calendar-date-style 'european) 1 2))
1459 (monpos (if (eq calendar-date-style 'european) 2 1))
1460 (day (read (substring entry-main
1461 (match-beginning daypos)
1462 (match-end daypos))))
1463 (month (icalendar--get-month-number
1464 (substring entry-main
1465 (match-beginning monpos)
1466 (match-end monpos))))
1467 (starttimestring (icalendar--diarytime-to-isotime
1468 (if (match-beginning 4)
1469 (substring entry-main
1470 (match-beginning 4)
1471 (match-end 4))
1472 nil)
1473 (if (match-beginning 5)
1474 (substring entry-main
1475 (match-beginning 5)
1476 (match-end 5))
1477 nil)))
1478 (endtimestring (icalendar--diarytime-to-isotime
1479 (if (match-beginning 7)
1480 (substring entry-main
1481 (match-beginning 7)
1482 (match-end 7))
1483 nil)
1484 (if (match-beginning 8)
1485 (substring entry-main
1486 (match-beginning 8)
1487 (match-end 8))
1488 nil)))
1489 (summary (icalendar--convert-string-for-export
1490 (substring entry-main (match-beginning 9)
1491 (match-end 9)))))
1492 (icalendar--dmsg "yearly %s" entry-main)
1494 (when starttimestring
1495 (unless endtimestring
1496 (let ((time (read
1497 (icalendar--rris "^T0?" ""
1498 starttimestring))))
1499 (setq endtimestring (format "T%06d"
1500 (+ 10000 time))))))
1501 (cons (concat "\nDTSTART;"
1502 (if starttimestring "VALUE=DATE-TIME:"
1503 "VALUE=DATE:")
1504 (format "1900%02d%02d" month day)
1505 (or starttimestring "")
1506 "\nDTEND;"
1507 (if endtimestring "VALUE=DATE-TIME:"
1508 "VALUE=DATE:")
1509 ;; end is not included! shift by one day
1510 (icalendar--date-to-isodate
1511 (list month day 1900)
1512 (if endtimestring 0 1))
1513 (or endtimestring "")
1514 "\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH="
1515 (format "%d" month)
1516 ";BYMONTHDAY="
1517 (format "%d" day))
1518 summary))
1519 ;; no match
1520 nil))
1522 (defun icalendar--convert-sexp-to-ical (nonmarker entry-main &optional start)
1523 "Convert sexp diary entry to iCalendar format.
1524 Enumerate the evaluated sexp entry for the next
1525 `icalendar-export-sexp-enumeration-days' days. NONMARKER is a
1526 regular expression matching the start of non-marking entries.
1527 ENTRY-MAIN is the first line of the diary entry.
1529 Optional argument START determines the first day of the
1530 enumeration, given as a time value, in same format as returned by
1531 `current-time' -- used for test purposes."
1532 (cond ((string-match (concat nonmarker
1533 "%%(and \\(([^)]+)\\))\\(\\s-*.*?\\) ?$")
1534 entry-main)
1535 ;; simple sexp entry as generated by icalendar.el: strip off the
1536 ;; unnecessary (and)
1537 (icalendar--dmsg "diary-sexp from icalendar.el %s" entry-main)
1538 (icalendar--convert-to-ical
1539 nonmarker
1540 (concat "%%"
1541 (substring entry-main (match-beginning 1) (match-end 1))
1542 (substring entry-main (match-beginning 2) (match-end 2)))))
1543 ((string-match (concat nonmarker
1544 "%%\\(([^)]+)\\)\\s-*\\(.*\\)")
1545 entry-main)
1546 ;; regular sexp entry
1547 (icalendar--dmsg "diary-sexp %s" entry-main)
1548 (let ((p1 (substring entry-main (match-beginning 1) (match-end 1)))
1549 (p2 (substring entry-main (match-beginning 2) (match-end 2)))
1550 (now (or start (current-time))))
1551 (delete nil
1552 (mapcar
1553 (lambda (offset)
1554 (let* ((day (decode-time (time-add now
1555 (seconds-to-time
1556 (* offset 60 60 24)))))
1557 (d (nth 3 day))
1558 (m (nth 4 day))
1559 (y (nth 5 day))
1560 (se (diary-sexp-entry p1 p2 (list m d y)))
1561 (see (cond ((stringp se) se)
1562 ((consp se) (cdr se))
1563 (t nil))))
1564 (cond ((null see)
1565 nil)
1566 ((stringp see)
1567 (let ((calendar-date-style 'iso))
1568 (icalendar--convert-ordinary-to-ical
1569 nonmarker (format "%4d/%02d/%02d %s" y m d see))))
1570 (;TODO:
1571 (error (format "Unsupported Sexp-entry: %s"
1572 entry-main))))))
1573 (number-sequence
1574 0 (- icalendar-export-sexp-enumeration-days 1))))))
1576 ;; no match
1577 nil)))
1579 (defun icalendar--convert-block-to-ical (nonmarker entry-main)
1580 "Convert block diary entry to iCalendar format.
1581 NONMARKER is a regular expression matching the start of non-marking
1582 entries. ENTRY-MAIN is the first line of the diary entry."
1583 (if (string-match (concat nonmarker
1584 "%%(diary-block \\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)"
1585 " +\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
1586 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1587 "\\("
1588 "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1589 "\\)?"
1590 "\\s-*\\(.*?\\) ?$")
1591 entry-main)
1592 (let* ((startstring (substring entry-main
1593 (match-beginning 1)
1594 (match-end 1)))
1595 (endstring (substring entry-main
1596 (match-beginning 2)
1597 (match-end 2)))
1598 (startisostring (icalendar--datestring-to-isodate
1599 startstring))
1600 (endisostring (icalendar--datestring-to-isodate
1601 endstring))
1602 (endisostring+1 (icalendar--datestring-to-isodate
1603 endstring 1))
1604 (starttimestring (icalendar--diarytime-to-isotime
1605 (if (match-beginning 4)
1606 (substring entry-main
1607 (match-beginning 4)
1608 (match-end 4))
1609 nil)
1610 (if (match-beginning 5)
1611 (substring entry-main
1612 (match-beginning 5)
1613 (match-end 5))
1614 nil)))
1615 (endtimestring (icalendar--diarytime-to-isotime
1616 (if (match-beginning 7)
1617 (substring entry-main
1618 (match-beginning 7)
1619 (match-end 7))
1620 nil)
1621 (if (match-beginning 8)
1622 (substring entry-main
1623 (match-beginning 8)
1624 (match-end 8))
1625 nil)))
1626 (summary (icalendar--convert-string-for-export
1627 (substring entry-main (match-beginning 9)
1628 (match-end 9)))))
1629 (icalendar--dmsg "diary-block %s" entry-main)
1630 (when starttimestring
1631 (unless endtimestring
1632 (let ((time
1633 (read (icalendar--rris "^T0?" ""
1634 starttimestring))))
1635 (setq endtimestring (format "T%06d"
1636 (+ 10000 time))))))
1637 (if starttimestring
1638 ;; with time -> write rrule
1639 (cons (concat "\nDTSTART;VALUE=DATE-TIME:"
1640 startisostring
1641 starttimestring
1642 "\nDTEND;VALUE=DATE-TIME:"
1643 startisostring
1644 endtimestring
1645 "\nRRULE:FREQ=DAILY;INTERVAL=1;UNTIL="
1646 endisostring)
1647 summary)
1648 ;; no time -> write long event
1649 (cons (concat "\nDTSTART;VALUE=DATE:" startisostring
1650 "\nDTEND;VALUE=DATE:" endisostring+1)
1651 summary)))
1652 ;; no match
1653 nil))
1655 (defun icalendar--convert-float-to-ical (nonmarker entry-main)
1656 "Convert float diary entry to iCalendar format -- partially unsupported!
1658 FIXME! DAY from diary-float yet unimplemented.
1660 NONMARKER is a regular expression matching the start of non-marking
1661 entries. ENTRY-MAIN is the first line of the diary entry."
1662 (if (string-match (concat nonmarker "%%\\((diary-float .+\\) ?$") entry-main)
1663 (with-temp-buffer
1664 (insert (match-string 1 entry-main))
1665 (goto-char (point-min))
1666 (let* ((sexp (read (current-buffer))) ;using `read' here
1667 ;easier than regexp
1668 ;matching, esp. with
1669 ;different forms of
1670 ;MONTH
1671 (month (nth 1 sexp))
1672 (dayname (nth 2 sexp))
1673 (n (nth 3 sexp))
1674 (day (nth 4 sexp))
1675 (summary
1676 (replace-regexp-in-string
1677 "\\(^\s+\\|\s+$\\)" ""
1678 (buffer-substring (point) (point-max)))))
1680 (when day
1681 (progn
1682 (icalendar--dmsg "diary-float %s" entry-main)
1683 (error "Don't know if or how to implement day in `diary-float'")))
1685 (cons (concat
1686 ;;Start today (yes this is an arbitrary choice):
1687 "\nDTSTART;VALUE=DATE:"
1688 (format-time-string "%Y%m%d")
1689 ;;BUT remove today if `diary-float'
1690 ;;expression does not hold true for today:
1691 (when
1692 (null (let ((date (calendar-current-date))
1693 (entry entry-main))
1694 (diary-float month dayname n)))
1695 (concat
1696 "\nEXDATE;VALUE=DATE:"
1697 (format-time-string "%Y%m%d")))
1698 "\nRRULE:"
1699 (if (or (numberp month) (listp month))
1700 "FREQ=YEARLY;BYMONTH="
1701 "FREQ=MONTHLY")
1702 (when
1703 (listp month)
1704 (mapconcat
1705 (lambda (m)
1706 (number-to-string m))
1707 (cadr month) ","))
1708 (when
1709 (numberp month)
1710 (number-to-string month))
1711 ";BYDAY="
1712 (number-to-string n)
1713 (aref icalendar--weekday-array dayname))
1714 summary)))
1715 ;; no match
1716 nil))
1718 (defun icalendar--convert-date-to-ical (nonmarker entry-main)
1719 "Convert `diary-date' diary entry to iCalendar format -- unsupported!
1721 FIXME!
1723 NONMARKER is a regular expression matching the start of non-marking
1724 entries. ENTRY-MAIN is the first line of the diary entry."
1725 (if (string-match (concat nonmarker
1726 "%%(diary-date \\([^)]+\\))\\s-*\\(.*?\\) ?$")
1727 entry-main)
1728 (progn
1729 (icalendar--dmsg "diary-date %s" entry-main)
1730 (error "`diary-date' is not supported yet"))
1731 ;; no match
1732 nil))
1734 (defun icalendar--convert-cyclic-to-ical (nonmarker entry-main)
1735 "Convert `diary-cyclic' diary entry to iCalendar format.
1736 NONMARKER is a regular expression matching the start of non-marking
1737 entries. ENTRY-MAIN is the first line of the diary entry."
1738 (if (string-match (concat nonmarker
1739 "%%(diary-cyclic \\([^ ]+\\) +"
1740 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
1741 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1742 "\\("
1743 "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1744 "\\)?"
1745 "\\s-*\\(.*?\\) ?$")
1746 entry-main)
1747 (let* ((frequency (substring entry-main (match-beginning 1)
1748 (match-end 1)))
1749 (datetime (substring entry-main (match-beginning 2)
1750 (match-end 2)))
1751 (startisostring (icalendar--datestring-to-isodate
1752 datetime))
1753 (endisostring (icalendar--datestring-to-isodate
1754 datetime))
1755 (endisostring+1 (icalendar--datestring-to-isodate
1756 datetime 1))
1757 (starttimestring (icalendar--diarytime-to-isotime
1758 (if (match-beginning 4)
1759 (substring entry-main
1760 (match-beginning 4)
1761 (match-end 4))
1762 nil)
1763 (if (match-beginning 5)
1764 (substring entry-main
1765 (match-beginning 5)
1766 (match-end 5))
1767 nil)))
1768 (endtimestring (icalendar--diarytime-to-isotime
1769 (if (match-beginning 7)
1770 (substring entry-main
1771 (match-beginning 7)
1772 (match-end 7))
1773 nil)
1774 (if (match-beginning 8)
1775 (substring entry-main
1776 (match-beginning 8)
1777 (match-end 8))
1778 nil)))
1779 (summary (icalendar--convert-string-for-export
1780 (substring entry-main (match-beginning 9)
1781 (match-end 9)))))
1782 (icalendar--dmsg "diary-cyclic %s" entry-main)
1783 (when starttimestring
1784 (unless endtimestring
1785 (let ((time
1786 (read (icalendar--rris "^T0?" ""
1787 starttimestring))))
1788 (setq endtimestring (format "T%06d"
1789 (+ 10000 time))))))
1790 (cons (concat "\nDTSTART;"
1791 (if starttimestring "VALUE=DATE-TIME:"
1792 "VALUE=DATE:")
1793 startisostring
1794 (or starttimestring "")
1795 "\nDTEND;"
1796 (if endtimestring "VALUE=DATE-TIME:"
1797 "VALUE=DATE:")
1798 (if endtimestring endisostring endisostring+1)
1799 (or endtimestring "")
1800 "\nRRULE:FREQ=DAILY;INTERVAL=" frequency
1801 ;; strange: korganizer does not expect
1802 ;; BYSOMETHING here...
1804 summary))
1805 ;; no match
1806 nil))
1808 (defun icalendar--convert-anniversary-to-ical (nonmarker entry-main)
1809 "Convert `diary-anniversary' diary entry to iCalendar format.
1810 NONMARKER is a regular expression matching the start of non-marking
1811 entries. ENTRY-MAIN is the first line of the diary entry."
1812 (if (string-match (concat nonmarker
1813 "%%(diary-anniversary \\([^)]+\\))\\s-*"
1814 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1815 "\\("
1816 "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1817 "\\)?"
1818 "\\s-*\\(.*?\\) ?$")
1819 entry-main)
1820 (let* ((datetime (substring entry-main (match-beginning 1)
1821 (match-end 1)))
1822 (startisostring (icalendar--datestring-to-isodate
1823 datetime))
1824 (endisostring (icalendar--datestring-to-isodate
1825 datetime 1))
1826 (starttimestring (icalendar--diarytime-to-isotime
1827 (if (match-beginning 3)
1828 (substring entry-main
1829 (match-beginning 3)
1830 (match-end 3))
1831 nil)
1832 (if (match-beginning 4)
1833 (substring entry-main
1834 (match-beginning 4)
1835 (match-end 4))
1836 nil)))
1837 (endtimestring (icalendar--diarytime-to-isotime
1838 (if (match-beginning 6)
1839 (substring entry-main
1840 (match-beginning 6)
1841 (match-end 6))
1842 nil)
1843 (if (match-beginning 7)
1844 (substring entry-main
1845 (match-beginning 7)
1846 (match-end 7))
1847 nil)))
1848 (summary (icalendar--convert-string-for-export
1849 (substring entry-main (match-beginning 8)
1850 (match-end 8)))))
1851 (icalendar--dmsg "diary-anniversary %s" entry-main)
1852 (when starttimestring
1853 (unless endtimestring
1854 (let ((time
1855 (read (icalendar--rris "^T0?" ""
1856 starttimestring))))
1857 (setq endtimestring (format "T%06d"
1858 (+ 10000 time))))))
1859 (cons (concat "\nDTSTART;"
1860 (if starttimestring "VALUE=DATE-TIME:"
1861 "VALUE=DATE:")
1862 startisostring
1863 (or starttimestring "")
1864 "\nDTEND;"
1865 (if endtimestring "VALUE=DATE-TIME:"
1866 "VALUE=DATE:")
1867 endisostring
1868 (or endtimestring "")
1869 "\nRRULE:FREQ=YEARLY;INTERVAL=1"
1870 ;; the following is redundant,
1871 ;; but korganizer seems to expect this... ;(
1872 ;; and evolution doesn't understand it... :(
1873 ;; so... who is wrong?!
1874 ";BYMONTH="
1875 (substring startisostring 4 6)
1876 ";BYMONTHDAY="
1877 (substring startisostring 6 8))
1878 summary))
1879 ;; no match
1880 nil))
1882 ;; ======================================================================
1883 ;; Import -- convert iCalendar to emacs-diary
1884 ;; ======================================================================
1886 ;;;###autoload
1887 (defun icalendar-import-file (ical-filename diary-filename
1888 &optional non-marking)
1889 "Import an iCalendar file and append to a diary file.
1890 Argument ICAL-FILENAME output iCalendar file.
1891 Argument DIARY-FILENAME input `diary-file'.
1892 Optional argument NON-MARKING determines whether events are created as
1893 non-marking or not."
1894 (interactive "fImport iCalendar data from file: \n\
1895 Finto diary file:
1897 ;; clean up the diary file
1898 (save-current-buffer
1899 ;; now load and convert from the ical file
1900 (set-buffer (find-file ical-filename))
1901 (icalendar-import-buffer diary-filename t non-marking)))
1903 ;;;###autoload
1904 (defun icalendar-import-buffer (&optional diary-file do-not-ask
1905 non-marking)
1906 "Extract iCalendar events from current buffer.
1908 This function searches the current buffer for the first iCalendar
1909 object, reads it and adds all VEVENT elements to the diary
1910 DIARY-FILE.
1912 It will ask for each appointment whether to add it to the diary
1913 unless DO-NOT-ASK is non-nil. When called interactively,
1914 DO-NOT-ASK is nil, so that you are asked for each event.
1916 NON-MARKING determines whether diary events are created as
1917 non-marking.
1919 Return code t means that importing worked well, return code nil
1920 means that an error has occurred. Error messages will be in the
1921 buffer `*icalendar-errors*'."
1922 (interactive)
1923 (save-current-buffer
1924 ;; prepare ical
1925 (message "Preparing iCalendar...")
1926 (set-buffer (icalendar--get-unfolded-buffer (current-buffer)))
1927 (goto-char (point-min))
1928 (message "Preparing iCalendar...done")
1929 (if (re-search-forward "^BEGIN:VCALENDAR\\s-*$" nil t)
1930 (let (ical-contents ical-errors)
1931 ;; read ical
1932 (message "Reading iCalendar...")
1933 (beginning-of-line)
1934 (setq ical-contents (icalendar--read-element nil nil))
1935 (message "Reading iCalendar...done")
1936 ;; convert ical
1937 (message "Converting iCalendar...")
1938 (setq ical-errors (icalendar--convert-ical-to-diary
1939 ical-contents
1940 diary-file do-not-ask non-marking))
1941 (when diary-file
1942 ;; save the diary file if it is visited already
1943 (let ((b (find-buffer-visiting diary-file)))
1944 (when b
1945 (save-current-buffer
1946 (set-buffer b)
1947 (save-buffer)))))
1948 (message "Converting iCalendar...done")
1949 ;; return t if no error occurred
1950 (not ical-errors))
1951 (message
1952 "Current buffer does not contain iCalendar contents!")
1953 ;; return nil, i.e. import did not work
1954 nil)))
1956 (define-obsolete-function-alias 'icalendar-extract-ical-from-buffer
1957 'icalendar-import-buffer "22.1")
1959 (defun icalendar--format-ical-event (event)
1960 "Create a string representation of an iCalendar EVENT."
1961 (if (functionp icalendar-import-format)
1962 (funcall icalendar-import-format event)
1963 (let ((string icalendar-import-format)
1964 (case-fold-search nil)
1965 (conversion-list
1966 '(("%c" CLASS icalendar-import-format-class)
1967 ("%d" DESCRIPTION icalendar-import-format-description)
1968 ("%l" LOCATION icalendar-import-format-location)
1969 ("%o" ORGANIZER icalendar-import-format-organizer)
1970 ("%s" SUMMARY icalendar-import-format-summary)
1971 ("%t" STATUS icalendar-import-format-status)
1972 ("%u" URL icalendar-import-format-url)
1973 ("%U" UID icalendar-import-format-uid))))
1974 ;; convert the specifiers in the format string
1975 (mapc (lambda (i)
1976 (let* ((spec (car i))
1977 (prop (cadr i))
1978 (format (car (cddr i)))
1979 (contents (icalendar--get-event-property event prop))
1980 (formatted-contents ""))
1981 (when (and contents (> (length contents) 0))
1982 (setq formatted-contents
1983 (icalendar--rris "%s"
1984 (icalendar--convert-string-for-import
1985 contents)
1986 (symbol-value format)
1987 t t)))
1988 (setq string (icalendar--rris spec
1989 formatted-contents
1990 string
1991 t t))))
1992 conversion-list)
1993 string)))
1995 (defun icalendar--convert-ical-to-diary (ical-list diary-file
1996 &optional do-not-ask
1997 non-marking)
1998 "Convert iCalendar data to an Emacs diary file.
1999 Import VEVENTS from the iCalendar object ICAL-LIST and saves them to a
2000 DIARY-FILE. If DO-NOT-ASK is nil the user is asked for each event
2001 whether to actually import it. NON-MARKING determines whether diary
2002 events are created as non-marking.
2003 This function attempts to return t if something goes wrong. In this
2004 case an error string which describes all the errors and problems is
2005 written into the buffer `*icalendar-errors*'."
2006 (let* ((ev (icalendar--all-events ical-list))
2007 (error-string "")
2008 (event-ok t)
2009 (found-error nil)
2010 (zone-map (icalendar--convert-all-timezones ical-list))
2011 e diary-string)
2012 ;; step through all events/appointments
2013 (while ev
2014 (setq e (car ev))
2015 (setq ev (cdr ev))
2016 (setq event-ok nil)
2017 (condition-case error-val
2018 (let* ((dtstart (icalendar--get-event-property e 'DTSTART))
2019 (dtstart-zone (icalendar--find-time-zone
2020 (icalendar--get-event-property-attributes
2021 e 'DTSTART)
2022 zone-map))
2023 (dtstart-dec (icalendar--decode-isodatetime dtstart nil
2024 dtstart-zone))
2025 (start-d (icalendar--datetime-to-diary-date
2026 dtstart-dec))
2027 (start-t (icalendar--datetime-to-colontime dtstart-dec))
2028 (dtend (icalendar--get-event-property e 'DTEND))
2029 (dtend-zone (icalendar--find-time-zone
2030 (icalendar--get-event-property-attributes
2031 e 'DTEND)
2032 zone-map))
2033 (dtend-dec (icalendar--decode-isodatetime dtend
2034 nil dtend-zone))
2035 (dtend-1-dec (icalendar--decode-isodatetime dtend -1
2036 dtend-zone))
2037 end-d
2038 end-1-d
2039 end-t
2040 (summary (icalendar--convert-string-for-import
2041 (or (icalendar--get-event-property e 'SUMMARY)
2042 "No summary")))
2043 (rrule (icalendar--get-event-property e 'RRULE))
2044 (rdate (icalendar--get-event-property e 'RDATE))
2045 (duration (icalendar--get-event-property e 'DURATION)))
2046 (icalendar--dmsg "%s: `%s'" start-d summary)
2047 ;; check whether start-time is missing
2048 (if (and dtstart
2049 (string=
2050 (cadr (icalendar--get-event-property-attributes
2051 e 'DTSTART))
2052 "DATE"))
2053 (setq start-t nil))
2054 (when duration
2055 (let ((dtend-dec-d (icalendar--add-decoded-times
2056 dtstart-dec
2057 (icalendar--decode-isoduration duration)))
2058 (dtend-1-dec-d (icalendar--add-decoded-times
2059 dtstart-dec
2060 (icalendar--decode-isoduration duration
2061 t))))
2062 (if (and dtend-dec (not (eq dtend-dec dtend-dec-d)))
2063 (message "Inconsistent endtime and duration for %s"
2064 summary))
2065 (setq dtend-dec dtend-dec-d)
2066 (setq dtend-1-dec dtend-1-dec-d)))
2067 (setq end-d (if dtend-dec
2068 (icalendar--datetime-to-diary-date dtend-dec)
2069 start-d))
2070 (setq end-1-d (if dtend-1-dec
2071 (icalendar--datetime-to-diary-date dtend-1-dec)
2072 start-d))
2073 (setq end-t (if (and
2074 dtend-dec
2075 (not (string=
2076 (cadr
2077 (icalendar--get-event-property-attributes
2078 e 'DTEND))
2079 "DATE")))
2080 (icalendar--datetime-to-colontime dtend-dec)
2081 start-t))
2082 (icalendar--dmsg "start-d: %s, end-d: %s" start-d end-d)
2083 (cond
2084 ;; recurring event
2085 (rrule
2086 (setq diary-string
2087 (icalendar--convert-recurring-to-diary e dtstart-dec start-t
2088 end-t))
2089 (setq event-ok t))
2090 (rdate
2091 (icalendar--dmsg "rdate event")
2092 (setq diary-string "")
2093 (mapc (lambda (datestring)
2094 (setq diary-string
2095 (concat diary-string
2096 (format "......"))))
2097 (icalendar--split-value rdate)))
2098 ;; non-recurring event
2099 ;; all-day event
2100 ((not (string= start-d end-d))
2101 (setq diary-string
2102 (icalendar--convert-non-recurring-all-day-to-diary
2103 e start-d end-1-d))
2104 (setq event-ok t))
2105 ;; not all-day
2106 ((and start-t (or (not end-t)
2107 (not (string= start-t end-t))))
2108 (setq diary-string
2109 (icalendar--convert-non-recurring-not-all-day-to-diary
2110 e dtstart-dec dtend-dec start-t end-t))
2111 (setq event-ok t))
2112 ;; all-day event
2114 (icalendar--dmsg "all day event")
2115 (setq diary-string (icalendar--datetime-to-diary-date
2116 dtstart-dec "/"))
2117 (setq event-ok t)))
2118 ;; add all other elements unless the user doesn't want to have
2119 ;; them
2120 (if event-ok
2121 (progn
2122 (setq diary-string
2123 (concat diary-string " "
2124 (icalendar--format-ical-event e)))
2125 (if do-not-ask (setq summary nil))
2126 ;; add entry to diary and store actual name of diary
2127 ;; file (in case it was nil)
2128 (setq diary-file
2129 (icalendar--add-diary-entry diary-string diary-file
2130 non-marking summary)))
2131 ;; event was not ok
2132 (setq found-error t)
2133 (setq error-string
2134 (format "%s\nCannot handle this event:%s"
2135 error-string e))))
2136 ;; FIXME: inform user about ignored event properties
2137 ;; handle errors
2138 (error
2139 (message "Ignoring event \"%s\"" e)
2140 (setq found-error t)
2141 (setq error-string (format "%s\n%s\nCannot handle this event: %s"
2142 error-val error-string e))
2143 (message "%s" error-string))))
2145 ;; insert final newline
2146 (if diary-file
2147 (let ((b (find-buffer-visiting diary-file)))
2148 (when b
2149 (save-current-buffer
2150 (set-buffer b)
2151 (goto-char (point-max))
2152 (insert "\n")))))
2153 (if found-error
2154 (save-current-buffer
2155 (set-buffer (get-buffer-create "*icalendar-errors*"))
2156 (erase-buffer)
2157 (insert error-string)))
2158 (message "Converting iCalendar...done")
2159 found-error))
2161 ;; subroutines for importing
2162 (defun icalendar--convert-recurring-to-diary (e dtstart-dec start-t end-t)
2163 "Convert recurring iCalendar event E to diary format.
2165 DTSTART-DEC is the DTSTART property of E.
2166 START-T is the event's start time in diary format.
2167 END-T is the event's end time in diary format."
2168 (icalendar--dmsg "recurring event")
2169 (let* ((rrule (icalendar--get-event-property e 'RRULE))
2170 (rrule-props (icalendar--split-value rrule))
2171 (frequency (cadr (assoc 'FREQ rrule-props)))
2172 (until (cadr (assoc 'UNTIL rrule-props)))
2173 (count (cadr (assoc 'COUNT rrule-props)))
2174 (interval (read (or (cadr (assoc 'INTERVAL rrule-props)) "1")))
2175 (dtstart-conv (icalendar--datetime-to-diary-date dtstart-dec))
2176 (until-conv (icalendar--datetime-to-diary-date
2177 (icalendar--decode-isodatetime until)))
2178 (until-1-conv (icalendar--datetime-to-diary-date
2179 (icalendar--decode-isodatetime until -1)))
2180 (result ""))
2182 ;; FIXME FIXME interval!!!!!!!!!!!!!
2184 (when count
2185 (if until
2186 (message "Must not have UNTIL and COUNT -- ignoring COUNT element!")
2187 (let ((until-1 0))
2188 (cond ((string-equal frequency "DAILY")
2189 (setq until (icalendar--add-decoded-times
2190 dtstart-dec
2191 (list 0 0 0 (* (read count) interval) 0 0)))
2192 (setq until-1 (icalendar--add-decoded-times
2193 dtstart-dec
2194 (list 0 0 0 (* (- (read count) 1) interval)
2195 0 0)))
2197 ((string-equal frequency "WEEKLY")
2198 (setq until (icalendar--add-decoded-times
2199 dtstart-dec
2200 (list 0 0 0 (* (read count) 7 interval) 0 0)))
2201 (setq until-1 (icalendar--add-decoded-times
2202 dtstart-dec
2203 (list 0 0 0 (* (- (read count) 1) 7
2204 interval) 0 0)))
2206 ((string-equal frequency "MONTHLY")
2207 (setq until (icalendar--add-decoded-times
2208 dtstart-dec (list 0 0 0 0 (* (- (read count) 1)
2209 interval) 0)))
2210 (setq until-1 (icalendar--add-decoded-times
2211 dtstart-dec (list 0 0 0 0 (* (- (read count) 1)
2212 interval) 0)))
2214 ((string-equal frequency "YEARLY")
2215 (setq until (icalendar--add-decoded-times
2216 dtstart-dec (list 0 0 0 0 0 (* (- (read count) 1)
2217 interval))))
2218 (setq until-1 (icalendar--add-decoded-times
2219 dtstart-dec
2220 (list 0 0 0 0 0 (* (- (read count) 1)
2221 interval))))
2224 (message "Cannot handle COUNT attribute for `%s' events."
2225 frequency)))
2226 (setq until-conv (icalendar--datetime-to-diary-date until))
2227 (setq until-1-conv (icalendar--datetime-to-diary-date until-1))
2230 (cond ((string-equal frequency "WEEKLY")
2231 (let* ((byday (cadr (assoc 'BYDAY rrule-props)))
2232 (weekdays
2233 (icalendar--get-weekday-numbers byday))
2234 (weekday-clause
2235 (when (> (length weekdays) 1)
2236 (format "(memq (calendar-day-of-week date) '%s) "
2237 weekdays))))
2238 (if (not start-t)
2239 (progn
2240 ;; weekly and all-day
2241 (icalendar--dmsg "weekly all-day")
2242 (if until
2243 (setq result
2244 (format
2245 (concat "%%%%(and "
2246 "%s"
2247 "(diary-block %s %s))")
2248 (or weekday-clause
2249 (format "(diary-cyclic %d %s) "
2250 (* interval 7)
2251 dtstart-conv))
2252 dtstart-conv
2253 (if count until-1-conv until-conv)
2255 (setq result
2256 (format "%%%%(and %s(diary-cyclic %d %s))"
2257 (or weekday-clause "")
2258 (if weekday-clause 1 (* interval 7))
2259 dtstart-conv))))
2260 ;; weekly and not all-day
2261 (icalendar--dmsg "weekly not-all-day")
2262 (if until
2263 (setq result
2264 (format
2265 (concat "%%%%(and "
2266 "%s"
2267 "(diary-block %s %s)) "
2268 "%s%s%s")
2269 (or weekday-clause
2270 (format "(diary-cyclic %d %s) "
2271 (* interval 7)
2272 dtstart-conv))
2273 dtstart-conv
2274 until-conv
2275 (or start-t "")
2276 (if end-t "-" "") (or end-t "")))
2277 ;; no limit
2278 ;; FIXME!!!!
2279 ;; DTSTART;VALUE=DATE-TIME:20030919T090000
2280 ;; DTEND;VALUE=DATE-TIME:20030919T113000
2281 (setq result
2282 (format
2283 "%%%%(and %s(diary-cyclic %d %s)) %s%s%s"
2284 (or weekday-clause "")
2285 (if weekday-clause 1 (* interval 7))
2286 dtstart-conv
2287 (or start-t "")
2288 (if end-t "-" "") (or end-t "")))))))
2289 ;; yearly
2290 ((string-equal frequency "YEARLY")
2291 (icalendar--dmsg "yearly")
2292 (if until
2293 (let ((day (nth 3 dtstart-dec))
2294 (month (nth 4 dtstart-dec)))
2295 (setq result (concat "%%(and (diary-date "
2296 (cond ((eq calendar-date-style 'iso)
2297 (format "t %d %d" month day))
2298 ((eq calendar-date-style 'european)
2299 (format "%d %d t" day month))
2300 ((eq calendar-date-style 'american)
2301 (format "%d %d t" month day)))
2302 ") (diary-block "
2303 dtstart-conv
2305 until-conv
2306 ")) "
2307 (or start-t "")
2308 (if end-t "-" "")
2309 (or end-t ""))))
2310 (setq result (format
2311 "%%%%(and (diary-anniversary %s)) %s%s%s"
2312 dtstart-conv
2313 (or start-t "")
2314 (if end-t "-" "") (or end-t "")))))
2315 ;; monthly
2316 ((string-equal frequency "MONTHLY")
2317 (icalendar--dmsg "monthly")
2318 (setq result
2319 (format
2320 "%%%%(and (diary-date %s) (diary-block %s %s)) %s%s%s"
2321 (let ((day (nth 3 dtstart-dec)))
2322 (cond ((eq calendar-date-style 'iso)
2323 (format "t t %d" day))
2324 ((eq calendar-date-style 'european)
2325 (format "%d t t" day))
2326 ((eq calendar-date-style 'american)
2327 (format "t %d t" day))))
2328 dtstart-conv
2329 (if until
2330 until-conv
2331 (if (eq calendar-date-style 'iso) "9999 1 1" "1 1 9999")) ;; FIXME: should be unlimited
2332 (or start-t "")
2333 (if end-t "-" "") (or end-t ""))))
2334 ;; daily
2335 ((and (string-equal frequency "DAILY"))
2336 (if until
2337 (setq result
2338 (format
2339 (concat "%%%%(and (diary-cyclic %s %s) "
2340 "(diary-block %s %s)) %s%s%s")
2341 interval dtstart-conv dtstart-conv
2342 (if count until-1-conv until-conv)
2343 (or start-t "")
2344 (if end-t "-" "") (or end-t "")))
2345 (setq result
2346 (format
2347 "%%%%(and (diary-cyclic %s %s)) %s%s%s"
2348 interval
2349 dtstart-conv
2350 (or start-t "")
2351 (if end-t "-" "") (or end-t ""))))))
2352 ;; Handle exceptions from recurrence rules
2353 (let ((ex-dates (icalendar--get-event-properties e 'EXDATE)))
2354 (while ex-dates
2355 (let* ((ex-start (icalendar--decode-isodatetime
2356 (car ex-dates)))
2357 (ex-d (icalendar--datetime-to-diary-date
2358 ex-start)))
2359 (setq result
2360 (icalendar--rris "^%%(\\(and \\)?"
2361 (format
2362 "%%%%(and (not (diary-date %s)) "
2363 ex-d)
2364 result)))
2365 (setq ex-dates (cdr ex-dates))))
2366 ;; FIXME: exception rules are not recognized
2367 (if (icalendar--get-event-property e 'EXRULE)
2368 (setq result
2369 (concat result
2370 "\n Exception rules: "
2371 (icalendar--get-event-properties
2372 e 'EXRULE))))
2373 result))
2375 (defun icalendar--convert-non-recurring-all-day-to-diary (event start-d end-d)
2376 "Convert non-recurring iCalendar EVENT to diary format.
2378 DTSTART is the decoded DTSTART property of E.
2379 Argument START-D gives the first day.
2380 Argument END-D gives the last day."
2381 (icalendar--dmsg "non-recurring all-day event")
2382 (format "%%%%(and (diary-block %s %s))" start-d end-d))
2384 (defun icalendar--convert-non-recurring-not-all-day-to-diary (event dtstart-dec
2385 dtend-dec
2386 start-t
2387 end-t)
2388 "Convert recurring icalendar EVENT to diary format.
2390 DTSTART-DEC is the decoded DTSTART property of E.
2391 DTEND-DEC is the decoded DTEND property of E.
2392 START-T is the event's start time in diary format.
2393 END-T is the event's end time in diary format."
2394 (icalendar--dmsg "not all day event")
2395 (cond (end-t
2396 (format "%s %s-%s"
2397 (icalendar--datetime-to-diary-date
2398 dtstart-dec "/")
2399 start-t end-t))
2401 (format "%s %s"
2402 (icalendar--datetime-to-diary-date
2403 dtstart-dec "/")
2404 start-t))))
2406 (defun icalendar--add-diary-entry (string diary-file non-marking
2407 &optional summary)
2408 "Add STRING to the diary file DIARY-FILE.
2409 STRING must be a properly formatted valid diary entry. NON-MARKING
2410 determines whether diary events are created as non-marking. If
2411 SUMMARY is not nil it must be a string that gives the summary of the
2412 entry. In this case the user will be asked whether he wants to insert
2413 the entry."
2414 (when (or (not summary)
2415 (y-or-n-p (format "Add appointment for `%s' to diary? "
2416 summary)))
2417 (when summary
2418 (setq non-marking
2419 (y-or-n-p (format "Make appointment non-marking? "))))
2420 (save-window-excursion
2421 (unless diary-file
2422 (setq diary-file
2423 (read-file-name "Add appointment to this diary file: ")))
2424 ;; Note: diary-make-entry will add a trailing blank char.... :(
2425 (funcall (if (fboundp 'diary-make-entry)
2426 'diary-make-entry
2427 'make-diary-entry)
2428 string non-marking diary-file)))
2429 ;; Würgaround to remove the trailing blank char
2430 (with-current-buffer (find-file diary-file)
2431 (goto-char (point-max))
2432 (if (= (char-before) ? )
2433 (delete-char -1)))
2434 ;; return diary-file in case it has been changed interactively
2435 diary-file)
2437 ;; ======================================================================
2438 ;; Examples
2439 ;; ======================================================================
2440 (defun icalendar-import-format-sample (event)
2441 "Example function for formatting an iCalendar EVENT."
2442 (format (concat "SUMMARY=`%s' DESCRIPTION=`%s' LOCATION=`%s' ORGANIZER=`%s' "
2443 "STATUS=`%s' URL=`%s' CLASS=`%s'")
2444 (or (icalendar--get-event-property event 'SUMMARY) "")
2445 (or (icalendar--get-event-property event 'DESCRIPTION) "")
2446 (or (icalendar--get-event-property event 'LOCATION) "")
2447 (or (icalendar--get-event-property event 'ORGANIZER) "")
2448 (or (icalendar--get-event-property event 'STATUS) "")
2449 (or (icalendar--get-event-property event 'URL) "")
2450 (or (icalendar--get-event-property event 'CLASS) "")))
2452 (provide 'icalendar)
2454 ;;; icalendar.el ends here