(fill-delete-prefix): Don't ignore excessively long prefixes.
[emacs.git] / lisp / calendar / icalendar.el
blob570fb626747ea90e44594c75266769fa285daf8b
1 ;;; icalendar.el --- iCalendar implementation -*-coding: utf-8 -*-
3 ;; Copyright (C) 2002, 2003, 2004, 2005, 2006 Free Software Foundation, Inc.
5 ;; Author: Ulf Jasper <ulf.jasper@web.de>
6 ;; Created: August 2002
7 ;; Keywords: calendar
8 ;; Human-Keywords: calendar, diary, iCalendar, vCalendar
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)
15 ;; any later version.
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., 51 Franklin Street, Fifth Floor,
25 ;; Boston, MA 02110-1301, USA.
27 ;;; Commentary:
29 ;; This package is documented in the Emacs Manual.
31 ;; Please note:
32 ;; - Diary entries which have a start time but no end time are assumed to
33 ;; last for one hour when they are exported.
34 ;; - Weekly diary entries are assumed to occur the first time in the first
35 ;; week of the year 2000 when they are exported.
36 ;; - Yearly diary entries are assumed to occur the first time in the year
37 ;; 1900 when they are exported.
39 ;;; History:
41 ;; 0.07 onwards: see lisp/ChangeLog
43 ;; 0.06: Bugfixes regarding icalendar-import-format-*.
44 ;; Fix in icalendar-convert-diary-to-ical -- thanks to Philipp
45 ;; Grau.
47 ;; 0.05: New import format scheme: Replaced icalendar-import-prefix-*,
48 ;; icalendar-import-ignored-properties, and
49 ;; icalendar-import-separator with icalendar-import-format(-*).
50 ;; icalendar-import-file and icalendar-convert-diary-to-ical
51 ;; have an extra parameter which should prevent them from
52 ;; erasing their target files (untested!).
53 ;; Tested with Emacs 21.3.2
55 ;; 0.04: Bugfix: import: double quoted param values did not work
56 ;; Read DURATION property when importing.
57 ;; Added parameter icalendar-duration-correction.
59 ;; 0.03: Export takes care of european-calendar-style.
60 ;; Tested with Emacs 21.3.2 and XEmacs 21.4.12
62 ;; 0.02: Should work in XEmacs now. Thanks to Len Trigg for the
63 ;; XEmacs patches!
64 ;; Added exporting from Emacs diary to ical.
65 ;; Some bugfixes, after testing with calendars from
66 ;; http://icalshare.com.
67 ;; Tested with Emacs 21.3.2 and XEmacs 21.4.12
69 ;; 0.01: First published version. Trial version. Alpha version.
71 ;; ======================================================================
72 ;; To Do:
74 ;; * Import from ical to diary:
75 ;; + Need more properties for icalendar-import-format
76 ;; + check vcalendar version
77 ;; + check (unknown) elements
78 ;; + recurring events!
79 ;; + works for european style calendars only! Does it?
80 ;; + alarm
81 ;; + exceptions in recurring events
82 ;; + the parser is too soft
83 ;; + error log is incomplete
84 ;; + nice to have: #include "webcal://foo.com/some-calendar.ics"
85 ;; + timezones, currently all times are local!
87 ;; * Export from diary to ical
88 ;; + diary-date, diary-float, and self-made sexp entries are not
89 ;; understood
91 ;; * Other things
92 ;; + clean up all those date/time parsing functions
93 ;; + Handle todo items?
94 ;; + Check iso 8601 for datetime and period
95 ;; + Which chars to (un)escape?
98 ;;; Code:
100 (defconst icalendar-version "0.13"
101 "Version number of icalendar.el.")
103 ;; ======================================================================
104 ;; Customizables
105 ;; ======================================================================
106 (defgroup icalendar nil
107 "Icalendar support."
108 :prefix "icalendar-"
109 :group 'calendar)
111 (defcustom icalendar-import-format
112 "%s%d%l%o"
113 "Format string for importing events from iCalendar into Emacs diary.
114 This string defines how iCalendar events are inserted into diary
115 file. Meaning of the specifiers:
116 %c Class, see `icalendar-import-format-class'
117 %d Description, see `icalendar-import-format-description'
118 %l Location, see `icalendar-import-format-location'
119 %o Organizer, see `icalendar-import-format-organizer'
120 %s Summary, see `icalendar-import-format-summary'
121 %t Status, see `icalendar-import-format-status'
122 %u URL, see `icalendar-import-format-url'"
123 :type 'string
124 :group 'icalendar)
126 (defcustom icalendar-import-format-summary
127 "%s"
128 "Format string defining how the summary element is formatted.
129 This applies only if the summary is not empty! `%s' is replaced
130 by the summary."
131 :type 'string
132 :group 'icalendar)
134 (defcustom icalendar-import-format-description
135 "\n Desc: %s"
136 "Format string defining how the description element is formatted.
137 This applies only if the description is not empty! `%s' is
138 replaced by the description."
139 :type 'string
140 :group 'icalendar)
142 (defcustom icalendar-import-format-location
143 "\n Location: %s"
144 "Format string defining how the location element is formatted.
145 This applies only if the location is not empty! `%s' is replaced
146 by the location."
147 :type 'string
148 :group 'icalendar)
150 (defcustom icalendar-import-format-organizer
151 "\n Organizer: %s"
152 "Format string defining how the organizer element is formatted.
153 This applies only if the organizer is not empty! `%s' is
154 replaced by the organizer."
155 :type 'string
156 :group 'icalendar)
158 (defcustom icalendar-import-format-url
159 "\n URL: %s"
160 "Format string defining how the URL element is formatted.
161 This applies only if the URL is not empty! `%s' is replaced by
162 the URL."
163 :type 'string
164 :group 'icalendar)
166 (defcustom icalendar-import-format-status
167 "\n Status: %s"
168 "Format string defining how the status element is formatted.
169 This applies only if the status is not empty! `%s' is replaced by
170 the status."
171 :type 'string
172 :group 'icalendar)
174 (defcustom icalendar-import-format-class
175 "\n Class: %s"
176 "Format string defining how the class element is formatted.
177 This applies only if the class is not empty! `%s' is replaced by
178 the class."
179 :type 'string
180 :group 'icalendar)
182 (defvar icalendar-debug nil
183 "Enable icalendar debug messages.")
185 ;; ======================================================================
186 ;; NO USER SERVICABLE PARTS BELOW THIS LINE
187 ;; ======================================================================
189 (defconst icalendar--weekday-array ["SU" "MO" "TU" "WE" "TH" "FR" "SA"])
191 ;; ======================================================================
192 ;; all the other libs we need
193 ;; ======================================================================
194 (require 'calendar)
196 ;; ======================================================================
197 ;; misc
198 ;; ======================================================================
199 (defun icalendar--dmsg (&rest args)
200 "Print message ARGS if `icalendar-debug' is non-nil."
201 (if icalendar-debug
202 (apply 'message args)))
204 ;; ======================================================================
205 ;; Core functionality
206 ;; Functions for parsing icalendars, importing and so on
207 ;; ======================================================================
209 (defun icalendar--get-unfolded-buffer (folded-ical-buffer)
210 "Return a new buffer containing the unfolded contents of a buffer.
211 Folding is the iCalendar way of wrapping long lines. In the
212 created buffer all occurrences of CR LF BLANK are replaced by the
213 empty string. Argument FOLDED-ICAL-BUFFER is the unfolded input
214 buffer."
215 (let ((unfolded-buffer (get-buffer-create " *icalendar-work*")))
216 (save-current-buffer
217 (set-buffer unfolded-buffer)
218 (erase-buffer)
219 (insert-buffer-substring folded-ical-buffer)
220 (goto-char (point-min))
221 (while (re-search-forward "\r?\n[ \t]" nil t)
222 (replace-match "" nil nil)))
223 unfolded-buffer))
225 (defsubst icalendar--rris (&rest args)
226 "Replace regular expression in string.
227 Pass ARGS to `replace-regexp-in-string' (Emacs) or to
228 `replace-in-string' (XEmacs)."
229 ;; XEmacs:
230 (if (fboundp 'replace-in-string)
231 (save-match-data ;; apparently XEmacs needs save-match-data
232 (apply 'replace-in-string args))
233 ;; Emacs:
234 (apply 'replace-regexp-in-string args)))
236 (defun icalendar--read-element (invalue inparams)
237 "Recursively read the next iCalendar element in the current buffer.
238 INVALUE gives the current iCalendar element we are reading.
239 INPARAMS gives the current parameters.....
240 This function calls itself recursively for each nested calendar element
241 it finds"
242 (let (element children line name params param param-name param-value
243 value
244 (continue t))
245 (setq children '())
246 (while (and continue
247 (re-search-forward "^\\([A-Za-z0-9-]+\\)[;:]" nil t))
248 (setq name (intern (match-string 1)))
249 (backward-char 1)
250 (setq params '())
251 (setq line '())
252 (while (looking-at ";")
253 (re-search-forward ";\\([A-Za-z0-9-]+\\)=" nil nil)
254 (setq param-name (intern (match-string 1)))
255 (re-search-forward "\\(\\([^;,:\"]+\\)\\|\"\\([^\"]+\\)\"\\)[;:]"
256 nil t)
257 (backward-char 1)
258 (setq param-value (or (match-string 2) (match-string 3)))
259 (setq param (list param-name param-value))
260 (while (looking-at ",")
261 (re-search-forward "\\(\\([^;,:]+\\)\\|\"\\([^\"]+\\)\"\\)"
262 nil t)
263 (if (match-string 2)
264 (setq param-value (match-string 2))
265 (setq param-value (match-string 3)))
266 (setq param (append param param-value)))
267 (setq params (append params param)))
268 (unless (looking-at ":")
269 (error "Oops"))
270 (forward-char 1)
271 (re-search-forward "\\(.*\\)\\(\r?\n[ \t].*\\)*" nil t)
272 (setq value (icalendar--rris "\r?\n[ \t]" "" (match-string 0)))
273 (setq line (list name params value))
274 (cond ((eq name 'BEGIN)
275 (setq children
276 (append children
277 (list (icalendar--read-element (intern value)
278 params)))))
279 ((eq name 'END)
280 (setq continue nil))
282 (setq element (append element (list line))))))
283 (if invalue
284 (list invalue inparams element children)
285 children)))
287 ;; ======================================================================
288 ;; helper functions for examining events
289 ;; ======================================================================
291 ;;(defsubst icalendar--get-all-event-properties (event)
292 ;; "Return the list of properties in this EVENT."
293 ;; (car (cddr event)))
295 (defun icalendar--get-event-property (event prop)
296 "For the given EVENT return the value of the first occurrence of PROP."
297 (catch 'found
298 (let ((props (car (cddr event))) pp)
299 (while props
300 (setq pp (car props))
301 (if (eq (car pp) prop)
302 (throw 'found (car (cddr pp))))
303 (setq props (cdr props))))
304 nil))
306 (defun icalendar--get-event-property-attributes (event prop)
307 "For the given EVENT return attributes of the first occurrence of PROP."
308 (catch 'found
309 (let ((props (car (cddr event))) pp)
310 (while props
311 (setq pp (car props))
312 (if (eq (car pp) prop)
313 (throw 'found (cadr pp)))
314 (setq props (cdr props))))
315 nil))
317 (defun icalendar--get-event-properties (event prop)
318 "For the given EVENT return a list of all values of the property PROP."
319 (let ((props (car (cddr event))) pp result)
320 (while props
321 (setq pp (car props))
322 (if (eq (car pp) prop)
323 (setq result (append (split-string (car (cddr pp)) ",") result)))
324 (setq props (cdr props)))
325 result))
327 ;; (defun icalendar--set-event-property (event prop new-value)
328 ;; "For the given EVENT set the property PROP to the value NEW-VALUE."
329 ;; (catch 'found
330 ;; (let ((props (car (cddr event))) pp)
331 ;; (while props
332 ;; (setq pp (car props))
333 ;; (when (eq (car pp) prop)
334 ;; (setcdr (cdr pp) new-value)
335 ;; (throw 'found (car (cddr pp))))
336 ;; (setq props (cdr props)))
337 ;; (setq props (car (cddr event)))
338 ;; (setcar (cddr event)
339 ;; (append props (list (list prop nil new-value)))))))
341 (defun icalendar--get-children (node name)
342 "Return all children of the given NODE which have a name NAME.
343 For instance the VCALENDAR node can have VEVENT children as well as VTODO
344 children."
345 (let ((result nil)
346 (children (cadr (cddr node))))
347 (when (eq (car node) name)
348 (setq result node))
349 ;;(message "%s" node)
350 (when children
351 (let ((subresult
352 (delq nil
353 (mapcar (lambda (n)
354 (icalendar--get-children n name))
355 children))))
356 (if subresult
357 (if result
358 (setq result (append result subresult))
359 (setq result subresult)))))
360 result))
362 ; private
363 (defun icalendar--all-events (icalendar)
364 "Return the list of all existing events in the given ICALENDAR."
365 (icalendar--get-children (car icalendar) 'VEVENT))
367 (defun icalendar--split-value (value-string)
368 "Split VALUE-STRING at ';='."
369 (let ((result '())
370 param-name param-value)
371 (when value-string
372 (save-current-buffer
373 (set-buffer (get-buffer-create " *icalendar-work*"))
374 (set-buffer-modified-p nil)
375 (erase-buffer)
376 (insert value-string)
377 (goto-char (point-min))
378 (while
379 (re-search-forward
380 "\\([A-Za-z0-9-]+\\)=\\(\\([^;,:]+\\)\\|\"\\([^\"]+\\)\"\\);?"
381 nil t)
382 (setq param-name (intern (match-string 1)))
383 (setq param-value (match-string 2))
384 (setq result
385 (append result (list (list param-name param-value)))))))
386 result))
388 (defun icalendar--decode-isodatetime (isodatetimestring &optional day-shift)
389 "Return ISODATETIMESTRING in format like `decode-time'.
390 Converts from ISO-8601 to Emacs representation. If
391 ISODATETIMESTRING specifies UTC time (trailing letter Z) the
392 decoded time is given in the local time zone! If optional
393 parameter DAY-SHIFT is non-nil the result is shifted by DAY-SHIFT
394 days.
396 FIXME: TZID-attributes are ignored....!
397 FIXME: multiple comma-separated values should be allowed!"
398 (icalendar--dmsg isodatetimestring)
399 (if isodatetimestring
400 ;; day/month/year must be present
401 (let ((year (read (substring isodatetimestring 0 4)))
402 (month (read (substring isodatetimestring 4 6)))
403 (day (read (substring isodatetimestring 6 8)))
404 (hour 0)
405 (minute 0)
406 (second 0))
407 (when (> (length isodatetimestring) 12)
408 ;; hour/minute present
409 (setq hour (read (substring isodatetimestring 9 11)))
410 (setq minute (read (substring isodatetimestring 11 13))))
411 (when (> (length isodatetimestring) 14)
412 ;; seconds present
413 (setq second (read (substring isodatetimestring 13 15))))
414 (when (and (> (length isodatetimestring) 15)
415 ;; UTC specifier present
416 (char-equal ?Z (aref isodatetimestring 15)))
417 ;; if not UTC add current-time-zone offset
418 (setq second (+ (car (current-time-zone)) second)))
419 ;; shift if necessary
420 (if day-shift
421 (let ((mdy (calendar-gregorian-from-absolute
422 (+ (calendar-absolute-from-gregorian
423 (list month day year))
424 day-shift))))
425 (setq month (nth 0 mdy))
426 (setq day (nth 1 mdy))
427 (setq year (nth 2 mdy))))
428 ;; create the decoded date-time
429 ;; FIXME!?!
430 (condition-case nil
431 (decode-time (encode-time second minute hour day month year))
432 (error
433 (message "Cannot decode \"%s\"" isodatetimestring)
434 ;; hope for the best...
435 (list second minute hour day month year 0 nil 0))))
436 ;; isodatetimestring == nil
437 nil))
439 (defun icalendar--decode-isoduration (isodurationstring
440 &optional duration-correction)
441 "Convert ISODURATIONSTRING into format provided by `decode-time'.
442 Converts from ISO-8601 to Emacs representation. If ISODURATIONSTRING
443 specifies UTC time (trailing letter Z) the decoded time is given in
444 the local time zone!
446 Optional argument DURATION-CORRECTION shortens result by one day.
448 FIXME: TZID-attributes are ignored....!
449 FIXME: multiple comma-separated values should be allowed!"
450 (if isodurationstring
451 (save-match-data
452 (string-match
453 (concat
454 "^P[+-]?\\("
455 "\\(\\([0-9]+\\)D\\)" ; days only
456 "\\|"
457 "\\(\\(\\([0-9]+\\)D\\)?T\\(\\([0-9]+\\)H\\)?" ; opt days
458 "\\(\\([0-9]+\\)M\\)?\\(\\([0-9]+\\)S\\)?\\)" ; mand. time
459 "\\|"
460 "\\(\\([0-9]+\\)W\\)" ; weeks only
461 "\\)$") isodurationstring)
462 (let ((seconds 0)
463 (minutes 0)
464 (hours 0)
465 (days 0)
466 (months 0)
467 (years 0))
468 (cond
469 ((match-beginning 2) ;days only
470 (setq days (read (substring isodurationstring
471 (match-beginning 3)
472 (match-end 3))))
473 (when duration-correction
474 (setq days (1- days))))
475 ((match-beginning 4) ;days and time
476 (if (match-beginning 5)
477 (setq days (* 7 (read (substring isodurationstring
478 (match-beginning 6)
479 (match-end 6))))))
480 (if (match-beginning 7)
481 (setq hours (read (substring isodurationstring
482 (match-beginning 8)
483 (match-end 8)))))
484 (if (match-beginning 9)
485 (setq minutes (read (substring isodurationstring
486 (match-beginning 10)
487 (match-end 10)))))
488 (if (match-beginning 11)
489 (setq seconds (read (substring isodurationstring
490 (match-beginning 12)
491 (match-end 12))))))
492 ((match-beginning 13) ;weeks only
493 (setq days (* 7 (read (substring isodurationstring
494 (match-beginning 14)
495 (match-end 14)))))))
496 (list seconds minutes hours days months years)))
497 ;; isodatetimestring == nil
498 nil))
500 (defun icalendar--add-decoded-times (time1 time2)
501 "Add TIME1 to TIME2.
502 Both times must be given in decoded form. One of these times must be
503 valid (year > 1900 or something)."
504 ;; FIXME: does this function exist already?
505 (decode-time (encode-time
506 (+ (nth 0 time1) (nth 0 time2))
507 (+ (nth 1 time1) (nth 1 time2))
508 (+ (nth 2 time1) (nth 2 time2))
509 (+ (nth 3 time1) (nth 3 time2))
510 (+ (nth 4 time1) (nth 4 time2))
511 (+ (nth 5 time1) (nth 5 time2))
514 ;;(or (nth 6 time1) (nth 6 time2)) ;; FIXME?
517 (defun icalendar--datetime-to-noneuropean-date (datetime &optional separator)
518 "Convert the decoded DATETIME to non-european-style format.
519 Optional argument SEPARATOR gives the separator between month,
520 day, and year. If nil a blank character is used as separator.
521 Non-European format: \"month day year\"."
522 (if datetime
523 (format "%d%s%d%s%d" (nth 4 datetime) ;month
524 (or separator " ")
525 (nth 3 datetime) ;day
526 (or separator " ")
527 (nth 5 datetime)) ;year
528 ;; datetime == nil
529 nil))
531 (defun icalendar--datetime-to-european-date (datetime &optional separator)
532 "Convert the decoded DATETIME to European format.
533 Optional argument SEPARATOR gives the separator between month,
534 day, and year. If nil a blank character is used as separator.
535 European format: (day month year).
536 FIXME"
537 (if datetime
538 (format "%d%s%d%s%d" (nth 3 datetime) ;day
539 (or separator " ")
540 (nth 4 datetime) ;month
541 (or separator " ")
542 (nth 5 datetime)) ;year
543 ;; datetime == nil
544 nil))
546 (defun icalendar--datetime-to-diary-date (datetime &optional separator)
547 "Convert the decoded DATETIME to diary format.
548 Optional argument SEPARATOR gives the separator between month,
549 day, and year. If nil a blank character is used as separator.
550 Call icalendar--datetime-to-(non)-european-date according to
551 value of `european-calendar-style'."
552 (if european-calendar-style
553 (icalendar--datetime-to-european-date datetime separator)
554 (icalendar--datetime-to-noneuropean-date datetime separator)))
556 (defun icalendar--datetime-to-colontime (datetime)
557 "Extract the time part of a decoded DATETIME into 24-hour format.
558 Note that this silently ignores seconds."
559 (format "%02d:%02d" (nth 2 datetime) (nth 1 datetime)))
561 (defun icalendar--get-month-number (monthname)
562 "Return the month number for the given MONTHNAME."
563 (catch 'found
564 (let ((num 1)
565 (m (downcase monthname)))
566 (mapc (lambda (month)
567 (let ((mm (downcase month)))
568 (if (or (string-equal mm m)
569 (string-equal (substring mm 0 3) m))
570 (throw 'found num))
571 (setq num (1+ num))))
572 calendar-month-name-array))
573 ;; Error:
574 -1))
576 (defun icalendar--get-weekday-number (abbrevweekday)
577 "Return the number for the ABBREVWEEKDAY."
578 (if abbrevweekday
579 (catch 'found
580 (let ((num 0)
581 (aw (downcase abbrevweekday)))
582 (mapc (lambda (day)
583 (let ((d (downcase day)))
584 (if (string-equal d aw)
585 (throw 'found num))
586 (setq num (1+ num))))
587 icalendar--weekday-array)))
588 ;; Error:
589 -1))
591 (defun icalendar--get-weekday-abbrev (weekday)
592 "Return the abbreviated WEEKDAY."
593 (catch 'found
594 (let ((num 0)
595 (w (downcase weekday)))
596 (mapc (lambda (day)
597 (let ((d (downcase day)))
598 (if (or (string-equal d w)
599 (string-equal (substring d 0 3) w))
600 (throw 'found (aref icalendar--weekday-array num)))
601 (setq num (1+ num))))
602 calendar-day-name-array))
603 ;; Error:
604 nil))
606 (defun icalendar--date-to-isodate (date &optional day-shift)
607 "Convert DATE to iso-style date.
608 DATE must be a list of the form (month day year).
609 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days."
610 (let ((mdy (calendar-gregorian-from-absolute
611 (+ (calendar-absolute-from-gregorian date)
612 (or day-shift 0)))))
613 (format "%04d%02d%02d" (nth 2 mdy) (nth 0 mdy) (nth 1 mdy))))
616 (defun icalendar--datestring-to-isodate (datestring &optional day-shift)
617 "Convert diary-style DATESTRING to iso-style date.
618 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days
619 -- DAY-SHIFT must be either nil or an integer. This function
620 takes care of european-style."
621 (let ((day -1) month year)
622 (save-match-data
623 (cond ( ;; numeric date
624 (string-match (concat "\\s-*"
625 "0?\\([1-9][0-9]?\\)[ \t/]\\s-*"
626 "0?\\([1-9][0-9]?\\),?[ \t/]\\s-*"
627 "\\([0-9]\\{4\\}\\)")
628 datestring)
629 (setq day (read (substring datestring (match-beginning 1)
630 (match-end 1))))
631 (setq month (read (substring datestring (match-beginning 2)
632 (match-end 2))))
633 (setq year (read (substring datestring (match-beginning 3)
634 (match-end 3))))
635 (unless european-calendar-style
636 (let ((x month))
637 (setq month day)
638 (setq day x))))
639 ( ;; date contains month names -- european-style
640 (string-match (concat "\\s-*"
641 "0?\\([123]?[0-9]\\)[ \t/]\\s-*"
642 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
643 "\\([0-9]\\{4\\}\\)")
644 datestring)
645 (setq day (read (substring datestring (match-beginning 1)
646 (match-end 1))))
647 (setq month (icalendar--get-month-number
648 (substring datestring (match-beginning 2)
649 (match-end 2))))
650 (setq year (read (substring datestring (match-beginning 3)
651 (match-end 3)))))
652 ( ;; date contains month names -- non-european-style
653 (string-match (concat "\\s-*"
654 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
655 "0?\\([123]?[0-9]\\),?[ \t/]\\s-*"
656 "\\([0-9]\\{4\\}\\)")
657 datestring)
658 (setq day (read (substring datestring (match-beginning 2)
659 (match-end 2))))
660 (setq month (icalendar--get-month-number
661 (substring datestring (match-beginning 1)
662 (match-end 1))))
663 (setq year (read (substring datestring (match-beginning 3)
664 (match-end 3)))))
666 nil)))
667 (if (> day 0)
668 (let ((mdy (calendar-gregorian-from-absolute
669 (+ (calendar-absolute-from-gregorian (list month day
670 year))
671 (or day-shift 0)))))
672 (format "%04d%02d%02d" (nth 2 mdy) (nth 0 mdy) (nth 1 mdy)))
673 nil)))
675 (defun icalendar--diarytime-to-isotime (timestring ampmstring)
676 "Convert a a time like 9:30pm to an iso-conform string like T213000.
677 In this example the TIMESTRING would be \"9:30\" and the AMPMSTRING
678 would be \"pm\"."
679 (if timestring
680 (let ((starttimenum (read (icalendar--rris ":" "" timestring))))
681 ;; take care of am/pm style
682 (if (and ampmstring (string= "pm" ampmstring))
683 (setq starttimenum (+ starttimenum 1200)))
684 (format "T%04d00" starttimenum))
685 nil))
687 (defun icalendar--convert-string-for-export (string)
688 "Escape comma and other critical characters in STRING."
689 (icalendar--rris "," "\\\\," string))
691 (defun icalendar--convert-string-for-import (string)
692 "Remove escape chars for comma, semicolon etc. from STRING."
693 (icalendar--rris
694 "\\\\n" "\n " (icalendar--rris
695 "\\\\\"" "\"" (icalendar--rris
696 "\\\\;" ";" (icalendar--rris
697 "\\\\," "," string)))))
699 ;; ======================================================================
700 ;; Export -- convert emacs-diary to icalendar
701 ;; ======================================================================
703 ;;;###autoload
704 (defun icalendar-export-file (diary-filename ical-filename)
705 "Export diary file to iCalendar format.
706 All diary entries in the file DIARY-FILENAME are converted to iCalendar
707 format. The result is appended to the file ICAL-FILENAME."
708 (interactive "FExport diary data from file:
709 Finto iCalendar file: ")
710 (save-current-buffer
711 (set-buffer (find-file diary-filename))
712 (icalendar-export-region (point-min) (point-max) ical-filename)))
714 (defalias 'icalendar-convert-diary-to-ical 'icalendar-export-file)
715 (make-obsolete 'icalendar-convert-diary-to-ical 'icalendar-export-file)
717 ;;;###autoload
718 (defun icalendar-export-region (min max ical-filename)
719 "Export region in diary file to iCalendar format.
720 All diary entries in the region from MIN to MAX in the current buffer are
721 converted to iCalendar format. The result is appended to the file
722 ICAL-FILENAME.
723 This function attempts to return t if something goes wrong. In this
724 case an error string which describes all the errors and problems is
725 written into the buffer `*icalendar-errors*'."
726 (interactive "r
727 FExport diary data into iCalendar file: ")
728 (let ((result "")
729 (start 0)
730 (entry-main "")
731 (entry-rest "")
732 (header "")
733 (contents-n-summary)
734 (contents)
735 (found-error nil)
736 (nonmarker (concat "^" (regexp-quote diary-nonmarking-symbol)
737 "?"))
738 (other-elements nil))
739 ;; prepare buffer with error messages
740 (save-current-buffer
741 (set-buffer (get-buffer-create "*icalendar-errors*"))
742 (erase-buffer))
744 ;; here we go
745 (save-excursion
746 (goto-char min)
747 (while (re-search-forward
748 "^\\([^ \t\n].+\\)\\(\\(\n[ \t].*\\)*\\)" max t)
749 (setq entry-main (match-string 1))
750 (if (match-beginning 2)
751 (setq entry-rest (match-string 2))
752 (setq entry-rest ""))
753 (setq header (format "\nBEGIN:VEVENT\nUID:emacs%d%d%d"
754 (car (current-time))
755 (cadr (current-time))
756 (car (cddr (current-time)))))
757 (condition-case error-val
758 (progn
759 (setq contents-n-summary
760 (icalendar--convert-to-ical nonmarker entry-main))
761 (setq other-elements (icalendar--parse-summary-and-rest
762 (concat entry-main entry-rest)))
763 (setq contents (concat (car contents-n-summary)
764 "\nSUMMARY:" (cadr contents-n-summary)))
765 (let ((cla (cdr (assoc 'cla other-elements)))
766 (des (cdr (assoc 'des other-elements)))
767 (loc (cdr (assoc 'loc other-elements)))
768 (org (cdr (assoc 'org other-elements)))
769 (sta (cdr (assoc 'sta other-elements)))
770 (sum (cdr (assoc 'sum other-elements)))
771 (url (cdr (assoc 'url other-elements))))
772 (if cla
773 (setq contents (concat contents "\nCLASS:" cla)))
774 (if des
775 (setq contents (concat contents "\nDESCRIPTION:" des)))
776 (if loc
777 (setq contents (concat contents "\nLOCATION:" loc)))
778 (if org
779 (setq contents (concat contents "\nORGANIZER:" org)))
780 (if sta
781 (setq contents (concat contents "\nSTATUS:" sta)))
782 ;;(if sum
783 ;; (setq contents (concat contents "\nSUMMARY:" sum)))
784 (if url
785 (setq contents (concat contents "\nURL:" url))))
786 (setq result (concat result header contents "\nEND:VEVENT")))
787 ;; handle errors
788 (error
789 (setq found-error t)
790 (save-current-buffer
791 (set-buffer (get-buffer-create "*icalendar-errors*"))
792 (insert (format "Error in line %d -- %s: `%s'\n"
793 (count-lines (point-min) (point))
794 (cadr error-val)
795 entry-main))))))
797 ;; we're done, insert everything into the file
798 (save-current-buffer
799 (let ((coding-system-for-write 'utf-8))
800 (set-buffer (find-file ical-filename))
801 (goto-char (point-max))
802 (insert "BEGIN:VCALENDAR")
803 (insert "\nPRODID:-//Emacs//NONSGML icalendar.el//EN")
804 (insert "\nVERSION:2.0")
805 (insert result)
806 (insert "\nEND:VCALENDAR\n")
807 ;; save the diary file
808 (save-buffer)
809 (unless found-error
810 (bury-buffer)))))
811 found-error))
813 (defun icalendar--convert-to-ical (nonmarker entry-main)
814 "Convert a diary entry to icalendar format.
815 NONMARKER is a regular expression matching the start of non-marking
816 entries. ENTRY-MAIN is the first line of the diary entry."
818 ;; anniversaries -- %%(diary-anniversary ...)
819 (icalendar--convert-anniversary-to-ical nonmarker entry-main)
820 ;; cyclic events -- %%(diary-cyclic ...)
821 (icalendar--convert-cyclic-to-ical nonmarker entry-main)
822 ;; diary-date -- %%(diary-date ...)
823 (icalendar--convert-date-to-ical nonmarker entry-main)
824 ;; float events -- %%(diary-float ...)
825 (icalendar--convert-float-to-ical nonmarker entry-main)
826 ;; block events -- %%(diary-block ...)
827 (icalendar--convert-block-to-ical nonmarker entry-main)
828 ;; other sexp diary entries
829 (icalendar--convert-sexp-to-ical nonmarker entry-main)
830 ;; weekly by day -- Monday 8:30 Team meeting
831 (icalendar--convert-weekly-to-ical nonmarker entry-main)
832 ;; yearly by day -- 1 May Tag der Arbeit
833 (icalendar--convert-yearly-to-ical nonmarker entry-main)
834 ;; "ordinary" events, start and end time given
835 ;; 1 Feb 2003 blah
836 (icalendar--convert-ordinary-to-ical nonmarker entry-main)
837 ;; everything else
838 ;; Oops! what's that?
839 (error "Could not parse entry")))
841 (defun icalendar--parse-summary-and-rest (summary-and-rest)
842 "Parse SUMMARY-AND-REST from a diary to fill iCalendar properties."
843 (save-match-data
844 (let* ((s icalendar-import-format)
845 (p-cla (or (string-match "%c" icalendar-import-format) -1))
846 (p-des (or (string-match "%d" icalendar-import-format) -1))
847 (p-loc (or (string-match "%l" icalendar-import-format) -1))
848 (p-org (or (string-match "%o" icalendar-import-format) -1))
849 (p-sum (or (string-match "%s" icalendar-import-format) -1))
850 (p-sta (or (string-match "%t" icalendar-import-format) -1))
851 (p-url (or (string-match "%u" icalendar-import-format) -1))
852 (p-list (sort (list p-cla p-des p-loc p-org p-sta p-sum p-url) '<))
853 pos-cla pos-des pos-loc pos-org pos-sta pos-sum pos-url)
854 (dotimes (i (length p-list))
855 (cond ((and (>= p-cla 0) (= (nth i p-list) p-cla))
856 (setq pos-cla (+ 2 (* 2 i))))
857 ((and (>= p-des 0) (= (nth i p-list) p-des))
858 (setq pos-des (+ 2 (* 2 i))))
859 ((and (>= p-loc 0) (= (nth i p-list) p-loc))
860 (setq pos-loc (+ 2 (* 2 i))))
861 ((and (>= p-org 0) (= (nth i p-list) p-org))
862 (setq pos-org (+ 2 (* 2 i))))
863 ((and (>= p-sta 0) (= (nth i p-list) p-sta))
864 (setq pos-sta (+ 2 (* 2 i))))
865 ((and (>= p-sum 0) (= (nth i p-list) p-sum))
866 (setq pos-sum (+ 2 (* 2 i))))
867 ((and (>= p-url 0) (= (nth i p-list) p-url))
868 (setq pos-url (+ 2 (* 2 i))))))
869 (mapc (lambda (ij)
870 (setq s (icalendar--rris (car ij) (cadr ij) s t t)))
871 (list
872 ;; summary must be first! because of %s
873 (list "%s"
874 (concat "\\(" icalendar-import-format-summary "\\)?"))
875 (list "%c"
876 (concat "\\(" icalendar-import-format-class "\\)?"))
877 (list "%d"
878 (concat "\\(" icalendar-import-format-description "\\)?"))
879 (list "%l"
880 (concat "\\(" icalendar-import-format-location "\\)?"))
881 (list "%o"
882 (concat "\\(" icalendar-import-format-organizer "\\)?"))
883 (list "%t"
884 (concat "\\(" icalendar-import-format-status "\\)?"))
885 (list "%u"
886 (concat "\\(" icalendar-import-format-url "\\)?"))))
887 (setq s (concat (icalendar--rris "%s" "\\(.*\\)" s nil t) " "))
888 (if (string-match s summary-and-rest)
889 (let (cla des loc org sta sum url)
890 (if (and pos-sum (match-beginning pos-sum))
891 (setq sum (substring summary-and-rest
892 (match-beginning pos-sum)
893 (match-end pos-sum))))
894 (if (and pos-cla (match-beginning pos-cla))
895 (setq cla (substring summary-and-rest
896 (match-beginning pos-cla)
897 (match-end pos-cla))))
898 (if (and pos-des (match-beginning pos-des))
899 (setq des (substring summary-and-rest
900 (match-beginning pos-des)
901 (match-end pos-des))))
902 (if (and pos-loc (match-beginning pos-loc))
903 (setq loc (substring summary-and-rest
904 (match-beginning pos-loc)
905 (match-end pos-loc))))
906 (if (and pos-org (match-beginning pos-org))
907 (setq org (substring summary-and-rest
908 (match-beginning pos-org)
909 (match-end pos-org))))
910 (if (and pos-sta (match-beginning pos-sta))
911 (setq sta (substring summary-and-rest
912 (match-beginning pos-sta)
913 (match-end pos-sta))))
914 (if (and pos-url (match-beginning pos-url))
915 (setq url (substring summary-and-rest
916 (match-beginning pos-url)
917 (match-end pos-url))))
918 (list (if cla (cons 'cla cla) nil)
919 (if des (cons 'des des) nil)
920 (if loc (cons 'loc loc) nil)
921 (if org (cons 'org org) nil)
922 (if sta (cons 'sta sta) nil)
923 ;;(if sum (cons 'sum sum) nil)
924 (if url (cons 'url url) nil)))))))
926 ;; subroutines for icalendar-export-region
927 (defun icalendar--convert-ordinary-to-ical (nonmarker entry-main)
928 "Convert \"ordinary\" diary entry to icalendar format.
929 NONMARKER is a regular expression matching the start of non-marking
930 entries. ENTRY-MAIN is the first line of the diary entry."
931 (if (string-match (concat nonmarker
932 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)\\s-*"
933 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
934 "\\("
935 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
936 "\\)?"
937 "\\s-*\\(.*?\\) ?$")
938 entry-main)
939 (let* ((datetime (substring entry-main (match-beginning 1)
940 (match-end 1)))
941 (startisostring (icalendar--datestring-to-isodate
942 datetime))
943 (endisostring (icalendar--datestring-to-isodate
944 datetime 1))
945 (starttimestring (icalendar--diarytime-to-isotime
946 (if (match-beginning 3)
947 (substring entry-main
948 (match-beginning 3)
949 (match-end 3))
950 nil)
951 (if (match-beginning 4)
952 (substring entry-main
953 (match-beginning 4)
954 (match-end 4))
955 nil)))
956 (endtimestring (icalendar--diarytime-to-isotime
957 (if (match-beginning 6)
958 (substring entry-main
959 (match-beginning 6)
960 (match-end 6))
961 nil)
962 (if (match-beginning 7)
963 (substring entry-main
964 (match-beginning 7)
965 (match-end 7))
966 nil)))
967 (summary (icalendar--convert-string-for-export
968 (substring entry-main (match-beginning 8)
969 (match-end 8)))))
970 (icalendar--dmsg "ordinary %s" entry-main)
972 (unless startisostring
973 (error "Could not parse date"))
974 (when starttimestring
975 (unless endtimestring
976 (let ((time
977 (read (icalendar--rris "^T0?" ""
978 starttimestring))))
979 (setq endtimestring (format "T%06d"
980 (+ 10000 time))))))
981 (list (concat "\nDTSTART;"
982 (if starttimestring "VALUE=DATE-TIME:"
983 "VALUE=DATE:")
984 startisostring
985 (or starttimestring "")
986 "\nDTEND;"
987 (if endtimestring "VALUE=DATE-TIME:"
988 "VALUE=DATE:")
989 (if starttimestring
990 startisostring
991 endisostring)
992 (or endtimestring ""))
993 summary))
994 ;; no match
995 nil))
997 (defun icalendar--convert-weekly-to-ical (nonmarker entry-main)
998 "Convert weekly diary entry to icalendar format.
999 NONMARKER is a regular expression matching the start of non-marking
1000 entries. ENTRY-MAIN is the first line of the diary entry."
1001 (if (and (string-match (concat nonmarker
1002 "\\([a-z]+\\)\\s-+"
1003 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)"
1004 "\\([ap]m\\)?"
1005 "\\(-0?"
1006 "\\([1-9][0-9]?:[0-9][0-9]\\)"
1007 "\\([ap]m\\)?\\)?"
1008 "\\)?"
1009 "\\s-*\\(.*?\\) ?$")
1010 entry-main)
1011 (icalendar--get-weekday-abbrev
1012 (substring entry-main (match-beginning 1)
1013 (match-end 1))))
1014 (let* ((day (icalendar--get-weekday-abbrev
1015 (substring entry-main (match-beginning 1)
1016 (match-end 1))))
1017 (starttimestring (icalendar--diarytime-to-isotime
1018 (if (match-beginning 3)
1019 (substring entry-main
1020 (match-beginning 3)
1021 (match-end 3))
1022 nil)
1023 (if (match-beginning 4)
1024 (substring entry-main
1025 (match-beginning 4)
1026 (match-end 4))
1027 nil)))
1028 (endtimestring (icalendar--diarytime-to-isotime
1029 (if (match-beginning 6)
1030 (substring entry-main
1031 (match-beginning 6)
1032 (match-end 6))
1033 nil)
1034 (if (match-beginning 7)
1035 (substring entry-main
1036 (match-beginning 7)
1037 (match-end 7))
1038 nil)))
1039 (summary (icalendar--convert-string-for-export
1040 (substring entry-main (match-beginning 8)
1041 (match-end 8)))))
1042 (icalendar--dmsg "weekly %s" entry-main)
1044 (when starttimestring
1045 (unless endtimestring
1046 (let ((time (read
1047 (icalendar--rris "^T0?" ""
1048 starttimestring))))
1049 (setq endtimestring (format "T%06d"
1050 (+ 10000 time))))))
1051 (list (concat "\nDTSTART;"
1052 (if starttimestring
1053 "VALUE=DATE-TIME:"
1054 "VALUE=DATE:")
1055 ;; find the correct week day,
1056 ;; 1st january 2000 was a saturday
1057 (format
1058 "200001%02d"
1059 (+ (icalendar--get-weekday-number day) 2))
1060 (or starttimestring "")
1061 "\nDTEND;"
1062 (if endtimestring
1063 "VALUE=DATE-TIME:"
1064 "VALUE=DATE:")
1065 (format
1066 "200001%02d"
1067 ;; end is non-inclusive!
1068 (+ (icalendar--get-weekday-number day)
1069 (if endtimestring 2 3)))
1070 (or endtimestring "")
1071 "\nRRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY="
1072 day)
1073 summary))
1074 ;; no match
1075 nil))
1077 (defun icalendar--convert-yearly-to-ical (nonmarker entry-main)
1078 "Convert yearly diary entry to icalendar format.
1079 NONMARKER is a regular expression matching the start of non-marking
1080 entries. ENTRY-MAIN is the first line of the diary entry."
1081 (if (string-match (concat nonmarker
1082 (if european-calendar-style
1083 "0?\\([1-9]+[0-9]?\\)\\s-+\\([a-z]+\\)\\s-+"
1084 "\\([a-z]+\\)\\s-+0?\\([1-9]+[0-9]?\\)\\s-+")
1085 "\\*?\\s-*"
1086 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1087 "\\("
1088 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1089 "\\)?"
1090 "\\s-*\\([^0-9]+.*?\\) ?$" ; must not match years
1092 entry-main)
1093 (let* ((daypos (if european-calendar-style 1 2))
1094 (monpos (if european-calendar-style 2 1))
1095 (day (read (substring entry-main
1096 (match-beginning daypos)
1097 (match-end daypos))))
1098 (month (icalendar--get-month-number
1099 (substring entry-main
1100 (match-beginning monpos)
1101 (match-end monpos))))
1102 (starttimestring (icalendar--diarytime-to-isotime
1103 (if (match-beginning 4)
1104 (substring entry-main
1105 (match-beginning 4)
1106 (match-end 4))
1107 nil)
1108 (if (match-beginning 5)
1109 (substring entry-main
1110 (match-beginning 5)
1111 (match-end 5))
1112 nil)))
1113 (endtimestring (icalendar--diarytime-to-isotime
1114 (if (match-beginning 7)
1115 (substring entry-main
1116 (match-beginning 7)
1117 (match-end 7))
1118 nil)
1119 (if (match-beginning 8)
1120 (substring entry-main
1121 (match-beginning 8)
1122 (match-end 8))
1123 nil)))
1124 (summary (icalendar--convert-string-for-export
1125 (substring entry-main (match-beginning 9)
1126 (match-end 9)))))
1127 (icalendar--dmsg "yearly %s" entry-main)
1129 (when starttimestring
1130 (unless endtimestring
1131 (let ((time (read
1132 (icalendar--rris "^T0?" ""
1133 starttimestring))))
1134 (setq endtimestring (format "T%06d"
1135 (+ 10000 time))))))
1136 (list (concat "\nDTSTART;"
1137 (if starttimestring "VALUE=DATE-TIME:"
1138 "VALUE=DATE:")
1139 (format "1900%02d%02d" month day)
1140 (or starttimestring "")
1141 "\nDTEND;"
1142 (if endtimestring "VALUE=DATE-TIME:"
1143 "VALUE=DATE:")
1144 ;; end is not included! shift by one day
1145 (icalendar--date-to-isodate
1146 (list month day 1900)
1147 (if endtimestring 0 1))
1148 (or endtimestring "")
1149 "\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH="
1150 (format "%2d" month)
1151 ";BYMONTHDAY="
1152 (format "%2d" day))
1153 summary))
1154 ;; no match
1155 nil))
1157 (defun icalendar--convert-sexp-to-ical (nonmarker entry-main)
1158 "Convert complex sexp diary entry to icalendar format -- unsupported!
1160 FIXME!
1162 NONMARKER is a regular expression matching the start of non-marking
1163 entries. ENTRY-MAIN is the first line of the diary entry."
1164 (cond ((string-match (concat nonmarker
1165 "%%(and \\(([^)]+)\\))\\(\\s-*.*?\\) ?$")
1166 entry-main)
1167 ;; simple sexp entry as generated by icalendar.el: strip off the
1168 ;; unnecessary (and)
1169 (icalendar--dmsg "diary-sexp from icalendar.el %s" entry-main)
1170 (icalendar--convert-to-ical
1171 nonmarker
1172 (concat "%%"
1173 (substring entry-main (match-beginning 1) (match-end 1))
1174 (substring entry-main (match-beginning 2) (match-end 2)))))
1175 ((string-match (concat nonmarker
1176 "%%([^)]+)\\s-*.*")
1177 entry-main)
1178 (icalendar--dmsg "diary-sexp %s" entry-main)
1179 (error "Sexp-entries are not supported yet"))
1181 ;; no match
1182 nil)))
1184 (defun icalendar--convert-block-to-ical (nonmarker entry-main)
1185 "Convert block diary entry to icalendar format.
1186 NONMARKER is a regular expression matching the start of non-marking
1187 entries. ENTRY-MAIN is the first line of the diary entry."
1188 (if (string-match (concat nonmarker
1189 "%%(diary-block \\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)"
1190 " +\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
1191 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1192 "\\("
1193 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1194 "\\)?"
1195 "\\s-*\\(.*?\\) ?$")
1196 entry-main)
1197 (let* ((startstring (substring entry-main
1198 (match-beginning 1)
1199 (match-end 1)))
1200 (endstring (substring entry-main
1201 (match-beginning 2)
1202 (match-end 2)))
1203 (startisostring (icalendar--datestring-to-isodate
1204 startstring))
1205 (endisostring (icalendar--datestring-to-isodate
1206 endstring))
1207 (endisostring+1 (icalendar--datestring-to-isodate
1208 endstring 1))
1209 (starttimestring (icalendar--diarytime-to-isotime
1210 (if (match-beginning 4)
1211 (substring entry-main
1212 (match-beginning 4)
1213 (match-end 4))
1214 nil)
1215 (if (match-beginning 5)
1216 (substring entry-main
1217 (match-beginning 5)
1218 (match-end 5))
1219 nil)))
1220 (endtimestring (icalendar--diarytime-to-isotime
1221 (if (match-beginning 7)
1222 (substring entry-main
1223 (match-beginning 7)
1224 (match-end 7))
1225 nil)
1226 (if (match-beginning 8)
1227 (substring entry-main
1228 (match-beginning 8)
1229 (match-end 8))
1230 nil)))
1231 (summary (icalendar--convert-string-for-export
1232 (substring entry-main (match-beginning 9)
1233 (match-end 9)))))
1234 (icalendar--dmsg "diary-block %s" entry-main)
1235 (when starttimestring
1236 (unless endtimestring
1237 (let ((time
1238 (read (icalendar--rris "^T0?" ""
1239 starttimestring))))
1240 (setq endtimestring (format "T%06d"
1241 (+ 10000 time))))))
1242 (if starttimestring
1243 ;; with time -> write rrule
1244 (list (concat "\nDTSTART;VALUE=DATE-TIME:"
1245 startisostring
1246 starttimestring
1247 "\nDTEND;VALUE=DATE-TIME:"
1248 startisostring
1249 endtimestring
1250 "\nRRULE:FREQ=DAILY;INTERVAL=1;UNTIL="
1251 endisostring)
1252 summary)
1253 ;; no time -> write long event
1254 (list (concat "\nDTSTART;VALUE=DATE:" startisostring
1255 "\nDTEND;VALUE=DATE:" endisostring+1)
1256 summary)))
1257 ;; no match
1258 nil))
1260 (defun icalendar--convert-float-to-ical (nonmarker entry-main)
1261 "Convert float diary entry to icalendar format -- unsupported!
1263 FIXME!
1265 NONMARKER is a regular expression matching the start of non-marking
1266 entries. ENTRY-MAIN is the first line of the diary entry."
1267 (if (string-match (concat nonmarker
1268 "%%(diary-float \\([^)]+\\))\\s-*\\(.*?\\) ?$")
1269 entry-main)
1270 (progn
1271 (icalendar--dmsg "diary-float %s" entry-main)
1272 (error "`diary-float' is not supported yet"))
1273 ;; no match
1274 nil))
1276 (defun icalendar--convert-date-to-ical (nonmarker entry-main)
1277 "Convert `diary-date' diary entry to icalendar format -- unsupported!
1279 FIXME!
1281 NONMARKER is a regular expression matching the start of non-marking
1282 entries. ENTRY-MAIN is the first line of the diary entry."
1283 (if (string-match (concat nonmarker
1284 "%%(diary-date \\([^)]+\\))\\s-*\\(.*?\\) ?$")
1285 entry-main)
1286 (progn
1287 (icalendar--dmsg "diary-date %s" entry-main)
1288 (error "`diary-date' is not supported yet"))
1289 ;; no match
1290 nil))
1292 (defun icalendar--convert-cyclic-to-ical (nonmarker entry-main)
1293 "Convert `diary-cyclic' diary entry to icalendar format.
1294 NONMARKER is a regular expression matching the start of non-marking
1295 entries. ENTRY-MAIN is the first line of the diary entry."
1296 (if (string-match (concat nonmarker
1297 "%%(diary-cyclic \\([^ ]+\\) +"
1298 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
1299 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1300 "\\("
1301 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1302 "\\)?"
1303 "\\s-*\\(.*?\\) ?$")
1304 entry-main)
1305 (let* ((frequency (substring entry-main (match-beginning 1)
1306 (match-end 1)))
1307 (datetime (substring entry-main (match-beginning 2)
1308 (match-end 2)))
1309 (startisostring (icalendar--datestring-to-isodate
1310 datetime))
1311 (endisostring (icalendar--datestring-to-isodate
1312 datetime))
1313 (endisostring+1 (icalendar--datestring-to-isodate
1314 datetime 1))
1315 (starttimestring (icalendar--diarytime-to-isotime
1316 (if (match-beginning 4)
1317 (substring entry-main
1318 (match-beginning 4)
1319 (match-end 4))
1320 nil)
1321 (if (match-beginning 5)
1322 (substring entry-main
1323 (match-beginning 5)
1324 (match-end 5))
1325 nil)))
1326 (endtimestring (icalendar--diarytime-to-isotime
1327 (if (match-beginning 7)
1328 (substring entry-main
1329 (match-beginning 7)
1330 (match-end 7))
1331 nil)
1332 (if (match-beginning 8)
1333 (substring entry-main
1334 (match-beginning 8)
1335 (match-end 8))
1336 nil)))
1337 (summary (icalendar--convert-string-for-export
1338 (substring entry-main (match-beginning 9)
1339 (match-end 9)))))
1340 (icalendar--dmsg "diary-cyclic %s" entry-main)
1341 (when starttimestring
1342 (unless endtimestring
1343 (let ((time
1344 (read (icalendar--rris "^T0?" ""
1345 starttimestring))))
1346 (setq endtimestring (format "T%06d"
1347 (+ 10000 time))))))
1348 (list (concat "\nDTSTART;"
1349 (if starttimestring "VALUE=DATE-TIME:"
1350 "VALUE=DATE:")
1351 startisostring
1352 (or starttimestring "")
1353 "\nDTEND;"
1354 (if endtimestring "VALUE=DATE-TIME:"
1355 "VALUE=DATE:")
1356 (if endtimestring endisostring endisostring+1)
1357 (or endtimestring "")
1358 "\nRRULE:FREQ=DAILY;INTERVAL=" frequency
1359 ;; strange: korganizer does not expect
1360 ;; BYSOMETHING here...
1362 summary))
1363 ;; no match
1364 nil))
1366 (defun icalendar--convert-anniversary-to-ical (nonmarker entry-main)
1367 "Convert `diary-anniversary' diary entry to icalendar format.
1368 NONMARKER is a regular expression matching the start of non-marking
1369 entries. ENTRY-MAIN is the first line of the diary entry."
1370 (if (string-match (concat nonmarker
1371 "%%(diary-anniversary \\([^)]+\\))\\s-*"
1372 "\\(0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1373 "\\("
1374 "-0?\\([1-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1375 "\\)?"
1376 "\\s-*\\(.*?\\) ?$")
1377 entry-main)
1378 (let* ((datetime (substring entry-main (match-beginning 1)
1379 (match-end 1)))
1380 (startisostring (icalendar--datestring-to-isodate
1381 datetime))
1382 (endisostring (icalendar--datestring-to-isodate
1383 datetime 1))
1384 (starttimestring (icalendar--diarytime-to-isotime
1385 (if (match-beginning 3)
1386 (substring entry-main
1387 (match-beginning 3)
1388 (match-end 3))
1389 nil)
1390 (if (match-beginning 4)
1391 (substring entry-main
1392 (match-beginning 4)
1393 (match-end 4))
1394 nil)))
1395 (endtimestring (icalendar--diarytime-to-isotime
1396 (if (match-beginning 6)
1397 (substring entry-main
1398 (match-beginning 6)
1399 (match-end 6))
1400 nil)
1401 (if (match-beginning 7)
1402 (substring entry-main
1403 (match-beginning 7)
1404 (match-end 7))
1405 nil)))
1406 (summary (icalendar--convert-string-for-export
1407 (substring entry-main (match-beginning 8)
1408 (match-end 8)))))
1409 (icalendar--dmsg "diary-anniversary %s" entry-main)
1410 (when starttimestring
1411 (unless endtimestring
1412 (let ((time
1413 (read (icalendar--rris "^T0?" ""
1414 starttimestring))))
1415 (setq endtimestring (format "T%06d"
1416 (+ 10000 time))))))
1417 (list (concat "\nDTSTART;"
1418 (if starttimestring "VALUE=DATE-TIME:"
1419 "VALUE=DATE:")
1420 startisostring
1421 (or starttimestring "")
1422 "\nDTEND;"
1423 (if endtimestring "VALUE=DATE-TIME:"
1424 "VALUE=DATE:")
1425 endisostring
1426 (or endtimestring "")
1427 "\nRRULE:FREQ=YEARLY;INTERVAL=1"
1428 ;; the following is redundant,
1429 ;; but korganizer seems to expect this... ;(
1430 ;; and evolution doesn't understand it... :(
1431 ;; so... who is wrong?!
1432 ";BYMONTH="
1433 (substring startisostring 4 6)
1434 ";BYMONTHDAY="
1435 (substring startisostring 6 8))
1436 summary))
1437 ;; no match
1438 nil))
1440 ;; ======================================================================
1441 ;; Import -- convert icalendar to emacs-diary
1442 ;; ======================================================================
1444 ;;;###autoload
1445 (defun icalendar-import-file (ical-filename diary-filename
1446 &optional non-marking)
1447 "Import an iCalendar file and append to a diary file.
1448 Argument ICAL-FILENAME output iCalendar file.
1449 Argument DIARY-FILENAME input `diary-file'.
1450 Optional argument NON-MARKING determines whether events are created as
1451 non-marking or not."
1452 (interactive "fImport iCalendar data from file:
1453 Finto diary file:
1455 ;; clean up the diary file
1456 (save-current-buffer
1457 ;; now load and convert from the ical file
1458 (set-buffer (find-file ical-filename))
1459 (icalendar-import-buffer diary-filename t non-marking)))
1461 ;;;###autoload
1462 (defun icalendar-import-buffer (&optional diary-file do-not-ask
1463 non-marking)
1464 "Extract iCalendar events from current buffer.
1466 This function searches the current buffer for the first iCalendar
1467 object, reads it and adds all VEVENT elements to the diary
1468 DIARY-FILE.
1470 It will ask for each appointment whether to add it to the diary
1471 when DO-NOT-ASK is non-nil. When called interactively,
1472 DO-NOT-ASK is set to t, so that you are asked fore each event.
1474 NON-MARKING determines whether diary events are created as
1475 non-marking.
1477 Return code t means that importing worked well, return code nil
1478 means that an error has occured. Error messages will be in the
1479 buffer `*icalendar-errors*'."
1480 (interactive)
1481 (save-current-buffer
1482 ;; prepare ical
1483 (message "Preparing icalendar...")
1484 (set-buffer (icalendar--get-unfolded-buffer (current-buffer)))
1485 (goto-char (point-min))
1486 (message "Preparing icalendar...done")
1487 (if (re-search-forward "^BEGIN:VCALENDAR\\s-*$" nil t)
1488 (let (ical-contents ical-errors)
1489 ;; read ical
1490 (message "Reading icalendar...")
1491 (beginning-of-line)
1492 (setq ical-contents (icalendar--read-element nil nil))
1493 (message "Reading icalendar...done")
1494 ;; convert ical
1495 (message "Converting icalendar...")
1496 (setq ical-errors (icalendar--convert-ical-to-diary
1497 ical-contents
1498 diary-file do-not-ask non-marking))
1499 (when diary-file
1500 ;; save the diary file if it is visited already
1501 (let ((b (find-buffer-visiting diary-file)))
1502 (when b
1503 (save-current-buffer
1504 (set-buffer b)
1505 (save-buffer)))))
1506 (message "Converting icalendar...done")
1507 ;; return t if no error occured
1508 (not ical-errors))
1509 (message
1510 "Current buffer does not contain icalendar contents!")
1511 ;; return nil, i.e. import did not work
1512 nil)))
1514 (defalias 'icalendar-extract-ical-from-buffer 'icalendar-import-buffer)
1515 (make-obsolete 'icalendar-extract-ical-from-buffer 'icalendar-import-buffer)
1517 (defun icalendar--format-ical-event (event)
1518 "Create a string representation of an iCalendar EVENT."
1519 (let ((string icalendar-import-format)
1520 (conversion-list
1521 '(("%c" CLASS icalendar-import-format-class)
1522 ("%d" DESCRIPTION icalendar-import-format-description)
1523 ("%l" LOCATION icalendar-import-format-location)
1524 ("%o" ORGANIZER icalendar-import-format-organizer)
1525 ("%s" SUMMARY icalendar-import-format-summary)
1526 ("%t" STATUS icalendar-import-format-status)
1527 ("%u" URL icalendar-import-format-url))))
1528 ;; convert the specifiers in the format string
1529 (mapcar (lambda (i)
1530 (let* ((spec (car i))
1531 (prop (cadr i))
1532 (format (car (cddr i)))
1533 (contents (icalendar--get-event-property event prop))
1534 (formatted-contents ""))
1535 (when (and contents (> (length contents) 0))
1536 (setq formatted-contents
1537 (icalendar--rris "%s"
1538 (icalendar--convert-string-for-import
1539 contents)
1540 (symbol-value format)
1541 t t)))
1542 (setq string (icalendar--rris spec
1543 formatted-contents
1544 string
1545 t t))))
1546 conversion-list)
1547 string))
1549 (defun icalendar--convert-ical-to-diary (ical-list diary-file
1550 &optional do-not-ask
1551 non-marking)
1552 "Convert Calendar data to an Emacs diary file.
1553 Import VEVENTS from the iCalendar object ICAL-LIST and saves them to a
1554 DIARY-FILE. If DO-NOT-ASK is nil the user is asked for each event
1555 whether to actually import it. NON-MARKING determines whether diary
1556 events are created as non-marking.
1557 This function attempts to return t if something goes wrong. In this
1558 case an error string which describes all the errors and problems is
1559 written into the buffer `*icalendar-errors*'."
1560 (let* ((ev (icalendar--all-events ical-list))
1561 (error-string "")
1562 (event-ok t)
1563 (found-error nil)
1564 e diary-string)
1565 ;; step through all events/appointments
1566 (while ev
1567 (setq e (car ev))
1568 (setq ev (cdr ev))
1569 (setq event-ok nil)
1570 (condition-case error-val
1571 (let* ((dtstart (icalendar--get-event-property e 'DTSTART))
1572 (dtstart-dec (icalendar--decode-isodatetime dtstart))
1573 (start-d (icalendar--datetime-to-diary-date
1574 dtstart-dec))
1575 (start-t (icalendar--datetime-to-colontime dtstart-dec))
1576 (dtend (icalendar--get-event-property e 'DTEND))
1577 (dtend-dec (icalendar--decode-isodatetime dtend))
1578 (dtend-1-dec (icalendar--decode-isodatetime dtend -1))
1579 end-d
1580 end-1-d
1581 end-t
1582 (summary (icalendar--convert-string-for-import
1583 (or (icalendar--get-event-property e 'SUMMARY)
1584 "No summary")))
1585 (rrule (icalendar--get-event-property e 'RRULE))
1586 (rdate (icalendar--get-event-property e 'RDATE))
1587 (duration (icalendar--get-event-property e 'DURATION)))
1588 (icalendar--dmsg "%s: `%s'" start-d summary)
1589 ;; check whether start-time is missing
1590 (if (and dtstart
1591 (string=
1592 (cadr (icalendar--get-event-property-attributes
1593 e 'DTSTART))
1594 "DATE"))
1595 (setq start-t nil))
1596 (when duration
1597 (let ((dtend-dec-d (icalendar--add-decoded-times
1598 dtstart-dec
1599 (icalendar--decode-isoduration duration)))
1600 (dtend-1-dec-d (icalendar--add-decoded-times
1601 dtstart-dec
1602 (icalendar--decode-isoduration duration
1603 t))))
1604 (if (and dtend-dec (not (eq dtend-dec dtend-dec-d)))
1605 (message "Inconsistent endtime and duration for %s"
1606 summary))
1607 (setq dtend-dec dtend-dec-d)
1608 (setq dtend-1-dec dtend-1-dec-d)))
1609 (setq end-d (if dtend-dec
1610 (icalendar--datetime-to-diary-date dtend-dec)
1611 start-d))
1612 (setq end-1-d (if dtend-1-dec
1613 (icalendar--datetime-to-diary-date dtend-1-dec)
1614 start-d))
1615 (setq end-t (if (and
1616 dtend-dec
1617 (not (string=
1618 (cadr
1619 (icalendar--get-event-property-attributes
1620 e 'DTEND))
1621 "DATE")))
1622 (icalendar--datetime-to-colontime dtend-dec)
1623 start-t))
1624 (icalendar--dmsg "start-d: %s, end-d: %s" start-d end-d)
1625 (cond
1626 ;; recurring event
1627 (rrule
1628 (setq diary-string
1629 (icalendar--convert-recurring-to-diary e dtstart-dec start-t
1630 end-t))
1631 (setq event-ok t))
1632 (rdate
1633 (icalendar--dmsg "rdate event")
1634 (setq diary-string "")
1635 (mapcar (lambda (datestring)
1636 (setq diary-string
1637 (concat diary-string
1638 (format "......"))))
1639 (icalendar--split-value rdate)))
1640 ;; non-recurring event
1641 ;; all-day event
1642 ((not (string= start-d end-d))
1643 (setq diary-string
1644 (icalendar--convert-non-recurring-all-day-to-diary
1645 e start-d end-1-d))
1646 (setq event-ok t))
1647 ;; not all-day
1648 ((and start-t (or (not end-t)
1649 (not (string= start-t end-t))))
1650 (setq diary-string
1651 (icalendar--convert-non-recurring-not-all-day-to-diary
1652 e dtstart-dec dtend-dec start-t end-t))
1653 (setq event-ok t))
1654 ;; all-day event
1656 (icalendar--dmsg "all day event")
1657 (setq diary-string (icalendar--datetime-to-diary-date
1658 dtstart-dec "/"))
1659 (setq event-ok t)))
1660 ;; add all other elements unless the user doesn't want to have
1661 ;; them
1662 (if event-ok
1663 (progn
1664 (setq diary-string
1665 (concat diary-string " "
1666 (icalendar--format-ical-event e)))
1667 (if do-not-ask (setq summary nil))
1668 (icalendar--add-diary-entry diary-string diary-file
1669 non-marking summary))
1670 ;; event was not ok
1671 (setq found-error t)
1672 (setq error-string
1673 (format "%s\nCannot handle this event:%s"
1674 error-string e))))
1675 ;; FIXME: inform user about ignored event properties
1676 ;; handle errors
1677 (error
1678 (message "Ignoring event \"%s\"" e)
1679 (setq found-error t)
1680 (setq error-string (format "%s\n%s\nCannot handle this event: %s"
1681 error-val error-string e))
1682 (message "%s" error-string))))
1683 (if found-error
1684 (save-current-buffer
1685 (set-buffer (get-buffer-create "*icalendar-errors*"))
1686 (erase-buffer)
1687 (insert error-string)))
1688 (message "Converting icalendar...done")
1689 found-error))
1691 ;; subroutines for importing
1692 (defun icalendar--convert-recurring-to-diary (e dtstart-dec start-t end-t)
1693 "Convert recurring icalendar event E to diary format.
1695 DTSTART-DEC is the DTSTART property of E.
1696 START-T is the event's start time in diary format.
1697 END-T is the event's end time in diary format."
1698 (icalendar--dmsg "recurring event")
1699 (let* ((rrule (icalendar--get-event-property e 'RRULE))
1700 (rrule-props (icalendar--split-value rrule))
1701 (frequency (cadr (assoc 'FREQ rrule-props)))
1702 (until (cadr (assoc 'UNTIL rrule-props)))
1703 (count (cadr (assoc 'COUNT rrule-props)))
1704 (interval (read (or (cadr (assoc 'INTERVAL rrule-props)) "1")))
1705 (dtstart-conv (icalendar--datetime-to-diary-date dtstart-dec))
1706 (until-conv (icalendar--datetime-to-diary-date
1707 (icalendar--decode-isodatetime until)))
1708 (until-1-conv (icalendar--datetime-to-diary-date
1709 (icalendar--decode-isodatetime until -1)))
1710 (result ""))
1712 ;; FIXME FIXME interval!!!!!!!!!!!!!
1714 (when count
1715 (if until
1716 (message "Must not have UNTIL and COUNT -- ignoring COUNT element!")
1717 (let ((until-1 0))
1718 (cond ((string-equal frequency "DAILY")
1719 (setq until (icalendar--add-decoded-times
1720 dtstart-dec
1721 (list 0 0 0 (* (read count) interval) 0 0)))
1722 (setq until-1 (icalendar--add-decoded-times
1723 dtstart-dec
1724 (list 0 0 0 (* (- (read count) 1) interval)
1725 0 0)))
1727 ((string-equal frequency "WEEKLY")
1728 (setq until (icalendar--add-decoded-times
1729 dtstart-dec
1730 (list 0 0 0 (* (read count) 7 interval) 0 0)))
1731 (setq until-1 (icalendar--add-decoded-times
1732 dtstart-dec
1733 (list 0 0 0 (* (- (read count) 1) 7
1734 interval) 0 0)))
1736 ((string-equal frequency "MONTHLY")
1737 (setq until (icalendar--add-decoded-times
1738 dtstart-dec (list 0 0 0 0 (* (- (read count) 1)
1739 interval) 0)))
1740 (setq until-1 (icalendar--add-decoded-times
1741 dtstart-dec (list 0 0 0 0 (* (- (read count) 1)
1742 interval) 0)))
1744 ((string-equal frequency "YEARLY")
1745 (setq until (icalendar--add-decoded-times
1746 dtstart-dec (list 0 0 0 0 0 (* (- (read count) 1)
1747 interval))))
1748 (setq until-1 (icalendar--add-decoded-times
1749 dtstart-dec
1750 (list 0 0 0 0 0 (* (- (read count) 1)
1751 interval))))
1754 (message "Cannot handle COUNT attribute for `%s' events."
1755 frequency)))
1756 (setq until-conv (icalendar--datetime-to-diary-date until))
1757 (setq until-1-conv (icalendar--datetime-to-diary-date until-1))
1760 (cond ((string-equal frequency "WEEKLY")
1761 (if (not start-t)
1762 (progn
1763 ;; weekly and all-day
1764 (icalendar--dmsg "weekly all-day")
1765 (if until
1766 (setq result
1767 (format
1768 (concat "%%%%(and "
1769 "(diary-cyclic %d %s) "
1770 "(diary-block %s %s))")
1771 (* interval 7)
1772 dtstart-conv
1773 dtstart-conv
1774 (if count until-1-conv until-conv)
1776 (setq result
1777 (format "%%%%(and (diary-cyclic %d %s))"
1778 (* interval 7)
1779 dtstart-conv))))
1780 ;; weekly and not all-day
1781 (let* ((byday (cadr (assoc 'BYDAY rrule-props)))
1782 (weekday
1783 (icalendar--get-weekday-number byday)))
1784 (icalendar--dmsg "weekly not-all-day")
1785 (if until
1786 (setq result
1787 (format
1788 (concat "%%%%(and "
1789 "(diary-cyclic %d %s) "
1790 "(diary-block %s %s)) "
1791 "%s%s%s")
1792 (* interval 7)
1793 dtstart-conv
1794 dtstart-conv
1795 until-conv
1796 (or start-t "")
1797 (if end-t "-" "") (or end-t "")))
1798 ;; no limit
1799 ;; FIXME!!!!
1800 ;; DTSTART;VALUE=DATE-TIME:20030919T090000
1801 ;; DTEND;VALUE=DATE-TIME:20030919T113000
1802 (setq result
1803 (format
1804 "%%%%(and (diary-cyclic %s %s)) %s%s%s"
1805 (* interval 7)
1806 dtstart-conv
1807 (or start-t "")
1808 (if end-t "-" "") (or end-t "")))))))
1809 ;; yearly
1810 ((string-equal frequency "YEARLY")
1811 (icalendar--dmsg "yearly")
1812 (if until
1813 (setq result (format
1814 (concat "%%%%(and (diary-date %s %s t) "
1815 "(diary-block %s %s)) %s%s%s")
1816 (if european-calendar-style (nth 3 dtstart-dec)
1817 (nth 4 dtstart-dec))
1818 (if european-calendar-style (nth 4 dtstart-dec)
1819 (nth 3 dtstart-dec))
1820 dtstart-conv
1821 until-conv
1822 (or start-t "")
1823 (if end-t "-" "") (or end-t "")))
1824 (setq result (format
1825 "%%%%(and (diary-anniversary %s)) %s%s%s"
1826 dtstart-conv
1827 (or start-t "")
1828 (if end-t "-" "") (or end-t "")))))
1829 ;; monthly
1830 ((string-equal frequency "MONTHLY")
1831 (icalendar--dmsg "monthly")
1832 (setq result
1833 (format
1834 "%%%%(and (diary-date %s %s %s) (diary-block %s %s)) %s%s%s"
1835 (if european-calendar-style (nth 3 dtstart-dec) "t")
1836 (if european-calendar-style "t" (nth 3 dtstart-dec))
1838 dtstart-conv
1839 (if until
1840 until-conv
1841 "1 1 9999") ;; FIXME: should be unlimited
1842 (or start-t "")
1843 (if end-t "-" "") (or end-t ""))))
1844 ;; daily
1845 ((and (string-equal frequency "DAILY"))
1846 (if until
1847 (setq result
1848 (format
1849 (concat "%%%%(and (diary-cyclic %s %s) "
1850 "(diary-block %s %s)) %s%s%s")
1851 interval dtstart-conv dtstart-conv
1852 (if count until-1-conv until-conv)
1853 (or start-t "")
1854 (if end-t "-" "") (or end-t "")))
1855 (setq result
1856 (format
1857 "%%%%(and (diary-cyclic %s %s)) %s%s%s"
1858 interval
1859 dtstart-conv
1860 (or start-t "")
1861 (if end-t "-" "") (or end-t ""))))))
1862 ;; Handle exceptions from recurrence rules
1863 (let ((ex-dates (icalendar--get-event-properties e 'EXDATE)))
1864 (while ex-dates
1865 (let* ((ex-start (icalendar--decode-isodatetime
1866 (car ex-dates)))
1867 (ex-d (icalendar--datetime-to-diary-date
1868 ex-start)))
1869 (setq result
1870 (icalendar--rris "^%%(\\(and \\)?"
1871 (format
1872 "%%%%(and (not (diary-date %s)) "
1873 ex-d)
1874 result)))
1875 (setq ex-dates (cdr ex-dates))))
1876 ;; FIXME: exception rules are not recognized
1877 (if (icalendar--get-event-property e 'EXRULE)
1878 (setq result
1879 (concat result
1880 "\n Exception rules: "
1881 (icalendar--get-event-properties
1882 e 'EXRULE))))
1883 result))
1885 (defun icalendar--convert-non-recurring-all-day-to-diary (event start-d end-d)
1886 "Convert non-recurring icalendar EVENT to diary format.
1888 DTSTART is the decoded DTSTART property of E.
1889 Argument START-D gives the first day.
1890 Argument END-D gives the last day."
1891 (icalendar--dmsg "non-recurring all-day event")
1892 (format "%%%%(and (diary-block %s %s))" start-d end-d))
1894 (defun icalendar--convert-non-recurring-not-all-day-to-diary (event dtstart-dec
1895 dtend-dec
1896 start-t
1897 end-t)
1898 "Convert recurring icalendar EVENT to diary format.
1900 DTSTART-DEC is the decoded DTSTART property of E.
1901 DTEND-DEC is the decoded DTEND property of E.
1902 START-T is the event's start time in diary format.
1903 END-T is the event's end time in diary format."
1904 (icalendar--dmsg "not all day event")
1905 (cond (end-t
1906 (format "%s %s-%s"
1907 (icalendar--datetime-to-diary-date
1908 dtstart-dec "/")
1909 start-t end-t))
1911 (format "%s %s"
1912 (icalendar--datetime-to-diary-date
1913 dtstart-dec "/")
1914 start-t))))
1916 (defun icalendar--add-diary-entry (string diary-file non-marking
1917 &optional summary)
1918 "Add STRING to the diary file DIARY-FILE.
1919 STRING must be a properly formatted valid diary entry. NON-MARKING
1920 determines whether diary events are created as non-marking. If
1921 SUMMARY is not nil it must be a string that gives the summary of the
1922 entry. In this case the user will be asked whether he wants to insert
1923 the entry."
1924 (when (or (not summary)
1925 (y-or-n-p (format "Add appointment for `%s' to diary? "
1926 summary)))
1927 (when summary
1928 (setq non-marking
1929 (y-or-n-p (format "Make appointment non-marking? "))))
1930 (save-window-excursion
1931 (unless diary-file
1932 (setq diary-file
1933 (read-file-name "Add appointment to this diary file: ")))
1934 ;; Note: make-diary-entry will add a trailing blank char.... :(
1935 (make-diary-entry string non-marking diary-file))))
1937 (provide 'icalendar)
1939 ;; arch-tag: 74fdbe8e-0451-4e38-bb61-4416e822f4fc
1940 ;;; icalendar.el ends here