1 ;;; icalendar.el --- iCalendar implementation
3 ;; Copyright (C) 2002, 2003, 2004 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.06 Bugfixes regarding icalendar-import-format-*.
35 ;; Fix in icalendar-convert-diary-to-ical -- thanks to Philipp Grau.
37 ;; 0.05: New import format scheme: Replaced icalendar-import-prefix-*,
38 ;; icalendar-import-ignored-properties, and
39 ;; icalendar-import-separator with icalendar-import-format(-*).
40 ;; icalendar-import-file and icalendar-convert-diary-to-ical
41 ;; have an extra parameter which should prevent them from
42 ;; erasing their target files (untested!).
43 ;; Tested with Emacs 21.3.2
45 ;; 0.04: Bugfix: import: double quoted param values did not work
46 ;; Read DURATION property when importing.
47 ;; Added parameter icalendar-duration-correction.
49 ;; 0.03: Export takes care of european-calendar-style.
50 ;; Tested with Emacs 21.3.2 and XEmacs 21.4.12
52 ;; 0.02: Should work in XEmacs now. Thanks to Len Trigg for the
54 ;; Added exporting from Emacs diary to ical.
55 ;; Some bugfixes, after testing with calendars from
56 ;; http://icalshare.com.
57 ;; Tested with Emacs 21.3.2 and XEmacs 21.4.12
59 ;; 0.01: First published version. Trial version. Alpha version.
61 ;; ======================================================================
64 ;; * Import from ical:
65 ;; + Need more properties for icalendar-import-format
66 ;; + check vcalendar version
67 ;; + check (unknown) elements
68 ;; + recurring events!
69 ;; + works for european style calendars only! Does it?
71 ;; + exceptions in recurring events
72 ;; + the parser is too soft
73 ;; + error log is incomplete
74 ;; + nice to have: #include "webcal://foo.com/some-calendar.ics"
77 ;; + diary-date, diary-float, and self-made sexp entries are not
79 ;; + timezones, currently all times are local!
82 ;; + defcustom icalendar-import-ignored-properties does not work with
84 ;; + clean up all those date/time parsing functions
85 ;; + Handle todo items?
86 ;; + Check iso 8601 for datetime and period
87 ;; + Which chars to (un)escape?
88 ;; + Time to find out how the profiler works?
93 (defconst icalendar-version
0.06
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-weekdayabbrev-table
164 '(("mon\\(day\\)?" .
"MO")
165 ("tue\\(sday\\)?" .
"TU")
166 ("wed\\(nesday\\)?" .
"WE")
167 ("thu\\(rsday\\)?" .
"TH")
168 ("fri\\(day\\)?" .
"FR")
169 ("sat\\(urday\\)?" .
"SA")
170 ("sun\\(day\\)?" .
"SU"))
171 "Translation table for weekdays.")
173 (defconst icalendar-monthnumber-table
174 '(("^jan\\(uar\\)?y?$" .
1)
175 ("^feb\\(ruar\\)?y?$" .
2)
176 ("^mar\\(ch\\)?\\|märz?$" .
3)
177 ("^apr\\(il\\)?$" .
4)
181 ("^aug\\(ust\\)?$" .
8)
182 ("^sep\\(tember\\)?$" .
9)
183 ("^o[ck]t\\(ober\\)?$" .
10)
184 ("^nov\\(ember\\)?$" .
11)
185 ("^de[cz]\\(ember\\)?$" .
12))
186 "Regular expressions for month names.
187 Currently this matches only German and English.")
189 (defvar icalendar-debug nil
".")
191 ;; ======================================================================
192 ;; all the other libs we need
193 ;; ======================================================================
197 ;; ======================================================================
198 ;; Core functionality
199 ;; Functions for parsing icalendars, importing and so on
200 ;; ======================================================================
202 (defun icalendar-get-unfolded-buffer (folded-ical-buffer)
203 "Return a new buffer containing the unfolded contents of a buffer.
204 Folding is the iCalendar way of wrapping long lines. In the
205 created buffer all occurrences of CR LF BLANK are replaced by the
206 empty string. Argument FOLDED-ICAL-BUFFER is the unfolded input
208 (let ((unfolded-buffer (get-buffer-create " *icalendar-work*")))
210 (set-buffer unfolded-buffer
)
212 (insert-buffer folded-ical-buffer
)
213 (while (re-search-forward "\r?\n[ \t]" nil t
)
214 (replace-match "" nil nil
))
218 ;; Replace regexp RE with RP in string ST and return the new string.
219 ;; This is here for compatibility with XEmacs.
220 (defsubst icalendar-rris
(re rp st
)
222 (if (fboundp 'replace-in-string
)
223 (save-match-data ;; apparently XEmacs needs save-match-data
224 (replace-in-string st re rp
))
226 (replace-regexp-in-string re rp st
)))
228 (defun icalendar-read-element (invalue inparams
)
229 "Recursively read the next iCalendar element in the current buffer.
230 INVALUE gives the current iCalendar element we are reading.
231 INPARAMS gives the current parameters.....
232 This function calls itself recursively for each nested calendar element
234 (let (element children line name params param param-name param-value
239 (re-search-forward "^\\([A-Za-z0-9-]+\\)[;:]" nil t
))
240 (setq name
(intern (match-string 1)))
244 (while (looking-at ";")
245 (re-search-forward ";\\([A-Za-z0-9-]+\\)=" nil nil
)
246 (setq param-name
(intern (match-string 1)))
247 (re-search-forward "\\(\\([^;,:\"]+\\)\\|\"\\([^\"]+\\)\"\\)[;:]"
250 (setq param-value
(or (match-string 2) (match-string 3)))
251 (setq param
(list param-name param-value
))
252 (while (looking-at ",")
253 (re-search-forward "\\(\\([^;,:]+\\)\\|\"\\([^\"]+\\)\"\\)"
256 (setq param-value
(match-string 2))
257 (setq param-value
(match-string 3)))
258 (setq param
(append param param-value
)))
259 (setq params
(append params param
)))
260 (unless (looking-at ":")
263 (re-search-forward "\\(.*\\)\\(\r?\n[ \t].*\\)*" nil t
)
264 (setq value
(icalendar-rris "\r?\n[ \t]" "" (match-string 0)))
265 (setq line
(list name params value
))
266 (cond ((eq name
'BEGIN
)
269 (list (icalendar-read-element (intern value
)
274 (setq element
(append element
(list line
))))))
276 (list invalue inparams element children
)
279 ;; ======================================================================
280 ;; helper functions for examining events
281 ;; ======================================================================
283 (defsubst icalendar-get-all-event-properties
(event)
284 "Return the list of properties in this EVENT."
287 (defun icalendar-get-event-property (event prop
)
288 "For the given EVENT return the value of the property PROP."
290 (let ((props (car (cddr event
))) pp
)
292 (setq pp
(car props
))
293 (if (eq (car pp
) prop
)
294 (throw 'found
(car (cddr pp
))))
295 (setq props
(cdr props
))))
298 (defun icalendar-set-event-property (event prop new-value
)
299 "For the given EVENT set the property PROP to the value NEW-VALUE."
301 (let ((props (car (cddr event
))) pp
)
303 (setq pp
(car props
))
304 (when (eq (car pp
) prop
)
305 (setcdr (cdr pp
) new-value
)
306 (throw 'found
(car (cddr pp
))))
307 (setq props
(cdr props
)))
308 (setq props
(car (cddr event
)))
310 (append props
(list (list prop nil new-value
)))))))
312 (defun icalendar-get-children (node name
)
313 "Return all children of the given NODE which have a name NAME.
314 For instance the VCALENDAR node can have VEVENT children as well as VTODO
317 (children (cadr (cddr node
))))
318 (when (eq (car node
) name
)
320 ;;(message "%s" node)
325 (icalendar-get-children n name
))
329 (setq result
(append result subresult
))
330 (setq result subresult
)))))
334 (defun icalendar-all-events (icalendar)
335 "Return the list of all existing events in the given ICALENDAR."
337 (icalendar-get-children (car icalendar
) 'VEVENT
))
339 (defun icalendar-split-value (value-string)
340 "Splits VALUE-STRING at ';='."
342 param-name param-value
)
345 (set-buffer (get-buffer-create " *ical-temp*"))
346 (set-buffer-modified-p nil
)
348 (insert value-string
)
349 (goto-char (point-min))
352 "\\([A-Za-z0-9-]+\\)=\\(\\([^;,:]+\\)\\|\"\\([^\"]+\\)\"\\);?"
354 (setq param-name
(intern (match-string 1)))
355 (setq param-value
(match-string 2))
357 (append result
(list (list param-name param-value
)))))))
360 (defun icalendar-decode-isodatetime (isodatetimestring)
361 "Return ISODATETIMESTRING in format like `decode-time'.
362 Converts from ISO-8601 to Emacs representation. If ISODATETIMESTRING
363 specifies UTC time (trailing letter Z) the decoded time is given in
364 the local time zone! FIXME: TZID-attributes are ignored....! FIXME:
365 multiple comma-separated values should be allowed!"
366 (icalendar-dmsg isodatetimestring
)
367 (if isodatetimestring
368 ;; day/month/year must be present
369 (let ((year (read (substring isodatetimestring
0 4)))
370 (month (read (substring isodatetimestring
4 6)))
371 (day (read (substring isodatetimestring
6 8)))
375 (when (> (length isodatetimestring
) 12)
376 ;; hour/minute present
377 (setq hour
(read (substring isodatetimestring
9 11)))
378 (setq minute
(read (substring isodatetimestring
11 13))))
379 (when (> (length isodatetimestring
) 14)
381 (setq second
(read (substring isodatetimestring
13 15))))
382 (when (and (> (length isodatetimestring
) 15)
383 ;; UTC specifier present
384 (char-equal ?Z
(aref isodatetimestring
15)))
385 ;; if not UTC add current-time-zone offset
386 (setq second
(+ (car (current-time-zone)) second
)))
387 ;; create the decoded date-time
390 (decode-time (encode-time second minute hour day month year
))
392 (message "Cannot decode \"%s\"" isodatetimestring
)
393 ;; hope for the best...
394 (list second minute hour day month year
0 nil
0))))
395 ;; isodatetimestring == nil
398 (defun icalendar-decode-isoduration (isodurationstring)
399 "Return ISODURATIONSTRING in format like `decode-time'.
400 Converts from ISO-8601 to Emacs representation. If ISODURATIONSTRING
401 specifies UTC time (trailing letter Z) the decoded time is given in
402 the local time zone! FIXME: TZID-attributes are ignored....! FIXME:
403 multiple comma-separated values should be allowed!"
404 (if isodurationstring
409 "\\(\\([0-9]+\\)D\\)" ; days only
411 "\\(\\(\\([0-9]+\\)D\\)?T\\(\\([0-9]+\\)H\\)?" ; opt days
412 "\\(\\([0-9]+\\)M\\)?\\(\\([0-9]+\\)S\\)?\\)" ; mand. time
414 "\\(\\([0-9]+\\)W\\)" ; weeks only
415 "\\)$") isodurationstring
)
423 ((match-beginning 2) ;days only
424 (setq days
(read (substring isodurationstring
427 (when icalendar-duration-correction
428 (setq days
(1- days
))))
429 ((match-beginning 4) ;days and time
430 (if (match-beginning 5)
431 (setq days
(* 7 (read (substring isodurationstring
434 (if (match-beginning 7)
435 (setq hours
(read (substring isodurationstring
438 (if (match-beginning 9)
439 (setq minutes
(read (substring isodurationstring
442 (if (match-beginning 11)
443 (setq seconds
(read (substring isodurationstring
447 ((match-beginning 13) ;weeks only
448 (setq days
(* 7 (read (substring isodurationstring
452 (list seconds minutes hours days months years
)))
453 ;; isodatetimestring == nil
456 (defun icalendar-add-decoded-times (time1 time2
)
458 Both times must be given in decoded form. One of these times must be
459 valid (year > 1900 or something)."
460 ;; FIXME: does this function exist already?
461 (decode-time (encode-time
462 (+ (nth 0 time1
) (nth 0 time2
))
463 (+ (nth 1 time1
) (nth 1 time2
))
464 (+ (nth 2 time1
) (nth 2 time2
))
465 (+ (nth 3 time1
) (nth 3 time2
))
466 (+ (nth 4 time1
) (nth 4 time2
))
467 (+ (nth 5 time1
) (nth 5 time2
))
470 ;;(or (nth 6 time1) (nth 6 time2)) ;; FIXME?
473 (defun icalendar-datetime-to-noneuropean-date (datetime)
474 "Convert the decoded DATETIME to non-european-style format.
475 Non-European format: (month day year)."
477 (list (nth 4 datetime
) ;month
478 (nth 3 datetime
) ;day
479 (nth 5 datetime
));year
483 (defun icalendar-datetime-to-european-date (datetime)
484 "Convert the decoded DATETIME to European format.
485 European format: (day month year).
488 (format "%d %d %d" (nth 3 datetime
); day
489 (nth 4 datetime
) ;month
490 (nth 5 datetime
));year
494 (defun icalendar-datetime-to-colontime (datetime)
495 "Extract the time part of a decoded DATETIME into 24-hour format.
496 Note that this silently ignores seconds."
497 (format "%02d:%02d" (nth 2 datetime
) (nth 1 datetime
)))
499 (defun icalendar-get-month-number (monthname)
500 "Return the month number for the given MONTHNAME."
502 (let ((case-fold-search t
))
503 (assoc-default monthname icalendar-monthnumber-table
506 (defun icalendar-get-weekday-abbrev (weekday)
507 "Return the abbreviated WEEKDAY."
508 ;;FIXME: ISO-like(?).
510 (let ((case-fold-search t
))
511 (assoc-default weekday icalendar-weekdayabbrev-table
514 (defun icalendar-datestring-to-isodate (datestring &optional day-shift
)
515 "Convert diary-style DATESTRING to iso-style date.
516 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days
517 -- DAY-SHIFT must be either nil or an integer. This function
518 takes care of european-style."
519 (let ((day -
1) month year
)
521 (cond (;; numeric date
522 (string-match (concat "\\s-*"
523 "0?\\([1-9][0-9]?\\)[ \t/]\\s-*"
524 "0?\\([1-9][0-9]?\\),?[ \t/]\\s-*"
525 "\\([0-9]\\{4\\}\\)")
527 (setq day
(read (substring datestring
(match-beginning 1)
529 (setq month
(read (substring datestring
(match-beginning 2)
531 (setq year
(read (substring datestring
(match-beginning 3)
533 (unless european-calendar-style
537 (;; date contains month names -- european-style
538 (and european-calendar-style
539 (string-match (concat "\\s-*"
540 "0?\\([123]?[0-9]\\)[ \t/]\\s-*"
541 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
542 "\\([0-9]\\{4\\}\\)")
544 (setq day
(read (substring datestring
(match-beginning 1)
546 (setq month
(icalendar-get-month-number
547 (substring datestring
(match-beginning 2)
549 (setq year
(read (substring datestring
(match-beginning 3)
551 (;; date contains month names -- non-european-style
552 (and (not european-calendar-style
)
553 (string-match (concat "\\s-*"
554 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
555 "0?\\([123]?[0-9]\\),?[ \t/]\\s-*"
556 "\\([0-9]\\{4\\}\\)")
558 (setq day
(read (substring datestring
(match-beginning 2)
560 (setq month
(icalendar-get-month-number
561 (substring datestring
(match-beginning 1)
563 (setq year
(read (substring datestring
(match-beginning 3)
568 (let ((mdy (calendar-gregorian-from-absolute
569 (+ (calendar-absolute-from-gregorian (list month day year
))
571 (format "%04d%02d%02d" (nth 2 mdy
) (nth 0 mdy
) (nth 1 mdy
)))
574 (defun icalendar-dmsg (&rest args
)
575 "Print message ARGS if `icalendar-debug' is non-nil."
577 (apply 'message args
)))
579 (defun icalendar-diarytime-to-isotime (timestring ampmstring
)
580 "Convert a a time like 9:30pm to an iso-conform string like T213000.
581 In this example the TIMESTRING would be \"9:30\" and the AMPMSTRING
584 (let ((starttimenum (read (icalendar-rris ":" "" timestring
))))
585 ;; take care of am/pm style
586 (if (and ampmstring
(string= "pm" ampmstring
))
587 (setq starttimenum
(+ starttimenum
1200)))
588 (format "T%04d00" starttimenum
))
591 (defun icalendar-convert-string-for-export (s)
592 "Escape comma and other critical characters in string S."
593 (icalendar-rris "," "\\\\," s
))
595 (defun icalendar-convert-for-import (string)
596 "Remove escape chars for comma, semicolon etc. from STRING."
598 "\\\\n" "\n " (icalendar-rris
599 "\\\\\"" "\"" (icalendar-rris
600 "\\\\;" ";" (icalendar-rris
601 "\\\\," "," string
)))))
603 ;; ======================================================================
604 ;; export -- convert emacs-diary to icalendar
605 ;; ======================================================================
607 (defun icalendar-convert-diary-to-ical (diary-filename ical-filename
608 &optional do-not-clear-diary-file
)
609 "Export diary file to iCalendar format -- erases ical-filename!!!.
610 Argument DIARY-FILENAME is the input `diary-file'.
611 Argument ICAL-FILENAME is the output iCalendar file.
612 If DO-NOT-CLEAR-DIARY-FILE is not nil the target iCalendar file
614 (interactive "FExport diary data from file:
615 Finto iCalendar file: ")
623 (nonmarker (concat "^" (regexp-quote diary-nonmarking-symbol
)
626 (set-buffer (find-file diary-filename
))
627 (goto-char (point-min))
628 (while (re-search-forward
629 "^\\([^ \t\n].*\\)\\(\n[ \t].*\\)*" nil t
)
630 (setq entry-main
(match-string 1))
631 (if (match-beginning 2)
632 (setq entry-rest
(match-string 2))
633 (setq entry-rest
""))
634 (setq header
(format "\nBEGIN:VEVENT\nUID:emacs%d%d%d"
636 (cadr (current-time))
637 (car (cddr (current-time)))))
643 "%%(diary-anniversary \\([^)]+\\))\\s-*\\(.*\\)")
645 (icalendar-dmsg "diary-anniversary %s" entry-main
)
646 (let* ((datetime (substring entry-main
(match-beginning 1)
648 (summary (icalendar-convert-string-for-export
649 (substring entry-main
(match-beginning 2)
651 (startisostring (icalendar-datestring-to-isodate
653 (endisostring (icalendar-datestring-to-isodate
656 (concat "\nDTSTART;VALUE=DATE:" startisostring
657 "\nDTEND;VALUE=DATE:" endisostring
659 "\nRRULE:FREQ=YEARLY;INTERVAL=1"
660 ;; the following is redundant,
661 ;; but korganizer seems to expect this... ;(
662 ;; and evolution doesn't understand it... :(
663 ;; so... who is wrong?!
664 ";BYMONTH=" (substring startisostring
4 6)
665 ";BYMONTHDAY=" (substring startisostring
6 8)
667 (unless (string= entry-rest
"")
668 (setq contents
(concat contents
"\nDESCRIPTION:"
669 (icalendar-convert-string-for-export
675 "%%(diary-cyclic \\([^ ]+\\) +"
676 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*\\(.*\\)")
678 (icalendar-dmsg "diary-cyclic %s" entry-main
)
679 (let* ((frequency (substring entry-main
(match-beginning 1)
681 (datetime (substring entry-main
(match-beginning 2)
683 (summary (icalendar-convert-string-for-export
684 (substring entry-main
(match-beginning 3)
686 (startisostring (icalendar-datestring-to-isodate
688 (endisostring (icalendar-datestring-to-isodate
691 (concat "\nDTSTART;VALUE=DATE:" startisostring
692 "\nDTEND;VALUE=DATE:" endisostring
694 "\nRRULE:FREQ=DAILY;INTERVAL=" frequency
695 ;; strange: korganizer does not expect
696 ;; BYSOMETHING here...
698 (unless (string= entry-rest
"")
699 (setq contents
(concat contents
"\nDESCRIPTION:"
700 (icalendar-convert-string-for-export
702 ;; diary-date -- FIXME
705 "%%(diary-date \\([^)]+\\))\\s-*\\(.*\\)")
707 (icalendar-dmsg "diary-date %s" entry-main
)
709 ;; float events -- FIXME
712 "%%(diary-float \\([^)]+\\))\\s-*\\(.*\\)")
714 (icalendar-dmsg "diary-float %s" entry-main
)
719 "%%(diary-block \\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\) +"
720 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*\\(.*\\)")
722 (icalendar-dmsg "diary-block %s" entry-main
)
723 (let* ((startstring (substring entry-main
(match-beginning 1)
725 (endstring (substring entry-main
(match-beginning 2)
727 (summary (icalendar-convert-string-for-export
728 (substring entry-main
(match-beginning 3)
730 (startisostring (icalendar-datestring-to-isodate
732 (endisostring (icalendar-datestring-to-isodate
735 (concat "\nDTSTART;VALUE=DATE:" startisostring
736 "\nDTEND;VALUE=DATE:" endisostring
739 (unless (string= entry-rest
"")
740 (setq contents
(concat contents
"\nDESCRIPTION:"
741 (icalendar-convert-string-for-export
743 ;; other sexp diary entries -- FIXME
746 "%%(\\([^)]+\\))\\s-*\\(.*\\)")
748 (icalendar-dmsg "diary-sexp %s" entry-main
)
751 ;; Monday 8:30 Team meeting
755 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
757 "\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
761 (icalendar-get-weekday-abbrev
762 (substring entry-main
(match-beginning 1) (match-end 1))))
763 (icalendar-dmsg "weekly %s" entry-main
)
764 (let* ((day (icalendar-get-weekday-abbrev
765 (substring entry-main
(match-beginning 1)
767 (starttimestring (icalendar-diarytime-to-isotime
768 (if (match-beginning 3)
769 (substring entry-main
773 (if (match-beginning 4)
774 (substring entry-main
778 (endtimestring (icalendar-diarytime-to-isotime
779 (if (match-beginning 6)
780 (substring entry-main
784 (if (match-beginning 7)
785 (substring entry-main
789 (summary (icalendar-convert-string-for-export
790 (substring entry-main
(match-beginning 8)
792 (when starttimestring
793 (unless endtimestring
794 (let ((time (read (icalendar-rris "^T0?" ""
796 (setq endtimestring
(format "T%06d" (+ 10000 time
))))))
799 (if starttimestring
"" ";VALUE=DATE")
800 ":19000101" ;; FIXME? Probability that this
801 ;; is the right day is 1/7
802 (or starttimestring
"")
804 (if endtimestring
"" ";VALUE=DATE")
805 ":19000101" ;; FIXME?
806 (or endtimestring
"")
808 "\nRRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=" day
810 (unless (string= entry-rest
"")
811 (setq contents
(concat contents
"\nDESCRIPTION:"
812 (icalendar-convert-string-for-export
815 ;; 1 May Tag der Arbeit
818 (if european-calendar-style
819 "0?\\([1-9]+[0-9]?\\)\\s-+\\([a-z]+\\)\\s-+"
820 "\\([a-z]+\\)\\s-+0?\\([1-9]+[0-9]?\\)\\s-+")
822 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
824 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
826 "\\s-*\\([^0-9]+.*\\)$"; must not match years
829 (icalendar-dmsg "yearly %s" entry-main
)
830 (let* ((daypos (if european-calendar-style
1 2))
831 (monpos (if european-calendar-style
2 1))
832 (day (read (substring entry-main
(match-beginning daypos
)
833 (match-end daypos
))))
834 (month (icalendar-get-month-number
835 (substring entry-main
(match-beginning monpos
)
836 (match-end monpos
))))
837 (starttimestring (icalendar-diarytime-to-isotime
838 (if (match-beginning 4)
839 (substring entry-main
843 (if (match-beginning 5)
844 (substring entry-main
848 (endtimestring (icalendar-diarytime-to-isotime
849 (if (match-beginning 7)
850 (substring entry-main
854 (if (match-beginning 8)
855 (substring entry-main
859 (summary (icalendar-convert-string-for-export
860 (substring entry-main
(match-beginning 9)
862 (when starttimestring
863 (unless endtimestring
864 (let ((time (read (icalendar-rris "^T0?" ""
866 (setq endtimestring
(format "T%06d" (+ 10000 time
))))))
869 (if starttimestring
"" ";VALUE=DATE")
870 (format ":1900%02d%02d" month day
)
871 (or starttimestring
"")
873 (if endtimestring
"" ";VALUE=DATE")
874 (format ":1900%02d%02d" month day
)
875 (or endtimestring
"")
877 "\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH="
882 (unless (string= entry-rest
"")
883 (setq contents
(concat contents
"\nDESCRIPTION:"
884 (icalendar-convert-string-for-export
886 ;; "ordinary" events, start and end time given
887 ;; 1 Feb 2003 Hs Hochzeitsfeier, Dreieich
890 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)\\s-+"
891 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
893 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
897 (icalendar-dmsg "ordinary %s" entry-main
)
898 (let* ((datestring (icalendar-datestring-to-isodate
899 (substring entry-main
(match-beginning 1)
901 (starttimestring (icalendar-diarytime-to-isotime
902 (if (match-beginning 3)
903 (substring entry-main
907 (if (match-beginning 4)
908 (substring entry-main
912 (endtimestring (icalendar-diarytime-to-isotime
913 (if (match-beginning 6)
914 (substring entry-main
918 (if (match-beginning 7)
919 (substring entry-main
923 (summary (icalendar-convert-string-for-export
924 (substring entry-main
(match-beginning 8)
926 (when starttimestring
927 (unless endtimestring
928 (let ((time (read (icalendar-rris "^T0?" ""
930 (setq endtimestring
(format "T%06d" (+ 10000 time
))))))
931 (setq contents
(format
932 "\nDTSTART%s:%s%s\nDTEND%s:%s%s\nSUMMARY:%s"
933 (if starttimestring
"" ";VALUE=DATE")
935 (or starttimestring
"")
939 (or endtimestring
"")
941 (unless (string= entry-rest
"")
942 (setq contents
(concat contents
"\nDESCRIPTION:"
943 (icalendar-convert-string-for-export
947 ;; Oops! what's that?
950 (message "Cannot export entry on line %d"
951 (count-lines (point-min) (point)))
952 (setq result
(concat result header contents
"\nEND:VEVENT"))))
953 ;; we're done, insert everything into the file
954 (let ((coding-system-for-write 'utf8
))
955 (set-buffer (find-file ical-filename
))
956 (unless do-not-clear-diary-file
959 "BEGIN:VCALENDAR\nPRODID:-//Emacs//NONSGML icalendar.el//EN")
960 (insert "\nVERSION:2.0")
962 (insert "\nEND:VCALENDAR\n")))))
965 ;; ======================================================================
966 ;; import -- convert icalendar to emacs-diary
967 ;; ======================================================================
970 (defun icalendar-import-file (ical-filename diary-filename
971 &optional non-marking
972 do-not-clear-diary-file
)
973 "Import a iCalendar file and save to a diary file -- erases diary-file!
974 Argument ICAL-FILENAME output iCalendar file.
975 Argument DIARY-FILENAME input `diary-file'.
976 Optional argument NON-MARKING determines whether events are created as
978 If DO-NOT-CLEAR-DIARY-FILE is not nil the target diary file is
980 (interactive "fImport iCalendar data from file:
981 Finto diary file (will be erased!):
983 ;; clean up the diary file
985 (unless do-not-clear-diary-file
986 ;; clear the target diary file
987 (set-buffer (find-file diary-filename
))
989 ;; now load and convert from the ical file
990 (set-buffer (find-file ical-filename
))
991 (icalendar-extract-ical-from-buffer diary-filename t non-marking
)))
994 (defun icalendar-extract-ical-from-buffer (&optional
995 diary-file do-not-ask
997 "Extract iCalendar events from current buffer.
999 This function searches the current buffer for the first iCalendar
1000 object, reads it and adds all VEVENT elements to the diary
1003 It will ask for each appointment whether to add it to the diary
1004 when DO-NOT-ASK is non-nil. When called interactively,
1005 DO-NOT-ASK is set to t, so that you are asked fore each event.
1007 NON-MARKING determines whether diary events are created as
1010 This function attempts to notify about problems that occur when
1011 reading, parsing, or converting iCalendar data!"
1013 (save-current-buffer
1015 (message "Preparing icalendar...")
1016 (set-buffer (icalendar-get-unfolded-buffer (current-buffer)))
1017 (goto-char (point-min))
1018 (message "Preparing icalendar...done")
1019 (if (re-search-forward "^BEGIN:VCALENDAR\\s-*$" nil t
)
1020 (let (ical-contents ical-errors
)
1022 (message "Reading icalendar...")
1024 (setq ical-contents
(icalendar-read-element nil nil
))
1025 (message "Reading icalendar...done")
1027 (message "Converting icalendar...")
1028 (setq ical-errors
(icalendar-convert-ical-to-diary
1030 diary-file do-not-ask non-marking
))
1032 ;; save the diary file
1033 (save-current-buffer
1034 (set-buffer (find-buffer-visiting diary-file
))
1036 (message "Converting icalendar...done")
1037 (if (and ical-errors
(y-or-n-p
1038 (concat "Something went wrong -- "
1039 "do you want to see the "
1041 (switch-to-buffer " *icalendar-errors*")))
1043 "Current buffer does not contain icalendar contents!"))))
1045 ;; ----------------------------------------------------------------------
1047 ;; ----------------------------------------------------------------------
1048 (defun icalendar-format-ical-event (event)
1049 "Create a string representation of an iCalendar EVENT."
1050 (let ((string icalendar-import-format
)
1052 '(("%d" DESCRIPTION icalendar-import-format-description
)
1053 ("%s" SUMMARY icalendar-import-format-subject
)
1054 ("%l" LOCATION icalendar-import-format-location
)
1055 ("%o" ORGANIZER icalendar-import-format-organizer
))))
1056 ;; convert the specifiers in the format string
1058 (let* ((spec (car i
))
1060 (format (car (cddr i
)))
1061 (contents (icalendar-get-event-property event prop
))
1062 (formatted-contents ""))
1063 ;;(message "%s" event)
1064 ;;(message "contents%s = %s" prop contents)
1065 (when (and contents
(> (length contents
) 0))
1066 (setq formatted-contents
1067 (icalendar-rris "%s"
1068 (icalendar-convert-for-import
1070 (symbol-value format
))))
1071 (setq string
(icalendar-rris spec
1077 (defun icalendar-convert-ical-to-diary (ical-list diary-file
1078 &optional do-not-ask
1080 "Convert an iCalendar file to an Emacs diary file.
1081 Import VEVENTS from the iCalendar object ICAL-LIST and saves them to a
1082 DIARY-FILE. If DO-NOT-ASK is nil the user is asked for each event
1083 whether to actually import it. NON-MARKING determines whether diary
1084 events are created as non-marking.
1085 This function attempts to return t if something goes wrong. In this
1086 case an error string which describes all the errors and problems is
1087 written into the buffer ` *icalendar-errors*'."
1088 (let* ((ev (icalendar-all-events ical-list
))
1093 ;; step through all events/appointments
1098 (condition-case error-val
1099 (let* ((dtstart (icalendar-decode-isodatetime
1100 (icalendar-get-event-property e
'DTSTART
)))
1101 (start-d (calendar-date-string
1102 (icalendar-datetime-to-noneuropean-date
1105 (start-t (icalendar-datetime-to-colontime dtstart
))
1106 (dtend (icalendar-decode-isodatetime
1107 (icalendar-get-event-property e
'DTEND
)))
1110 (subject (icalendar-convert-for-import
1111 (or (icalendar-get-event-property e
'SUMMARY
)
1113 (rrule (icalendar-get-event-property e
'RRULE
))
1114 (rdate (icalendar-get-event-property e
'RDATE
))
1115 (duration (icalendar-get-event-property e
'DURATION
)))
1116 (icalendar-dmsg "%s: %s" start-d subject
)
1118 (let ((dtend2 (icalendar-add-decoded-times
1120 (icalendar-decode-isoduration duration
))))
1121 (if (and dtend
(not (eq dtend dtend2
)))
1122 (message "Inconsistent endtime and duration for %s"
1124 (setq dtend dtend2
)))
1125 (setq end-d
(if dtend
1126 (calendar-date-string
1127 (icalendar-datetime-to-noneuropean-date
1131 (setq end-t
(if dtend
1132 (icalendar-datetime-to-colontime dtend
)
1134 (icalendar-dmsg "start-d: %s, end-d: %s" start-d end-d
)
1138 (icalendar-dmsg "recurring event")
1139 (let* ((rrule-props (icalendar-split-value rrule
))
1140 (frequency (car (cdr (assoc 'FREQ rrule-props
))))
1141 (until (car (cdr (assoc 'UNTIL rrule-props
))))
1142 (interval (read (car (cdr (assoc 'INTERVAL
1144 (cond ((string-equal frequency
"WEEKLY")
1147 ;; weekly and all-day
1148 (icalendar-dmsg "weekly all-day")
1151 "%%%%(diary-cyclic %d %s)"
1153 (icalendar-datetime-to-european-date
1155 ;; weekly and not all-day
1156 (let* ((byday (cadr (assoc 'BYDAY rrule-props
)))
1160 icalendar-weekdayabbrev-table
))))
1161 (icalendar-dmsg "weekly not-all-day")
1164 (format "%s %s%s%s" weekday
1165 start-t
(if end-t
"-" "")
1168 ;; DTSTART;VALUE=DATE-TIME:20030919T090000
1169 ;; DTEND;VALUE=DATE-TIME:20030919T113000
1172 "%%%%(diary-cyclic %s %s) %s%s%s"
1174 (icalendar-datetime-to-european-date
1176 start-t
(if end-t
"-" "") (or end-t
""))))
1177 (setq event-ok t
))))
1179 ((string-equal frequency
"YEARLY")
1180 (icalendar-dmsg "yearly")
1183 "%%%%(diary-anniversary %s)"
1184 (icalendar-datetime-to-european-date dtstart
)))
1186 ;; FIXME: war auskommentiert:
1187 ((and (string-equal frequency
"DAILY")
1188 ;;(not (string= start-d end-d))
1192 (let ((ds (icalendar-datetime-to-noneuropean-date
1193 (icalendar-decode-isodatetime
1194 (icalendar-get-event-property e
1196 (de (icalendar-datetime-to-noneuropean-date
1197 (icalendar-decode-isodatetime
1201 "%%%%(diary-block %d %d %d %d %d %d)"
1202 (nth 1 ds
) (nth 0 ds
) (nth 2 ds
)
1203 (nth 1 de
) (nth 0 de
) (nth 2 de
))))
1207 (icalendar-dmsg "rdate event")
1208 (setq diary-string
"")
1209 (mapcar (lambda (datestring)
1211 (concat diary-string
1212 (format "......"))))
1213 (icalendar-split-value rdate
)))
1214 ;; non-recurring event
1216 ((not (string= start-d end-d
))
1217 (icalendar-dmsg "non-recurring event")
1218 (let ((ds (icalendar-datetime-to-noneuropean-date dtstart
))
1219 (de (icalendar-datetime-to-noneuropean-date dtend
)))
1221 (format "%%%%(diary-block %d %d %d %d %d %d)"
1222 (nth 1 ds
) (nth 0 ds
) (nth 2 ds
)
1223 (nth 1 de
) (nth 0 de
) (nth 2 de
))))
1226 ((and start-t
(or (not end-t
)
1227 (not (string= start-t end-t
))))
1228 (icalendar-dmsg "not all day event")
1230 (setq diary-string
(format "%s %s-%s" start-d
1233 (setq diary-string
(format "%s %s" start-d
1238 (icalendar-dmsg "all day event")
1239 (setq diary-string start-d
)
1241 ;; add all other elements unless the user doesn't want to have
1246 (concat diary-string
" "
1247 (icalendar-format-ical-event e
)))
1248 (if do-not-ask
(setq subject nil
))
1249 (icalendar-add-diary-entry diary-string diary-file
1250 non-marking subject
))
1252 (setq found-error t
)
1254 (format "%s\nCannot handle this event:%s"
1258 (message "Ignoring event \"%s\"" e
)
1259 (setq found-error t
)
1260 (setq error-string
(format "%s\nCannot handle this event: %s"
1263 (save-current-buffer
1264 (set-buffer (get-buffer-create " *icalendar-errors*"))
1266 (insert error-string
)))
1267 (message "Converting icalendar...done")
1270 (defun icalendar-add-diary-entry (string diary-file non-marking
1272 "Add STRING to the diary file DIARY-FILE.
1273 STRING must be a properly formatted valid diary entry. NON-MARKING
1274 determines whether diary events are created as non-marking. If
1275 SUBJECT is not nil it must be a string that gives the subject of the
1276 entry. In this case the user will be asked whether he wants to insert
1278 (when (or (not subject
) ;
1279 (y-or-n-p (format "Add appointment for `%s' to diary? "
1283 (y-or-n-p (format "Make appointment non-marking? "))))
1284 (save-window-excursion
1287 (read-file-name "Add appointment to this diary file: ")))
1288 (make-diary-entry string non-marking diary-file
))))
1290 ;; ======================================================================
1291 ;; (add-hook 'list-diary-entries-hook 'include-icalendar-files)
1292 ;; ======================================================================
1293 (defun include-icalendar-files ()
1294 "Not yet implemented.")
1296 (provide 'icalendar
)
1298 ;; arch-tag: 74fdbe8e-0451-4e38-bb61-4416e822f4fc
1299 ;;; icalendar.el ends here