1 ;;; icalendar.el --- iCalendar implementation -*-coding: utf-8 -*-
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.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.08
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 property 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--set-event-property (event prop new-value)
282 ;; "For the given EVENT set the property PROP to the value NEW-VALUE."
284 ;; (let ((props (car (cddr event))) pp)
286 ;; (setq pp (car props))
287 ;; (when (eq (car pp) prop)
288 ;; (setcdr (cdr pp) new-value)
289 ;; (throw 'found (car (cddr pp))))
290 ;; (setq props (cdr props)))
291 ;; (setq props (car (cddr event)))
292 ;; (setcar (cddr event)
293 ;; (append props (list (list prop nil new-value)))))))
295 (defun icalendar--get-children (node name
)
296 "Return all children of the given NODE which have a name NAME.
297 For instance the VCALENDAR node can have VEVENT children as well as VTODO
300 (children (cadr (cddr node
))))
301 (when (eq (car node
) name
)
303 ;;(message "%s" node)
308 (icalendar--get-children n name
))
312 (setq result
(append result subresult
))
313 (setq result subresult
)))))
317 (defun icalendar--all-events (icalendar)
318 "Return the list of all existing events in the given ICALENDAR."
319 (icalendar--get-children (car icalendar
) 'VEVENT
))
321 (defun icalendar--split-value (value-string)
322 "Splits VALUE-STRING at ';='."
324 param-name param-value
)
327 (set-buffer (get-buffer-create " *icalendar-work*"))
328 (set-buffer-modified-p nil
)
330 (insert value-string
)
331 (goto-char (point-min))
334 "\\([A-Za-z0-9-]+\\)=\\(\\([^;,:]+\\)\\|\"\\([^\"]+\\)\"\\);?"
336 (setq param-name
(intern (match-string 1)))
337 (setq param-value
(match-string 2))
339 (append result
(list (list param-name param-value
)))))))
342 (defun icalendar--decode-isodatetime (isodatetimestring)
343 "Return ISODATETIMESTRING in format like `decode-time'.
344 Converts from ISO-8601 to Emacs representation. If ISODATETIMESTRING
345 specifies UTC time (trailing letter Z) the decoded time is given in
346 the local time zone! FIXME: TZID-attributes are ignored....! FIXME:
347 multiple comma-separated values should be allowed!"
348 (icalendar--dmsg isodatetimestring
)
349 (if isodatetimestring
350 ;; day/month/year must be present
351 (let ((year (read (substring isodatetimestring
0 4)))
352 (month (read (substring isodatetimestring
4 6)))
353 (day (read (substring isodatetimestring
6 8)))
357 (when (> (length isodatetimestring
) 12)
358 ;; hour/minute present
359 (setq hour
(read (substring isodatetimestring
9 11)))
360 (setq minute
(read (substring isodatetimestring
11 13))))
361 (when (> (length isodatetimestring
) 14)
363 (setq second
(read (substring isodatetimestring
13 15))))
364 (when (and (> (length isodatetimestring
) 15)
365 ;; UTC specifier present
366 (char-equal ?Z
(aref isodatetimestring
15)))
367 ;; if not UTC add current-time-zone offset
368 (setq second
(+ (car (current-time-zone)) second
)))
369 ;; create the decoded date-time
372 (decode-time (encode-time second minute hour day month year
))
374 (message "Cannot decode \"%s\"" isodatetimestring
)
375 ;; hope for the best...
376 (list second minute hour day month year
0 nil
0))))
377 ;; isodatetimestring == nil
380 (defun icalendar--decode-isoduration (isodurationstring)
381 "Return ISODURATIONSTRING in format like `decode-time'.
382 Converts from ISO-8601 to Emacs representation. If ISODURATIONSTRING
383 specifies UTC time (trailing letter Z) the decoded time is given in
384 the local time zone! FIXME: TZID-attributes are ignored....! FIXME:
385 multiple comma-separated values should be allowed!"
386 (if isodurationstring
391 "\\(\\([0-9]+\\)D\\)" ; days only
393 "\\(\\(\\([0-9]+\\)D\\)?T\\(\\([0-9]+\\)H\\)?" ; opt days
394 "\\(\\([0-9]+\\)M\\)?\\(\\([0-9]+\\)S\\)?\\)" ; mand. time
396 "\\(\\([0-9]+\\)W\\)" ; weeks only
397 "\\)$") isodurationstring
)
405 ((match-beginning 2) ;days only
406 (setq days
(read (substring isodurationstring
409 (when icalendar-duration-correction
410 (setq days
(1- days
))))
411 ((match-beginning 4) ;days and time
412 (if (match-beginning 5)
413 (setq days
(* 7 (read (substring isodurationstring
416 (if (match-beginning 7)
417 (setq hours
(read (substring isodurationstring
420 (if (match-beginning 9)
421 (setq minutes
(read (substring isodurationstring
424 (if (match-beginning 11)
425 (setq seconds
(read (substring isodurationstring
429 ((match-beginning 13) ;weeks only
430 (setq days
(* 7 (read (substring isodurationstring
434 (list seconds minutes hours days months years
)))
435 ;; isodatetimestring == nil
438 (defun icalendar--add-decoded-times (time1 time2
)
440 Both times must be given in decoded form. One of these times must be
441 valid (year > 1900 or something)."
442 ;; FIXME: does this function exist already?
443 (decode-time (encode-time
444 (+ (nth 0 time1
) (nth 0 time2
))
445 (+ (nth 1 time1
) (nth 1 time2
))
446 (+ (nth 2 time1
) (nth 2 time2
))
447 (+ (nth 3 time1
) (nth 3 time2
))
448 (+ (nth 4 time1
) (nth 4 time2
))
449 (+ (nth 5 time1
) (nth 5 time2
))
452 ;;(or (nth 6 time1) (nth 6 time2)) ;; FIXME?
455 (defun icalendar--datetime-to-noneuropean-date (datetime)
456 "Convert the decoded DATETIME to non-european-style format.
457 Non-European format: (month day year)."
459 (list (nth 4 datetime
) ;month
460 (nth 3 datetime
) ;day
461 (nth 5 datetime
)) ;year
465 (defun icalendar--datetime-to-european-date (datetime)
466 "Convert the decoded DATETIME to European format.
467 European format: (day month year).
470 (format "%d %d %d" (nth 3 datetime
) ; day
471 (nth 4 datetime
) ;month
472 (nth 5 datetime
)) ;year
476 (defun icalendar--datetime-to-colontime (datetime)
477 "Extract the time part of a decoded DATETIME into 24-hour format.
478 Note that this silently ignores seconds."
479 (format "%02d:%02d" (nth 2 datetime
) (nth 1 datetime
)))
481 (defun icalendar--get-month-number (monthname)
482 "Return the month number for the given MONTHNAME."
485 (m (downcase monthname
)))
486 (mapc (lambda (month)
487 (let ((mm (downcase month
)))
488 (if (or (string-equal mm m
)
489 (string-equal (substring mm
0 3) m
))
491 (setq num
(1+ num
))))
492 calendar-month-name-array
))
496 (defun icalendar--get-weekday-number (abbrevweekday)
497 "Return the number for the ABBREVWEEKDAY."
500 (aw (downcase abbrevweekday
)))
502 (let ((d (downcase day
)))
503 (if (string-equal d aw
)
505 (setq num
(1+ num
))))
506 icalendar--weekday-array
))
510 (defun icalendar--get-weekday-abbrev (weekday)
511 "Return the abbreviated WEEKDAY."
514 (w (downcase weekday
)))
516 (let ((d (downcase day
)))
517 (if (or (string-equal d w
)
518 (string-equal (substring d
0 3) w
))
519 (throw 'found
(aref icalendar--weekday-array num
)))
520 (setq num
(1+ num
))))
521 calendar-day-name-array
))
525 (defun icalendar--date-to-isodate (date &optional day-shift
)
526 "Convert DATE to iso-style date.
527 DATE must be a list of the form (month day year).
528 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days."
529 (let ((mdy (calendar-gregorian-from-absolute
530 (+ (calendar-absolute-from-gregorian date
)
532 (format "%04d%02d%02d" (nth 2 mdy
) (nth 0 mdy
) (nth 1 mdy
))))
535 (defun icalendar--datestring-to-isodate (datestring &optional day-shift
)
536 "Convert diary-style DATESTRING to iso-style date.
537 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days
538 -- DAY-SHIFT must be either nil or an integer. This function
539 takes care of european-style."
540 (let ((day -
1) month year
)
542 (cond ( ;; numeric date
543 (string-match (concat "\\s-*"
544 "0?\\([1-9][0-9]?\\)[ \t/]\\s-*"
545 "0?\\([1-9][0-9]?\\),?[ \t/]\\s-*"
546 "\\([0-9]\\{4\\}\\)")
548 (setq day
(read (substring datestring
(match-beginning 1)
550 (setq month
(read (substring datestring
(match-beginning 2)
552 (setq year
(read (substring datestring
(match-beginning 3)
554 (unless european-calendar-style
558 ( ;; date contains month names -- european-style
559 (and european-calendar-style
560 (string-match (concat "\\s-*"
561 "0?\\([123]?[0-9]\\)[ \t/]\\s-*"
562 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
563 "\\([0-9]\\{4\\}\\)")
565 (setq day
(read (substring datestring
(match-beginning 1)
567 (setq month
(icalendar--get-month-number
568 (substring datestring
(match-beginning 2)
570 (setq year
(read (substring datestring
(match-beginning 3)
572 ( ;; date contains month names -- non-european-style
573 (and (not european-calendar-style
)
574 (string-match (concat "\\s-*"
575 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
576 "0?\\([123]?[0-9]\\),?[ \t/]\\s-*"
577 "\\([0-9]\\{4\\}\\)")
579 (setq day
(read (substring datestring
(match-beginning 2)
581 (setq month
(icalendar--get-month-number
582 (substring datestring
(match-beginning 1)
584 (setq year
(read (substring datestring
(match-beginning 3)
589 (let ((mdy (calendar-gregorian-from-absolute
590 (+ (calendar-absolute-from-gregorian (list month day
593 (format "%04d%02d%02d" (nth 2 mdy
) (nth 0 mdy
) (nth 1 mdy
)))
596 (defun icalendar--diarytime-to-isotime (timestring ampmstring
)
597 "Convert a a time like 9:30pm to an iso-conform string like T213000.
598 In this example the TIMESTRING would be \"9:30\" and the AMPMSTRING
601 (let ((starttimenum (read (icalendar--rris ":" "" timestring
))))
602 ;; take care of am/pm style
603 (if (and ampmstring
(string= "pm" ampmstring
))
604 (setq starttimenum
(+ starttimenum
1200)))
605 (format "T%04d00" starttimenum
))
608 (defun icalendar--convert-string-for-export (s)
609 "Escape comma and other critical characters in string S."
610 (icalendar--rris "," "\\\\," s
))
612 (defun icalendar--convert-string-for-import (string)
613 "Remove escape chars for comma, semicolon etc. from STRING."
615 "\\\\n" "\n " (icalendar--rris
616 "\\\\\"" "\"" (icalendar--rris
617 "\\\\;" ";" (icalendar--rris
618 "\\\\," "," string
)))))
620 ;; ======================================================================
621 ;; Export -- convert emacs-diary to icalendar
622 ;; ======================================================================
625 (defun icalendar-export-file (diary-filename ical-filename
)
626 "Export diary file to iCalendar format.
627 All diary entries in the file DIARY-FILENAME are converted to iCalendar
628 format. The result is appended to the file ICAL-FILENAME."
629 (interactive "FExport diary data from file:
630 Finto iCalendar file: ")
632 (set-buffer (find-file diary-filename
))
633 (icalendar-export-region (point-min) (point-max) ical-filename
)))
635 (defalias 'icalendar-convert-diary-to-ical
'icalendar-export-file
)
636 (make-obsolete 'icalendar-convert-diary-to-ical
'icalendar-export-file
)
639 (defun icalendar-export-region (min max ical-filename
)
640 "Export region in diary file to iCalendar format.
641 All diary entries in the region from MIN to MAX in the current buffer are
642 converted to iCalendar format. The result is appended to the file
645 Returns non-nil if an error occurred. In this case an error message is
646 written to the buffer ` *icalendar-errors*'."
648 FExport diary data into iCalendar file: ")
656 (nonmarker (concat "^" (regexp-quote diary-nonmarking-symbol
)
658 ;; prepare buffer with error messages
660 (set-buffer (get-buffer-create " *icalendar-errors*"))
665 (while (re-search-forward
666 "^\\([^ \t\n].*\\)\\(\\(\n[ \t].*\\)*\\)" max t
)
667 (setq entry-main
(match-string 1))
668 (if (match-beginning 2)
669 (setq entry-rest
(match-string 2))
670 (setq entry-rest
""))
671 (setq header
(format "\nBEGIN:VEVENT\nUID:emacs%d%d%d"
673 (cadr (current-time))
674 (car (cddr (current-time)))))
675 (condition-case error-val
681 "%%(diary-anniversary \\([^)]+\\))\\s-*\\(.*\\)")
683 (icalendar--dmsg "diary-anniversary %s" entry-main
)
684 (let* ((datetime (substring entry-main
(match-beginning 1)
686 (summary (icalendar--convert-string-for-export
687 (substring entry-main
(match-beginning 2)
689 (startisostring (icalendar--datestring-to-isodate
691 (endisostring (icalendar--datestring-to-isodate
694 (concat "\nDTSTART;VALUE=DATE:" startisostring
695 "\nDTEND;VALUE=DATE:" endisostring
697 "\nRRULE:FREQ=YEARLY;INTERVAL=1"
698 ;; the following is redundant,
699 ;; but korganizer seems to expect this... ;(
700 ;; and evolution doesn't understand it... :(
701 ;; so... who is wrong?!
702 ";BYMONTH=" (substring startisostring
4 6)
703 ";BYMONTHDAY=" (substring startisostring
6 8)
705 (unless (string= entry-rest
"")
706 (setq contents
(concat contents
"\nDESCRIPTION:"
707 (icalendar--convert-string-for-export
713 "%%(diary-cyclic \\([^ ]+\\) +"
714 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*\\(.*\\)")
716 (icalendar--dmsg "diary-cyclic %s" entry-main
)
717 (let* ((frequency (substring entry-main
(match-beginning 1)
719 (datetime (substring entry-main
(match-beginning 2)
721 (summary (icalendar--convert-string-for-export
722 (substring entry-main
(match-beginning 3)
724 (startisostring (icalendar--datestring-to-isodate
726 (endisostring (icalendar--datestring-to-isodate
729 (concat "\nDTSTART;VALUE=DATE:" startisostring
730 "\nDTEND;VALUE=DATE:" endisostring
732 "\nRRULE:FREQ=DAILY;INTERVAL=" frequency
733 ;; strange: korganizer does not expect
734 ;; BYSOMETHING here...
736 (unless (string= entry-rest
"")
737 (setq contents
(concat contents
"\nDESCRIPTION:"
738 (icalendar--convert-string-for-export
740 ;; diary-date -- FIXME
743 "%%(diary-date \\([^)]+\\))\\s-*\\(.*\\)")
745 (icalendar--dmsg "diary-date %s" entry-main
)
746 (error "`diary-date' is not supported yet"))
747 ;; float events -- FIXME
750 "%%(diary-float \\([^)]+\\))\\s-*\\(.*\\)")
752 (icalendar--dmsg "diary-float %s" entry-main
)
753 (error "`diary-float' is not supported yet"))
757 "%%(diary-block \\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\) +"
758 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*\\(.*\\)")
760 (icalendar--dmsg "diary-block %s" entry-main
)
761 (let* ((startstring (substring entry-main
(match-beginning 1)
763 (endstring (substring entry-main
(match-beginning 2)
765 (summary (icalendar--convert-string-for-export
766 (substring entry-main
(match-beginning 3)
768 (startisostring (icalendar--datestring-to-isodate
770 (endisostring (icalendar--datestring-to-isodate
773 (concat "\nDTSTART;VALUE=DATE:" startisostring
774 "\nDTEND;VALUE=DATE:" endisostring
777 (unless (string= entry-rest
"")
778 (setq contents
(concat contents
"\nDESCRIPTION:"
779 (icalendar--convert-string-for-export
781 ;; other sexp diary entries -- FIXME
784 "%%(\\([^)]+\\))\\s-*\\(.*\\)")
786 (icalendar--dmsg "diary-sexp %s" entry-main
)
787 (error "sexp-entries are not supported yet"))
789 ;; Monday 8:30 Team meeting
793 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
795 "\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
799 (icalendar--get-weekday-abbrev
800 (substring entry-main
(match-beginning 1) (match-end 1))))
801 (icalendar--dmsg "weekly %s" entry-main
)
802 (let* ((day (icalendar--get-weekday-abbrev
803 (substring entry-main
(match-beginning 1)
805 (starttimestring (icalendar--diarytime-to-isotime
806 (if (match-beginning 3)
807 (substring entry-main
811 (if (match-beginning 4)
812 (substring entry-main
816 (endtimestring (icalendar--diarytime-to-isotime
817 (if (match-beginning 6)
818 (substring entry-main
822 (if (match-beginning 7)
823 (substring entry-main
827 (summary (icalendar--convert-string-for-export
828 (substring entry-main
(match-beginning 8)
830 (when starttimestring
831 (unless endtimestring
832 (let ((time (read (icalendar--rris "^T0?" ""
834 (setq endtimestring
(format "T%06d" (+ 10000 time
))))))
840 ;; find the correct week day,
841 ;; 1st january 2000 was a saturday
844 (+ (icalendar--get-weekday-number day
) 2))
845 (or starttimestring
"")
852 ;; end is non-inclusive!
853 (+ (icalendar--get-weekday-number day
)
854 (if endtimestring
2 3)))
855 (or endtimestring
"")
857 "\nRRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=" day
859 (unless (string= entry-rest
"")
860 (setq contents
(concat contents
"\nDESCRIPTION:"
861 (icalendar--convert-string-for-export
864 ;; 1 May Tag der Arbeit
867 (if european-calendar-style
868 "0?\\([1-9]+[0-9]?\\)\\s-+\\([a-z]+\\)\\s-+"
869 "\\([a-z]+\\)\\s-+0?\\([1-9]+[0-9]?\\)\\s-+")
871 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
873 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
875 "\\s-*\\([^0-9]+.*\\)$" ; must not match years
878 (icalendar--dmsg "yearly %s" entry-main
)
879 (let* ((daypos (if european-calendar-style
1 2))
880 (monpos (if european-calendar-style
2 1))
881 (day (read (substring entry-main
(match-beginning daypos
)
882 (match-end daypos
))))
883 (month (icalendar--get-month-number
884 (substring entry-main
(match-beginning monpos
)
885 (match-end monpos
))))
886 (starttimestring (icalendar--diarytime-to-isotime
887 (if (match-beginning 4)
888 (substring entry-main
892 (if (match-beginning 5)
893 (substring entry-main
897 (endtimestring (icalendar--diarytime-to-isotime
898 (if (match-beginning 7)
899 (substring entry-main
903 (if (match-beginning 8)
904 (substring entry-main
908 (summary (icalendar--convert-string-for-export
909 (substring entry-main
(match-beginning 9)
911 (when starttimestring
912 (unless endtimestring
913 (let ((time (read (icalendar--rris "^T0?" ""
915 (setq endtimestring
(format "T%06d" (+ 10000 time
))))))
918 (if starttimestring
"VALUE=DATE-TIME:"
920 (format "1900%02d%02d" month day
)
921 (or starttimestring
"")
923 (if endtimestring
"VALUE=DATE-TIME:"
925 ;; end is not included! shift by one day
926 (icalendar--date-to-isodate
927 (list month day
1900) (if endtimestring
0 1))
928 (or endtimestring
"")
931 "\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH="
936 (unless (string= entry-rest
"")
937 (setq contents
(concat contents
"\nDESCRIPTION:"
938 (icalendar--convert-string-for-export
940 ;; "ordinary" events, start and end time given
941 ;; 1 Feb 2003 Hs Hochzeitsfeier, Dreieich
944 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)\\s-+"
945 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
947 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
951 (icalendar--dmsg "ordinary %s" entry-main
)
952 (let* ((startdatestring (icalendar--datestring-to-isodate
953 (substring entry-main
956 (starttimestring (icalendar--diarytime-to-isotime
957 (if (match-beginning 3)
958 (substring entry-main
962 (if (match-beginning 4)
963 (substring entry-main
967 (endtimestring (icalendar--diarytime-to-isotime
968 (if (match-beginning 6)
969 (substring entry-main
973 (if (match-beginning 7)
974 (substring entry-main
978 (summary (icalendar--convert-string-for-export
979 (substring entry-main
(match-beginning 8)
981 (unless startdatestring
982 (error "Could not parse date"))
983 (when starttimestring
984 (unless endtimestring
985 (let ((time (read (icalendar--rris "^T0?" ""
987 (setq endtimestring
(format "T%06d" (+ 10000 time
))))))
988 (setq contents
(concat
990 (if starttimestring
"VALUE=DATE-TIME:"
993 (or starttimestring
"")
995 (if endtimestring
"VALUE=DATE-TIME:"
997 (icalendar--datestring-to-isodate
998 (substring entry-main
1001 (if endtimestring
0 1))
1002 (or endtimestring
"")
1005 ;; could not parse the date
1006 (unless (string= entry-rest
"")
1007 (setq contents
(concat contents
"\nDESCRIPTION:"
1008 (icalendar--convert-string-for-export
1012 ;; Oops! what's that?
1013 (error "Could not parse entry")))
1014 (setq result
(concat result header contents
"\nEND:VEVENT")))
1017 (setq found-error t
)
1018 (save-current-buffer
1019 (set-buffer (get-buffer-create " *icalendar-errors*"))
1020 (insert (format "Error in line %d -- %s: `%s'\n"
1021 (count-lines (point-min) (point))
1025 ;; we're done, insert everything into the file
1026 (let ((coding-system-for-write 'utf8
))
1027 (set-buffer (find-file ical-filename
))
1028 (goto-char (point-max))
1029 (insert "BEGIN:VCALENDAR")
1030 (insert "\nPRODID:-//Emacs//NONSGML icalendar.el//EN")
1031 (insert "\nVERSION:2.0")
1033 (insert "\nEND:VCALENDAR\n")))
1036 ;; ======================================================================
1037 ;; Import -- convert icalendar to emacs-diary
1038 ;; ======================================================================
1041 (defun icalendar-import-file (ical-filename diary-filename
1042 &optional non-marking
)
1043 "Import a iCalendar file and append to a diary file.
1044 Argument ICAL-FILENAME output iCalendar file.
1045 Argument DIARY-FILENAME input `diary-file'.
1046 Optional argument NON-MARKING determines whether events are created as
1047 non-marking or not."
1048 (interactive "fImport iCalendar data from file:
1051 ;; clean up the diary file
1052 (save-current-buffer
1053 ;; now load and convert from the ical file
1054 (set-buffer (find-file ical-filename
))
1055 (icalendar-import-buffer diary-filename t non-marking
)))
1058 (defun icalendar-import-buffer (&optional diary-file do-not-ask
1060 "Extract iCalendar events from current buffer.
1062 This function searches the current buffer for the first iCalendar
1063 object, reads it and adds all VEVENT elements to the diary
1066 It will ask for each appointment whether to add it to the diary
1067 when DO-NOT-ASK is non-nil. When called interactively,
1068 DO-NOT-ASK is set to t, so that you are asked fore each event.
1070 NON-MARKING determines whether diary events are created as
1073 This function attempts to notify about problems that occur when
1074 reading, parsing, or converting iCalendar data!"
1076 (save-current-buffer
1078 (message "Preparing icalendar...")
1079 (set-buffer (icalendar--get-unfolded-buffer (current-buffer)))
1080 (goto-char (point-min))
1081 (message "Preparing icalendar...done")
1082 (if (re-search-forward "^BEGIN:VCALENDAR\\s-*$" nil t
)
1083 (let (ical-contents ical-errors
)
1085 (message "Reading icalendar...")
1087 (setq ical-contents
(icalendar--read-element nil nil
))
1088 (message "Reading icalendar...done")
1090 (message "Converting icalendar...")
1091 (setq ical-errors
(icalendar--convert-ical-to-diary
1093 diary-file do-not-ask non-marking
))
1095 ;; save the diary file
1096 (save-current-buffer
1097 (set-buffer (find-buffer-visiting diary-file
))
1099 (message "Converting icalendar...done")
1100 (if (and ical-errors
(y-or-n-p
1101 (concat "Something went wrong -- "
1102 "do you want to see the "
1104 (switch-to-buffer " *icalendar-errors*")))
1106 "Current buffer does not contain icalendar contents!"))))
1108 (defalias 'icalendar-extract-ical-from-buffer
'icalendar-import-buffer
)
1109 (make-obsolete 'icalendar-extract-ical-from-buffer
'icalendar-import-buffer
)
1111 ;; ======================================================================
1113 ;; ======================================================================
1115 (defun icalendar--format-ical-event (event)
1116 "Create a string representation of an iCalendar EVENT."
1117 (let ((string icalendar-import-format
)
1119 '(("%d" DESCRIPTION icalendar-import-format-description
)
1120 ("%s" SUMMARY icalendar-import-format-subject
)
1121 ("%l" LOCATION icalendar-import-format-location
)
1122 ("%o" ORGANIZER icalendar-import-format-organizer
))))
1123 ;; convert the specifiers in the format string
1125 (let* ((spec (car i
))
1127 (format (car (cddr i
)))
1128 (contents (icalendar--get-event-property event prop
))
1129 (formatted-contents ""))
1130 (when (and contents
(> (length contents
) 0))
1131 (setq formatted-contents
1132 (icalendar--rris "%s"
1133 (icalendar--convert-string-for-import
1135 (symbol-value format
))))
1136 (setq string
(icalendar--rris spec
1142 (defun icalendar--convert-ical-to-diary (ical-list diary-file
1143 &optional do-not-ask
1145 "Convert an iCalendar file to an Emacs diary file.
1146 Import VEVENTS from the iCalendar object ICAL-LIST and saves them to a
1147 DIARY-FILE. If DO-NOT-ASK is nil the user is asked for each event
1148 whether to actually import it. NON-MARKING determines whether diary
1149 events are created as non-marking.
1150 This function attempts to return t if something goes wrong. In this
1151 case an error string which describes all the errors and problems is
1152 written into the buffer ` *icalendar-errors*'."
1153 (let* ((ev (icalendar--all-events ical-list
))
1158 ;; step through all events/appointments
1163 (condition-case error-val
1164 (let* ((dtstart (icalendar--decode-isodatetime
1165 (icalendar--get-event-property e
'DTSTART
)))
1166 (start-d (calendar-date-string
1167 (icalendar--datetime-to-noneuropean-date
1170 (start-t (icalendar--datetime-to-colontime dtstart
))
1171 (dtend (icalendar--decode-isodatetime
1172 (icalendar--get-event-property e
'DTEND
)))
1175 (subject (icalendar--convert-string-for-import
1176 (or (icalendar--get-event-property e
'SUMMARY
)
1178 (rrule (icalendar--get-event-property e
'RRULE
))
1179 (rdate (icalendar--get-event-property e
'RDATE
))
1180 (duration (icalendar--get-event-property e
'DURATION
)))
1181 (icalendar--dmsg "%s: %s" start-d subject
)
1183 (let ((dtend2 (icalendar--add-decoded-times
1185 (icalendar--decode-isoduration duration
))))
1186 (if (and dtend
(not (eq dtend dtend2
)))
1187 (message "Inconsistent endtime and duration for %s"
1189 (setq dtend dtend2
)))
1190 (setq end-d
(if dtend
1191 (calendar-date-string
1192 (icalendar--datetime-to-noneuropean-date
1196 (setq end-t
(if dtend
1197 (icalendar--datetime-to-colontime dtend
)
1199 (icalendar--dmsg "start-d: %s, end-d: %s" start-d end-d
)
1203 (icalendar--dmsg "recurring event")
1204 (let* ((rrule-props (icalendar--split-value rrule
))
1205 (frequency (car (cdr (assoc 'FREQ rrule-props
))))
1206 (until (car (cdr (assoc 'UNTIL rrule-props
))))
1207 (interval (read (car (cdr (assoc 'INTERVAL
1209 (cond ((string-equal frequency
"WEEKLY")
1212 ;; weekly and all-day
1213 (icalendar--dmsg "weekly all-day")
1216 "%%%%(diary-cyclic %d %s)"
1218 (icalendar--datetime-to-european-date
1220 ;; weekly and not all-day
1221 (let* ((byday (cadr (assoc 'BYDAY rrule-props
)))
1223 (icalendar--get-weekday-number byday
)))
1224 (icalendar--dmsg "weekly not-all-day")
1228 (aref calendar-day-name-array
1230 start-t
(if end-t
"-" "")
1233 ;; DTSTART;VALUE=DATE-TIME:20030919T090000
1234 ;; DTEND;VALUE=DATE-TIME:20030919T113000
1237 "%%%%(diary-cyclic %s %s) %s%s%s"
1239 (icalendar--datetime-to-european-date
1241 start-t
(if end-t
"-" "") (or end-t
""))))
1242 (setq event-ok t
))))
1244 ((string-equal frequency
"YEARLY")
1245 (icalendar--dmsg "yearly")
1248 "%%%%(diary-anniversary %s)"
1249 (icalendar--datetime-to-european-date dtstart
)))
1251 ;; FIXME: war auskommentiert:
1252 ((and (string-equal frequency
"DAILY")
1253 ;;(not (string= start-d end-d))
1257 (let ((ds (icalendar--datetime-to-noneuropean-date
1258 (icalendar--decode-isodatetime
1259 (icalendar--get-event-property e
1261 (de (icalendar--datetime-to-noneuropean-date
1262 (icalendar--decode-isodatetime
1266 "%%%%(diary-block %d %d %d %d %d %d)"
1267 (nth 1 ds
) (nth 0 ds
) (nth 2 ds
)
1268 (nth 1 de
) (nth 0 de
) (nth 2 de
))))
1272 (icalendar--dmsg "rdate event")
1273 (setq diary-string
"")
1274 (mapcar (lambda (datestring)
1276 (concat diary-string
1277 (format "......"))))
1278 (icalendar--split-value rdate
)))
1279 ;; non-recurring event
1281 ((not (string= start-d end-d
))
1282 (icalendar--dmsg "non-recurring event")
1283 (let ((ds (icalendar--datetime-to-noneuropean-date dtstart
))
1284 (de (icalendar--datetime-to-noneuropean-date dtend
)))
1286 (format "%%%%(diary-block %d %d %d %d %d %d)"
1287 (nth 1 ds
) (nth 0 ds
) (nth 2 ds
)
1288 (nth 1 de
) (nth 0 de
) (nth 2 de
))))
1291 ((and start-t
(or (not end-t
)
1292 (not (string= start-t end-t
))))
1293 (icalendar--dmsg "not all day event")
1295 (setq diary-string
(format "%s %s-%s" start-d
1298 (setq diary-string
(format "%s %s" start-d
1303 (icalendar--dmsg "all day event")
1304 (setq diary-string start-d
)
1306 ;; add all other elements unless the user doesn't want to have
1311 (concat diary-string
" "
1312 (icalendar--format-ical-event e
)))
1313 (if do-not-ask
(setq subject nil
))
1314 (icalendar--add-diary-entry diary-string diary-file
1315 non-marking subject
))
1317 (setq found-error t
)
1319 (format "%s\nCannot handle this event:%s"
1323 (message "Ignoring event \"%s\"" e
)
1324 (setq found-error t
)
1325 (setq error-string
(format "%s\nCannot handle this event: %s"
1328 (save-current-buffer
1329 (set-buffer (get-buffer-create " *icalendar-errors*"))
1331 (insert error-string
)))
1332 (message "Converting icalendar...done")
1335 (defun icalendar--add-diary-entry (string diary-file non-marking
1337 "Add STRING to the diary file DIARY-FILE.
1338 STRING must be a properly formatted valid diary entry. NON-MARKING
1339 determines whether diary events are created as non-marking. If
1340 SUBJECT is not nil it must be a string that gives the subject of the
1341 entry. In this case the user will be asked whether he wants to insert
1343 (when (or (not subject
) ;
1344 (y-or-n-p (format "Add appointment for `%s' to diary? "
1348 (y-or-n-p (format "Make appointment non-marking? "))))
1349 (save-window-excursion
1352 (read-file-name "Add appointment to this diary file: ")))
1353 (make-diary-entry string non-marking diary-file
))))
1355 (provide 'icalendar
)
1357 ;; arch-tag: 74fdbe8e-0451-4e38-bb61-4416e822f4fc
1358 ;;; icalendar.el ends here