1 ;;; icalendar.el --- iCalendar implementation -*-coding: utf-8 -*-
3 ;; Copyright (C) 2002, 2003, 2004, 2005 Free Software Foundation, Inc.
5 ;; Author: Ulf Jasper <ulf.jasper@web.de>
6 ;; Created: August 2002
8 ;; Human-Keywords: calendar, diary, iCalendar, vCalendar
10 ;; This file is part of GNU Emacs.
12 ;; GNU Emacs is free software; you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation; either version 2, or (at your option)
17 ;; GNU Emacs is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with GNU Emacs; see the file COPYING. If not, write to the
24 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
25 ;; Boston, MA 02111-1307, USA.
29 ;; This package is documented in the Emacs Manual.
34 ;; 0.07 onwards: see lisp/ChangeLog
36 ;; 0.06: Bugfixes regarding icalendar-import-format-*.
37 ;; Fix in icalendar-convert-diary-to-ical -- thanks to Philipp
40 ;; 0.05: New import format scheme: Replaced icalendar-import-prefix-*,
41 ;; icalendar-import-ignored-properties, and
42 ;; icalendar-import-separator with icalendar-import-format(-*).
43 ;; icalendar-import-file and icalendar-convert-diary-to-ical
44 ;; have an extra parameter which should prevent them from
45 ;; erasing their target files (untested!).
46 ;; Tested with Emacs 21.3.2
48 ;; 0.04: Bugfix: import: double quoted param values did not work
49 ;; Read DURATION property when importing.
50 ;; Added parameter icalendar-duration-correction.
52 ;; 0.03: Export takes care of european-calendar-style.
53 ;; Tested with Emacs 21.3.2 and XEmacs 21.4.12
55 ;; 0.02: Should work in XEmacs now. Thanks to Len Trigg for the
57 ;; Added exporting from Emacs diary to ical.
58 ;; Some bugfixes, after testing with calendars from
59 ;; http://icalshare.com.
60 ;; Tested with Emacs 21.3.2 and XEmacs 21.4.12
62 ;; 0.01: First published version. Trial version. Alpha version.
64 ;; ======================================================================
67 ;; * Import from ical to diary:
68 ;; + Need more properties for icalendar-import-format
69 ;; + check vcalendar version
70 ;; + check (unknown) elements
71 ;; + recurring events!
72 ;; + works for european style calendars only! Does it?
74 ;; + exceptions in recurring events
75 ;; + the parser is too soft
76 ;; + error log is incomplete
77 ;; + nice to have: #include "webcal://foo.com/some-calendar.ics"
79 ;; * Export from diary to ical
80 ;; + diary-date, diary-float, and self-made sexp entries are not
82 ;; + timezones, currently all times are local!
85 ;; + clean up all those date/time parsing functions
86 ;; + Handle todo items?
87 ;; + Check iso 8601 for datetime and period
88 ;; + Which chars to (un)escape?
93 (defconst icalendar-version
0.10
94 "Version number of icalendar.el.")
96 ;; ======================================================================
98 ;; ======================================================================
99 (defgroup icalendar nil
104 (defcustom icalendar-import-format
106 "Format string for importing events from iCalendar into Emacs diary.
107 This string defines how iCalendar events are inserted into diary
108 file. Meaning of the specifiers:
109 %d Description, see `icalendar-import-format-description'
110 %l Location, see `icalendar-import-format-location'
111 %o Organizer, see `icalendar-import-format-organizer'
112 %s Subject, see `icalendar-import-format-subject'"
116 (defcustom icalendar-import-format-subject
118 "Format string defining how the subject element is formatted.
119 This applies only if the subject is not empty! `%s' is replaced
124 (defcustom icalendar-import-format-description
126 "Format string defining how the description element is formatted.
127 This applies only if the description is not empty! `%s' is
128 replaced by the description."
132 (defcustom icalendar-import-format-location
134 "Format string defining how the location element is formatted.
135 This applies only if the location is not empty! `%s' is replaced
140 (defcustom icalendar-import-format-organizer
142 "Format string defining how the organizer element is formatted.
143 This applies only if the organizer is not empty! `%s' is
144 replaced by the organizer."
148 (defcustom icalendar-duration-correction
150 "Workaround for all-day events.
151 If non-nil the length=duration of iCalendar appointments that
152 have a length of exactly n days is decreased by one day. This
153 fixes problems with all-day events, which appear to be one day
154 longer than they are."
159 ;; ======================================================================
160 ;; NO USER SERVICABLE PARTS BELOW THIS LINE
161 ;; ======================================================================
163 (defconst icalendar--weekday-array
["SU" "MO" "TU" "WE" "TH" "FR" "SA"])
165 (defvar icalendar-debug nil
".")
167 ;; ======================================================================
168 ;; all the other libs we need
169 ;; ======================================================================
173 ;; ======================================================================
175 ;; ======================================================================
176 (defun icalendar--dmsg (&rest args
)
177 "Print message ARGS if `icalendar-debug' is non-nil."
179 (apply 'message args
)))
181 ;; ======================================================================
182 ;; Core functionality
183 ;; Functions for parsing icalendars, importing and so on
184 ;; ======================================================================
186 (defun icalendar--get-unfolded-buffer (folded-ical-buffer)
187 "Return a new buffer containing the unfolded contents of a buffer.
188 Folding is the iCalendar way of wrapping long lines. In the
189 created buffer all occurrences of CR LF BLANK are replaced by the
190 empty string. Argument FOLDED-ICAL-BUFFER is the unfolded input
192 (let ((unfolded-buffer (get-buffer-create " *icalendar-work*")))
194 (set-buffer unfolded-buffer
)
196 (insert-buffer folded-ical-buffer
)
197 (while (re-search-forward "\r?\n[ \t]" nil t
)
198 (replace-match "" nil nil
)))
201 (defsubst icalendar--rris
(re rp st
)
202 "Replace regexp RE with RP in string ST and return the new string.
203 This is here for compatibility with XEmacs."
205 (if (fboundp 'replace-in-string
)
206 (save-match-data ;; apparently XEmacs needs save-match-data
207 (replace-in-string st re rp
))
209 (replace-regexp-in-string re rp st
)))
211 (defun icalendar--read-element (invalue inparams
)
212 "Recursively read the next iCalendar element in the current buffer.
213 INVALUE gives the current iCalendar element we are reading.
214 INPARAMS gives the current parameters.....
215 This function calls itself recursively for each nested calendar element
217 (let (element children line name params param param-name param-value
222 (re-search-forward "^\\([A-Za-z0-9-]+\\)[;:]" nil t
))
223 (setq name
(intern (match-string 1)))
227 (while (looking-at ";")
228 (re-search-forward ";\\([A-Za-z0-9-]+\\)=" nil nil
)
229 (setq param-name
(intern (match-string 1)))
230 (re-search-forward "\\(\\([^;,:\"]+\\)\\|\"\\([^\"]+\\)\"\\)[;:]"
233 (setq param-value
(or (match-string 2) (match-string 3)))
234 (setq param
(list param-name param-value
))
235 (while (looking-at ",")
236 (re-search-forward "\\(\\([^;,:]+\\)\\|\"\\([^\"]+\\)\"\\)"
239 (setq param-value
(match-string 2))
240 (setq param-value
(match-string 3)))
241 (setq param
(append param param-value
)))
242 (setq params
(append params param
)))
243 (unless (looking-at ":")
246 (re-search-forward "\\(.*\\)\\(\r?\n[ \t].*\\)*" nil t
)
247 (setq value
(icalendar--rris "\r?\n[ \t]" "" (match-string 0)))
248 (setq line
(list name params value
))
249 (cond ((eq name
'BEGIN
)
252 (list (icalendar--read-element (intern value
)
257 (setq element
(append element
(list line
))))))
259 (list invalue inparams element children
)
262 ;; ======================================================================
263 ;; helper functions for examining events
264 ;; ======================================================================
266 ;;(defsubst icalendar--get-all-event-properties (event)
267 ;; "Return the list of properties in this EVENT."
268 ;; (car (cddr event)))
270 (defun icalendar--get-event-property (event prop
)
271 "For the given EVENT return the value of the first occurence of PROP."
273 (let ((props (car (cddr event
))) pp
)
275 (setq pp
(car props
))
276 (if (eq (car pp
) prop
)
277 (throw 'found
(car (cddr pp
))))
278 (setq props
(cdr props
))))
281 (defun icalendar--get-event-property-attributes (event prop
)
282 "For the given EVENT return attributes of the first occurence of PROP."
284 (let ((props (car (cddr event
))) pp
)
286 (setq pp
(car props
))
287 (if (eq (car pp
) prop
)
288 (throw 'found
(cadr pp
)))
289 (setq props
(cdr props
))))
292 (defun icalendar--get-event-properties (event prop
)
293 "For the given EVENT return a list of all values of the property PROP."
294 (let ((props (car (cddr event
))) pp result
)
296 (setq pp
(car props
))
297 (if (eq (car pp
) prop
)
298 (setq result
(cons (car (cddr pp
)) result
)))
299 (setq props
(cdr props
)))
302 ;; (defun icalendar--set-event-property (event prop new-value)
303 ;; "For the given EVENT set the property PROP to the value NEW-VALUE."
305 ;; (let ((props (car (cddr event))) pp)
307 ;; (setq pp (car props))
308 ;; (when (eq (car pp) prop)
309 ;; (setcdr (cdr pp) new-value)
310 ;; (throw 'found (car (cddr pp))))
311 ;; (setq props (cdr props)))
312 ;; (setq props (car (cddr event)))
313 ;; (setcar (cddr event)
314 ;; (append props (list (list prop nil new-value)))))))
316 (defun icalendar--get-children (node name
)
317 "Return all children of the given NODE which have a name NAME.
318 For instance the VCALENDAR node can have VEVENT children as well as VTODO
321 (children (cadr (cddr node
))))
322 (when (eq (car node
) name
)
324 ;;(message "%s" node)
329 (icalendar--get-children n name
))
333 (setq result
(append result subresult
))
334 (setq result subresult
)))))
338 (defun icalendar--all-events (icalendar)
339 "Return the list of all existing events in the given ICALENDAR."
340 (icalendar--get-children (car icalendar
) 'VEVENT
))
342 (defun icalendar--split-value (value-string)
343 "Split VALUE-STRING at ';='."
345 param-name param-value
)
348 (set-buffer (get-buffer-create " *icalendar-work*"))
349 (set-buffer-modified-p nil
)
351 (insert value-string
)
352 (goto-char (point-min))
355 "\\([A-Za-z0-9-]+\\)=\\(\\([^;,:]+\\)\\|\"\\([^\"]+\\)\"\\);?"
357 (setq param-name
(intern (match-string 1)))
358 (setq param-value
(match-string 2))
360 (append result
(list (list param-name param-value
)))))))
363 (defun icalendar--decode-isodatetime (isodatetimestring &optional day-shift
)
364 "Return ISODATETIMESTRING in format like `decode-time'.
365 Converts from ISO-8601 to Emacs representation. If
366 ISODATETIMESTRING specifies UTC time (trailing letter Z) the
367 decoded time is given in the local time zone! If optional
368 parameter DAY-SHIFT is non-nil the result is shifted by DAY-SHIFT
371 FIXME: TZID-attributes are ignored....!
372 FIXME: multiple comma-separated values should be allowed!"
373 (icalendar--dmsg isodatetimestring
)
374 (if isodatetimestring
375 ;; day/month/year must be present
376 (let ((year (read (substring isodatetimestring
0 4)))
377 (month (read (substring isodatetimestring
4 6)))
378 (day (read (substring isodatetimestring
6 8)))
382 (when (> (length isodatetimestring
) 12)
383 ;; hour/minute present
384 (setq hour
(read (substring isodatetimestring
9 11)))
385 (setq minute
(read (substring isodatetimestring
11 13))))
386 (when (> (length isodatetimestring
) 14)
388 (setq second
(read (substring isodatetimestring
13 15))))
389 (when (and (> (length isodatetimestring
) 15)
390 ;; UTC specifier present
391 (char-equal ?Z
(aref isodatetimestring
15)))
392 ;; if not UTC add current-time-zone offset
393 (setq second
(+ (car (current-time-zone)) second
)))
394 ;; shift if necessary
396 (let ((mdy (calendar-gregorian-from-absolute
397 (+ (calendar-absolute-from-gregorian
398 (list month day year
))
400 (setq month
(nth 0 mdy
))
401 (setq day
(nth 1 mdy
))
402 (setq year
(nth 2 mdy
))))
403 ;; create the decoded date-time
406 (decode-time (encode-time second minute hour day month year
))
408 (message "Cannot decode \"%s\"" isodatetimestring
)
409 ;; hope for the best...
410 (list second minute hour day month year
0 nil
0))))
411 ;; isodatetimestring == nil
414 (defun icalendar--decode-isoduration (isodurationstring)
415 "Return ISODURATIONSTRING in format like `decode-time'.
416 Converts from ISO-8601 to Emacs representation. If ISODURATIONSTRING
417 specifies UTC time (trailing letter Z) the decoded time is given in
418 the local time zone! FIXME: TZID-attributes are ignored....! FIXME:
419 multiple comma-separated values should be allowed!"
420 (if isodurationstring
425 "\\(\\([0-9]+\\)D\\)" ; days only
427 "\\(\\(\\([0-9]+\\)D\\)?T\\(\\([0-9]+\\)H\\)?" ; opt days
428 "\\(\\([0-9]+\\)M\\)?\\(\\([0-9]+\\)S\\)?\\)" ; mand. time
430 "\\(\\([0-9]+\\)W\\)" ; weeks only
431 "\\)$") isodurationstring
)
439 ((match-beginning 2) ;days only
440 (setq days
(read (substring isodurationstring
443 (when icalendar-duration-correction
444 (setq days
(1- days
))))
445 ((match-beginning 4) ;days and time
446 (if (match-beginning 5)
447 (setq days
(* 7 (read (substring isodurationstring
450 (if (match-beginning 7)
451 (setq hours
(read (substring isodurationstring
454 (if (match-beginning 9)
455 (setq minutes
(read (substring isodurationstring
458 (if (match-beginning 11)
459 (setq seconds
(read (substring isodurationstring
462 ((match-beginning 13) ;weeks only
463 (setq days
(* 7 (read (substring isodurationstring
466 (list seconds minutes hours days months years
)))
467 ;; isodatetimestring == nil
470 (defun icalendar--add-decoded-times (time1 time2
)
472 Both times must be given in decoded form. One of these times must be
473 valid (year > 1900 or something)."
474 ;; FIXME: does this function exist already?
475 (decode-time (encode-time
476 (+ (nth 0 time1
) (nth 0 time2
))
477 (+ (nth 1 time1
) (nth 1 time2
))
478 (+ (nth 2 time1
) (nth 2 time2
))
479 (+ (nth 3 time1
) (nth 3 time2
))
480 (+ (nth 4 time1
) (nth 4 time2
))
481 (+ (nth 5 time1
) (nth 5 time2
))
484 ;;(or (nth 6 time1) (nth 6 time2)) ;; FIXME?
487 (defun icalendar--datetime-to-noneuropean-date (datetime &optional separator
)
488 "Convert the decoded DATETIME to non-european-style format.
489 Optional argument SEPARATOR gives the separator between month,
490 day, and year. If nil a blank character is used as separator.
491 Non-European format: \"month day year\"."
493 (format "%d%s%d%s%d" (nth 4 datetime
) ;month
495 (nth 3 datetime
) ;day
497 (nth 5 datetime
)) ;year
501 (defun icalendar--datetime-to-european-date (datetime &optional separator
)
502 "Convert the decoded DATETIME to European format.
503 Optional argument SEPARATOR gives the separator between month,
504 day, and year. If nil a blank character is used as separator.
505 European format: (day month year).
508 (format "%d%s%d%s%d" (nth 3 datetime
) ;day
510 (nth 4 datetime
) ;month
512 (nth 5 datetime
)) ;year
516 (defun icalendar--datetime-to-diary-date (datetime &optional separator
)
517 "Convert the decoded DATETIME to diary format.
518 Optional argument SEPARATOR gives the separator between month,
519 day, and year. If nil a blank character is used as separator.
520 Call icalendar--datetime-to-(non)-european-date according to
521 value of `european-calendar-style'."
522 (if european-calendar-style
523 (icalendar--datetime-to-european-date datetime separator
)
524 (icalendar--datetime-to-noneuropean-date datetime separator
)))
526 (defun icalendar--datetime-to-colontime (datetime)
527 "Extract the time part of a decoded DATETIME into 24-hour format.
528 Note that this silently ignores seconds."
529 (format "%02d:%02d" (nth 2 datetime
) (nth 1 datetime
)))
531 (defun icalendar--get-month-number (monthname)
532 "Return the month number for the given MONTHNAME."
535 (m (downcase monthname
)))
536 (mapc (lambda (month)
537 (let ((mm (downcase month
)))
538 (if (or (string-equal mm m
)
539 (string-equal (substring mm
0 3) m
))
541 (setq num
(1+ num
))))
542 calendar-month-name-array
))
546 (defun icalendar--get-weekday-number (abbrevweekday)
547 "Return the number for the ABBREVWEEKDAY."
551 (aw (downcase abbrevweekday
)))
553 (let ((d (downcase day
)))
554 (if (string-equal d aw
)
556 (setq num
(1+ num
))))
557 icalendar--weekday-array
)))
561 (defun icalendar--get-weekday-abbrev (weekday)
562 "Return the abbreviated WEEKDAY."
565 (w (downcase weekday
)))
567 (let ((d (downcase day
)))
568 (if (or (string-equal d w
)
569 (string-equal (substring d
0 3) w
))
570 (throw 'found
(aref icalendar--weekday-array num
)))
571 (setq num
(1+ num
))))
572 calendar-day-name-array
))
576 (defun icalendar--date-to-isodate (date &optional day-shift
)
577 "Convert DATE to iso-style date.
578 DATE must be a list of the form (month day year).
579 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days."
580 (let ((mdy (calendar-gregorian-from-absolute
581 (+ (calendar-absolute-from-gregorian date
)
583 (format "%04d%02d%02d" (nth 2 mdy
) (nth 0 mdy
) (nth 1 mdy
))))
586 (defun icalendar--datestring-to-isodate (datestring &optional day-shift
)
587 "Convert diary-style DATESTRING to iso-style date.
588 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days
589 -- DAY-SHIFT must be either nil or an integer. This function
590 takes care of european-style."
591 (let ((day -
1) month year
)
593 (cond ( ;; numeric date
594 (string-match (concat "\\s-*"
595 "0?\\([1-9][0-9]?\\)[ \t/]\\s-*"
596 "0?\\([1-9][0-9]?\\),?[ \t/]\\s-*"
597 "\\([0-9]\\{4\\}\\)")
599 (setq day
(read (substring datestring
(match-beginning 1)
601 (setq month
(read (substring datestring
(match-beginning 2)
603 (setq year
(read (substring datestring
(match-beginning 3)
605 (unless european-calendar-style
609 ( ;; date contains month names -- european-style
610 (and european-calendar-style
611 (string-match (concat "\\s-*"
612 "0?\\([123]?[0-9]\\)[ \t/]\\s-*"
613 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
614 "\\([0-9]\\{4\\}\\)")
616 (setq day
(read (substring datestring
(match-beginning 1)
618 (setq month
(icalendar--get-month-number
619 (substring datestring
(match-beginning 2)
621 (setq year
(read (substring datestring
(match-beginning 3)
623 ( ;; date contains month names -- non-european-style
624 (and (not european-calendar-style
)
625 (string-match (concat "\\s-*"
626 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
627 "0?\\([123]?[0-9]\\),?[ \t/]\\s-*"
628 "\\([0-9]\\{4\\}\\)")
630 (setq day
(read (substring datestring
(match-beginning 2)
632 (setq month
(icalendar--get-month-number
633 (substring datestring
(match-beginning 1)
635 (setq year
(read (substring datestring
(match-beginning 3)
640 (let ((mdy (calendar-gregorian-from-absolute
641 (+ (calendar-absolute-from-gregorian (list month day
644 (format "%04d%02d%02d" (nth 2 mdy
) (nth 0 mdy
) (nth 1 mdy
)))
647 (defun icalendar--diarytime-to-isotime (timestring ampmstring
)
648 "Convert a a time like 9:30pm to an iso-conform string like T213000.
649 In this example the TIMESTRING would be \"9:30\" and the AMPMSTRING
652 (let ((starttimenum (read (icalendar--rris ":" "" timestring
))))
653 ;; take care of am/pm style
654 (if (and ampmstring
(string= "pm" ampmstring
))
655 (setq starttimenum
(+ starttimenum
1200)))
656 (format "T%04d00" starttimenum
))
659 (defun icalendar--convert-string-for-export (string)
660 "Escape comma and other critical characters in STRING."
661 (icalendar--rris "," "\\\\," string
))
663 (defun icalendar--convert-string-for-import (string)
664 "Remove escape chars for comma, semicolon etc. from STRING."
666 "\\\\n" "\n " (icalendar--rris
667 "\\\\\"" "\"" (icalendar--rris
668 "\\\\;" ";" (icalendar--rris
669 "\\\\," "," string
)))))
671 ;; ======================================================================
672 ;; Export -- convert emacs-diary to icalendar
673 ;; ======================================================================
676 (defun icalendar-export-file (diary-filename ical-filename
)
677 "Export diary file to iCalendar format.
678 All diary entries in the file DIARY-FILENAME are converted to iCalendar
679 format. The result is appended to the file ICAL-FILENAME."
680 (interactive "FExport diary data from file:
681 Finto iCalendar file: ")
683 (set-buffer (find-file diary-filename
))
684 (icalendar-export-region (point-min) (point-max) ical-filename
)))
686 (defalias 'icalendar-convert-diary-to-ical
'icalendar-export-file
)
687 (make-obsolete 'icalendar-convert-diary-to-ical
'icalendar-export-file
)
690 (defun icalendar-export-region (min max ical-filename
)
691 "Export region in diary file to iCalendar format.
692 All diary entries in the region from MIN to MAX in the current buffer are
693 converted to iCalendar format. The result is appended to the file
695 This function attempts to return t if something goes wrong. In this
696 case an error string which describes all the errors and problems is
697 written into the buffer `*icalendar-errors*'."
699 FExport diary data into iCalendar file: ")
707 (nonmarker (concat "^" (regexp-quote diary-nonmarking-symbol
)
709 ;; prepare buffer with error messages
711 (set-buffer (get-buffer-create " *icalendar-errors*"))
717 (while (re-search-forward
718 "^\\([^ \t\n].*\\)\\(\\(\n[ \t].*\\)*\\)" max t
)
719 (setq entry-main
(match-string 1))
720 (if (match-beginning 2)
721 (setq entry-rest
(match-string 2))
722 (setq entry-rest
""))
723 (setq header
(format "\nBEGIN:VEVENT\nUID:emacs%d%d%d"
725 (cadr (current-time))
726 (car (cddr (current-time)))))
727 (condition-case error-val
733 "%%(diary-anniversary \\([^)]+\\))\\s-*\\(.*\\)")
735 (icalendar--dmsg "diary-anniversary %s" entry-main
)
736 (let* ((datetime (substring entry-main
(match-beginning 1)
738 (summary (icalendar--convert-string-for-export
739 (substring entry-main
(match-beginning 2)
741 (startisostring (icalendar--datestring-to-isodate
743 (endisostring (icalendar--datestring-to-isodate
746 (concat "\nDTSTART;VALUE=DATE:" startisostring
747 "\nDTEND;VALUE=DATE:" endisostring
749 "\nRRULE:FREQ=YEARLY;INTERVAL=1"
750 ;; the following is redundant,
751 ;; but korganizer seems to expect this... ;(
752 ;; and evolution doesn't understand it... :(
753 ;; so... who is wrong?!
755 (substring startisostring
4 6)
757 (substring startisostring
6 8))))
758 (unless (string= entry-rest
"")
760 (concat contents
"\nDESCRIPTION:"
761 (icalendar--convert-string-for-export
767 "%%(diary-cyclic \\([^ ]+\\) +"
768 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*\\(.*\\)")
770 (icalendar--dmsg "diary-cyclic %s" entry-main
)
771 (let* ((frequency (substring entry-main
(match-beginning 1)
773 (datetime (substring entry-main
(match-beginning 2)
775 (summary (icalendar--convert-string-for-export
776 (substring entry-main
(match-beginning 3)
778 (startisostring (icalendar--datestring-to-isodate
780 (endisostring (icalendar--datestring-to-isodate
783 (concat "\nDTSTART;VALUE=DATE:" startisostring
784 "\nDTEND;VALUE=DATE:" endisostring
786 "\nRRULE:FREQ=DAILY;INTERVAL=" frequency
787 ;; strange: korganizer does not expect
788 ;; BYSOMETHING here...
790 (unless (string= entry-rest
"")
792 (concat contents
"\nDESCRIPTION:"
793 (icalendar--convert-string-for-export
795 ;; diary-date -- FIXME
798 "%%(diary-date \\([^)]+\\))\\s-*\\(.*\\)")
800 (icalendar--dmsg "diary-date %s" entry-main
)
801 (error "`diary-date' is not supported yet"))
802 ;; float events -- FIXME
805 "%%(diary-float \\([^)]+\\))\\s-*\\(.*\\)")
807 (icalendar--dmsg "diary-float %s" entry-main
)
808 (error "`diary-float' is not supported yet"))
812 "%%(diary-block \\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)"
813 " +\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
816 (icalendar--dmsg "diary-block %s" entry-main
)
817 (let* ((startstring (substring entry-main
820 (endstring (substring entry-main
823 (summary (icalendar--convert-string-for-export
824 (substring entry-main
(match-beginning 3)
826 (startisostring (icalendar--datestring-to-isodate
828 (endisostring (icalendar--datestring-to-isodate
831 (concat "\nDTSTART;VALUE=DATE:" startisostring
832 "\nDTEND;VALUE=DATE:" endisostring
833 "\nSUMMARY:" summary
))
834 (unless (string= entry-rest
"")
836 (concat contents
"\nDESCRIPTION:"
837 (icalendar--convert-string-for-export
839 ;; other sexp diary entries -- FIXME
842 "%%(\\([^)]+\\))\\s-*\\(.*\\)")
844 (icalendar--dmsg "diary-sexp %s" entry-main
)
845 (error "sexp-entries are not supported yet"))
847 ;; Monday 8:30 Team meeting
851 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)"
854 "\\([1-9][0-9]?:[0-9][0-9]\\)"
859 (icalendar--get-weekday-abbrev
860 (substring entry-main
(match-beginning 1)
862 (icalendar--dmsg "weekly %s" entry-main
)
863 (let* ((day (icalendar--get-weekday-abbrev
864 (substring entry-main
(match-beginning 1)
866 (starttimestring (icalendar--diarytime-to-isotime
867 (if (match-beginning 3)
868 (substring entry-main
872 (if (match-beginning 4)
873 (substring entry-main
877 (endtimestring (icalendar--diarytime-to-isotime
878 (if (match-beginning 6)
879 (substring entry-main
883 (if (match-beginning 7)
884 (substring entry-main
888 (summary (icalendar--convert-string-for-export
889 (substring entry-main
(match-beginning 8)
891 (when starttimestring
892 (unless endtimestring
894 (icalendar--rris "^T0?" ""
896 (setq endtimestring
(format "T%06d"
903 ;; find the correct week day,
904 ;; 1st january 2000 was a saturday
907 (+ (icalendar--get-weekday-number day
) 2))
908 (or starttimestring
"")
915 ;; end is non-inclusive!
916 (+ (icalendar--get-weekday-number day
)
917 (if endtimestring
2 3)))
918 (or endtimestring
"")
920 "\nRRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY="
922 (unless (string= entry-rest
"")
924 (concat contents
"\nDESCRIPTION:"
925 (icalendar--convert-string-for-export
928 ;; 1 May Tag der Arbeit
931 (if european-calendar-style
932 "0?\\([1-9]+[0-9]?\\)\\s-+\\([a-z]+\\)\\s-+"
933 "\\([a-z]+\\)\\s-+0?\\([1-9]+[0-9]?\\)\\s-+")
935 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
937 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
939 "\\s-*\\([^0-9]+.*\\)$" ; must not match years
942 (icalendar--dmsg "yearly %s" entry-main
)
943 (let* ((daypos (if european-calendar-style
1 2))
944 (monpos (if european-calendar-style
2 1))
945 (day (read (substring entry-main
946 (match-beginning daypos
)
947 (match-end daypos
))))
948 (month (icalendar--get-month-number
949 (substring entry-main
950 (match-beginning monpos
)
951 (match-end monpos
))))
952 (starttimestring (icalendar--diarytime-to-isotime
953 (if (match-beginning 4)
954 (substring entry-main
958 (if (match-beginning 5)
959 (substring entry-main
963 (endtimestring (icalendar--diarytime-to-isotime
964 (if (match-beginning 7)
965 (substring entry-main
969 (if (match-beginning 8)
970 (substring entry-main
974 (summary (icalendar--convert-string-for-export
975 (substring entry-main
(match-beginning 9)
977 (when starttimestring
978 (unless endtimestring
980 (icalendar--rris "^T0?" ""
982 (setq endtimestring
(format "T%06d"
986 (if starttimestring
"VALUE=DATE-TIME:"
988 (format "1900%02d%02d" month day
)
989 (or starttimestring
"")
991 (if endtimestring
"VALUE=DATE-TIME:"
993 ;; end is not included! shift by one day
994 (icalendar--date-to-isodate
995 (list month day
1900)
996 (if endtimestring
0 1))
997 (or endtimestring
"")
1000 "\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH="
1001 (format "%2d" month
)
1003 (format "%2d" day
))))
1004 (unless (string= entry-rest
"")
1006 (concat contents
"\nDESCRIPTION:"
1007 (icalendar--convert-string-for-export
1009 ;; "ordinary" events, start and end time given
1010 ;; 1 Feb 2003 Hs Hochzeitsfeier, Dreieich
1013 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)\\s-+"
1014 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1016 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1020 (icalendar--dmsg "ordinary %s" entry-main
)
1021 (let* ((startdatestring (icalendar--datestring-to-isodate
1022 (substring entry-main
1025 (starttimestring (icalendar--diarytime-to-isotime
1026 (if (match-beginning 3)
1027 (substring entry-main
1031 (if (match-beginning 4)
1032 (substring entry-main
1036 (endtimestring (icalendar--diarytime-to-isotime
1037 (if (match-beginning 6)
1038 (substring entry-main
1042 (if (match-beginning 7)
1043 (substring entry-main
1047 (summary (icalendar--convert-string-for-export
1048 (substring entry-main
(match-beginning 8)
1050 (unless startdatestring
1051 (error "Could not parse date"))
1052 (when starttimestring
1053 (unless endtimestring
1055 (read (icalendar--rris "^T0?" ""
1057 (setq endtimestring
(format "T%06d"
1059 (setq contents
(concat
1061 (if starttimestring
"VALUE=DATE-TIME:"
1064 (or starttimestring
"")
1066 (if endtimestring
"VALUE=DATE-TIME:"
1068 (icalendar--datestring-to-isodate
1069 (substring entry-main
1072 (if endtimestring
0 1))
1073 (or endtimestring
"")
1076 ;; could not parse the date
1077 (unless (string= entry-rest
"")
1079 (concat contents
"\nDESCRIPTION:"
1080 (icalendar--convert-string-for-export
1084 ;; Oops! what's that?
1085 (error "Could not parse entry")))
1086 (setq result
(concat result header contents
"\nEND:VEVENT")))
1089 (setq found-error t
)
1090 (save-current-buffer
1091 (set-buffer (get-buffer-create " *icalendar-errors*"))
1092 (insert (format "Error in line %d -- %s: `%s'\n"
1093 (count-lines (point-min) (point))
1097 ;; we're done, insert everything into the file
1098 (save-current-buffer
1099 (let ((coding-system-for-write 'utf-8
))
1100 (set-buffer (find-file ical-filename
))
1101 (goto-char (point-max))
1102 (insert "BEGIN:VCALENDAR")
1103 (insert "\nPRODID:-//Emacs//NONSGML icalendar.el//EN")
1104 (insert "\nVERSION:2.0")
1106 (insert "\nEND:VCALENDAR\n")
1107 ;; save the diary file
1111 ;; ======================================================================
1112 ;; Import -- convert icalendar to emacs-diary
1113 ;; ======================================================================
1116 (defun icalendar-import-file (ical-filename diary-filename
1117 &optional non-marking
)
1118 "Import a iCalendar file and append to a diary file.
1119 Argument ICAL-FILENAME output iCalendar file.
1120 Argument DIARY-FILENAME input `diary-file'.
1121 Optional argument NON-MARKING determines whether events are created as
1122 non-marking or not."
1123 (interactive "fImport iCalendar data from file:
1126 ;; clean up the diary file
1127 (save-current-buffer
1128 ;; now load and convert from the ical file
1129 (set-buffer (find-file ical-filename
))
1130 (icalendar-import-buffer diary-filename t non-marking
)))
1133 (defun icalendar-import-buffer (&optional diary-file do-not-ask
1135 "Extract iCalendar events from current buffer.
1137 This function searches the current buffer for the first iCalendar
1138 object, reads it and adds all VEVENT elements to the diary
1141 It will ask for each appointment whether to add it to the diary
1142 when DO-NOT-ASK is non-nil. When called interactively,
1143 DO-NOT-ASK is set to t, so that you are asked fore each event.
1145 NON-MARKING determines whether diary events are created as
1148 Return code t means that importing worked well, return code nil
1149 means that an error has occured. Error messages will be in the
1150 buffer `*icalendar-errors*'."
1152 (save-current-buffer
1154 (message "Preparing icalendar...")
1155 (set-buffer (icalendar--get-unfolded-buffer (current-buffer)))
1156 (goto-char (point-min))
1157 (message "Preparing icalendar...done")
1158 (if (re-search-forward "^BEGIN:VCALENDAR\\s-*$" nil t
)
1159 (let (ical-contents ical-errors
)
1161 (message "Reading icalendar...")
1163 (setq ical-contents
(icalendar--read-element nil nil
))
1164 (message "Reading icalendar...done")
1166 (message "Converting icalendar...")
1167 (setq ical-errors
(icalendar--convert-ical-to-diary
1169 diary-file do-not-ask non-marking
))
1171 ;; save the diary file
1172 (save-current-buffer
1173 (set-buffer (find-buffer-visiting diary-file
))
1175 (message "Converting icalendar...done")
1176 ;; return t if no error occured
1179 "Current buffer does not contain icalendar contents!")
1180 ;; return nil, i.e. import did not work
1183 (defalias 'icalendar-extract-ical-from-buffer
'icalendar-import-buffer
)
1184 (make-obsolete 'icalendar-extract-ical-from-buffer
'icalendar-import-buffer
)
1186 ;; ======================================================================
1188 ;; ======================================================================
1190 (defun icalendar--format-ical-event (event)
1191 "Create a string representation of an iCalendar EVENT."
1192 (let ((string icalendar-import-format
)
1194 '(("%d" DESCRIPTION icalendar-import-format-description
)
1195 ("%s" SUMMARY icalendar-import-format-subject
)
1196 ("%l" LOCATION icalendar-import-format-location
)
1197 ("%o" ORGANIZER icalendar-import-format-organizer
))))
1198 ;; convert the specifiers in the format string
1200 (let* ((spec (car i
))
1202 (format (car (cddr i
)))
1203 (contents (icalendar--get-event-property event prop
))
1204 (formatted-contents ""))
1205 (when (and contents
(> (length contents
) 0))
1206 (setq formatted-contents
1207 (icalendar--rris "%s"
1208 (icalendar--convert-string-for-import
1210 (symbol-value format
))))
1211 (setq string
(icalendar--rris spec
1217 (defun icalendar--convert-ical-to-diary (ical-list diary-file
1218 &optional do-not-ask
1220 "Convert an iCalendar file to an Emacs diary file.
1221 Import VEVENTS from the iCalendar object ICAL-LIST and saves them to a
1222 DIARY-FILE. If DO-NOT-ASK is nil the user is asked for each event
1223 whether to actually import it. NON-MARKING determines whether diary
1224 events are created as non-marking.
1225 This function attempts to return t if something goes wrong. In this
1226 case an error string which describes all the errors and problems is
1227 written into the buffer ` *icalendar-errors*'."
1228 (let* ((ev (icalendar--all-events ical-list
))
1233 ;; step through all events/appointments
1238 (condition-case error-val
1239 (let* ((dtstart (icalendar--decode-isodatetime
1240 (icalendar--get-event-property e
'DTSTART
)))
1241 (start-d (icalendar--datetime-to-diary-date
1243 (start-t (icalendar--datetime-to-colontime dtstart
))
1244 (dtend (icalendar--decode-isodatetime
1245 (icalendar--get-event-property e
'DTEND
)))
1248 (subject (icalendar--convert-string-for-import
1249 (or (icalendar--get-event-property e
'SUMMARY
)
1251 (rrule (icalendar--get-event-property e
'RRULE
))
1252 (rdate (icalendar--get-event-property e
'RDATE
))
1253 (duration (icalendar--get-event-property e
'DURATION
)))
1254 (icalendar--dmsg "%s: %s" start-d subject
)
1255 ;; check whether start-time is missing
1256 (if (and (icalendar--get-event-property-attributes
1258 (string= (cadr (icalendar--get-event-property-attributes
1263 (let ((dtend2 (icalendar--add-decoded-times
1265 (icalendar--decode-isoduration duration
))))
1266 (if (and dtend
(not (eq dtend dtend2
)))
1267 (message "Inconsistent endtime and duration for %s"
1269 (setq dtend dtend2
)))
1270 (setq end-d
(if dtend
1271 (icalendar--datetime-to-diary-date dtend
)
1273 (setq end-t
(if dtend
1274 (icalendar--datetime-to-colontime dtend
)
1276 (icalendar--dmsg "start-d: %s, end-d: %s" start-d end-d
)
1280 (icalendar--dmsg "recurring event")
1281 (let* ((rrule-props (icalendar--split-value rrule
))
1282 (frequency (cadr (assoc 'FREQ rrule-props
)))
1283 (until (cadr (assoc 'UNTIL rrule-props
)))
1284 (interval (read (cadr (assoc 'INTERVAL rrule-props
)))))
1285 (cond ((string-equal frequency
"WEEKLY")
1288 ;; weekly and all-day
1289 (icalendar--dmsg "weekly all-day")
1292 (icalendar--datetime-to-diary-date
1293 (icalendar--decode-isodatetime
1294 (icalendar--get-event-property
1298 (icalendar--datetime-to-diary-date
1299 (icalendar--decode-isodatetime
1304 "(diary-cyclic %d %s) "
1305 "(diary-block %s %s))")
1307 (icalendar--datetime-to-diary-date
1309 (icalendar--datetime-to-diary-date
1311 (icalendar--datetime-to-diary-date
1312 (icalendar--decode-isodatetime
1315 (format "%%%%(and (diary-cyclic %d %s))"
1317 (icalendar--datetime-to-diary-date
1320 ;; weekly and not all-day
1321 (let* ((byday (cadr (assoc 'BYDAY rrule-props
)))
1323 (icalendar--get-weekday-number byday
)))
1324 (icalendar--dmsg "weekly not-all-day")
1327 (icalendar--datetime-to-diary-date
1328 (icalendar--decode-isodatetime
1329 (icalendar--get-event-property
1333 (icalendar--datetime-to-diary-date
1334 (icalendar--decode-isodatetime
1339 "(diary-cyclic %d %s) "
1340 "(diary-block %s %s)) "
1343 (icalendar--datetime-to-diary-date
1345 (icalendar--datetime-to-diary-date
1347 (icalendar--datetime-to-diary-date
1348 (icalendar--decode-isodatetime
1351 (if end-t
"-" "") (or end-t
""))))
1354 ;; DTSTART;VALUE=DATE-TIME:20030919T090000
1355 ;; DTEND;VALUE=DATE-TIME:20030919T113000
1358 "%%%%(and (diary-cyclic %s %s)) %s%s%s"
1360 (icalendar--datetime-to-diary-date
1363 (if end-t
"-" "") (or end-t
""))))
1364 (setq event-ok t
))))
1366 ((string-equal frequency
"YEARLY")
1367 (icalendar--dmsg "yearly")
1370 "%%%%(and (diary-anniversary %s))"
1371 (icalendar--datetime-to-diary-date dtstart
)))
1373 ;; FIXME: war auskommentiert:
1374 ((and (string-equal frequency
"DAILY")
1375 ;;(not (string= start-d end-d))
1379 (let ((ds (icalendar--datetime-to-diary-date
1380 (icalendar--decode-isodatetime
1381 (icalendar--get-event-property
1383 (de (icalendar--datetime-to-diary-date
1384 (icalendar--decode-isodatetime
1388 "%%%%(and (diary-block %s %s))"
1390 (setq event-ok t
))))
1391 ;; Handle exceptions from recurrence rules
1392 (let ((ex-dates (icalendar--get-event-properties e
1395 (let* ((ex-start (icalendar--decode-isodatetime
1397 (ex-d (icalendar--datetime-to-diary-date
1400 (icalendar--rris "^%%(\\(and \\)?"
1402 "%%%%(and (not (diary-date %s)) "
1405 (setq ex-dates
(cdr ex-dates
))))
1406 ;; FIXME: exception rules are not recognized
1407 (if (icalendar--get-event-property e
'EXRULE
)
1409 (concat diary-string
1410 "\n Exception rules: "
1411 (icalendar--get-event-properties
1414 (icalendar--dmsg "rdate event")
1415 (setq diary-string
"")
1416 (mapcar (lambda (datestring)
1418 (concat diary-string
1419 (format "......"))))
1420 (icalendar--split-value rdate
)))
1421 ;; non-recurring event
1423 ((not (string= start-d end-d
))
1424 (icalendar--dmsg "non-recurring event")
1425 (let ((ds (icalendar--datetime-to-diary-date dtstart
))
1426 (de (icalendar--datetime-to-diary-date
1427 (icalendar--decode-isodatetime
1428 (icalendar--get-event-property e
'DTEND
)
1431 (format "%%%%(and (diary-block %s %s))"
1435 ((and start-t
(or (not end-t
)
1436 (not (string= start-t end-t
))))
1437 (icalendar--dmsg "not all day event")
1441 (icalendar--datetime-to-diary-date
1447 (icalendar--datetime-to-diary-date
1453 (icalendar--dmsg "all day event")
1454 (setq diary-string
(icalendar--datetime-to-diary-date
1457 ;; add all other elements unless the user doesn't want to have
1462 (concat diary-string
" "
1463 (icalendar--format-ical-event e
)))
1464 (if do-not-ask
(setq subject nil
))
1465 (icalendar--add-diary-entry diary-string diary-file
1466 non-marking subject
))
1468 (setq found-error t
)
1470 (format "%s\nCannot handle this event:%s"
1472 ;; FIXME: inform user about ignored event properties
1475 (message "Ignoring event \"%s\"" e
)
1476 (setq found-error t
)
1477 (setq error-string
(format "%s\n%s\nCannot handle this event: %s"
1478 error-val error-string e
))
1479 (message error-string
))))
1481 (save-current-buffer
1482 (set-buffer (get-buffer-create " *icalendar-errors*"))
1484 (insert error-string
)))
1485 (message "Converting icalendar...done")
1488 (defun icalendar--add-diary-entry (string diary-file non-marking
1490 "Add STRING to the diary file DIARY-FILE.
1491 STRING must be a properly formatted valid diary entry. NON-MARKING
1492 determines whether diary events are created as non-marking. If
1493 SUBJECT is not nil it must be a string that gives the subject of the
1494 entry. In this case the user will be asked whether he wants to insert
1496 (when (or (not subject
)
1497 (y-or-n-p (format "Add appointment for `%s' to diary? "
1501 (y-or-n-p (format "Make appointment non-marking? "))))
1502 (save-window-excursion
1505 (read-file-name "Add appointment to this diary file: ")))
1506 (make-diary-entry string non-marking diary-file
))))
1508 (provide 'icalendar
)
1510 ;; arch-tag: 74fdbe8e-0451-4e38-bb61-4416e822f4fc
1511 ;;; icalendar.el ends here