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