1 ;;; icalendar.el --- iCalendar implementation -*-coding: utf-8 -*-
3 ;; Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008
4 ;; Free Software Foundation, Inc.
6 ;; Author: Ulf Jasper <ulf.jasper@web.de>
7 ;; Created: August 2002
9 ;; Human-Keywords: calendar, diary, iCalendar, vCalendar
11 ;; This file is part of GNU Emacs.
13 ;; GNU Emacs is free software; you can redistribute it and/or modify
14 ;; it under the terms of the GNU General Public License as published by
15 ;; the Free Software Foundation; either version 3, or (at your option)
18 ;; GNU Emacs is distributed in the hope that it will be useful,
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;; GNU General Public License for more details.
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with GNU Emacs; see the file COPYING. If not, write to the
25 ;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
26 ;; Boston, MA 02110-1301, USA.
30 ;; This package is documented in the Emacs Manual.
33 ;; - Diary entries which have a start time but no end time are assumed to
34 ;; last for one hour when they are exported.
35 ;; - Weekly diary entries are assumed to occur the first time in the first
36 ;; week of the year 2000 when they are exported.
37 ;; - Yearly diary entries are assumed to occur the first time in the year
38 ;; 1900 when they are exported.
42 ;; 0.07 onwards: see lisp/ChangeLog
45 ;; - Bugfixes regarding icalendar-import-format-*.
46 ;; - Fix in icalendar-convert-diary-to-ical -- thanks to Philipp Grau.
49 ;; - New import format scheme: Replaced icalendar-import-prefix-*,
50 ;; icalendar-import-ignored-properties, and
51 ;; icalendar-import-separator with icalendar-import-format(-*).
52 ;; - icalendar-import-file and icalendar-convert-diary-to-ical
53 ;; have an extra parameter which should prevent them from
54 ;; erasing their target files (untested!).
55 ;; - Tested with Emacs 21.3.2
58 ;; - Bugfix: import: double quoted param values did not work
59 ;; - Read DURATION property when importing.
60 ;; - Added parameter icalendar-duration-correction.
63 ;; - Export takes care of european-calendar-style.
64 ;; - Tested with Emacs 21.3.2 and XEmacs 21.4.12
67 ;; - Should work in XEmacs now. Thanks to Len Trigg for the XEmacs patches!
68 ;; - Added exporting from Emacs diary to ical.
69 ;; - Some bugfixes, after testing with calendars from http://icalshare.com.
70 ;; - Tested with Emacs 21.3.2 and XEmacs 21.4.12
73 ;; - First published version. Trial version. Alpha version.
75 ;; ======================================================================
78 ;; * Import from ical to diary:
79 ;; + Need more properties for icalendar-import-format
80 ;; (added all that Mozilla Calendar uses)
81 ;; From iCal specifications (RFC2445: 4.8.1), icalendar.el lacks
82 ;; ATTACH, CATEGORIES, COMMENT, GEO, PERCENT-COMPLETE (VTODO),
83 ;; PRIORITY, RESOURCES) not considering date/time and time-zone
84 ;; + check vcalendar version
85 ;; + check (unknown) elements
86 ;; + recurring events!
87 ;; + works for european style calendars only! Does it?
89 ;; + exceptions in recurring events
90 ;; + the parser is too soft
91 ;; + error log is incomplete
92 ;; + nice to have: #include "webcal://foo.com/some-calendar.ics"
93 ;; + timezones probably still need some improvements.
95 ;; * Export from diary to ical
96 ;; + diary-date, diary-float, and self-made sexp entries are not
100 ;; + clean up all those date/time parsing functions
101 ;; + Handle todo items?
102 ;; + Check iso 8601 for datetime and period
103 ;; + Which chars to (un)escape?
108 (defconst icalendar-version
"0.17"
109 "Version number of icalendar.el.")
111 ;; ======================================================================
113 ;; ======================================================================
114 (defgroup icalendar nil
119 (defcustom icalendar-import-format
121 "Format for importing events from iCalendar into Emacs diary.
122 It defines how iCalendar events are inserted into diary file.
123 This may either be a string or a function.
125 In case of a formatting STRING the following specifiers can be used:
126 %c Class, see `icalendar-import-format-class'
127 %d Description, see `icalendar-import-format-description'
128 %l Location, see `icalendar-import-format-location'
129 %o Organizer, see `icalendar-import-format-organizer'
130 %s Summary, see `icalendar-import-format-summary'
131 %t Status, see `icalendar-import-format-status'
132 %u URL, see `icalendar-import-format-url'
134 A formatting FUNCTION will be called with a VEVENT as its only
135 argument. It must return a string. See
136 `icalendar-import-format-sample' for an example."
138 (string :tag
"String")
139 (function :tag
"Function"))
142 (defcustom icalendar-import-format-summary
144 "Format string defining how the summary element is formatted.
145 This applies only if the summary is not empty! `%s' is replaced
150 (defcustom icalendar-import-format-description
152 "Format string defining how the description element is formatted.
153 This applies only if the description is not empty! `%s' is
154 replaced by the description."
158 (defcustom icalendar-import-format-location
160 "Format string defining how the location element is formatted.
161 This applies only if the location is not empty! `%s' is replaced
166 (defcustom icalendar-import-format-organizer
168 "Format string defining how the organizer element is formatted.
169 This applies only if the organizer is not empty! `%s' is
170 replaced by the organizer."
174 (defcustom icalendar-import-format-url
176 "Format string defining how the URL element is formatted.
177 This applies only if the URL is not empty! `%s' is replaced by
182 (defcustom icalendar-import-format-status
184 "Format string defining how the status element is formatted.
185 This applies only if the status is not empty! `%s' is replaced by
190 (defcustom icalendar-import-format-class
192 "Format string defining how the class element is formatted.
193 This applies only if the class is not empty! `%s' is replaced by
198 (defvar icalendar-debug nil
199 "Enable icalendar debug messages.")
201 ;; ======================================================================
202 ;; NO USER SERVICABLE PARTS BELOW THIS LINE
203 ;; ======================================================================
205 (defconst icalendar--weekday-array
["SU" "MO" "TU" "WE" "TH" "FR" "SA"])
207 ;; ======================================================================
208 ;; all the other libs we need
209 ;; ======================================================================
212 ;; ======================================================================
214 ;; ======================================================================
215 (defun icalendar--dmsg (&rest args
)
216 "Print message ARGS if `icalendar-debug' is non-nil."
218 (apply 'message args
)))
220 ;; ======================================================================
221 ;; Core functionality
222 ;; Functions for parsing icalendars, importing and so on
223 ;; ======================================================================
225 (defun icalendar--get-unfolded-buffer (folded-ical-buffer)
226 "Return a new buffer containing the unfolded contents of a buffer.
227 Folding is the iCalendar way of wrapping long lines. In the
228 created buffer all occurrences of CR LF BLANK are replaced by the
229 empty string. Argument FOLDED-ICAL-BUFFER is the unfolded input
231 (let ((unfolded-buffer (get-buffer-create " *icalendar-work*")))
233 (set-buffer unfolded-buffer
)
235 (insert-buffer-substring folded-ical-buffer
)
236 (goto-char (point-min))
237 (while (re-search-forward "\r?\n[ \t]" nil t
)
238 (replace-match "" nil nil
)))
241 (defsubst icalendar--rris
(regexp rep string
&optional fixedcase literal
)
242 "Replace regular expression in string.
243 Pass arguments REGEXP REP STRING FIXEDCASE LITERAL to
244 `replace-regexp-in-string' (Emacs) or to `replace-in-string' (XEmacs)."
245 (cond ((fboundp 'replace-regexp-in-string
)
247 (replace-regexp-in-string regexp rep string fixedcase literal
))
248 ((fboundp 'replace-in-string
)
250 (save-match-data ;; apparently XEmacs needs save-match-data
251 (replace-in-string string regexp rep literal
)))))
253 (defun icalendar--read-element (invalue inparams
)
254 "Recursively read the next iCalendar element in the current buffer.
255 INVALUE gives the current iCalendar element we are reading.
256 INPARAMS gives the current parameters.....
257 This function calls itself recursively for each nested calendar element
259 (let (element children line name params param param-name param-value
264 (re-search-forward "^\\([A-Za-z0-9-]+\\)[;:]" nil t
))
265 (setq name
(intern (match-string 1)))
269 (while (looking-at ";")
270 (re-search-forward ";\\([A-Za-z0-9-]+\\)=" nil nil
)
271 (setq param-name
(intern (match-string 1)))
272 (re-search-forward "\\(\\([^;,:\"]+\\)\\|\"\\([^\"]+\\)\"\\)[;:]"
275 (setq param-value
(or (match-string 2) (match-string 3)))
276 (setq param
(list param-name param-value
))
277 (while (looking-at ",")
278 (re-search-forward "\\(\\([^;,:]+\\)\\|\"\\([^\"]+\\)\"\\)"
281 (setq param-value
(match-string 2))
282 (setq param-value
(match-string 3)))
283 (setq param
(append param param-value
)))
284 (setq params
(append params param
)))
285 (unless (looking-at ":")
288 (re-search-forward "\\(.*\\)\\(\r?\n[ \t].*\\)*" nil t
)
289 (setq value
(icalendar--rris "\r?\n[ \t]" "" (match-string 0)))
290 (setq line
(list name params value
))
291 (cond ((eq name
'BEGIN
)
294 (list (icalendar--read-element (intern value
)
299 (setq element
(append element
(list line
))))))
301 (list invalue inparams element children
)
304 ;; ======================================================================
305 ;; helper functions for examining events
306 ;; ======================================================================
308 ;;(defsubst icalendar--get-all-event-properties (event)
309 ;; "Return the list of properties in this EVENT."
310 ;; (car (cddr event)))
312 (defun icalendar--get-event-property (event prop
)
313 "For the given EVENT return the value of the first occurrence of PROP."
315 (let ((props (car (cddr event
))) pp
)
317 (setq pp
(car props
))
318 (if (eq (car pp
) prop
)
319 (throw 'found
(car (cddr pp
))))
320 (setq props
(cdr props
))))
323 (defun icalendar--get-event-property-attributes (event prop
)
324 "For the given EVENT return attributes of the first occurrence of PROP."
326 (let ((props (car (cddr event
))) pp
)
328 (setq pp
(car props
))
329 (if (eq (car pp
) prop
)
330 (throw 'found
(cadr pp
)))
331 (setq props
(cdr props
))))
334 (defun icalendar--get-event-properties (event prop
)
335 "For the given EVENT return a list of all values of the property PROP."
336 (let ((props (car (cddr event
))) pp result
)
338 (setq pp
(car props
))
339 (if (eq (car pp
) prop
)
340 (setq result
(append (split-string (car (cddr pp
)) ",") result
)))
341 (setq props
(cdr props
)))
344 ;; (defun icalendar--set-event-property (event prop new-value)
345 ;; "For the given EVENT set the property PROP to the value NEW-VALUE."
347 ;; (let ((props (car (cddr event))) pp)
349 ;; (setq pp (car props))
350 ;; (when (eq (car pp) prop)
351 ;; (setcdr (cdr pp) new-value)
352 ;; (throw 'found (car (cddr pp))))
353 ;; (setq props (cdr props)))
354 ;; (setq props (car (cddr event)))
355 ;; (setcar (cddr event)
356 ;; (append props (list (list prop nil new-value)))))))
358 (defun icalendar--get-children (node name
)
359 "Return all children of the given NODE which have a name NAME.
360 For instance the VCALENDAR node can have VEVENT children as well as VTODO
363 (children (cadr (cddr node
))))
364 (when (eq (car node
) name
)
366 ;;(message "%s" node)
371 (icalendar--get-children n name
))
375 (setq result
(append result subresult
))
376 (setq result subresult
)))))
380 (defun icalendar--all-events (icalendar)
381 "Return the list of all existing events in the given ICALENDAR."
382 (icalendar--get-children (car icalendar
) 'VEVENT
))
384 (defun icalendar--split-value (value-string)
385 "Split VALUE-STRING at ';='."
387 param-name param-value
)
390 (set-buffer (get-buffer-create " *icalendar-work*"))
391 (set-buffer-modified-p nil
)
393 (insert value-string
)
394 (goto-char (point-min))
397 "\\([A-Za-z0-9-]+\\)=\\(\\([^;,:]+\\)\\|\"\\([^\"]+\\)\"\\);?"
399 (setq param-name
(intern (match-string 1)))
400 (setq param-value
(match-string 2))
402 (append result
(list (list param-name param-value
)))))))
405 (defun icalendar--convert-tz-offset (alist dst-p
)
406 "Return a cons of two strings representing a timezone start.
407 ALIST is an alist entry from a VTIMEZONE, like STANDARD.
408 DST-P is non-nil if this is for daylight savings time.
409 The strings are suitable for assembling into a TZ variable."
410 (let ((offset (car (cddr (assq 'TZOFFSETTO alist
))))
411 (rrule-value (car (cddr (assq 'RRULE alist
))))
412 (dtstart (car (cddr (assq 'DTSTART alist
)))))
413 ;; FIXME: for now we only handle RRULE and not RDATE here.
414 (when (and offset rrule-value dtstart
)
415 (let* ((rrule (icalendar--split-value rrule-value
))
416 (freq (cadr (assq 'FREQ rrule
)))
417 (bymonth (cadr (assq 'BYMONTH rrule
)))
418 (byday (cadr (assq 'BYDAY rrule
))))
419 ;; FIXME: we don't correctly handle WKST here.
420 (if (and (string= freq
"YEARLY") bymonth
)
424 (if dst-p
"(DST?)" "(STD?)")
425 ;; For TZ, OFFSET is added to the local time. So,
426 ;; invert the values.
427 (if (eq (aref offset
0) ?-
) "+" "-")
428 (substring offset
1 3)
430 (substring offset
3 5))
432 (let* ((day (icalendar--get-weekday-number (substring byday -
2)))
433 (week (if (eq day -
1)
435 (substring byday
0 -
2))))
436 (concat "M" bymonth
"." week
"." (if (eq day -
1) "0"
440 (substring dtstart -
6 -
4)
442 (substring dtstart -
4 -
2)
444 (substring dtstart -
2)))))))))
446 (defun icalendar--parse-vtimezone (alist)
447 "Turn a VTIMEZONE ALIST into a cons (ID . TZ-STRING).
448 Return nil if timezone cannot be parsed."
449 (let* ((tz-id (icalendar--get-event-property alist
'TZID
))
450 (daylight (cadr (cdar (icalendar--get-children alist
'DAYLIGHT
))))
451 (day (and daylight
(icalendar--convert-tz-offset daylight t
)))
452 (standard (cadr (cdar (icalendar--get-children alist
'STANDARD
))))
453 (std (and standard
(icalendar--convert-tz-offset standard nil
))))
457 (concat (car std
) (car day
)
458 "," (cdr day
) "," (cdr std
))
461 (defun icalendar--convert-all-timezones (icalendar)
462 "Convert all timezones in the ICALENDAR into an alist.
463 Each element of the alist is a cons (ID . TZ-STRING),
464 like `icalendar--parse-vtimezone'."
466 (dolist (zone (icalendar--get-children (car icalendar
) 'VTIMEZONE
))
467 (setq zone
(icalendar--parse-vtimezone zone
))
469 (setq result
(cons zone result
))))
472 (defun icalendar--find-time-zone (prop-list zone-map
)
473 "Return a timezone string for the time zone in PROP-LIST, or nil if none.
474 ZONE-MAP is a timezone alist as returned by `icalendar--convert-all-timezones'."
475 (let ((id (plist-get prop-list
'TZID
)))
477 (cdr (assoc id zone-map
)))))
479 (defun icalendar--decode-isodatetime (isodatetimestring &optional day-shift
481 "Return ISODATETIMESTRING in format like `decode-time'.
482 Converts from ISO-8601 to Emacs representation. If
483 ISODATETIMESTRING specifies UTC time (trailing letter Z) the
484 decoded time is given in the local time zone! If optional
485 parameter DAY-SHIFT is non-nil the result is shifted by DAY-SHIFT
487 ZONE, if provided, is the timezone, in any format understood by `encode-time'.
489 FIXME: multiple comma-separated values should be allowed!"
490 (icalendar--dmsg isodatetimestring
)
491 (if isodatetimestring
492 ;; day/month/year must be present
493 (let ((year (read (substring isodatetimestring
0 4)))
494 (month (read (substring isodatetimestring
4 6)))
495 (day (read (substring isodatetimestring
6 8)))
499 (when (> (length isodatetimestring
) 12)
500 ;; hour/minute present
501 (setq hour
(read (substring isodatetimestring
9 11)))
502 (setq minute
(read (substring isodatetimestring
11 13))))
503 (when (> (length isodatetimestring
) 14)
505 (setq second
(read (substring isodatetimestring
13 15))))
506 (when (and (> (length isodatetimestring
) 15)
507 ;; UTC specifier present
508 (char-equal ?Z
(aref isodatetimestring
15)))
509 ;; if not UTC add current-time-zone offset
510 (setq second
(+ (car (current-time-zone)) second
)))
511 ;; shift if necessary
513 (let ((mdy (calendar-gregorian-from-absolute
514 (+ (calendar-absolute-from-gregorian
515 (list month day year
))
517 (setq month
(nth 0 mdy
))
518 (setq day
(nth 1 mdy
))
519 (setq year
(nth 2 mdy
))))
520 ;; create the decoded date-time
523 (decode-time (encode-time second minute hour day month year zone
))
525 (message "Cannot decode \"%s\"" isodatetimestring
)
526 ;; hope for the best...
527 (list second minute hour day month year
0 nil
0))))
528 ;; isodatetimestring == nil
531 (defun icalendar--decode-isoduration (isodurationstring
532 &optional duration-correction
)
533 "Convert ISODURATIONSTRING into format provided by `decode-time'.
534 Converts from ISO-8601 to Emacs representation. If ISODURATIONSTRING
535 specifies UTC time (trailing letter Z) the decoded time is given in
538 Optional argument DURATION-CORRECTION shortens result by one day.
540 FIXME: TZID-attributes are ignored....!
541 FIXME: multiple comma-separated values should be allowed!"
542 (if isodurationstring
547 "\\(\\([0-9]+\\)D\\)" ; days only
549 "\\(\\(\\([0-9]+\\)D\\)?T\\(\\([0-9]+\\)H\\)?" ; opt days
550 "\\(\\([0-9]+\\)M\\)?\\(\\([0-9]+\\)S\\)?\\)" ; mand. time
552 "\\(\\([0-9]+\\)W\\)" ; weeks only
553 "\\)$") isodurationstring
)
561 ((match-beginning 2) ;days only
562 (setq days
(read (substring isodurationstring
565 (when duration-correction
566 (setq days
(1- days
))))
567 ((match-beginning 4) ;days and time
568 (if (match-beginning 5)
569 (setq days
(* 7 (read (substring isodurationstring
572 (if (match-beginning 7)
573 (setq hours
(read (substring isodurationstring
576 (if (match-beginning 9)
577 (setq minutes
(read (substring isodurationstring
580 (if (match-beginning 11)
581 (setq seconds
(read (substring isodurationstring
584 ((match-beginning 13) ;weeks only
585 (setq days
(* 7 (read (substring isodurationstring
588 (list seconds minutes hours days months years
)))
589 ;; isodatetimestring == nil
592 (defun icalendar--add-decoded-times (time1 time2
)
594 Both times must be given in decoded form. One of these times must be
595 valid (year > 1900 or something)."
596 ;; FIXME: does this function exist already?
597 (decode-time (encode-time
598 (+ (nth 0 time1
) (nth 0 time2
))
599 (+ (nth 1 time1
) (nth 1 time2
))
600 (+ (nth 2 time1
) (nth 2 time2
))
601 (+ (nth 3 time1
) (nth 3 time2
))
602 (+ (nth 4 time1
) (nth 4 time2
))
603 (+ (nth 5 time1
) (nth 5 time2
))
606 ;;(or (nth 6 time1) (nth 6 time2)) ;; FIXME?
609 (defun icalendar--datetime-to-noneuropean-date (datetime &optional separator
)
610 "Convert the decoded DATETIME to non-european-style format.
611 Optional argument SEPARATOR gives the separator between month,
612 day, and year. If nil a blank character is used as separator.
613 Non-European format: \"month day year\"."
615 (format "%d%s%d%s%d" (nth 4 datetime
) ;month
617 (nth 3 datetime
) ;day
619 (nth 5 datetime
)) ;year
623 (defun icalendar--datetime-to-european-date (datetime &optional separator
)
624 "Convert the decoded DATETIME to European format.
625 Optional argument SEPARATOR gives the separator between month,
626 day, and year. If nil a blank character is used as separator.
627 European format: (day month year).
630 (format "%d%s%d%s%d" (nth 3 datetime
) ;day
632 (nth 4 datetime
) ;month
634 (nth 5 datetime
)) ;year
638 (defun icalendar--datetime-to-diary-date (datetime &optional separator
)
639 "Convert the decoded DATETIME to diary format.
640 Optional argument SEPARATOR gives the separator between month,
641 day, and year. If nil a blank character is used as separator.
642 Call icalendar--datetime-to-(non)-european-date according to
643 value of `european-calendar-style'."
644 (if european-calendar-style
645 (icalendar--datetime-to-european-date datetime separator
)
646 (icalendar--datetime-to-noneuropean-date datetime separator
)))
648 (defun icalendar--datetime-to-colontime (datetime)
649 "Extract the time part of a decoded DATETIME into 24-hour format.
650 Note that this silently ignores seconds."
651 (format "%02d:%02d" (nth 2 datetime
) (nth 1 datetime
)))
653 (defun icalendar--get-month-number (monthname)
654 "Return the month number for the given MONTHNAME."
657 (m (downcase monthname
)))
658 (mapc (lambda (month)
659 (let ((mm (downcase month
)))
660 (if (or (string-equal mm m
)
661 (string-equal (substring mm
0 3) m
))
663 (setq num
(1+ num
))))
664 calendar-month-name-array
))
668 (defun icalendar--get-weekday-number (abbrevweekday)
669 "Return the number for the ABBREVWEEKDAY."
673 (aw (downcase abbrevweekday
)))
675 (let ((d (downcase day
)))
676 (if (string-equal d aw
)
678 (setq num
(1+ num
))))
679 icalendar--weekday-array
)))
683 (defun icalendar--get-weekday-abbrev (weekday)
684 "Return the abbreviated WEEKDAY."
687 (w (downcase weekday
)))
689 (let ((d (downcase day
)))
690 (if (or (string-equal d w
)
691 (string-equal (substring d
0 3) w
))
692 (throw 'found
(aref icalendar--weekday-array num
)))
693 (setq num
(1+ num
))))
694 calendar-day-name-array
))
698 (defun icalendar--date-to-isodate (date &optional day-shift
)
699 "Convert DATE to iso-style date.
700 DATE must be a list of the form (month day year).
701 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days."
702 (let ((mdy (calendar-gregorian-from-absolute
703 (+ (calendar-absolute-from-gregorian date
)
705 (format "%04d%02d%02d" (nth 2 mdy
) (nth 0 mdy
) (nth 1 mdy
))))
708 (defun icalendar--datestring-to-isodate (datestring &optional day-shift
)
709 "Convert diary-style DATESTRING to iso-style date.
710 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days
711 -- DAY-SHIFT must be either nil or an integer. This function
712 takes care of european-style."
713 (let ((day -
1) month year
)
715 (cond ( ;; numeric date
716 (string-match (concat "\\s-*"
717 "0?\\([1-9][0-9]?\\)[ \t/]\\s-*"
718 "0?\\([1-9][0-9]?\\),?[ \t/]\\s-*"
719 "\\([0-9]\\{4\\}\\)")
721 (setq day
(read (substring datestring
(match-beginning 1)
723 (setq month
(read (substring datestring
(match-beginning 2)
725 (setq year
(read (substring datestring
(match-beginning 3)
727 (unless european-calendar-style
731 ( ;; date contains month names -- european-style
732 (string-match (concat "\\s-*"
733 "0?\\([123]?[0-9]\\)[ \t/]\\s-*"
734 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
735 "\\([0-9]\\{4\\}\\)")
737 (setq day
(read (substring datestring
(match-beginning 1)
739 (setq month
(icalendar--get-month-number
740 (substring datestring
(match-beginning 2)
742 (setq year
(read (substring datestring
(match-beginning 3)
744 ( ;; date contains month names -- non-european-style
745 (string-match (concat "\\s-*"
746 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
747 "0?\\([123]?[0-9]\\),?[ \t/]\\s-*"
748 "\\([0-9]\\{4\\}\\)")
750 (setq day
(read (substring datestring
(match-beginning 2)
752 (setq month
(icalendar--get-month-number
753 (substring datestring
(match-beginning 1)
755 (setq year
(read (substring datestring
(match-beginning 3)
760 (let ((mdy (calendar-gregorian-from-absolute
761 (+ (calendar-absolute-from-gregorian (list month day
764 (format "%04d%02d%02d" (nth 2 mdy
) (nth 0 mdy
) (nth 1 mdy
)))
767 (defun icalendar--diarytime-to-isotime (timestring ampmstring
)
768 "Convert a time like 9:30pm to an iso-conform string like T213000.
769 In this example the TIMESTRING would be \"9:30\" and the AMPMSTRING
772 (let ((starttimenum (read (icalendar--rris ":" "" timestring
))))
773 ;; take care of am/pm style
774 (if (and ampmstring
(string= "pm" ampmstring
))
775 (setq starttimenum
(+ starttimenum
1200)))
776 (format "T%04d00" starttimenum
))
779 (defun icalendar--convert-string-for-export (string)
780 "Escape comma and other critical characters in STRING."
781 (icalendar--rris "," "\\\\," string
))
783 (defun icalendar--convert-string-for-import (string)
784 "Remove escape chars for comma, semicolon etc. from STRING."
786 "\\\\n" "\n " (icalendar--rris
787 "\\\\\"" "\"" (icalendar--rris
788 "\\\\;" ";" (icalendar--rris
789 "\\\\," "," string
)))))
791 ;; ======================================================================
792 ;; Export -- convert emacs-diary to icalendar
793 ;; ======================================================================
796 (defun icalendar-export-file (diary-filename ical-filename
)
797 "Export diary file to iCalendar format.
798 All diary entries in the file DIARY-FILENAME are converted to iCalendar
799 format. The result is appended to the file ICAL-FILENAME."
800 (interactive "FExport diary data from file:
801 Finto iCalendar file: ")
803 (set-buffer (find-file diary-filename
))
804 (icalendar-export-region (point-min) (point-max) ical-filename
)))
806 (defalias 'icalendar-convert-diary-to-ical
'icalendar-export-file
)
807 (make-obsolete 'icalendar-convert-diary-to-ical
'icalendar-export-file
)
810 (defun icalendar-export-region (min max ical-filename
)
811 "Export region in diary file to iCalendar format.
812 All diary entries in the region from MIN to MAX in the current buffer are
813 converted to iCalendar format. The result is appended to the file
815 This function attempts to return t if something goes wrong. In this
816 case an error string which describes all the errors and problems is
817 written into the buffer `*icalendar-errors*'."
819 FExport diary data into iCalendar file: ")
828 (nonmarker (concat "^" (regexp-quote diary-nonmarking-symbol
)
830 (other-elements nil
))
831 ;; prepare buffer with error messages
833 (set-buffer (get-buffer-create "*icalendar-errors*"))
839 (while (re-search-forward
840 "^\\([^ \t\n].+\\)\\(\\(\n[ \t].*\\)*\\)" max t
)
841 (setq entry-main
(match-string 1))
842 (if (match-beginning 2)
843 (setq entry-rest
(match-string 2))
844 (setq entry-rest
""))
845 (setq header
(format "\nBEGIN:VEVENT\nUID:emacs%d%d%d"
847 (cadr (current-time))
848 (car (cddr (current-time)))))
849 (condition-case error-val
851 (setq contents-n-summary
852 (icalendar--convert-to-ical nonmarker entry-main
))
853 (setq other-elements
(icalendar--parse-summary-and-rest
854 (concat entry-main entry-rest
)))
855 (setq contents
(concat (car contents-n-summary
)
856 "\nSUMMARY:" (cadr contents-n-summary
)))
857 (let ((cla (cdr (assoc 'cla other-elements
)))
858 (des (cdr (assoc 'des other-elements
)))
859 (loc (cdr (assoc 'loc other-elements
)))
860 (org (cdr (assoc 'org other-elements
)))
861 (sta (cdr (assoc 'sta other-elements
)))
862 (sum (cdr (assoc 'sum other-elements
)))
863 (url (cdr (assoc 'url other-elements
))))
865 (setq contents
(concat contents
"\nCLASS:" cla
)))
867 (setq contents
(concat contents
"\nDESCRIPTION:" des
)))
869 (setq contents
(concat contents
"\nLOCATION:" loc
)))
871 (setq contents
(concat contents
"\nORGANIZER:" org
)))
873 (setq contents
(concat contents
"\nSTATUS:" sta
)))
875 ;; (setq contents (concat contents "\nSUMMARY:" sum)))
877 (setq contents
(concat contents
"\nURL:" url
))))
878 (setq result
(concat result header contents
"\nEND:VEVENT")))
883 (set-buffer (get-buffer-create "*icalendar-errors*"))
884 (insert (format "Error in line %d -- %s: `%s'\n"
885 (count-lines (point-min) (point))
889 ;; we're done, insert everything into the file
891 (let ((coding-system-for-write 'utf-8
))
892 (set-buffer (find-file ical-filename
))
893 (goto-char (point-max))
894 (insert "BEGIN:VCALENDAR")
895 (insert "\nPRODID:-//Emacs//NONSGML icalendar.el//EN")
896 (insert "\nVERSION:2.0")
898 (insert "\nEND:VCALENDAR\n")
899 ;; save the diary file
905 (defun icalendar--convert-to-ical (nonmarker entry-main
)
906 "Convert a diary entry to icalendar format.
907 NONMARKER is a regular expression matching the start of non-marking
908 entries. ENTRY-MAIN is the first line of the diary entry."
910 ;; anniversaries -- %%(diary-anniversary ...)
911 (icalendar--convert-anniversary-to-ical nonmarker entry-main
)
912 ;; cyclic events -- %%(diary-cyclic ...)
913 (icalendar--convert-cyclic-to-ical nonmarker entry-main
)
914 ;; diary-date -- %%(diary-date ...)
915 (icalendar--convert-date-to-ical nonmarker entry-main
)
916 ;; float events -- %%(diary-float ...)
917 (icalendar--convert-float-to-ical nonmarker entry-main
)
918 ;; block events -- %%(diary-block ...)
919 (icalendar--convert-block-to-ical nonmarker entry-main
)
920 ;; other sexp diary entries
921 (icalendar--convert-sexp-to-ical nonmarker entry-main
)
922 ;; weekly by day -- Monday 8:30 Team meeting
923 (icalendar--convert-weekly-to-ical nonmarker entry-main
)
924 ;; yearly by day -- 1 May Tag der Arbeit
925 (icalendar--convert-yearly-to-ical nonmarker entry-main
)
926 ;; "ordinary" events, start and end time given
928 (icalendar--convert-ordinary-to-ical nonmarker entry-main
)
930 ;; Oops! what's that?
931 (error "Could not parse entry")))
933 (defun icalendar--parse-summary-and-rest (summary-and-rest)
934 "Parse SUMMARY-AND-REST from a diary to fill iCalendar properties.
937 (if (functionp icalendar-import-format
)
940 ;; split summary-and-rest
941 (let* ((s icalendar-import-format
)
942 (p-cla (or (string-match "%c" icalendar-import-format
) -
1))
943 (p-des (or (string-match "%d" icalendar-import-format
) -
1))
944 (p-loc (or (string-match "%l" icalendar-import-format
) -
1))
945 (p-org (or (string-match "%o" icalendar-import-format
) -
1))
946 (p-sum (or (string-match "%s" icalendar-import-format
) -
1))
947 (p-sta (or (string-match "%t" icalendar-import-format
) -
1))
948 (p-url (or (string-match "%u" icalendar-import-format
) -
1))
949 (p-list (sort (list p-cla p-des p-loc p-org p-sta p-sum p-url
) '<))
950 pos-cla pos-des pos-loc pos-org pos-sta pos-sum pos-url
)
951 (dotimes (i (length p-list
))
952 (cond ((and (>= p-cla
0) (= (nth i p-list
) p-cla
))
953 (setq pos-cla
(+ 2 (* 2 i
))))
954 ((and (>= p-des
0) (= (nth i p-list
) p-des
))
955 (setq pos-des
(+ 2 (* 2 i
))))
956 ((and (>= p-loc
0) (= (nth i p-list
) p-loc
))
957 (setq pos-loc
(+ 2 (* 2 i
))))
958 ((and (>= p-org
0) (= (nth i p-list
) p-org
))
959 (setq pos-org
(+ 2 (* 2 i
))))
960 ((and (>= p-sta
0) (= (nth i p-list
) p-sta
))
961 (setq pos-sta
(+ 2 (* 2 i
))))
962 ((and (>= p-sum
0) (= (nth i p-list
) p-sum
))
963 (setq pos-sum
(+ 2 (* 2 i
))))
964 ((and (>= p-url
0) (= (nth i p-list
) p-url
))
965 (setq pos-url
(+ 2 (* 2 i
))))))
967 (setq s
(icalendar--rris (car ij
) (cadr ij
) s t t
)))
969 ;; summary must be first! because of %s
971 (concat "\\(" icalendar-import-format-summary
"\\)??"))
973 (concat "\\(" icalendar-import-format-class
"\\)??"))
975 (concat "\\(" icalendar-import-format-description
"\\)??"))
977 (concat "\\(" icalendar-import-format-location
"\\)??"))
979 (concat "\\(" icalendar-import-format-organizer
"\\)??"))
981 (concat "\\(" icalendar-import-format-status
"\\)??"))
983 (concat "\\(" icalendar-import-format-url
"\\)??"))))
984 (setq s
(concat "^" (icalendar--rris "%s" "\\(.*?\\)" s nil t
)
986 (if (string-match s summary-and-rest
)
987 (let (cla des loc org sta sum url
)
988 (if (and pos-sum
(match-beginning pos-sum
))
989 (setq sum
(substring summary-and-rest
990 (match-beginning pos-sum
)
991 (match-end pos-sum
))))
992 (if (and pos-cla
(match-beginning pos-cla
))
993 (setq cla
(substring summary-and-rest
994 (match-beginning pos-cla
)
995 (match-end pos-cla
))))
996 (if (and pos-des
(match-beginning pos-des
))
997 (setq des
(substring summary-and-rest
998 (match-beginning pos-des
)
999 (match-end pos-des
))))
1000 (if (and pos-loc
(match-beginning pos-loc
))
1001 (setq loc
(substring summary-and-rest
1002 (match-beginning pos-loc
)
1003 (match-end pos-loc
))))
1004 (if (and pos-org
(match-beginning pos-org
))
1005 (setq org
(substring summary-and-rest
1006 (match-beginning pos-org
)
1007 (match-end pos-org
))))
1008 (if (and pos-sta
(match-beginning pos-sta
))
1009 (setq sta
(substring summary-and-rest
1010 (match-beginning pos-sta
)
1011 (match-end pos-sta
))))
1012 (if (and pos-url
(match-beginning pos-url
))
1013 (setq url
(substring summary-and-rest
1014 (match-beginning pos-url
)
1015 (match-end pos-url
))))
1016 (list (if cla
(cons 'cla cla
) nil
)
1017 (if des
(cons 'des des
) nil
)
1018 (if loc
(cons 'loc loc
) nil
)
1019 (if org
(cons 'org org
) nil
)
1020 (if sta
(cons 'sta sta
) nil
)
1021 ;;(if sum (cons 'sum sum) nil)
1022 (if url
(cons 'url url
) nil
))))))))
1024 ;; subroutines for icalendar-export-region
1025 (defun icalendar--convert-ordinary-to-ical (nonmarker entry-main
)
1026 "Convert \"ordinary\" diary entry to icalendar format.
1027 NONMARKER is a regular expression matching the start of non-marking
1028 entries. ENTRY-MAIN is the first line of the diary entry."
1029 (if (string-match (concat nonmarker
1030 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)\\s-*"
1031 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1033 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1035 "\\s-*\\(.*?\\) ?$")
1037 (let* ((datetime (substring entry-main
(match-beginning 1)
1039 (startisostring (icalendar--datestring-to-isodate
1041 (endisostring (icalendar--datestring-to-isodate
1043 (starttimestring (icalendar--diarytime-to-isotime
1044 (if (match-beginning 3)
1045 (substring entry-main
1049 (if (match-beginning 4)
1050 (substring entry-main
1054 (endtimestring (icalendar--diarytime-to-isotime
1055 (if (match-beginning 6)
1056 (substring entry-main
1060 (if (match-beginning 7)
1061 (substring entry-main
1065 (summary (icalendar--convert-string-for-export
1066 (substring entry-main
(match-beginning 8)
1068 (icalendar--dmsg "ordinary %s" entry-main
)
1070 (unless startisostring
1071 (error "Could not parse date"))
1072 (when starttimestring
1073 (unless endtimestring
1075 (read (icalendar--rris "^T0?" ""
1077 (setq endtimestring
(format "T%06d"
1079 (list (concat "\nDTSTART;"
1080 (if starttimestring
"VALUE=DATE-TIME:"
1083 (or starttimestring
"")
1085 (if endtimestring
"VALUE=DATE-TIME:"
1090 (or endtimestring
""))
1095 (defun icalendar--convert-weekly-to-ical (nonmarker entry-main
)
1096 "Convert weekly diary entry to icalendar format.
1097 NONMARKER is a regular expression matching the start of non-marking
1098 entries. ENTRY-MAIN is the first line of the diary entry."
1099 (if (and (string-match (concat nonmarker
1101 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)"
1104 "\\([1-9][0-9]?:[0-9][0-9]\\)"
1107 "\\s-*\\(.*?\\) ?$")
1109 (icalendar--get-weekday-abbrev
1110 (substring entry-main
(match-beginning 1)
1112 (let* ((day (icalendar--get-weekday-abbrev
1113 (substring entry-main
(match-beginning 1)
1115 (starttimestring (icalendar--diarytime-to-isotime
1116 (if (match-beginning 3)
1117 (substring entry-main
1121 (if (match-beginning 4)
1122 (substring entry-main
1126 (endtimestring (icalendar--diarytime-to-isotime
1127 (if (match-beginning 6)
1128 (substring entry-main
1132 (if (match-beginning 7)
1133 (substring entry-main
1137 (summary (icalendar--convert-string-for-export
1138 (substring entry-main
(match-beginning 8)
1140 (icalendar--dmsg "weekly %s" entry-main
)
1142 (when starttimestring
1143 (unless endtimestring
1145 (icalendar--rris "^T0?" ""
1147 (setq endtimestring
(format "T%06d"
1149 (list (concat "\nDTSTART;"
1153 ;; find the correct week day,
1154 ;; 1st january 2000 was a saturday
1157 (+ (icalendar--get-weekday-number day
) 2))
1158 (or starttimestring
"")
1165 ;; end is non-inclusive!
1166 (+ (icalendar--get-weekday-number day
)
1167 (if endtimestring
2 3)))
1168 (or endtimestring
"")
1169 "\nRRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY="
1175 (defun icalendar--convert-yearly-to-ical (nonmarker entry-main
)
1176 "Convert yearly diary entry to icalendar format.
1177 NONMARKER is a regular expression matching the start of non-marking
1178 entries. ENTRY-MAIN is the first line of the diary entry."
1179 (if (string-match (concat nonmarker
1180 (if european-calendar-style
1181 "0?\\([1-9]+[0-9]?\\)\\s-+\\([a-z]+\\)\\s-+"
1182 "\\([a-z]+\\)\\s-+0?\\([1-9]+[0-9]?\\)\\s-+")
1184 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1186 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1188 "\\s-*\\([^0-9]+.*?\\) ?$" ; must not match years
1191 (let* ((daypos (if european-calendar-style
1 2))
1192 (monpos (if european-calendar-style
2 1))
1193 (day (read (substring entry-main
1194 (match-beginning daypos
)
1195 (match-end daypos
))))
1196 (month (icalendar--get-month-number
1197 (substring entry-main
1198 (match-beginning monpos
)
1199 (match-end monpos
))))
1200 (starttimestring (icalendar--diarytime-to-isotime
1201 (if (match-beginning 4)
1202 (substring entry-main
1206 (if (match-beginning 5)
1207 (substring entry-main
1211 (endtimestring (icalendar--diarytime-to-isotime
1212 (if (match-beginning 7)
1213 (substring entry-main
1217 (if (match-beginning 8)
1218 (substring entry-main
1222 (summary (icalendar--convert-string-for-export
1223 (substring entry-main
(match-beginning 9)
1225 (icalendar--dmsg "yearly %s" entry-main
)
1227 (when starttimestring
1228 (unless endtimestring
1230 (icalendar--rris "^T0?" ""
1232 (setq endtimestring
(format "T%06d"
1234 (list (concat "\nDTSTART;"
1235 (if starttimestring
"VALUE=DATE-TIME:"
1237 (format "1900%02d%02d" month day
)
1238 (or starttimestring
"")
1240 (if endtimestring
"VALUE=DATE-TIME:"
1242 ;; end is not included! shift by one day
1243 (icalendar--date-to-isodate
1244 (list month day
1900)
1245 (if endtimestring
0 1))
1246 (or endtimestring
"")
1247 "\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH="
1248 (format "%2d" month
)
1255 (defun icalendar--convert-sexp-to-ical (nonmarker entry-main
)
1256 "Convert complex sexp diary entry to icalendar format -- unsupported!
1260 NONMARKER is a regular expression matching the start of non-marking
1261 entries. ENTRY-MAIN is the first line of the diary entry."
1262 (cond ((string-match (concat nonmarker
1263 "%%(and \\(([^)]+)\\))\\(\\s-*.*?\\) ?$")
1265 ;; simple sexp entry as generated by icalendar.el: strip off the
1266 ;; unnecessary (and)
1267 (icalendar--dmsg "diary-sexp from icalendar.el %s" entry-main
)
1268 (icalendar--convert-to-ical
1271 (substring entry-main
(match-beginning 1) (match-end 1))
1272 (substring entry-main
(match-beginning 2) (match-end 2)))))
1273 ((string-match (concat nonmarker
1276 (icalendar--dmsg "diary-sexp %s" entry-main
)
1277 (error "Sexp-entries are not supported yet"))
1282 (defun icalendar--convert-block-to-ical (nonmarker entry-main
)
1283 "Convert block diary entry to icalendar format.
1284 NONMARKER is a regular expression matching the start of non-marking
1285 entries. ENTRY-MAIN is the first line of the diary entry."
1286 (if (string-match (concat nonmarker
1287 "%%(diary-block \\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)"
1288 " +\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
1289 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1291 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1293 "\\s-*\\(.*?\\) ?$")
1295 (let* ((startstring (substring entry-main
1298 (endstring (substring entry-main
1301 (startisostring (icalendar--datestring-to-isodate
1303 (endisostring (icalendar--datestring-to-isodate
1305 (endisostring+1 (icalendar--datestring-to-isodate
1307 (starttimestring (icalendar--diarytime-to-isotime
1308 (if (match-beginning 4)
1309 (substring entry-main
1313 (if (match-beginning 5)
1314 (substring entry-main
1318 (endtimestring (icalendar--diarytime-to-isotime
1319 (if (match-beginning 7)
1320 (substring entry-main
1324 (if (match-beginning 8)
1325 (substring entry-main
1329 (summary (icalendar--convert-string-for-export
1330 (substring entry-main
(match-beginning 9)
1332 (icalendar--dmsg "diary-block %s" entry-main
)
1333 (when starttimestring
1334 (unless endtimestring
1336 (read (icalendar--rris "^T0?" ""
1338 (setq endtimestring
(format "T%06d"
1341 ;; with time -> write rrule
1342 (list (concat "\nDTSTART;VALUE=DATE-TIME:"
1345 "\nDTEND;VALUE=DATE-TIME:"
1348 "\nRRULE:FREQ=DAILY;INTERVAL=1;UNTIL="
1351 ;; no time -> write long event
1352 (list (concat "\nDTSTART;VALUE=DATE:" startisostring
1353 "\nDTEND;VALUE=DATE:" endisostring
+1)
1358 (defun icalendar--convert-float-to-ical (nonmarker entry-main
)
1359 "Convert float diary entry to icalendar format -- unsupported!
1363 NONMARKER is a regular expression matching the start of non-marking
1364 entries. ENTRY-MAIN is the first line of the diary entry."
1365 (if (string-match (concat nonmarker
1366 "%%(diary-float \\([^)]+\\))\\s-*\\(.*?\\) ?$")
1369 (icalendar--dmsg "diary-float %s" entry-main
)
1370 (error "`diary-float' is not supported yet"))
1374 (defun icalendar--convert-date-to-ical (nonmarker entry-main
)
1375 "Convert `diary-date' diary entry to icalendar format -- unsupported!
1379 NONMARKER is a regular expression matching the start of non-marking
1380 entries. ENTRY-MAIN is the first line of the diary entry."
1381 (if (string-match (concat nonmarker
1382 "%%(diary-date \\([^)]+\\))\\s-*\\(.*?\\) ?$")
1385 (icalendar--dmsg "diary-date %s" entry-main
)
1386 (error "`diary-date' is not supported yet"))
1390 (defun icalendar--convert-cyclic-to-ical (nonmarker entry-main
)
1391 "Convert `diary-cyclic' diary entry to icalendar format.
1392 NONMARKER is a regular expression matching the start of non-marking
1393 entries. ENTRY-MAIN is the first line of the diary entry."
1394 (if (string-match (concat nonmarker
1395 "%%(diary-cyclic \\([^ ]+\\) +"
1396 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
1397 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1399 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1401 "\\s-*\\(.*?\\) ?$")
1403 (let* ((frequency (substring entry-main
(match-beginning 1)
1405 (datetime (substring entry-main
(match-beginning 2)
1407 (startisostring (icalendar--datestring-to-isodate
1409 (endisostring (icalendar--datestring-to-isodate
1411 (endisostring+1 (icalendar--datestring-to-isodate
1413 (starttimestring (icalendar--diarytime-to-isotime
1414 (if (match-beginning 4)
1415 (substring entry-main
1419 (if (match-beginning 5)
1420 (substring entry-main
1424 (endtimestring (icalendar--diarytime-to-isotime
1425 (if (match-beginning 7)
1426 (substring entry-main
1430 (if (match-beginning 8)
1431 (substring entry-main
1435 (summary (icalendar--convert-string-for-export
1436 (substring entry-main
(match-beginning 9)
1438 (icalendar--dmsg "diary-cyclic %s" entry-main
)
1439 (when starttimestring
1440 (unless endtimestring
1442 (read (icalendar--rris "^T0?" ""
1444 (setq endtimestring
(format "T%06d"
1446 (list (concat "\nDTSTART;"
1447 (if starttimestring
"VALUE=DATE-TIME:"
1450 (or starttimestring
"")
1452 (if endtimestring
"VALUE=DATE-TIME:"
1454 (if endtimestring endisostring endisostring
+1)
1455 (or endtimestring
"")
1456 "\nRRULE:FREQ=DAILY;INTERVAL=" frequency
1457 ;; strange: korganizer does not expect
1458 ;; BYSOMETHING here...
1464 (defun icalendar--convert-anniversary-to-ical (nonmarker entry-main
)
1465 "Convert `diary-anniversary' diary entry to icalendar format.
1466 NONMARKER is a regular expression matching the start of non-marking
1467 entries. ENTRY-MAIN is the first line of the diary entry."
1468 (if (string-match (concat nonmarker
1469 "%%(diary-anniversary \\([^)]+\\))\\s-*"
1470 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1472 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1474 "\\s-*\\(.*?\\) ?$")
1476 (let* ((datetime (substring entry-main
(match-beginning 1)
1478 (startisostring (icalendar--datestring-to-isodate
1480 (endisostring (icalendar--datestring-to-isodate
1482 (starttimestring (icalendar--diarytime-to-isotime
1483 (if (match-beginning 3)
1484 (substring entry-main
1488 (if (match-beginning 4)
1489 (substring entry-main
1493 (endtimestring (icalendar--diarytime-to-isotime
1494 (if (match-beginning 6)
1495 (substring entry-main
1499 (if (match-beginning 7)
1500 (substring entry-main
1504 (summary (icalendar--convert-string-for-export
1505 (substring entry-main
(match-beginning 8)
1507 (icalendar--dmsg "diary-anniversary %s" entry-main
)
1508 (when starttimestring
1509 (unless endtimestring
1511 (read (icalendar--rris "^T0?" ""
1513 (setq endtimestring
(format "T%06d"
1515 (list (concat "\nDTSTART;"
1516 (if starttimestring
"VALUE=DATE-TIME:"
1519 (or starttimestring
"")
1521 (if endtimestring
"VALUE=DATE-TIME:"
1524 (or endtimestring
"")
1525 "\nRRULE:FREQ=YEARLY;INTERVAL=1"
1526 ;; the following is redundant,
1527 ;; but korganizer seems to expect this... ;(
1528 ;; and evolution doesn't understand it... :(
1529 ;; so... who is wrong?!
1531 (substring startisostring
4 6)
1533 (substring startisostring
6 8))
1538 ;; ======================================================================
1539 ;; Import -- convert icalendar to emacs-diary
1540 ;; ======================================================================
1543 (defun icalendar-import-file (ical-filename diary-filename
1544 &optional non-marking
)
1545 "Import an iCalendar file and append to a diary file.
1546 Argument ICAL-FILENAME output iCalendar file.
1547 Argument DIARY-FILENAME input `diary-file'.
1548 Optional argument NON-MARKING determines whether events are created as
1549 non-marking or not."
1550 (interactive "fImport iCalendar data from file:
1553 ;; clean up the diary file
1554 (save-current-buffer
1555 ;; now load and convert from the ical file
1556 (set-buffer (find-file ical-filename
))
1557 (icalendar-import-buffer diary-filename t non-marking
)))
1560 (defun icalendar-import-buffer (&optional diary-file do-not-ask
1562 "Extract iCalendar events from current buffer.
1564 This function searches the current buffer for the first iCalendar
1565 object, reads it and adds all VEVENT elements to the diary
1568 It will ask for each appointment whether to add it to the diary
1569 unless DO-NOT-ASK is non-nil. When called interactively,
1570 DO-NOT-ASK is nil, so that you are asked for each event.
1572 NON-MARKING determines whether diary events are created as
1575 Return code t means that importing worked well, return code nil
1576 means that an error has occurred. Error messages will be in the
1577 buffer `*icalendar-errors*'."
1579 (save-current-buffer
1581 (message "Preparing icalendar...")
1582 (set-buffer (icalendar--get-unfolded-buffer (current-buffer)))
1583 (goto-char (point-min))
1584 (message "Preparing icalendar...done")
1585 (if (re-search-forward "^BEGIN:VCALENDAR\\s-*$" nil t
)
1586 (let (ical-contents ical-errors
)
1588 (message "Reading icalendar...")
1590 (setq ical-contents
(icalendar--read-element nil nil
))
1591 (message "Reading icalendar...done")
1593 (message "Converting icalendar...")
1594 (setq ical-errors
(icalendar--convert-ical-to-diary
1596 diary-file do-not-ask non-marking
))
1598 ;; save the diary file if it is visited already
1599 (let ((b (find-buffer-visiting diary-file
)))
1601 (save-current-buffer
1604 (message "Converting icalendar...done")
1605 ;; return t if no error occurred
1608 "Current buffer does not contain icalendar contents!")
1609 ;; return nil, i.e. import did not work
1612 (defalias 'icalendar-extract-ical-from-buffer
'icalendar-import-buffer
)
1613 (make-obsolete 'icalendar-extract-ical-from-buffer
'icalendar-import-buffer
)
1615 (defun icalendar--format-ical-event (event)
1616 "Create a string representation of an iCalendar EVENT."
1617 (if (functionp icalendar-import-format
)
1618 (funcall icalendar-import-format event
)
1619 (let ((string icalendar-import-format
)
1621 '(("%c" CLASS icalendar-import-format-class
)
1622 ("%d" DESCRIPTION icalendar-import-format-description
)
1623 ("%l" LOCATION icalendar-import-format-location
)
1624 ("%o" ORGANIZER icalendar-import-format-organizer
)
1625 ("%s" SUMMARY icalendar-import-format-summary
)
1626 ("%t" STATUS icalendar-import-format-status
)
1627 ("%u" URL icalendar-import-format-url
))))
1628 ;; convert the specifiers in the format string
1630 (let* ((spec (car i
))
1632 (format (car (cddr i
)))
1633 (contents (icalendar--get-event-property event prop
))
1634 (formatted-contents ""))
1635 (when (and contents
(> (length contents
) 0))
1636 (setq formatted-contents
1637 (icalendar--rris "%s"
1638 (icalendar--convert-string-for-import
1640 (symbol-value format
)
1642 (setq string
(icalendar--rris spec
1649 (defun icalendar--convert-ical-to-diary (ical-list diary-file
1650 &optional do-not-ask
1652 "Convert iCalendar data to an Emacs diary file.
1653 Import VEVENTS from the iCalendar object ICAL-LIST and saves them to a
1654 DIARY-FILE. If DO-NOT-ASK is nil the user is asked for each event
1655 whether to actually import it. NON-MARKING determines whether diary
1656 events are created as non-marking.
1657 This function attempts to return t if something goes wrong. In this
1658 case an error string which describes all the errors and problems is
1659 written into the buffer `*icalendar-errors*'."
1660 (let* ((ev (icalendar--all-events ical-list
))
1664 (zone-map (icalendar--convert-all-timezones ical-list
))
1666 ;; step through all events/appointments
1671 (condition-case error-val
1672 (let* ((dtstart (icalendar--get-event-property e
'DTSTART
))
1673 (dtstart-zone (icalendar--find-time-zone
1674 (icalendar--get-event-property-attributes
1677 (dtstart-dec (icalendar--decode-isodatetime dtstart nil
1679 (start-d (icalendar--datetime-to-diary-date
1681 (start-t (icalendar--datetime-to-colontime dtstart-dec
))
1682 (dtend (icalendar--get-event-property e
'DTEND
))
1683 (dtend-zone (icalendar--find-time-zone
1684 (icalendar--get-event-property-attributes
1687 (dtend-dec (icalendar--decode-isodatetime dtend
1689 (dtend-1-dec (icalendar--decode-isodatetime dtend -
1
1694 (summary (icalendar--convert-string-for-import
1695 (or (icalendar--get-event-property e
'SUMMARY
)
1697 (rrule (icalendar--get-event-property e
'RRULE
))
1698 (rdate (icalendar--get-event-property e
'RDATE
))
1699 (duration (icalendar--get-event-property e
'DURATION
)))
1700 (icalendar--dmsg "%s: `%s'" start-d summary
)
1701 ;; check whether start-time is missing
1704 (cadr (icalendar--get-event-property-attributes
1709 (let ((dtend-dec-d (icalendar--add-decoded-times
1711 (icalendar--decode-isoduration duration
)))
1712 (dtend-1-dec-d (icalendar--add-decoded-times
1714 (icalendar--decode-isoduration duration
1716 (if (and dtend-dec
(not (eq dtend-dec dtend-dec-d
)))
1717 (message "Inconsistent endtime and duration for %s"
1719 (setq dtend-dec dtend-dec-d
)
1720 (setq dtend-1-dec dtend-1-dec-d
)))
1721 (setq end-d
(if dtend-dec
1722 (icalendar--datetime-to-diary-date dtend-dec
)
1724 (setq end-1-d
(if dtend-1-dec
1725 (icalendar--datetime-to-diary-date dtend-1-dec
)
1727 (setq end-t
(if (and
1731 (icalendar--get-event-property-attributes
1734 (icalendar--datetime-to-colontime dtend-dec
)
1736 (icalendar--dmsg "start-d: %s, end-d: %s" start-d end-d
)
1741 (icalendar--convert-recurring-to-diary e dtstart-dec start-t
1745 (icalendar--dmsg "rdate event")
1746 (setq diary-string
"")
1747 (mapc (lambda (datestring)
1749 (concat diary-string
1750 (format "......"))))
1751 (icalendar--split-value rdate
)))
1752 ;; non-recurring event
1754 ((not (string= start-d end-d
))
1756 (icalendar--convert-non-recurring-all-day-to-diary
1760 ((and start-t
(or (not end-t
)
1761 (not (string= start-t end-t
))))
1763 (icalendar--convert-non-recurring-not-all-day-to-diary
1764 e dtstart-dec dtend-dec start-t end-t
))
1768 (icalendar--dmsg "all day event")
1769 (setq diary-string
(icalendar--datetime-to-diary-date
1772 ;; add all other elements unless the user doesn't want to have
1777 (concat diary-string
" "
1778 (icalendar--format-ical-event e
)))
1779 (if do-not-ask
(setq summary nil
))
1780 ;; add entry to diary and store actual name of diary
1781 ;; file (in case it was nil)
1783 (icalendar--add-diary-entry diary-string diary-file
1784 non-marking summary
)))
1786 (setq found-error t
)
1788 (format "%s\nCannot handle this event:%s"
1790 ;; FIXME: inform user about ignored event properties
1793 (message "Ignoring event \"%s\"" e
)
1794 (setq found-error t
)
1795 (setq error-string
(format "%s\n%s\nCannot handle this event: %s"
1796 error-val error-string e
))
1797 (message "%s" error-string
))))
1799 ;; insert final newline
1801 (let ((b (find-buffer-visiting diary-file
)))
1803 (save-current-buffer
1805 (goto-char (point-max))
1808 (save-current-buffer
1809 (set-buffer (get-buffer-create "*icalendar-errors*"))
1811 (insert error-string
)))
1812 (message "Converting icalendar...done")
1815 ;; subroutines for importing
1816 (defun icalendar--convert-recurring-to-diary (e dtstart-dec start-t end-t
)
1817 "Convert recurring icalendar event E to diary format.
1819 DTSTART-DEC is the DTSTART property of E.
1820 START-T is the event's start time in diary format.
1821 END-T is the event's end time in diary format."
1822 (icalendar--dmsg "recurring event")
1823 (let* ((rrule (icalendar--get-event-property e
'RRULE
))
1824 (rrule-props (icalendar--split-value rrule
))
1825 (frequency (cadr (assoc 'FREQ rrule-props
)))
1826 (until (cadr (assoc 'UNTIL rrule-props
)))
1827 (count (cadr (assoc 'COUNT rrule-props
)))
1828 (interval (read (or (cadr (assoc 'INTERVAL rrule-props
)) "1")))
1829 (dtstart-conv (icalendar--datetime-to-diary-date dtstart-dec
))
1830 (until-conv (icalendar--datetime-to-diary-date
1831 (icalendar--decode-isodatetime until
)))
1832 (until-1-conv (icalendar--datetime-to-diary-date
1833 (icalendar--decode-isodatetime until -
1)))
1836 ;; FIXME FIXME interval!!!!!!!!!!!!!
1840 (message "Must not have UNTIL and COUNT -- ignoring COUNT element!")
1842 (cond ((string-equal frequency
"DAILY")
1843 (setq until
(icalendar--add-decoded-times
1845 (list 0 0 0 (* (read count
) interval
) 0 0)))
1846 (setq until-1
(icalendar--add-decoded-times
1848 (list 0 0 0 (* (- (read count
) 1) interval
)
1851 ((string-equal frequency
"WEEKLY")
1852 (setq until
(icalendar--add-decoded-times
1854 (list 0 0 0 (* (read count
) 7 interval
) 0 0)))
1855 (setq until-1
(icalendar--add-decoded-times
1857 (list 0 0 0 (* (- (read count
) 1) 7
1860 ((string-equal frequency
"MONTHLY")
1861 (setq until
(icalendar--add-decoded-times
1862 dtstart-dec
(list 0 0 0 0 (* (- (read count
) 1)
1864 (setq until-1
(icalendar--add-decoded-times
1865 dtstart-dec
(list 0 0 0 0 (* (- (read count
) 1)
1868 ((string-equal frequency
"YEARLY")
1869 (setq until
(icalendar--add-decoded-times
1870 dtstart-dec
(list 0 0 0 0 0 (* (- (read count
) 1)
1872 (setq until-1
(icalendar--add-decoded-times
1874 (list 0 0 0 0 0 (* (- (read count
) 1)
1878 (message "Cannot handle COUNT attribute for `%s' events."
1880 (setq until-conv
(icalendar--datetime-to-diary-date until
))
1881 (setq until-1-conv
(icalendar--datetime-to-diary-date until-1
))
1884 (cond ((string-equal frequency
"WEEKLY")
1887 ;; weekly and all-day
1888 (icalendar--dmsg "weekly all-day")
1893 "(diary-cyclic %d %s) "
1894 "(diary-block %s %s))")
1898 (if count until-1-conv until-conv
)
1901 (format "%%%%(and (diary-cyclic %d %s))"
1904 ;; weekly and not all-day
1905 (let* ((byday (cadr (assoc 'BYDAY rrule-props
)))
1907 (icalendar--get-weekday-number byday
)))
1908 (icalendar--dmsg "weekly not-all-day")
1913 "(diary-cyclic %d %s) "
1914 "(diary-block %s %s)) "
1921 (if end-t
"-" "") (or end-t
"")))
1924 ;; DTSTART;VALUE=DATE-TIME:20030919T090000
1925 ;; DTEND;VALUE=DATE-TIME:20030919T113000
1928 "%%%%(and (diary-cyclic %s %s)) %s%s%s"
1932 (if end-t
"-" "") (or end-t
"")))))))
1934 ((string-equal frequency
"YEARLY")
1935 (icalendar--dmsg "yearly")
1937 (setq result
(format
1938 (concat "%%%%(and (diary-date %s %s t) "
1939 "(diary-block %s %s)) %s%s%s")
1940 (if european-calendar-style
(nth 3 dtstart-dec
)
1941 (nth 4 dtstart-dec
))
1942 (if european-calendar-style
(nth 4 dtstart-dec
)
1943 (nth 3 dtstart-dec
))
1947 (if end-t
"-" "") (or end-t
"")))
1948 (setq result
(format
1949 "%%%%(and (diary-anniversary %s)) %s%s%s"
1952 (if end-t
"-" "") (or end-t
"")))))
1954 ((string-equal frequency
"MONTHLY")
1955 (icalendar--dmsg "monthly")
1958 "%%%%(and (diary-date %s %s %s) (diary-block %s %s)) %s%s%s"
1959 (if european-calendar-style
(nth 3 dtstart-dec
) "t")
1960 (if european-calendar-style
"t" (nth 3 dtstart-dec
))
1965 "1 1 9999") ;; FIXME: should be unlimited
1967 (if end-t
"-" "") (or end-t
""))))
1969 ((and (string-equal frequency
"DAILY"))
1973 (concat "%%%%(and (diary-cyclic %s %s) "
1974 "(diary-block %s %s)) %s%s%s")
1975 interval dtstart-conv dtstart-conv
1976 (if count until-1-conv until-conv
)
1978 (if end-t
"-" "") (or end-t
"")))
1981 "%%%%(and (diary-cyclic %s %s)) %s%s%s"
1985 (if end-t
"-" "") (or end-t
""))))))
1986 ;; Handle exceptions from recurrence rules
1987 (let ((ex-dates (icalendar--get-event-properties e
'EXDATE
)))
1989 (let* ((ex-start (icalendar--decode-isodatetime
1991 (ex-d (icalendar--datetime-to-diary-date
1994 (icalendar--rris "^%%(\\(and \\)?"
1996 "%%%%(and (not (diary-date %s)) "
1999 (setq ex-dates
(cdr ex-dates
))))
2000 ;; FIXME: exception rules are not recognized
2001 (if (icalendar--get-event-property e
'EXRULE
)
2004 "\n Exception rules: "
2005 (icalendar--get-event-properties
2009 (defun icalendar--convert-non-recurring-all-day-to-diary (event start-d end-d
)
2010 "Convert non-recurring icalendar EVENT to diary format.
2012 DTSTART is the decoded DTSTART property of E.
2013 Argument START-D gives the first day.
2014 Argument END-D gives the last day."
2015 (icalendar--dmsg "non-recurring all-day event")
2016 (format "%%%%(and (diary-block %s %s))" start-d end-d
))
2018 (defun icalendar--convert-non-recurring-not-all-day-to-diary (event dtstart-dec
2022 "Convert recurring icalendar EVENT to diary format.
2024 DTSTART-DEC is the decoded DTSTART property of E.
2025 DTEND-DEC is the decoded DTEND property of E.
2026 START-T is the event's start time in diary format.
2027 END-T is the event's end time in diary format."
2028 (icalendar--dmsg "not all day event")
2031 (icalendar--datetime-to-diary-date
2036 (icalendar--datetime-to-diary-date
2040 (defun icalendar--add-diary-entry (string diary-file non-marking
2042 "Add STRING to the diary file DIARY-FILE.
2043 STRING must be a properly formatted valid diary entry. NON-MARKING
2044 determines whether diary events are created as non-marking. If
2045 SUMMARY is not nil it must be a string that gives the summary of the
2046 entry. In this case the user will be asked whether he wants to insert
2048 (when (or (not summary
)
2049 (y-or-n-p (format "Add appointment for `%s' to diary? "
2053 (y-or-n-p (format "Make appointment non-marking? "))))
2054 (save-window-excursion
2057 (read-file-name "Add appointment to this diary file: ")))
2058 ;; Note: make-diary-entry will add a trailing blank char.... :(
2059 (make-diary-entry string non-marking diary-file
)))
2060 ;; return diary-file in case it has been changed interactively
2063 ;; ======================================================================
2065 ;; ======================================================================
2066 (defun icalendar-import-format-sample (event)
2067 "Example function for formatting an icalendar EVENT."
2068 (format (concat "SUMMARY=`%s' DESCRIPTION=`%s' LOCATION=`%s' ORGANIZER=`%s' "
2069 "STATUS=`%s' URL=`%s' CLASS=`%s'")
2070 (or (icalendar--get-event-property event
'SUMMARY
) "")
2071 (or (icalendar--get-event-property event
'DESCRIPTION
) "")
2072 (or (icalendar--get-event-property event
'LOCATION
) "")
2073 (or (icalendar--get-event-property event
'ORGANIZER
) "")
2074 (or (icalendar--get-event-property event
'STATUS
) "")
2075 (or (icalendar--get-event-property event
'URL
) "")
2076 (or (icalendar--get-event-property event
'CLASS
) "")))
2078 (provide 'icalendar
)
2080 ;; arch-tag: 74fdbe8e-0451-4e38-bb61-4416e822f4fc
2081 ;;; icalendar.el ends here