Spelling fixes.
[emacs.git] / lisp / calendar / icalendar.el
blob1fffe377954855c9f3f5f8524e8367575d9a1c12
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 "24.5"
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 "24.5"
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 ((offset (car (cddr (assq 'TZOFFSETTO alist))))
489 (rrule-value (car (cddr (assq 'RRULE alist))))
490 (dtstart (car (cddr (assq 'DTSTART alist)))))
491 ;; FIXME: for now we only handle RRULE and not RDATE here.
492 (when (and offset rrule-value dtstart)
493 (let* ((rrule (icalendar--split-value rrule-value))
494 (freq (cadr (assq 'FREQ rrule)))
495 (bymonth (cadr (assq 'BYMONTH rrule)))
496 (byday (cadr (assq 'BYDAY rrule))))
497 ;; FIXME: we don't correctly handle WKST here.
498 (if (and (string= freq "YEARLY") bymonth)
499 (cons
500 (concat
501 ;; Fake a name.
502 (if dst-p "DST" "STD")
503 ;; For TZ, OFFSET is added to the local time. So,
504 ;; invert the values.
505 (if (eq (aref offset 0) ?-) "+" "-")
506 (substring offset 1 3)
508 (substring offset 3 5))
509 ;; The start time.
510 (let* ((day (icalendar--get-weekday-number (substring byday -2)))
511 (week (if (eq day -1)
512 byday
513 (substring byday 0 -2))))
514 ;; "Translate" the iCalendar way to specify the last
515 ;; (sun|mon|...)day in month to the tzset way.
516 (if (string= week "-1") ; last day as iCalendar calls it
517 (setq week "5")) ; last day as tzset calls it
518 (concat "M" bymonth "." week "." (if (eq day -1) "0"
519 (int-to-string day))
520 ;; Start time.
522 (substring dtstart -6 -4)
524 (substring dtstart -4 -2)
526 (substring dtstart -2)))))))))
528 (defun icalendar--parse-vtimezone (alist)
529 "Turn a VTIMEZONE ALIST into a cons (ID . TZ-STRING).
530 Return nil if timezone cannot be parsed."
531 (let* ((tz-id (icalendar--convert-string-for-import
532 (icalendar--get-event-property alist 'TZID)))
533 (daylight (cadr (cdar (icalendar--get-children alist 'DAYLIGHT))))
534 (day (and daylight (icalendar--convert-tz-offset daylight t)))
535 (standard (cadr (cdar (icalendar--get-children alist 'STANDARD))))
536 (std (and standard (icalendar--convert-tz-offset standard nil))))
537 (if (and tz-id std)
538 (cons tz-id
539 (if day
540 (concat (car std) (car day)
541 "," (cdr day) "," (cdr std))
542 (car std))))))
544 (defun icalendar--convert-all-timezones (icalendar)
545 "Convert all timezones in the ICALENDAR into an alist.
546 Each element of the alist is a cons (ID . TZ-STRING),
547 like `icalendar--parse-vtimezone'."
548 (let (result)
549 (dolist (zone (icalendar--get-children (car icalendar) 'VTIMEZONE))
550 (setq zone (icalendar--parse-vtimezone zone))
551 (if zone
552 (setq result (cons zone result))))
553 result))
555 (defun icalendar--find-time-zone (prop-list zone-map)
556 "Return a timezone string for the time zone in PROP-LIST, or nil if none.
557 ZONE-MAP is a timezone alist as returned by `icalendar--convert-all-timezones'."
558 (let ((id (plist-get prop-list 'TZID)))
559 (if id
560 (cdr (assoc id zone-map)))))
562 (defun icalendar--decode-isodatetime (isodatetimestring &optional day-shift
563 zone)
564 "Return ISODATETIMESTRING in format like `decode-time'.
565 Converts from ISO-8601 to Emacs representation. If
566 ISODATETIMESTRING specifies UTC time (trailing letter Z) the
567 decoded time is given in the local time zone! If optional
568 parameter DAY-SHIFT is non-nil the result is shifted by DAY-SHIFT
569 days.
570 ZONE, if provided, is the timezone, in any format understood by `encode-time'.
572 FIXME: multiple comma-separated values should be allowed!"
573 (icalendar--dmsg isodatetimestring)
574 (if isodatetimestring
575 ;; day/month/year must be present
576 (let ((year (read (substring isodatetimestring 0 4)))
577 (month (read (substring isodatetimestring 4 6)))
578 (day (read (substring isodatetimestring 6 8)))
579 (hour 0)
580 (minute 0)
581 (second 0))
582 (when (> (length isodatetimestring) 12)
583 ;; hour/minute present
584 (setq hour (read (substring isodatetimestring 9 11)))
585 (setq minute (read (substring isodatetimestring 11 13))))
586 (when (> (length isodatetimestring) 14)
587 ;; seconds present
588 (setq second (read (substring isodatetimestring 13 15))))
589 (when (and (> (length isodatetimestring) 15)
590 ;; UTC specifier present
591 (char-equal ?Z (aref isodatetimestring 15)))
592 ;; if not UTC add current-time-zone offset
593 ;; current-time-zone should be called with actual UTC time
594 ;; (daylight saving at that time may differ to current one)
595 (setq second (+ (car (current-time-zone
596 (encode-time second minute hour day month year
597 0)))
598 second)))
599 ;; shift if necessary
600 (if day-shift
601 (let ((mdy (calendar-gregorian-from-absolute
602 (+ (calendar-absolute-from-gregorian
603 (list month day year))
604 day-shift))))
605 (setq month (nth 0 mdy))
606 (setq day (nth 1 mdy))
607 (setq year (nth 2 mdy))))
608 ;; create the decoded date-time
609 ;; FIXME!?!
610 (condition-case nil
611 (decode-time (encode-time second minute hour day month year zone))
612 (error
613 (message "Cannot decode \"%s\"" isodatetimestring)
614 ;; hope for the best...
615 (list second minute hour day month year 0 nil 0))))
616 ;; isodatetimestring == nil
617 nil))
619 (defun icalendar--decode-isoduration (isodurationstring
620 &optional duration-correction)
621 "Convert ISODURATIONSTRING into format provided by `decode-time'.
622 Converts from ISO-8601 to Emacs representation. If ISODURATIONSTRING
623 specifies UTC time (trailing letter Z) the decoded time is given in
624 the local time zone!
626 Optional argument DURATION-CORRECTION shortens result by one day.
628 FIXME: TZID-attributes are ignored....!
629 FIXME: multiple comma-separated values should be allowed!"
630 (if isodurationstring
631 (save-match-data
632 (string-match
633 (concat
634 "^P[+-]?\\("
635 "\\(\\([0-9]+\\)D\\)" ; days only
636 "\\|"
637 "\\(\\(\\([0-9]+\\)D\\)?T\\(\\([0-9]+\\)H\\)?" ; opt days
638 "\\(\\([0-9]+\\)M\\)?\\(\\([0-9]+\\)S\\)?\\)" ; mand. time
639 "\\|"
640 "\\(\\([0-9]+\\)W\\)" ; weeks only
641 "\\)$") isodurationstring)
642 (let ((seconds 0)
643 (minutes 0)
644 (hours 0)
645 (days 0)
646 (months 0)
647 (years 0))
648 (cond
649 ((match-beginning 2) ;days only
650 (setq days (read (substring isodurationstring
651 (match-beginning 3)
652 (match-end 3))))
653 (when duration-correction
654 (setq days (1- days))))
655 ((match-beginning 4) ;days and time
656 (if (match-beginning 5)
657 (setq days (* 7 (read (substring isodurationstring
658 (match-beginning 6)
659 (match-end 6))))))
660 (if (match-beginning 7)
661 (setq hours (read (substring isodurationstring
662 (match-beginning 8)
663 (match-end 8)))))
664 (if (match-beginning 9)
665 (setq minutes (read (substring isodurationstring
666 (match-beginning 10)
667 (match-end 10)))))
668 (if (match-beginning 11)
669 (setq seconds (read (substring isodurationstring
670 (match-beginning 12)
671 (match-end 12))))))
672 ((match-beginning 13) ;weeks only
673 (setq days (* 7 (read (substring isodurationstring
674 (match-beginning 14)
675 (match-end 14)))))))
676 (list seconds minutes hours days months years)))
677 ;; isodatetimestring == nil
678 nil))
680 (defun icalendar--add-decoded-times (time1 time2)
681 "Add TIME1 to TIME2.
682 Both times must be given in decoded form. One of these times must be
683 valid (year > 1900 or something)."
684 ;; FIXME: does this function exist already?
685 (decode-time (encode-time
686 (+ (nth 0 time1) (nth 0 time2))
687 (+ (nth 1 time1) (nth 1 time2))
688 (+ (nth 2 time1) (nth 2 time2))
689 (+ (nth 3 time1) (nth 3 time2))
690 (+ (nth 4 time1) (nth 4 time2))
691 (+ (nth 5 time1) (nth 5 time2))
694 ;;(or (nth 6 time1) (nth 6 time2)) ;; FIXME?
697 (defun icalendar--datetime-to-american-date (datetime &optional separator)
698 "Convert the decoded DATETIME to American-style format.
699 Optional argument SEPARATOR gives the separator between month,
700 day, and year. If nil a blank character is used as separator.
701 American format: \"month day year\"."
702 (if datetime
703 (format "%d%s%d%s%d" (nth 4 datetime) ;month
704 (or separator " ")
705 (nth 3 datetime) ;day
706 (or separator " ")
707 (nth 5 datetime)) ;year
708 ;; datetime == nil
709 nil))
711 (define-obsolete-function-alias 'icalendar--datetime-to-noneuropean-date
712 'icalendar--datetime-to-american-date "icalendar 0.19")
714 (defun icalendar--datetime-to-european-date (datetime &optional separator)
715 "Convert the decoded DATETIME to European format.
716 Optional argument SEPARATOR gives the separator between month,
717 day, and year. If nil a blank character is used as separator.
718 European format: (day month year).
719 FIXME"
720 (if datetime
721 (format "%d%s%d%s%d" (nth 3 datetime) ;day
722 (or separator " ")
723 (nth 4 datetime) ;month
724 (or separator " ")
725 (nth 5 datetime)) ;year
726 ;; datetime == nil
727 nil))
729 (defun icalendar--datetime-to-iso-date (datetime &optional separator)
730 "Convert the decoded DATETIME to ISO format.
731 Optional argument SEPARATOR gives the separator between month,
732 day, and year. If nil a blank character is used as separator.
733 ISO format: (year month day)."
734 (if datetime
735 (format "%d%s%d%s%d" (nth 5 datetime) ;year
736 (or separator " ")
737 (nth 4 datetime) ;month
738 (or separator " ")
739 (nth 3 datetime)) ;day
740 ;; datetime == nil
741 nil))
743 (defun icalendar--date-style ()
744 "Return current calendar date style.
745 Convenience function to handle transition from old
746 `european-calendar-style' to new `calendar-date-style'."
747 (if (boundp 'calendar-date-style)
748 calendar-date-style
749 (if (with-no-warnings european-calendar-style)
750 'european
751 'american)))
753 (defun icalendar--datetime-to-diary-date (datetime &optional separator)
754 "Convert the decoded DATETIME to diary format.
755 Optional argument SEPARATOR gives the separator between month,
756 day, and year. If nil a blank character is used as separator.
757 Call icalendar--datetime-to-*-date according to the current
758 calendar date style."
759 (funcall (intern-soft (format "icalendar--datetime-to-%s-date"
760 (icalendar--date-style)))
761 datetime separator))
763 (defun icalendar--datetime-to-colontime (datetime)
764 "Extract the time part of a decoded DATETIME into 24-hour format.
765 Note that this silently ignores seconds."
766 (format "%02d:%02d" (nth 2 datetime) (nth 1 datetime)))
768 (defun icalendar--get-month-number (monthname)
769 "Return the month number for the given MONTHNAME."
770 (catch 'found
771 (let ((num 1)
772 (m (downcase monthname)))
773 (mapc (lambda (month)
774 (let ((mm (downcase month)))
775 (if (or (string-equal mm m)
776 (string-equal (substring mm 0 3) m))
777 (throw 'found num))
778 (setq num (1+ num))))
779 calendar-month-name-array))
780 ;; Error:
781 -1))
783 (defun icalendar--get-weekday-number (abbrevweekday)
784 "Return the number for the ABBREVWEEKDAY."
785 (if abbrevweekday
786 (catch 'found
787 (let ((num 0)
788 (aw (downcase abbrevweekday)))
789 (mapc (lambda (day)
790 (let ((d (downcase day)))
791 (if (string-equal d aw)
792 (throw 'found num))
793 (setq num (1+ num))))
794 icalendar--weekday-array)))
795 ;; Error:
796 -1))
798 (defun icalendar--get-weekday-numbers (abbrevweekdays)
799 "Return the list of numbers for the comma-separated ABBREVWEEKDAYS."
800 (when abbrevweekdays
801 (let* ((num -1)
802 (weekday-alist (mapcar (lambda (day)
803 (progn
804 (setq num (1+ num))
805 (cons (downcase day) num)))
806 icalendar--weekday-array)))
807 (delq nil
808 (mapcar (lambda (abbrevday)
809 (cdr (assoc abbrevday weekday-alist)))
810 (split-string (downcase abbrevweekdays) ","))))))
812 (defun icalendar--get-weekday-abbrev (weekday)
813 "Return the abbreviated WEEKDAY."
814 (catch 'found
815 (let ((num 0)
816 (w (downcase weekday)))
817 (mapc (lambda (day)
818 (let ((d (downcase day)))
819 (if (or (string-equal d w)
820 (string-equal (substring d 0 3) w))
821 (throw 'found (aref icalendar--weekday-array num)))
822 (setq num (1+ num))))
823 calendar-day-name-array))
824 ;; Error:
825 nil))
827 (defun icalendar--date-to-isodate (date &optional day-shift)
828 "Convert DATE to iso-style date.
829 DATE must be a list of the form (month day year).
830 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days."
831 (let ((mdy (calendar-gregorian-from-absolute
832 (+ (calendar-absolute-from-gregorian date)
833 (or day-shift 0)))))
834 (format "%04d%02d%02d" (nth 2 mdy) (nth 0 mdy) (nth 1 mdy))))
837 (defun icalendar--datestring-to-isodate (datestring &optional day-shift)
838 "Convert diary-style DATESTRING to iso-style date.
839 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days
840 -- DAY-SHIFT must be either nil or an integer. This function
841 tries to figure the date style from DATESTRING itself. If that
842 is not possible it uses the current calendar date style."
843 (let ((day -1) month year)
844 (save-match-data
845 (cond ( ;; iso-style numeric date
846 (string-match (concat "\\s-*"
847 "\\([0-9]\\{4\\}\\)[ \t/]\\s-*"
848 "0?\\([1-9][0-9]?\\)[ \t/]\\s-*"
849 "0?\\([1-9][0-9]?\\)")
850 datestring)
851 (setq year (read (substring datestring (match-beginning 1)
852 (match-end 1))))
853 (setq month (read (substring datestring (match-beginning 2)
854 (match-end 2))))
855 (setq day (read (substring datestring (match-beginning 3)
856 (match-end 3)))))
857 ( ;; non-iso numeric date -- must rely on configured
858 ;; calendar style
859 (string-match (concat "\\s-*"
860 "0?\\([1-9][0-9]?\\)[ \t/]\\s-*"
861 "0?\\([1-9][0-9]?\\),?[ \t/]\\s-*"
862 "\\([0-9]\\{4\\}\\)")
863 datestring)
864 (setq day (read (substring datestring (match-beginning 1)
865 (match-end 1))))
866 (setq month (read (substring datestring (match-beginning 2)
867 (match-end 2))))
868 (setq year (read (substring datestring (match-beginning 3)
869 (match-end 3))))
870 (if (eq (icalendar--date-style) 'american)
871 (let ((x month))
872 (setq month day)
873 (setq day x))))
874 ( ;; date contains month names -- iso style
875 (string-match (concat "\\s-*"
876 "\\([0-9]\\{4\\}\\)[ \t/]\\s-*"
877 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
878 "0?\\([123]?[0-9]\\)")
879 datestring)
880 (setq year (read (substring datestring (match-beginning 1)
881 (match-end 1))))
882 (setq month (icalendar--get-month-number
883 (substring datestring (match-beginning 2)
884 (match-end 2))))
885 (setq day (read (substring datestring (match-beginning 3)
886 (match-end 3)))))
887 ( ;; date contains month names -- european style
888 (string-match (concat "\\s-*"
889 "0?\\([123]?[0-9]\\)[ \t/]\\s-*"
890 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
891 "\\([0-9]\\{4\\}\\)")
892 datestring)
893 (setq day (read (substring datestring (match-beginning 1)
894 (match-end 1))))
895 (setq month (icalendar--get-month-number
896 (substring datestring (match-beginning 2)
897 (match-end 2))))
898 (setq year (read (substring datestring (match-beginning 3)
899 (match-end 3)))))
900 ( ;; date contains month names -- american style
901 (string-match (concat "\\s-*"
902 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
903 "0?\\([123]?[0-9]\\),?[ \t/]\\s-*"
904 "\\([0-9]\\{4\\}\\)")
905 datestring)
906 (setq day (read (substring datestring (match-beginning 2)
907 (match-end 2))))
908 (setq month (icalendar--get-month-number
909 (substring datestring (match-beginning 1)
910 (match-end 1))))
911 (setq year (read (substring datestring (match-beginning 3)
912 (match-end 3)))))
914 nil)))
915 (if (> day 0)
916 (let ((mdy (calendar-gregorian-from-absolute
917 (+ (calendar-absolute-from-gregorian (list month day
918 year))
919 (or day-shift 0)))))
920 (icalendar--dmsg (format "%04d%02d%02d" (nth 2 mdy) (nth 0 mdy) (nth 1 mdy)))
921 (format "%04d%02d%02d" (nth 2 mdy) (nth 0 mdy) (nth 1 mdy)))
922 nil)))
924 (defun icalendar--diarytime-to-isotime (timestring ampmstring)
925 "Convert a time like 9:30pm to an iso-conform string like T213000.
926 In this example the TIMESTRING would be \"9:30\" and the
927 AMPMSTRING would be \"pm\". The minutes may be missing as long
928 as the colon is missing as well, i.e. \"9\" is allowed as
929 TIMESTRING and has the same result as \"9:00\"."
930 (if timestring
931 (let* ((parts (save-match-data (split-string timestring ":")))
932 (h (car parts))
933 (m (if (cdr parts) (cadr parts)
934 (if (> (length h) 2) "" "00")))
935 (starttimenum (read (concat h m))))
936 ;; take care of am/pm style
937 ;; Be sure *not* to convert 12:00pm - 12:59pm to 2400-2459
938 (if (and ampmstring (string= "pm" ampmstring) (< starttimenum 1200))
939 (setq starttimenum (+ starttimenum 1200)))
940 ;; Similar effect with 12:00am - 12:59am (need to convert to 0000-0059)
941 (if (and ampmstring (string= "am" ampmstring) (>= starttimenum 1200))
942 (setq starttimenum (- starttimenum 1200)))
943 (format "T%04d00" starttimenum))
944 nil))
946 (defun icalendar--convert-string-for-export (string)
947 "Escape comma and other critical characters in STRING."
948 (icalendar--rris "," "\\\\," string))
950 (defun icalendar--convert-string-for-import (string)
951 "Remove escape chars for comma, semicolon etc. from STRING."
952 (icalendar--rris
953 "\\\\n" "\n " (icalendar--rris
954 "\\\\\"" "\"" (icalendar--rris
955 "\\\\;" ";" (icalendar--rris
956 "\\\\," "," string)))))
958 ;; ======================================================================
959 ;; Export -- convert emacs-diary to iCalendar
960 ;; ======================================================================
962 ;;;###autoload
963 (defun icalendar-export-file (diary-filename ical-filename)
964 "Export diary file to iCalendar format.
965 All diary entries in the file DIARY-FILENAME are converted to iCalendar
966 format. The result is appended to the file ICAL-FILENAME."
967 (interactive "FExport diary data from file: \n\
968 Finto iCalendar file: ")
969 (save-current-buffer
970 (set-buffer (find-file diary-filename))
971 (icalendar-export-region (point-min) (point-max) ical-filename)))
973 (define-obsolete-function-alias 'icalendar-convert-diary-to-ical
974 'icalendar-export-file "22.1")
976 (defvar icalendar--uid-count 0
977 "Auxiliary counter for creating unique ids.")
979 (defun icalendar--create-uid (entry-full contents)
980 "Construct a unique iCalendar UID for a diary entry.
981 ENTRY-FULL is the full diary entry string. CONTENTS is the
982 current iCalendar object, as a string. Increase
983 `icalendar--uid-count'. Returns the UID string."
984 (let ((uid icalendar-uid-format))
986 ;; Allow other apps (such as org-mode) to create its own uid
987 (get-text-property 0 'uid entry-full)
988 (setq uid (get-text-property 0 'uid entry-full))
989 (setq uid (replace-regexp-in-string
990 "%c"
991 (format "%d" icalendar--uid-count)
992 uid t t))
993 (setq icalendar--uid-count (1+ icalendar--uid-count))
994 (setq uid (replace-regexp-in-string
995 "%t"
996 (format "%d%d%d" (car (current-time))
997 (cadr (current-time))
998 (car (cddr (current-time))))
999 uid t t))
1000 (setq uid (replace-regexp-in-string
1001 "%h"
1002 (format "%d" (abs (sxhash entry-full))) uid t t))
1003 (setq uid (replace-regexp-in-string
1004 "%u" (or user-login-name "UNKNOWN_USER") uid t t))
1005 (let ((dtstart (if (string-match "^DTSTART[^:]*:\\([0-9]*\\)" contents)
1006 (substring contents (match-beginning 1) (match-end 1))
1007 "DTSTART")))
1008 (setq uid (replace-regexp-in-string "%s" dtstart uid t t))))
1010 ;; Return the UID string
1011 uid))
1013 ;;;###autoload
1014 (defun icalendar-export-region (min max ical-filename)
1015 "Export region in diary file to iCalendar format.
1016 All diary entries in the region from MIN to MAX in the current buffer are
1017 converted to iCalendar format. The result is appended to the file
1018 ICAL-FILENAME.
1019 This function attempts to return t if something goes wrong. In this
1020 case an error string which describes all the errors and problems is
1021 written into the buffer `*icalendar-errors*'."
1022 (interactive "r
1023 FExport diary data into iCalendar file: ")
1024 (let ((result "")
1025 (start 0)
1026 (entry-main "")
1027 (entry-rest "")
1028 (entry-full "")
1029 (header "")
1030 (contents-n-summary)
1031 (contents)
1032 (found-error nil)
1033 (nonmarker (concat "^" (regexp-quote diary-nonmarking-symbol)
1034 "?"))
1035 (other-elements nil))
1036 ;; prepare buffer with error messages
1037 (save-current-buffer
1038 (set-buffer (get-buffer-create "*icalendar-errors*"))
1039 (erase-buffer))
1041 ;; here we go
1042 (save-excursion
1043 (goto-char min)
1044 (while (re-search-forward
1045 ;; possibly ignore hidden entries beginning with "&"
1046 (if icalendar-export-hidden-diary-entries
1047 "^\\([^ \t\n#].+\\)\\(\\(\n[ \t].*\\)*\\)"
1048 "^\\([^ \t\n&#].+\\)\\(\\(\n[ \t].*\\)*\\)") max t)
1049 (setq entry-main (match-string 1))
1050 (if (match-beginning 2)
1051 (setq entry-rest (match-string 2))
1052 (setq entry-rest ""))
1053 (setq entry-full (concat entry-main entry-rest))
1055 (condition-case error-val
1056 (progn
1057 (setq cns-cons-or-list
1058 (icalendar--convert-to-ical nonmarker entry-main))
1059 (setq other-elements (icalendar--parse-summary-and-rest
1060 entry-full))
1061 (mapc (lambda (contents-n-summary)
1062 (setq contents (concat (car contents-n-summary)
1063 "\nSUMMARY:"
1064 (cdr contents-n-summary)))
1065 (let ((cla (cdr (assoc 'cla other-elements)))
1066 (des (cdr (assoc 'des other-elements)))
1067 (loc (cdr (assoc 'loc other-elements)))
1068 (org (cdr (assoc 'org other-elements)))
1069 (sta (cdr (assoc 'sta other-elements)))
1070 (sum (cdr (assoc 'sum other-elements)))
1071 (url (cdr (assoc 'url other-elements)))
1072 (uid (cdr (assoc 'uid other-elements))))
1073 (if cla
1074 (setq contents (concat contents "\nCLASS:" cla)))
1075 (if des
1076 (setq contents (concat contents "\nDESCRIPTION:"
1077 des)))
1078 (if loc
1079 (setq contents (concat contents "\nLOCATION:" loc)))
1080 (if org
1081 (setq contents (concat contents "\nORGANIZER:"
1082 org)))
1083 (if sta
1084 (setq contents (concat contents "\nSTATUS:" sta)))
1085 ;;(if sum
1086 ;; (setq contents (concat contents "\nSUMMARY:" sum)))
1087 (if url
1088 (setq contents (concat contents "\nURL:" url)))
1090 (setq header (concat "\nBEGIN:VEVENT\nUID:"
1091 (or uid
1092 (icalendar--create-uid
1093 entry-full contents)))))
1094 (setq result (concat result header contents
1095 "\nEND:VEVENT")))
1096 (if (consp cns-cons-or-list)
1097 (list cns-cons-or-list)
1098 cns-cons-or-list)))
1099 ;; handle errors
1100 (error
1101 (setq found-error t)
1102 (save-current-buffer
1103 (set-buffer (get-buffer-create "*icalendar-errors*"))
1104 (insert (format "Error in line %d -- %s: `%s'\n"
1105 (count-lines (point-min) (point))
1106 error-val
1107 entry-main))))))
1109 ;; we're done, insert everything into the file
1110 (save-current-buffer
1111 (let ((coding-system-for-write 'utf-8))
1112 (set-buffer (find-file ical-filename))
1113 (goto-char (point-max))
1114 (insert "BEGIN:VCALENDAR")
1115 (insert "\nPRODID:-//Emacs//NONSGML icalendar.el//EN")
1116 (insert "\nVERSION:2.0")
1117 (insert result)
1118 (insert "\nEND:VCALENDAR\n")
1119 ;; save the diary file
1120 (save-buffer)
1121 (unless found-error
1122 (bury-buffer)))))
1123 found-error))
1125 (defun icalendar--convert-to-ical (nonmarker entry-main)
1126 "Convert a diary entry to iCalendar format.
1127 NONMARKER is a regular expression matching the start of non-marking
1128 entries. ENTRY-MAIN is the first line of the diary entry."
1130 (unless icalendar-export-sexp-enumerate-all
1132 ;; anniversaries -- %%(diary-anniversary ...)
1133 (icalendar--convert-anniversary-to-ical nonmarker entry-main)
1134 ;; cyclic events -- %%(diary-cyclic ...)
1135 (icalendar--convert-cyclic-to-ical nonmarker entry-main)
1136 ;; diary-date -- %%(diary-date ...)
1137 (icalendar--convert-date-to-ical nonmarker entry-main)
1138 ;; float events -- %%(diary-float ...)
1139 (icalendar--convert-float-to-ical nonmarker entry-main)
1140 ;; block events -- %%(diary-block ...)
1141 (icalendar--convert-block-to-ical nonmarker entry-main)))
1142 ;; other sexp diary entries
1143 (icalendar--convert-sexp-to-ical nonmarker entry-main)
1144 ;; weekly by day -- Monday 8:30 Team meeting
1145 (icalendar--convert-weekly-to-ical nonmarker entry-main)
1146 ;; yearly by day -- 1 May Tag der Arbeit
1147 (icalendar--convert-yearly-to-ical nonmarker entry-main)
1148 ;; "ordinary" events, start and end time given
1149 ;; 1 Feb 2003 blah
1150 (icalendar--convert-ordinary-to-ical nonmarker entry-main)
1151 ;; everything else
1152 ;; Oops! what's that?
1153 (error "Could not parse entry")))
1155 (defun icalendar--parse-summary-and-rest (summary-and-rest)
1156 "Parse SUMMARY-AND-REST from a diary to fill iCalendar properties.
1157 Returns an alist."
1158 (save-match-data
1159 (if (functionp icalendar-import-format)
1160 ;; can't do anything
1162 ;; split summary-and-rest
1163 (let* ((case-fold-search nil)
1164 (s icalendar-import-format)
1165 (p-cla (or (string-match "%c" icalendar-import-format) -1))
1166 (p-des (or (string-match "%d" icalendar-import-format) -1))
1167 (p-loc (or (string-match "%l" icalendar-import-format) -1))
1168 (p-org (or (string-match "%o" icalendar-import-format) -1))
1169 (p-sum (or (string-match "%s" icalendar-import-format) -1))
1170 (p-sta (or (string-match "%t" icalendar-import-format) -1))
1171 (p-url (or (string-match "%u" icalendar-import-format) -1))
1172 (p-uid (or (string-match "%U" icalendar-import-format) -1))
1173 (p-list (sort (list p-cla p-des p-loc p-org p-sta p-sum p-url p-uid) '<))
1174 (ct 0)
1175 pos-cla pos-des pos-loc pos-org pos-sta pos-sum pos-url pos-uid)
1176 (dotimes (i (length p-list))
1177 ;; Use 'ct' to keep track of current position in list
1178 (cond ((and (>= p-cla 0) (= (nth i p-list) p-cla))
1179 (setq ct (+ ct 1))
1180 (setq pos-cla (* 2 ct)))
1181 ((and (>= p-des 0) (= (nth i p-list) p-des))
1182 (setq ct (+ ct 1))
1183 (setq pos-des (* 2 ct)))
1184 ((and (>= p-loc 0) (= (nth i p-list) p-loc))
1185 (setq ct (+ ct 1))
1186 (setq pos-loc (* 2 ct)))
1187 ((and (>= p-org 0) (= (nth i p-list) p-org))
1188 (setq ct (+ ct 1))
1189 (setq pos-org (* 2 ct)))
1190 ((and (>= p-sta 0) (= (nth i p-list) p-sta))
1191 (setq ct (+ ct 1))
1192 (setq pos-sta (* 2 ct)))
1193 ((and (>= p-sum 0) (= (nth i p-list) p-sum))
1194 (setq ct (+ ct 1))
1195 (setq pos-sum (* 2 ct)))
1196 ((and (>= p-url 0) (= (nth i p-list) p-url))
1197 (setq ct (+ ct 1))
1198 (setq pos-url (* 2 ct)))
1199 ((and (>= p-uid 0) (= (nth i p-list) p-uid))
1200 (setq ct (+ ct 1))
1201 (setq pos-uid (* 2 ct)))) )
1202 (mapc (lambda (ij)
1203 (setq s (icalendar--rris (car ij) (cadr ij) s t t)))
1204 (list
1205 ;; summary must be first! because of %s
1206 (list "%s"
1207 (concat "\\(" icalendar-import-format-summary "\\)??"))
1208 (list "%c"
1209 (concat "\\(" icalendar-import-format-class "\\)??"))
1210 (list "%d"
1211 (concat "\\(" icalendar-import-format-description "\\)??"))
1212 (list "%l"
1213 (concat "\\(" icalendar-import-format-location "\\)??"))
1214 (list "%o"
1215 (concat "\\(" icalendar-import-format-organizer "\\)??"))
1216 (list "%t"
1217 (concat "\\(" icalendar-import-format-status "\\)??"))
1218 (list "%u"
1219 (concat "\\(" icalendar-import-format-url "\\)??"))
1220 (list "%U"
1221 (concat "\\(" icalendar-import-format-uid "\\)??"))))
1222 ;; Need the \' regexp in order to detect multi-line items
1223 (setq s (concat "\\`"
1224 (icalendar--rris "%s" "\\(.*?\\)" s nil t)
1225 "\\'"))
1226 (if (string-match s summary-and-rest)
1227 (let (cla des loc org sta sum url uid)
1228 (if (and pos-sum (match-beginning pos-sum))
1229 (setq sum (substring summary-and-rest
1230 (match-beginning pos-sum)
1231 (match-end pos-sum))))
1232 (if (and pos-cla (match-beginning pos-cla))
1233 (setq cla (substring summary-and-rest
1234 (match-beginning pos-cla)
1235 (match-end pos-cla))))
1236 (if (and pos-des (match-beginning pos-des))
1237 (setq des (substring summary-and-rest
1238 (match-beginning pos-des)
1239 (match-end pos-des))))
1240 (if (and pos-loc (match-beginning pos-loc))
1241 (setq loc (substring summary-and-rest
1242 (match-beginning pos-loc)
1243 (match-end pos-loc))))
1244 (if (and pos-org (match-beginning pos-org))
1245 (setq org (substring summary-and-rest
1246 (match-beginning pos-org)
1247 (match-end pos-org))))
1248 (if (and pos-sta (match-beginning pos-sta))
1249 (setq sta (substring summary-and-rest
1250 (match-beginning pos-sta)
1251 (match-end pos-sta))))
1252 (if (and pos-url (match-beginning pos-url))
1253 (setq url (substring summary-and-rest
1254 (match-beginning pos-url)
1255 (match-end pos-url))))
1256 (if (and pos-uid (match-beginning pos-uid))
1257 (setq uid (substring summary-and-rest
1258 (match-beginning pos-uid)
1259 (match-end pos-uid))))
1260 (list (if cla (cons 'cla cla) nil)
1261 (if des (cons 'des des) nil)
1262 (if loc (cons 'loc loc) nil)
1263 (if org (cons 'org org) nil)
1264 (if sta (cons 'sta sta) nil)
1265 ;;(if sum (cons 'sum sum) nil)
1266 (if url (cons 'url url) nil)
1267 (if uid (cons 'uid uid) nil))))))))
1269 ;; subroutines for icalendar-export-region
1270 (defun icalendar--convert-ordinary-to-ical (nonmarker entry-main)
1271 "Convert \"ordinary\" diary entry to iCalendar format.
1272 NONMARKER is a regular expression matching the start of non-marking
1273 entries. ENTRY-MAIN is the first line of the diary entry."
1274 (if (string-match
1275 (concat nonmarker
1276 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)\\s-*" ; date
1277 "\\(\\([0-9][0-9]?\\(:[0-9][0-9]\\)?\\)\\([ap]m\\)?" ; start time
1278 "\\("
1279 "-\\([0-9][0-9]?\\(:[0-9][0-9]\\)?\\)\\([ap]m\\)?\\)?" ; end time
1280 "\\)?"
1281 "\\s-*\\(.*?\\) ?$")
1282 entry-main)
1283 (let* ((datetime (substring entry-main (match-beginning 1)
1284 (match-end 1)))
1285 (startisostring (icalendar--datestring-to-isodate
1286 datetime))
1287 (endisostring (icalendar--datestring-to-isodate
1288 datetime 1))
1289 (endisostring1)
1290 (starttimestring (icalendar--diarytime-to-isotime
1291 (if (match-beginning 3)
1292 (substring entry-main
1293 (match-beginning 3)
1294 (match-end 3))
1295 nil)
1296 (if (match-beginning 5)
1297 (substring entry-main
1298 (match-beginning 5)
1299 (match-end 5))
1300 nil)))
1301 (endtimestring (icalendar--diarytime-to-isotime
1302 (if (match-beginning 7)
1303 (substring entry-main
1304 (match-beginning 7)
1305 (match-end 7))
1306 nil)
1307 (if (match-beginning 9)
1308 (substring entry-main
1309 (match-beginning 9)
1310 (match-end 9))
1311 nil)))
1312 (summary (icalendar--convert-string-for-export
1313 (substring entry-main (match-beginning 10)
1314 (match-end 10)))))
1315 (icalendar--dmsg "ordinary %s" entry-main)
1317 (unless startisostring
1318 (error "Could not parse date"))
1320 ;; If only start-date is specified, then end-date is next day,
1321 ;; otherwise it is same day.
1322 (setq endisostring1 (if starttimestring
1323 startisostring
1324 endisostring))
1326 (when starttimestring
1327 (unless endtimestring
1328 (let ((time
1329 (read (icalendar--rris "^T0?" ""
1330 starttimestring))))
1331 (if (< time 230000)
1332 ;; Case: ends on same day
1333 (setq endtimestring (format "T%06d"
1334 (+ 10000 time)))
1335 ;; Case: ends on next day
1336 (setq endtimestring (format "T%06d"
1337 (- time 230000)))
1338 (setq endisostring1 endisostring)) )))
1340 (cons (concat "\nDTSTART;"
1341 (if starttimestring "VALUE=DATE-TIME:"
1342 "VALUE=DATE:")
1343 startisostring
1344 (or starttimestring "")
1345 "\nDTEND;"
1346 (if endtimestring "VALUE=DATE-TIME:"
1347 "VALUE=DATE:")
1348 endisostring1
1349 (or endtimestring ""))
1350 summary))
1351 ;; no match
1352 nil))
1354 (defun icalendar-first-weekday-of-year (abbrevweekday year)
1355 "Find the first ABBREVWEEKDAY in a given YEAR.
1356 Returns day number."
1357 (let* ((day-of-week-jan01 (calendar-day-of-week (list 1 1 year)))
1358 (result (+ 1
1359 (- (icalendar--get-weekday-number abbrevweekday)
1360 day-of-week-jan01))))
1361 (cond ((<= result 0)
1362 (setq result (+ result 7)))
1363 ((> result 7)
1364 (setq result (- result 7))))
1365 result))
1367 (defun icalendar--convert-weekly-to-ical (nonmarker entry-main)
1368 "Convert weekly diary entry to iCalendar format.
1369 NONMARKER is a regular expression matching the start of non-marking
1370 entries. ENTRY-MAIN is the first line of the diary entry."
1371 (if (and (string-match (concat nonmarker
1372 "\\([a-z]+\\)\\s-+"
1373 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)"
1374 "\\([ap]m\\)?"
1375 "\\(-"
1376 "\\([0-9][0-9]?:[0-9][0-9]\\)"
1377 "\\([ap]m\\)?\\)?"
1378 "\\)?"
1379 "\\s-*\\(.*?\\) ?$")
1380 entry-main)
1381 (icalendar--get-weekday-abbrev
1382 (substring entry-main (match-beginning 1)
1383 (match-end 1))))
1384 (let* ((day (icalendar--get-weekday-abbrev
1385 (substring entry-main (match-beginning 1)
1386 (match-end 1))))
1387 (starttimestring (icalendar--diarytime-to-isotime
1388 (if (match-beginning 3)
1389 (substring entry-main
1390 (match-beginning 3)
1391 (match-end 3))
1392 nil)
1393 (if (match-beginning 4)
1394 (substring entry-main
1395 (match-beginning 4)
1396 (match-end 4))
1397 nil)))
1398 (endtimestring (icalendar--diarytime-to-isotime
1399 (if (match-beginning 6)
1400 (substring entry-main
1401 (match-beginning 6)
1402 (match-end 6))
1403 nil)
1404 (if (match-beginning 7)
1405 (substring entry-main
1406 (match-beginning 7)
1407 (match-end 7))
1408 nil)))
1409 (summary (icalendar--convert-string-for-export
1410 (substring entry-main (match-beginning 8)
1411 (match-end 8)))))
1412 (icalendar--dmsg "weekly %s" entry-main)
1414 (when starttimestring
1415 (unless endtimestring
1416 (let ((time (read
1417 (icalendar--rris "^T0?" ""
1418 starttimestring))))
1419 (setq endtimestring (format "T%06d"
1420 (+ 10000 time))))))
1421 (cons (concat "\nDTSTART;"
1422 (if starttimestring
1423 "VALUE=DATE-TIME:"
1424 "VALUE=DATE:")
1425 ;; Find the first requested weekday of the
1426 ;; start year
1427 (funcall 'format "%04d%02d%02d"
1428 icalendar-recurring-start-year 1
1429 (icalendar-first-weekday-of-year
1430 day icalendar-recurring-start-year))
1431 (or starttimestring "")
1432 "\nDTEND;"
1433 (if endtimestring
1434 "VALUE=DATE-TIME:"
1435 "VALUE=DATE:")
1436 (funcall 'format "%04d%02d%02d"
1437 ;; end is non-inclusive!
1438 icalendar-recurring-start-year 1
1439 (+ (icalendar-first-weekday-of-year
1440 day icalendar-recurring-start-year)
1441 (if endtimestring 0 1)))
1442 (or endtimestring "")
1443 "\nRRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY="
1444 day)
1445 summary))
1446 ;; no match
1447 nil))
1449 (defun icalendar--convert-yearly-to-ical (nonmarker entry-main)
1450 "Convert yearly diary entry to iCalendar format.
1451 NONMARKER is a regular expression matching the start of non-marking
1452 entries. ENTRY-MAIN is the first line of the diary entry."
1453 (if (string-match (concat nonmarker
1454 (if (eq (icalendar--date-style) 'european)
1455 "\\([0-9]+[0-9]?\\)\\s-+\\([a-z]+\\)\\s-+"
1456 "\\([a-z]+\\)\\s-+\\([0-9]+[0-9]?\\)\\s-+")
1457 "\\*?\\s-*"
1458 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1459 "\\("
1460 "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1461 "\\)?"
1462 "\\s-*\\([^0-9]+.*?\\) ?$" ; must not match years
1464 entry-main)
1465 (let* ((daypos (if (eq (icalendar--date-style) 'european) 1 2))
1466 (monpos (if (eq (icalendar--date-style) 'european) 2 1))
1467 (day (read (substring entry-main
1468 (match-beginning daypos)
1469 (match-end daypos))))
1470 (month (icalendar--get-month-number
1471 (substring entry-main
1472 (match-beginning monpos)
1473 (match-end monpos))))
1474 (starttimestring (icalendar--diarytime-to-isotime
1475 (if (match-beginning 4)
1476 (substring entry-main
1477 (match-beginning 4)
1478 (match-end 4))
1479 nil)
1480 (if (match-beginning 5)
1481 (substring entry-main
1482 (match-beginning 5)
1483 (match-end 5))
1484 nil)))
1485 (endtimestring (icalendar--diarytime-to-isotime
1486 (if (match-beginning 7)
1487 (substring entry-main
1488 (match-beginning 7)
1489 (match-end 7))
1490 nil)
1491 (if (match-beginning 8)
1492 (substring entry-main
1493 (match-beginning 8)
1494 (match-end 8))
1495 nil)))
1496 (summary (icalendar--convert-string-for-export
1497 (substring entry-main (match-beginning 9)
1498 (match-end 9)))))
1499 (icalendar--dmsg "yearly %s" entry-main)
1501 (when starttimestring
1502 (unless endtimestring
1503 (let ((time (read
1504 (icalendar--rris "^T0?" ""
1505 starttimestring))))
1506 (setq endtimestring (format "T%06d"
1507 (+ 10000 time))))))
1508 (cons (concat "\nDTSTART;"
1509 (if starttimestring "VALUE=DATE-TIME:"
1510 "VALUE=DATE:")
1511 (format "1900%02d%02d" month day)
1512 (or starttimestring "")
1513 "\nDTEND;"
1514 (if endtimestring "VALUE=DATE-TIME:"
1515 "VALUE=DATE:")
1516 ;; end is not included! shift by one day
1517 (icalendar--date-to-isodate
1518 (list month day 1900)
1519 (if endtimestring 0 1))
1520 (or endtimestring "")
1521 "\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH="
1522 (format "%d" month)
1523 ";BYMONTHDAY="
1524 (format "%d" day))
1525 summary))
1526 ;; no match
1527 nil))
1529 (defun icalendar--convert-sexp-to-ical (nonmarker entry-main &optional start)
1530 "Convert sexp diary entry to iCalendar format.
1531 Enumerate the evaluated sexp entry for the next
1532 `icalendar-export-sexp-enumeration-days' days. NONMARKER is a
1533 regular expression matching the start of non-marking entries.
1534 ENTRY-MAIN is the first line of the diary entry.
1536 Optional argument START determines the first day of the
1537 enumeration, given as a time value, in same format as returned by
1538 `current-time' -- used for test purposes."
1539 (cond ((string-match (concat nonmarker
1540 "%%(and \\(([^)]+)\\))\\(\\s-*.*?\\) ?$")
1541 entry-main)
1542 ;; simple sexp entry as generated by icalendar.el: strip off the
1543 ;; unnecessary (and)
1544 (icalendar--dmsg "diary-sexp from icalendar.el %s" entry-main)
1545 (icalendar--convert-to-ical
1546 nonmarker
1547 (concat "%%"
1548 (substring entry-main (match-beginning 1) (match-end 1))
1549 (substring entry-main (match-beginning 2) (match-end 2)))))
1550 ((string-match (concat nonmarker
1551 "%%\\(([^)]+)\\)\\s-*\\(.*\\)")
1552 entry-main)
1553 ;; regular sexp entry
1554 (icalendar--dmsg "diary-sexp %s" entry-main)
1555 (let ((p1 (substring entry-main (match-beginning 1) (match-end 1)))
1556 (p2 (substring entry-main (match-beginning 2) (match-end 2)))
1557 (now (or start (current-time))))
1558 (delete nil
1559 (mapcar
1560 (lambda (offset)
1561 (let* ((day (decode-time (time-add now
1562 (seconds-to-time
1563 (* offset 60 60 24)))))
1564 (d (nth 3 day))
1565 (m (nth 4 day))
1566 (y (nth 5 day))
1567 (se (diary-sexp-entry p1 p2 (list m d y)))
1568 (see (cond ((stringp se) se)
1569 ((consp se) (cdr se))
1570 (t nil))))
1571 (cond ((null see)
1572 nil)
1573 ((stringp see)
1574 (let ((calendar-date-style 'iso))
1575 (icalendar--convert-ordinary-to-ical
1576 nonmarker (format "%4d/%02d/%02d %s" y m d see))))
1577 (;TODO:
1578 (error (format "Unsupported Sexp-entry: %s"
1579 entry-main))))))
1580 (number-sequence
1581 0 (- icalendar-export-sexp-enumeration-days 1))))))
1583 ;; no match
1584 nil)))
1586 (defun icalendar--convert-block-to-ical (nonmarker entry-main)
1587 "Convert block diary entry to iCalendar format.
1588 NONMARKER is a regular expression matching the start of non-marking
1589 entries. ENTRY-MAIN is the first line of the diary entry."
1590 (if (string-match (concat nonmarker
1591 "%%(diary-block \\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)"
1592 " +\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
1593 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1594 "\\("
1595 "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1596 "\\)?"
1597 "\\s-*\\(.*?\\) ?$")
1598 entry-main)
1599 (let* ((startstring (substring entry-main
1600 (match-beginning 1)
1601 (match-end 1)))
1602 (endstring (substring entry-main
1603 (match-beginning 2)
1604 (match-end 2)))
1605 (startisostring (icalendar--datestring-to-isodate
1606 startstring))
1607 (endisostring (icalendar--datestring-to-isodate
1608 endstring))
1609 (endisostring+1 (icalendar--datestring-to-isodate
1610 endstring 1))
1611 (starttimestring (icalendar--diarytime-to-isotime
1612 (if (match-beginning 4)
1613 (substring entry-main
1614 (match-beginning 4)
1615 (match-end 4))
1616 nil)
1617 (if (match-beginning 5)
1618 (substring entry-main
1619 (match-beginning 5)
1620 (match-end 5))
1621 nil)))
1622 (endtimestring (icalendar--diarytime-to-isotime
1623 (if (match-beginning 7)
1624 (substring entry-main
1625 (match-beginning 7)
1626 (match-end 7))
1627 nil)
1628 (if (match-beginning 8)
1629 (substring entry-main
1630 (match-beginning 8)
1631 (match-end 8))
1632 nil)))
1633 (summary (icalendar--convert-string-for-export
1634 (substring entry-main (match-beginning 9)
1635 (match-end 9)))))
1636 (icalendar--dmsg "diary-block %s" entry-main)
1637 (when starttimestring
1638 (unless endtimestring
1639 (let ((time
1640 (read (icalendar--rris "^T0?" ""
1641 starttimestring))))
1642 (setq endtimestring (format "T%06d"
1643 (+ 10000 time))))))
1644 (if starttimestring
1645 ;; with time -> write rrule
1646 (cons (concat "\nDTSTART;VALUE=DATE-TIME:"
1647 startisostring
1648 starttimestring
1649 "\nDTEND;VALUE=DATE-TIME:"
1650 startisostring
1651 endtimestring
1652 "\nRRULE:FREQ=DAILY;INTERVAL=1;UNTIL="
1653 endisostring)
1654 summary)
1655 ;; no time -> write long event
1656 (cons (concat "\nDTSTART;VALUE=DATE:" startisostring
1657 "\nDTEND;VALUE=DATE:" endisostring+1)
1658 summary)))
1659 ;; no match
1660 nil))
1662 (defun icalendar--convert-float-to-ical (nonmarker entry-main)
1663 "Convert float diary entry to iCalendar format -- partially unsupported!
1665 FIXME! DAY from diary-float yet unimplemented.
1667 NONMARKER is a regular expression matching the start of non-marking
1668 entries. ENTRY-MAIN is the first line of the diary entry."
1669 (if (string-match (concat nonmarker "%%\\((diary-float .+\\) ?$") entry-main)
1670 (with-temp-buffer
1671 (insert (match-string 1 entry-main))
1672 (goto-char (point-min))
1673 (let* ((sexp (read (current-buffer))) ;using `read' here
1674 ;easier than regexp
1675 ;matching, esp. with
1676 ;different forms of
1677 ;MONTH
1678 (month (nth 1 sexp))
1679 (dayname (nth 2 sexp))
1680 (n (nth 3 sexp))
1681 (day (nth 4 sexp))
1682 (summary
1683 (replace-regexp-in-string
1684 "\\(^\s+\\|\s+$\\)" ""
1685 (buffer-substring (point) (point-max)))))
1687 (when day
1688 (progn
1689 (icalendar--dmsg "diary-float %s" entry-main)
1690 (error "Don't know if or how to implement day in `diary-float'")))
1692 (cons (concat
1693 ;;Start today (yes this is an arbitrary choice):
1694 "\nDTSTART;VALUE=DATE:"
1695 (format-time-string "%Y%m%d" (current-time))
1696 ;;BUT remove today if `diary-float'
1697 ;;expression does not hold true for today:
1698 (when
1699 (null (let ((date (calendar-current-date))
1700 (entry entry-main))
1701 (diary-float month dayname n)))
1702 (concat
1703 "\nEXDATE;VALUE=DATE:"
1704 (format-time-string "%Y%m%d" (current-time))))
1705 "\nRRULE:"
1706 (if (or (numberp month) (listp month))
1707 "FREQ=YEARLY;BYMONTH="
1708 "FREQ=MONTHLY")
1709 (when
1710 (listp month)
1711 (mapconcat
1712 (lambda (m)
1713 (number-to-string m))
1714 (cadr month) ","))
1715 (when
1716 (numberp month)
1717 (number-to-string month))
1718 ";BYDAY="
1719 (number-to-string n)
1720 (aref icalendar--weekday-array dayname))
1721 summary)))
1722 ;; no match
1723 nil))
1725 (defun icalendar--convert-date-to-ical (nonmarker entry-main)
1726 "Convert `diary-date' diary entry to iCalendar format -- unsupported!
1728 FIXME!
1730 NONMARKER is a regular expression matching the start of non-marking
1731 entries. ENTRY-MAIN is the first line of the diary entry."
1732 (if (string-match (concat nonmarker
1733 "%%(diary-date \\([^)]+\\))\\s-*\\(.*?\\) ?$")
1734 entry-main)
1735 (progn
1736 (icalendar--dmsg "diary-date %s" entry-main)
1737 (error "`diary-date' is not supported yet"))
1738 ;; no match
1739 nil))
1741 (defun icalendar--convert-cyclic-to-ical (nonmarker entry-main)
1742 "Convert `diary-cyclic' diary entry to iCalendar format.
1743 NONMARKER is a regular expression matching the start of non-marking
1744 entries. ENTRY-MAIN is the first line of the diary entry."
1745 (if (string-match (concat nonmarker
1746 "%%(diary-cyclic \\([^ ]+\\) +"
1747 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
1748 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1749 "\\("
1750 "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1751 "\\)?"
1752 "\\s-*\\(.*?\\) ?$")
1753 entry-main)
1754 (let* ((frequency (substring entry-main (match-beginning 1)
1755 (match-end 1)))
1756 (datetime (substring entry-main (match-beginning 2)
1757 (match-end 2)))
1758 (startisostring (icalendar--datestring-to-isodate
1759 datetime))
1760 (endisostring (icalendar--datestring-to-isodate
1761 datetime))
1762 (endisostring+1 (icalendar--datestring-to-isodate
1763 datetime 1))
1764 (starttimestring (icalendar--diarytime-to-isotime
1765 (if (match-beginning 4)
1766 (substring entry-main
1767 (match-beginning 4)
1768 (match-end 4))
1769 nil)
1770 (if (match-beginning 5)
1771 (substring entry-main
1772 (match-beginning 5)
1773 (match-end 5))
1774 nil)))
1775 (endtimestring (icalendar--diarytime-to-isotime
1776 (if (match-beginning 7)
1777 (substring entry-main
1778 (match-beginning 7)
1779 (match-end 7))
1780 nil)
1781 (if (match-beginning 8)
1782 (substring entry-main
1783 (match-beginning 8)
1784 (match-end 8))
1785 nil)))
1786 (summary (icalendar--convert-string-for-export
1787 (substring entry-main (match-beginning 9)
1788 (match-end 9)))))
1789 (icalendar--dmsg "diary-cyclic %s" entry-main)
1790 (when starttimestring
1791 (unless endtimestring
1792 (let ((time
1793 (read (icalendar--rris "^T0?" ""
1794 starttimestring))))
1795 (setq endtimestring (format "T%06d"
1796 (+ 10000 time))))))
1797 (cons (concat "\nDTSTART;"
1798 (if starttimestring "VALUE=DATE-TIME:"
1799 "VALUE=DATE:")
1800 startisostring
1801 (or starttimestring "")
1802 "\nDTEND;"
1803 (if endtimestring "VALUE=DATE-TIME:"
1804 "VALUE=DATE:")
1805 (if endtimestring endisostring endisostring+1)
1806 (or endtimestring "")
1807 "\nRRULE:FREQ=DAILY;INTERVAL=" frequency
1808 ;; strange: korganizer does not expect
1809 ;; BYSOMETHING here...
1811 summary))
1812 ;; no match
1813 nil))
1815 (defun icalendar--convert-anniversary-to-ical (nonmarker entry-main)
1816 "Convert `diary-anniversary' diary entry to iCalendar format.
1817 NONMARKER is a regular expression matching the start of non-marking
1818 entries. ENTRY-MAIN is the first line of the diary entry."
1819 (if (string-match (concat nonmarker
1820 "%%(diary-anniversary \\([^)]+\\))\\s-*"
1821 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1822 "\\("
1823 "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1824 "\\)?"
1825 "\\s-*\\(.*?\\) ?$")
1826 entry-main)
1827 (let* ((datetime (substring entry-main (match-beginning 1)
1828 (match-end 1)))
1829 (startisostring (icalendar--datestring-to-isodate
1830 datetime))
1831 (endisostring (icalendar--datestring-to-isodate
1832 datetime 1))
1833 (starttimestring (icalendar--diarytime-to-isotime
1834 (if (match-beginning 3)
1835 (substring entry-main
1836 (match-beginning 3)
1837 (match-end 3))
1838 nil)
1839 (if (match-beginning 4)
1840 (substring entry-main
1841 (match-beginning 4)
1842 (match-end 4))
1843 nil)))
1844 (endtimestring (icalendar--diarytime-to-isotime
1845 (if (match-beginning 6)
1846 (substring entry-main
1847 (match-beginning 6)
1848 (match-end 6))
1849 nil)
1850 (if (match-beginning 7)
1851 (substring entry-main
1852 (match-beginning 7)
1853 (match-end 7))
1854 nil)))
1855 (summary (icalendar--convert-string-for-export
1856 (substring entry-main (match-beginning 8)
1857 (match-end 8)))))
1858 (icalendar--dmsg "diary-anniversary %s" entry-main)
1859 (when starttimestring
1860 (unless endtimestring
1861 (let ((time
1862 (read (icalendar--rris "^T0?" ""
1863 starttimestring))))
1864 (setq endtimestring (format "T%06d"
1865 (+ 10000 time))))))
1866 (cons (concat "\nDTSTART;"
1867 (if starttimestring "VALUE=DATE-TIME:"
1868 "VALUE=DATE:")
1869 startisostring
1870 (or starttimestring "")
1871 "\nDTEND;"
1872 (if endtimestring "VALUE=DATE-TIME:"
1873 "VALUE=DATE:")
1874 endisostring
1875 (or endtimestring "")
1876 "\nRRULE:FREQ=YEARLY;INTERVAL=1"
1877 ;; the following is redundant,
1878 ;; but korganizer seems to expect this... ;(
1879 ;; and evolution doesn't understand it... :(
1880 ;; so... who is wrong?!
1881 ";BYMONTH="
1882 (substring startisostring 4 6)
1883 ";BYMONTHDAY="
1884 (substring startisostring 6 8))
1885 summary))
1886 ;; no match
1887 nil))
1889 ;; ======================================================================
1890 ;; Import -- convert iCalendar to emacs-diary
1891 ;; ======================================================================
1893 ;;;###autoload
1894 (defun icalendar-import-file (ical-filename diary-filename
1895 &optional non-marking)
1896 "Import an iCalendar file and append to a diary file.
1897 Argument ICAL-FILENAME output iCalendar file.
1898 Argument DIARY-FILENAME input `diary-file'.
1899 Optional argument NON-MARKING determines whether events are created as
1900 non-marking or not."
1901 (interactive "fImport iCalendar data from file: \n\
1902 Finto diary file:
1904 ;; clean up the diary file
1905 (save-current-buffer
1906 ;; now load and convert from the ical file
1907 (set-buffer (find-file ical-filename))
1908 (icalendar-import-buffer diary-filename t non-marking)))
1910 ;;;###autoload
1911 (defun icalendar-import-buffer (&optional diary-file do-not-ask
1912 non-marking)
1913 "Extract iCalendar events from current buffer.
1915 This function searches the current buffer for the first iCalendar
1916 object, reads it and adds all VEVENT elements to the diary
1917 DIARY-FILE.
1919 It will ask for each appointment whether to add it to the diary
1920 unless DO-NOT-ASK is non-nil. When called interactively,
1921 DO-NOT-ASK is nil, so that you are asked for each event.
1923 NON-MARKING determines whether diary events are created as
1924 non-marking.
1926 Return code t means that importing worked well, return code nil
1927 means that an error has occurred. Error messages will be in the
1928 buffer `*icalendar-errors*'."
1929 (interactive)
1930 (save-current-buffer
1931 ;; prepare ical
1932 (message "Preparing iCalendar...")
1933 (set-buffer (icalendar--get-unfolded-buffer (current-buffer)))
1934 (goto-char (point-min))
1935 (message "Preparing iCalendar...done")
1936 (if (re-search-forward "^BEGIN:VCALENDAR\\s-*$" nil t)
1937 (let (ical-contents ical-errors)
1938 ;; read ical
1939 (message "Reading iCalendar...")
1940 (beginning-of-line)
1941 (setq ical-contents (icalendar--read-element nil nil))
1942 (message "Reading iCalendar...done")
1943 ;; convert ical
1944 (message "Converting iCalendar...")
1945 (setq ical-errors (icalendar--convert-ical-to-diary
1946 ical-contents
1947 diary-file do-not-ask non-marking))
1948 (when diary-file
1949 ;; save the diary file if it is visited already
1950 (let ((b (find-buffer-visiting diary-file)))
1951 (when b
1952 (save-current-buffer
1953 (set-buffer b)
1954 (save-buffer)))))
1955 (message "Converting iCalendar...done")
1956 ;; return t if no error occurred
1957 (not ical-errors))
1958 (message
1959 "Current buffer does not contain iCalendar contents!")
1960 ;; return nil, i.e. import did not work
1961 nil)))
1963 (define-obsolete-function-alias 'icalendar-extract-ical-from-buffer
1964 'icalendar-import-buffer "22.1")
1966 (defun icalendar--format-ical-event (event)
1967 "Create a string representation of an iCalendar EVENT."
1968 (if (functionp icalendar-import-format)
1969 (funcall icalendar-import-format event)
1970 (let ((string icalendar-import-format)
1971 (case-fold-search nil)
1972 (conversion-list
1973 '(("%c" CLASS icalendar-import-format-class)
1974 ("%d" DESCRIPTION icalendar-import-format-description)
1975 ("%l" LOCATION icalendar-import-format-location)
1976 ("%o" ORGANIZER icalendar-import-format-organizer)
1977 ("%s" SUMMARY icalendar-import-format-summary)
1978 ("%t" STATUS icalendar-import-format-status)
1979 ("%u" URL icalendar-import-format-url)
1980 ("%U" UID icalendar-import-format-uid))))
1981 ;; convert the specifiers in the format string
1982 (mapc (lambda (i)
1983 (let* ((spec (car i))
1984 (prop (cadr i))
1985 (format (car (cddr i)))
1986 (contents (icalendar--get-event-property event prop))
1987 (formatted-contents ""))
1988 (when (and contents (> (length contents) 0))
1989 (setq formatted-contents
1990 (icalendar--rris "%s"
1991 (icalendar--convert-string-for-import
1992 contents)
1993 (symbol-value format)
1994 t t)))
1995 (setq string (icalendar--rris spec
1996 formatted-contents
1997 string
1998 t t))))
1999 conversion-list)
2000 string)))
2002 (defun icalendar--convert-ical-to-diary (ical-list diary-file
2003 &optional do-not-ask
2004 non-marking)
2005 "Convert iCalendar data to an Emacs diary file.
2006 Import VEVENTS from the iCalendar object ICAL-LIST and saves them to a
2007 DIARY-FILE. If DO-NOT-ASK is nil the user is asked for each event
2008 whether to actually import it. NON-MARKING determines whether diary
2009 events are created as non-marking.
2010 This function attempts to return t if something goes wrong. In this
2011 case an error string which describes all the errors and problems is
2012 written into the buffer `*icalendar-errors*'."
2013 (let* ((ev (icalendar--all-events ical-list))
2014 (error-string "")
2015 (event-ok t)
2016 (found-error nil)
2017 (zone-map (icalendar--convert-all-timezones ical-list))
2018 e diary-string)
2019 ;; step through all events/appointments
2020 (while ev
2021 (setq e (car ev))
2022 (setq ev (cdr ev))
2023 (setq event-ok nil)
2024 (condition-case error-val
2025 (let* ((dtstart (icalendar--get-event-property e 'DTSTART))
2026 (dtstart-zone (icalendar--find-time-zone
2027 (icalendar--get-event-property-attributes
2028 e 'DTSTART)
2029 zone-map))
2030 (dtstart-dec (icalendar--decode-isodatetime dtstart nil
2031 dtstart-zone))
2032 (start-d (icalendar--datetime-to-diary-date
2033 dtstart-dec))
2034 (start-t (icalendar--datetime-to-colontime dtstart-dec))
2035 (dtend (icalendar--get-event-property e 'DTEND))
2036 (dtend-zone (icalendar--find-time-zone
2037 (icalendar--get-event-property-attributes
2038 e 'DTEND)
2039 zone-map))
2040 (dtend-dec (icalendar--decode-isodatetime dtend
2041 nil dtend-zone))
2042 (dtend-1-dec (icalendar--decode-isodatetime dtend -1
2043 dtend-zone))
2044 end-d
2045 end-1-d
2046 end-t
2047 (summary (icalendar--convert-string-for-import
2048 (or (icalendar--get-event-property e 'SUMMARY)
2049 "No summary")))
2050 (rrule (icalendar--get-event-property e 'RRULE))
2051 (rdate (icalendar--get-event-property e 'RDATE))
2052 (duration (icalendar--get-event-property e 'DURATION)))
2053 (icalendar--dmsg "%s: `%s'" start-d summary)
2054 ;; check whether start-time is missing
2055 (if (and dtstart
2056 (string=
2057 (cadr (icalendar--get-event-property-attributes
2058 e 'DTSTART))
2059 "DATE"))
2060 (setq start-t nil))
2061 (when duration
2062 (let ((dtend-dec-d (icalendar--add-decoded-times
2063 dtstart-dec
2064 (icalendar--decode-isoduration duration)))
2065 (dtend-1-dec-d (icalendar--add-decoded-times
2066 dtstart-dec
2067 (icalendar--decode-isoduration duration
2068 t))))
2069 (if (and dtend-dec (not (eq dtend-dec dtend-dec-d)))
2070 (message "Inconsistent endtime and duration for %s"
2071 summary))
2072 (setq dtend-dec dtend-dec-d)
2073 (setq dtend-1-dec dtend-1-dec-d)))
2074 (setq end-d (if dtend-dec
2075 (icalendar--datetime-to-diary-date dtend-dec)
2076 start-d))
2077 (setq end-1-d (if dtend-1-dec
2078 (icalendar--datetime-to-diary-date dtend-1-dec)
2079 start-d))
2080 (setq end-t (if (and
2081 dtend-dec
2082 (not (string=
2083 (cadr
2084 (icalendar--get-event-property-attributes
2085 e 'DTEND))
2086 "DATE")))
2087 (icalendar--datetime-to-colontime dtend-dec)
2088 start-t))
2089 (icalendar--dmsg "start-d: %s, end-d: %s" start-d end-d)
2090 (cond
2091 ;; recurring event
2092 (rrule
2093 (setq diary-string
2094 (icalendar--convert-recurring-to-diary e dtstart-dec start-t
2095 end-t))
2096 (setq event-ok t))
2097 (rdate
2098 (icalendar--dmsg "rdate event")
2099 (setq diary-string "")
2100 (mapc (lambda (datestring)
2101 (setq diary-string
2102 (concat diary-string
2103 (format "......"))))
2104 (icalendar--split-value rdate)))
2105 ;; non-recurring event
2106 ;; all-day event
2107 ((not (string= start-d end-d))
2108 (setq diary-string
2109 (icalendar--convert-non-recurring-all-day-to-diary
2110 e start-d end-1-d))
2111 (setq event-ok t))
2112 ;; not all-day
2113 ((and start-t (or (not end-t)
2114 (not (string= start-t end-t))))
2115 (setq diary-string
2116 (icalendar--convert-non-recurring-not-all-day-to-diary
2117 e dtstart-dec dtend-dec start-t end-t))
2118 (setq event-ok t))
2119 ;; all-day event
2121 (icalendar--dmsg "all day event")
2122 (setq diary-string (icalendar--datetime-to-diary-date
2123 dtstart-dec "/"))
2124 (setq event-ok t)))
2125 ;; add all other elements unless the user doesn't want to have
2126 ;; them
2127 (if event-ok
2128 (progn
2129 (setq diary-string
2130 (concat diary-string " "
2131 (icalendar--format-ical-event e)))
2132 (if do-not-ask (setq summary nil))
2133 ;; add entry to diary and store actual name of diary
2134 ;; file (in case it was nil)
2135 (setq diary-file
2136 (icalendar--add-diary-entry diary-string diary-file
2137 non-marking summary)))
2138 ;; event was not ok
2139 (setq found-error t)
2140 (setq error-string
2141 (format "%s\nCannot handle this event:%s"
2142 error-string e))))
2143 ;; FIXME: inform user about ignored event properties
2144 ;; handle errors
2145 (error
2146 (message "Ignoring event \"%s\"" e)
2147 (setq found-error t)
2148 (setq error-string (format "%s\n%s\nCannot handle this event: %s"
2149 error-val error-string e))
2150 (message "%s" error-string))))
2152 ;; insert final newline
2153 (if diary-file
2154 (let ((b (find-buffer-visiting diary-file)))
2155 (when b
2156 (save-current-buffer
2157 (set-buffer b)
2158 (goto-char (point-max))
2159 (insert "\n")))))
2160 (if found-error
2161 (save-current-buffer
2162 (set-buffer (get-buffer-create "*icalendar-errors*"))
2163 (erase-buffer)
2164 (insert error-string)))
2165 (message "Converting iCalendar...done")
2166 found-error))
2168 ;; subroutines for importing
2169 (defun icalendar--convert-recurring-to-diary (e dtstart-dec start-t end-t)
2170 "Convert recurring iCalendar event E to diary format.
2172 DTSTART-DEC is the DTSTART property of E.
2173 START-T is the event's start time in diary format.
2174 END-T is the event's end time in diary format."
2175 (icalendar--dmsg "recurring event")
2176 (let* ((rrule (icalendar--get-event-property e 'RRULE))
2177 (rrule-props (icalendar--split-value rrule))
2178 (frequency (cadr (assoc 'FREQ rrule-props)))
2179 (until (cadr (assoc 'UNTIL rrule-props)))
2180 (count (cadr (assoc 'COUNT rrule-props)))
2181 (interval (read (or (cadr (assoc 'INTERVAL rrule-props)) "1")))
2182 (dtstart-conv (icalendar--datetime-to-diary-date dtstart-dec))
2183 (until-conv (icalendar--datetime-to-diary-date
2184 (icalendar--decode-isodatetime until)))
2185 (until-1-conv (icalendar--datetime-to-diary-date
2186 (icalendar--decode-isodatetime until -1)))
2187 (result ""))
2189 ;; FIXME FIXME interval!!!!!!!!!!!!!
2191 (when count
2192 (if until
2193 (message "Must not have UNTIL and COUNT -- ignoring COUNT element!")
2194 (let ((until-1 0))
2195 (cond ((string-equal frequency "DAILY")
2196 (setq until (icalendar--add-decoded-times
2197 dtstart-dec
2198 (list 0 0 0 (* (read count) interval) 0 0)))
2199 (setq until-1 (icalendar--add-decoded-times
2200 dtstart-dec
2201 (list 0 0 0 (* (- (read count) 1) interval)
2202 0 0)))
2204 ((string-equal frequency "WEEKLY")
2205 (setq until (icalendar--add-decoded-times
2206 dtstart-dec
2207 (list 0 0 0 (* (read count) 7 interval) 0 0)))
2208 (setq until-1 (icalendar--add-decoded-times
2209 dtstart-dec
2210 (list 0 0 0 (* (- (read count) 1) 7
2211 interval) 0 0)))
2213 ((string-equal frequency "MONTHLY")
2214 (setq until (icalendar--add-decoded-times
2215 dtstart-dec (list 0 0 0 0 (* (- (read count) 1)
2216 interval) 0)))
2217 (setq until-1 (icalendar--add-decoded-times
2218 dtstart-dec (list 0 0 0 0 (* (- (read count) 1)
2219 interval) 0)))
2221 ((string-equal frequency "YEARLY")
2222 (setq until (icalendar--add-decoded-times
2223 dtstart-dec (list 0 0 0 0 0 (* (- (read count) 1)
2224 interval))))
2225 (setq until-1 (icalendar--add-decoded-times
2226 dtstart-dec
2227 (list 0 0 0 0 0 (* (- (read count) 1)
2228 interval))))
2231 (message "Cannot handle COUNT attribute for `%s' events."
2232 frequency)))
2233 (setq until-conv (icalendar--datetime-to-diary-date until))
2234 (setq until-1-conv (icalendar--datetime-to-diary-date until-1))
2237 (cond ((string-equal frequency "WEEKLY")
2238 (let* ((byday (cadr (assoc 'BYDAY rrule-props)))
2239 (weekdays
2240 (icalendar--get-weekday-numbers byday))
2241 (weekday-clause
2242 (when (> (length weekdays) 1)
2243 (format "(memq (calendar-day-of-week date) '%s) "
2244 weekdays))))
2245 (if (not start-t)
2246 (progn
2247 ;; weekly and all-day
2248 (icalendar--dmsg "weekly all-day")
2249 (if until
2250 (setq result
2251 (format
2252 (concat "%%%%(and "
2253 "%s"
2254 "(diary-block %s %s))")
2255 (or weekday-clause
2256 (format "(diary-cyclic %d %s) "
2257 (* interval 7)
2258 dtstart-conv))
2259 dtstart-conv
2260 (if count until-1-conv until-conv)
2262 (setq result
2263 (format "%%%%(and %s(diary-cyclic %d %s))"
2264 (or weekday-clause "")
2265 (if weekday-clause 1 (* interval 7))
2266 dtstart-conv))))
2267 ;; weekly and not all-day
2268 (icalendar--dmsg "weekly not-all-day")
2269 (if until
2270 (setq result
2271 (format
2272 (concat "%%%%(and "
2273 "%s"
2274 "(diary-block %s %s)) "
2275 "%s%s%s")
2276 (or weekday-clause
2277 (format "(diary-cyclic %d %s) "
2278 (* interval 7)
2279 dtstart-conv))
2280 dtstart-conv
2281 until-conv
2282 (or start-t "")
2283 (if end-t "-" "") (or end-t "")))
2284 ;; no limit
2285 ;; FIXME!!!!
2286 ;; DTSTART;VALUE=DATE-TIME:20030919T090000
2287 ;; DTEND;VALUE=DATE-TIME:20030919T113000
2288 (setq result
2289 (format
2290 "%%%%(and %s(diary-cyclic %d %s)) %s%s%s"
2291 (or weekday-clause "")
2292 (if weekday-clause 1 (* interval 7))
2293 dtstart-conv
2294 (or start-t "")
2295 (if end-t "-" "") (or end-t "")))))))
2296 ;; yearly
2297 ((string-equal frequency "YEARLY")
2298 (icalendar--dmsg "yearly")
2299 (if until
2300 (let ((day (nth 3 dtstart-dec))
2301 (month (nth 4 dtstart-dec)))
2302 (setq result (concat "%%(and (diary-date "
2303 (cond ((eq (icalendar--date-style) 'iso)
2304 (format "t %d %d" month day))
2305 ((eq (icalendar--date-style) 'european)
2306 (format "%d %d t" day month))
2307 ((eq (icalendar--date-style) 'american)
2308 (format "%d %d t" month day)))
2309 ") (diary-block "
2310 dtstart-conv
2312 until-conv
2313 ")) "
2314 (or start-t "")
2315 (if end-t "-" "")
2316 (or end-t ""))))
2317 (setq result (format
2318 "%%%%(and (diary-anniversary %s)) %s%s%s"
2319 dtstart-conv
2320 (or start-t "")
2321 (if end-t "-" "") (or end-t "")))))
2322 ;; monthly
2323 ((string-equal frequency "MONTHLY")
2324 (icalendar--dmsg "monthly")
2325 (setq result
2326 (format
2327 "%%%%(and (diary-date %s) (diary-block %s %s)) %s%s%s"
2328 (let ((day (nth 3 dtstart-dec)))
2329 (cond ((eq (icalendar--date-style) 'iso)
2330 (format "t t %d" day))
2331 ((eq (icalendar--date-style) 'european)
2332 (format "%d t t" day))
2333 ((eq (icalendar--date-style) 'american)
2334 (format "t %d t" day))))
2335 dtstart-conv
2336 (if until
2337 until-conv
2338 (if (eq (icalendar--date-style) 'iso) "9999 1 1" "1 1 9999")) ;; FIXME: should be unlimited
2339 (or start-t "")
2340 (if end-t "-" "") (or end-t ""))))
2341 ;; daily
2342 ((and (string-equal frequency "DAILY"))
2343 (if until
2344 (setq result
2345 (format
2346 (concat "%%%%(and (diary-cyclic %s %s) "
2347 "(diary-block %s %s)) %s%s%s")
2348 interval dtstart-conv dtstart-conv
2349 (if count until-1-conv until-conv)
2350 (or start-t "")
2351 (if end-t "-" "") (or end-t "")))
2352 (setq result
2353 (format
2354 "%%%%(and (diary-cyclic %s %s)) %s%s%s"
2355 interval
2356 dtstart-conv
2357 (or start-t "")
2358 (if end-t "-" "") (or end-t ""))))))
2359 ;; Handle exceptions from recurrence rules
2360 (let ((ex-dates (icalendar--get-event-properties e 'EXDATE)))
2361 (while ex-dates
2362 (let* ((ex-start (icalendar--decode-isodatetime
2363 (car ex-dates)))
2364 (ex-d (icalendar--datetime-to-diary-date
2365 ex-start)))
2366 (setq result
2367 (icalendar--rris "^%%(\\(and \\)?"
2368 (format
2369 "%%%%(and (not (diary-date %s)) "
2370 ex-d)
2371 result)))
2372 (setq ex-dates (cdr ex-dates))))
2373 ;; FIXME: exception rules are not recognized
2374 (if (icalendar--get-event-property e 'EXRULE)
2375 (setq result
2376 (concat result
2377 "\n Exception rules: "
2378 (icalendar--get-event-properties
2379 e 'EXRULE))))
2380 result))
2382 (defun icalendar--convert-non-recurring-all-day-to-diary (event start-d end-d)
2383 "Convert non-recurring iCalendar EVENT to diary format.
2385 DTSTART is the decoded DTSTART property of E.
2386 Argument START-D gives the first day.
2387 Argument END-D gives the last day."
2388 (icalendar--dmsg "non-recurring all-day event")
2389 (format "%%%%(and (diary-block %s %s))" start-d end-d))
2391 (defun icalendar--convert-non-recurring-not-all-day-to-diary (event dtstart-dec
2392 dtend-dec
2393 start-t
2394 end-t)
2395 "Convert recurring icalendar EVENT to diary format.
2397 DTSTART-DEC is the decoded DTSTART property of E.
2398 DTEND-DEC is the decoded DTEND property of E.
2399 START-T is the event's start time in diary format.
2400 END-T is the event's end time in diary format."
2401 (icalendar--dmsg "not all day event")
2402 (cond (end-t
2403 (format "%s %s-%s"
2404 (icalendar--datetime-to-diary-date
2405 dtstart-dec "/")
2406 start-t end-t))
2408 (format "%s %s"
2409 (icalendar--datetime-to-diary-date
2410 dtstart-dec "/")
2411 start-t))))
2413 (defun icalendar--add-diary-entry (string diary-file non-marking
2414 &optional summary)
2415 "Add STRING to the diary file DIARY-FILE.
2416 STRING must be a properly formatted valid diary entry. NON-MARKING
2417 determines whether diary events are created as non-marking. If
2418 SUMMARY is not nil it must be a string that gives the summary of the
2419 entry. In this case the user will be asked whether he wants to insert
2420 the entry."
2421 (when (or (not summary)
2422 (y-or-n-p (format "Add appointment for `%s' to diary? "
2423 summary)))
2424 (when summary
2425 (setq non-marking
2426 (y-or-n-p (format "Make appointment non-marking? "))))
2427 (save-window-excursion
2428 (unless diary-file
2429 (setq diary-file
2430 (read-file-name "Add appointment to this diary file: ")))
2431 ;; Note: diary-make-entry will add a trailing blank char.... :(
2432 (funcall (if (fboundp 'diary-make-entry)
2433 'diary-make-entry
2434 'make-diary-entry)
2435 string non-marking diary-file)))
2436 ;; Würgaround to remove the trailing blank char
2437 (with-current-buffer (find-file diary-file)
2438 (goto-char (point-max))
2439 (if (= (char-before) ? )
2440 (delete-char -1)))
2441 ;; return diary-file in case it has been changed interactively
2442 diary-file)
2444 ;; ======================================================================
2445 ;; Examples
2446 ;; ======================================================================
2447 (defun icalendar-import-format-sample (event)
2448 "Example function for formatting an iCalendar EVENT."
2449 (format (concat "SUMMARY=`%s' DESCRIPTION=`%s' LOCATION=`%s' ORGANIZER=`%s' "
2450 "STATUS=`%s' URL=`%s' CLASS=`%s'")
2451 (or (icalendar--get-event-property event 'SUMMARY) "")
2452 (or (icalendar--get-event-property event 'DESCRIPTION) "")
2453 (or (icalendar--get-event-property event 'LOCATION) "")
2454 (or (icalendar--get-event-property event 'ORGANIZER) "")
2455 (or (icalendar--get-event-property event 'STATUS) "")
2456 (or (icalendar--get-event-property event 'URL) "")
2457 (or (icalendar--get-event-property event 'CLASS) "")))
2459 (provide 'icalendar)
2461 ;;; icalendar.el ends here