]> code.delx.au - gnu-emacs/blob - lisp/calendar/icalendar.el
Merge from emacs-24; up to 2014-07-27T01:00:26Z!fgallina@gnu.org
[gnu-emacs] / lisp / calendar / icalendar.el
1 ;;; icalendar.el --- iCalendar implementation -*-coding: utf-8 -*-
2
3 ;; Copyright (C) 2002-2014 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 ;; Version: 0.19
10
11 ;; This file is part of GNU Emacs.
12
13 ;; GNU Emacs is free software: you can redistribute it and/or modify
14 ;; it under the terms of the GNU General Public License as published by
15 ;; the Free Software Foundation, either version 3 of the License, or
16 ;; (at your option) any later version.
17
18 ;; GNU Emacs is distributed in the hope that it will be useful,
19 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
20 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 ;; GNU General Public License for more details.
22
23 ;; You should have received a copy of the GNU General Public License
24 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
25
26 ;;; Commentary:
27
28 ;; This package is documented in the Emacs Manual.
29
30 ;; Please note:
31 ;; - Diary entries which have a start time but no end time are assumed to
32 ;; last for one hour when they are exported.
33 ;; - Weekly diary entries are assumed to occur the first time in the first
34 ;; week of the year 2000 when they are exported.
35 ;; - Yearly diary entries are assumed to occur the first time in the year
36 ;; 1900 when they are exported.
37 ;; - Float diary entries are assumed to occur the first time on the
38 ;; day when they are exported.
39
40 ;;; History:
41
42 ;; 0.07 onwards: see lisp/ChangeLog
43
44 ;; 0.06: (2004-10-06)
45 ;; - Bugfixes regarding icalendar-import-format-*.
46 ;; - Fix in icalendar-convert-diary-to-ical -- thanks to Philipp Grau.
47
48 ;; 0.05: (2003-06-19)
49 ;; - New import format scheme: Replaced icalendar-import-prefix-*,
50 ;; icalendar-import-ignored-properties, and
51 ;; icalendar-import-separator with icalendar-import-format(-*).
52 ;; - icalendar-import-file and icalendar-convert-diary-to-ical
53 ;; have an extra parameter which should prevent them from
54 ;; erasing their target files (untested!).
55 ;; - Tested with Emacs 21.3.2
56
57 ;; 0.04:
58 ;; - Bugfix: import: double quoted param values did not work
59 ;; - Read DURATION property when importing.
60 ;; - Added parameter icalendar-duration-correction.
61
62 ;; 0.03: (2003-05-07)
63 ;; - Export takes care of european-calendar-style.
64 ;; - Tested with Emacs 21.3.2 and XEmacs 21.4.12
65
66 ;; 0.02:
67 ;; - Should work in XEmacs now. Thanks to Len Trigg for the XEmacs patches!
68 ;; - Added exporting from Emacs diary to ical.
69 ;; - Some bugfixes, after testing with calendars from http://icalshare.com.
70 ;; - Tested with Emacs 21.3.2 and XEmacs 21.4.12
71
72 ;; 0.01: (2003-03-21)
73 ;; - First published version. Trial version. Alpha version.
74
75 ;; ======================================================================
76 ;; To Do:
77
78 ;; * Import from ical to diary:
79 ;; + Need more properties for icalendar-import-format
80 ;; (added all that Mozilla Calendar uses)
81 ;; From iCal specifications (RFC2445: 4.8.1), icalendar.el lacks
82 ;; ATTACH, CATEGORIES, COMMENT, GEO, PERCENT-COMPLETE (VTODO),
83 ;; PRIORITY, RESOURCES) not considering date/time and time-zone
84 ;; + check vcalendar version
85 ;; + check (unknown) elements
86 ;; + recurring events!
87 ;; + works for european style calendars only! Does it?
88 ;; + alarm
89 ;; + exceptions in recurring events
90 ;; + the parser is too soft
91 ;; + error log is incomplete
92 ;; + nice to have: #include "webcal://foo.com/some-calendar.ics"
93 ;; + timezones probably still need some improvements.
94
95 ;; * Export from diary to ical
96 ;; + diary-date, diary-float, and self-made sexp entries are not
97 ;; understood
98
99 ;; * Other things
100 ;; + clean up all those date/time parsing functions
101 ;; + Handle todo items?
102 ;; + Check iso 8601 for datetime and period
103 ;; + Which chars to (un)escape?
104
105
106 ;;; Code:
107
108 (defconst icalendar-version "0.19"
109 "Version number of icalendar.el.")
110
111 ;; ======================================================================
112 ;; Customizables
113 ;; ======================================================================
114 (defgroup icalendar nil
115 "iCalendar support."
116 :prefix "icalendar-"
117 :group 'calendar)
118
119 (defcustom icalendar-import-format
120 "%s%d%l%o"
121 "Format for importing events from iCalendar into Emacs diary.
122 It defines how iCalendar events are inserted into diary file.
123 This may either be a string or a function.
124
125 In case of a formatting STRING the following specifiers can be used:
126 %c Class, see `icalendar-import-format-class'
127 %d Description, see `icalendar-import-format-description'
128 %l Location, see `icalendar-import-format-location'
129 %o Organizer, see `icalendar-import-format-organizer'
130 %s Summary, see `icalendar-import-format-summary'
131 %t Status, see `icalendar-import-format-status'
132 %u URL, see `icalendar-import-format-url'
133 %U UID, see `icalendar-import-format-uid'
134
135 A formatting FUNCTION will be called with a VEVENT as its only
136 argument. It must return a string. See
137 `icalendar-import-format-sample' for an example."
138 :type '(choice
139 (string :tag "String")
140 (function :tag "Function"))
141 :group 'icalendar)
142
143 (defcustom icalendar-import-format-summary
144 "%s"
145 "Format string defining how the summary element is formatted.
146 This applies only if the summary is not empty! `%s' is replaced
147 by the summary."
148 :type 'string
149 :group 'icalendar)
150
151 (defcustom icalendar-import-format-description
152 "\n Desc: %s"
153 "Format string defining how the description element is formatted.
154 This applies only if the description is not empty! `%s' is
155 replaced by the description."
156 :type 'string
157 :group 'icalendar)
158
159 (defcustom icalendar-import-format-location
160 "\n Location: %s"
161 "Format string defining how the location element is formatted.
162 This applies only if the location is not empty! `%s' is replaced
163 by the location."
164 :type 'string
165 :group 'icalendar)
166
167 (defcustom icalendar-import-format-organizer
168 "\n Organizer: %s"
169 "Format string defining how the organizer element is formatted.
170 This applies only if the organizer is not empty! `%s' is
171 replaced by the organizer."
172 :type 'string
173 :group 'icalendar)
174
175 (defcustom icalendar-import-format-url
176 "\n URL: %s"
177 "Format string defining how the URL element is formatted.
178 This applies only if the URL is not empty! `%s' is replaced by
179 the URL."
180 :type 'string
181 :group 'icalendar)
182
183 (defcustom icalendar-import-format-uid
184 "\n UID: %s"
185 "Format string defining how the UID element is formatted.
186 This applies only if the UID is not empty! `%s' is replaced by
187 the UID."
188 :type 'string
189 :version "24.3"
190 :group 'icalendar)
191
192 (defcustom icalendar-import-format-status
193 "\n Status: %s"
194 "Format string defining how the status element is formatted.
195 This applies only if the status is not empty! `%s' is replaced by
196 the status."
197 :type 'string
198 :group 'icalendar)
199
200 (defcustom icalendar-import-format-class
201 "\n Class: %s"
202 "Format string defining how the class element is formatted.
203 This applies only if the class is not empty! `%s' is replaced by
204 the class."
205 :type 'string
206 :group 'icalendar)
207
208 (defcustom icalendar-recurring-start-year
209 2005
210 "Start year for recurring events.
211 Some calendar browsers only propagate recurring events for
212 several years beyond the start time. Set this string to a year
213 just before the start of your personal calendar."
214 :type 'integer
215 :group 'icalendar)
216
217 (defcustom icalendar-export-hidden-diary-entries
218 t
219 "Determines whether hidden diary entries are exported.
220 If non-nil hidden diary entries (starting with `&') get exported,
221 if nil they are ignored."
222 :type 'boolean
223 :group 'icalendar)
224
225 (defcustom icalendar-uid-format
226 "emacs%t%c"
227 "Format of unique ID code (UID) for each iCalendar object.
228 The following specifiers are available:
229 %c COUNTER, an integer value that is increased each time a uid is
230 generated. This may be necessary for systems which do not
231 provide time-resolution finer than a second.
232 %h HASH, a hash value of the diary entry,
233 %s DTSTART, the start date (excluding time) of the diary entry,
234 %t TIMESTAMP, a unique creation timestamp,
235 %u USERNAME, the variable `user-login-name'.
236
237 For example, a value of \"%s_%h@mydomain.com\" will generate a
238 UID code for each entry composed of the time of the event, a hash
239 code for the event, and your personal domain name."
240 :type 'string
241 :group 'icalendar)
242
243 (defcustom icalendar-export-sexp-enumeration-days
244 14
245 "Number of days over which a sexp diary entry is enumerated.
246 In general sexp entries cannot be translated to icalendar format.
247 They are therefore enumerated, i.e. explicitly evaluated for a
248 certain number of days, and then exported. The enumeration starts
249 on the current day and continues for the number of days given here.
250
251 See `icalendar-export-sexp-enumerate-all' for a list of sexp
252 entries which by default are NOT enumerated."
253 :version "25.1"
254 :type 'integer
255 :group 'icalendar)
256
257 (defcustom icalendar-export-sexp-enumerate-all
258 nil
259 "Determines whether ALL sexp diary entries are enumerated.
260 If non-nil all sexp diary entries are enumerated for
261 `icalendar-export-sexp-enumeration-days' days instead of
262 translating into an icalendar equivalent. This affects the
263 following sexp diary entries: `diary-anniversary',
264 `diary-cyclic', `diary-date', `diary-float',`diary-block'. All
265 other sexp entries are enumerated in any case."
266 :version "25.1"
267 :type 'boolean
268 :group 'icalendar)
269
270 (defvar icalendar-debug nil
271 "Enable icalendar debug messages.")
272
273 ;; ======================================================================
274 ;; NO USER SERVICEABLE PARTS BELOW THIS LINE
275 ;; ======================================================================
276
277 (defconst icalendar--weekday-array ["SU" "MO" "TU" "WE" "TH" "FR" "SA"])
278
279 ;; ======================================================================
280 ;; all the other libs we need
281 ;; ======================================================================
282 (require 'calendar)
283 (require 'diary-lib)
284
285 ;; ======================================================================
286 ;; misc
287 ;; ======================================================================
288 (defun icalendar--dmsg (&rest args)
289 "Print message ARGS if `icalendar-debug' is non-nil."
290 (if icalendar-debug
291 (apply 'message args)))
292
293 ;; ======================================================================
294 ;; Core functionality
295 ;; Functions for parsing icalendars, importing and so on
296 ;; ======================================================================
297
298 (defun icalendar--get-unfolded-buffer (folded-ical-buffer)
299 "Return a new buffer containing the unfolded contents of a buffer.
300 Folding is the iCalendar way of wrapping long lines. In the
301 created buffer all occurrences of CR LF BLANK are replaced by the
302 empty string. Argument FOLDED-ICAL-BUFFER is the unfolded input
303 buffer."
304 (let ((unfolded-buffer (get-buffer-create " *icalendar-work*")))
305 (save-current-buffer
306 (set-buffer unfolded-buffer)
307 (erase-buffer)
308 (insert-buffer-substring folded-ical-buffer)
309 (goto-char (point-min))
310 (while (re-search-forward "\r?\n[ \t]" nil t)
311 (replace-match "" nil nil)))
312 unfolded-buffer))
313
314 (defsubst icalendar--rris (regexp rep string &optional fixedcase literal)
315 "Replace regular expression in string.
316 Pass arguments REGEXP REP STRING FIXEDCASE LITERAL to
317 `replace-regexp-in-string' (Emacs) or to `replace-in-string' (XEmacs)."
318 (cond ((fboundp 'replace-regexp-in-string)
319 ;; Emacs:
320 (replace-regexp-in-string regexp rep string fixedcase literal))
321 ((fboundp 'replace-in-string)
322 ;; XEmacs:
323 (save-match-data ;; apparently XEmacs needs save-match-data
324 (replace-in-string string regexp rep literal)))))
325
326 (defun icalendar--read-element (invalue inparams)
327 "Recursively read the next iCalendar element in the current buffer.
328 INVALUE gives the current iCalendar element we are reading.
329 INPARAMS gives the current parameters.....
330 This function calls itself recursively for each nested calendar element
331 it finds."
332 (let (element children line name params param param-name param-value
333 value
334 (continue t))
335 (setq children '())
336 (while (and continue
337 (re-search-forward "^\\([A-Za-z0-9-]+\\)[;:]" nil t))
338 (setq name (intern (match-string 1)))
339 (backward-char 1)
340 (setq params '())
341 (setq line '())
342 (while (looking-at ";")
343 (re-search-forward ";\\([A-Za-z0-9-]+\\)=" nil nil)
344 (setq param-name (intern (match-string 1)))
345 (re-search-forward "\\(\\([^;,:\"]+\\)\\|\"\\([^\"]+\\)\"\\)[;:]"
346 nil t)
347 (backward-char 1)
348 (setq param-value (or (match-string 2) (match-string 3)))
349 (setq param (list param-name param-value))
350 (while (looking-at ",")
351 (re-search-forward "\\(\\([^;,:]+\\)\\|\"\\([^\"]+\\)\"\\)"
352 nil t)
353 (if (match-string 2)
354 (setq param-value (match-string 2))
355 (setq param-value (match-string 3)))
356 (setq param (append param param-value)))
357 (setq params (append params param)))
358 (unless (looking-at ":")
359 (error "Oops"))
360 (forward-char 1)
361 (re-search-forward "\\(.*\\)\\(\r?\n[ \t].*\\)*" nil t)
362 (setq value (icalendar--rris "\r?\n[ \t]" "" (match-string 0)))
363 (setq line (list name params value))
364 (cond ((eq name 'BEGIN)
365 (setq children
366 (append children
367 (list (icalendar--read-element (intern value)
368 params)))))
369 ((eq name 'END)
370 (setq continue nil))
371 (t
372 (setq element (append element (list line))))))
373 (if invalue
374 (list invalue inparams element children)
375 children)))
376
377 ;; ======================================================================
378 ;; helper functions for examining events
379 ;; ======================================================================
380
381 ;;(defsubst icalendar--get-all-event-properties (event)
382 ;; "Return the list of properties in this EVENT."
383 ;; (car (cddr event)))
384
385 (defun icalendar--get-event-property (event prop)
386 "For the given EVENT return the value of the first occurrence of PROP."
387 (catch 'found
388 (let ((props (car (cddr event))) pp)
389 (while props
390 (setq pp (car props))
391 (if (eq (car pp) prop)
392 (throw 'found (car (cddr pp))))
393 (setq props (cdr props))))
394 nil))
395
396 (defun icalendar--get-event-property-attributes (event prop)
397 "For the given EVENT return attributes of the first occurrence of PROP."
398 (catch 'found
399 (let ((props (car (cddr event))) pp)
400 (while props
401 (setq pp (car props))
402 (if (eq (car pp) prop)
403 (throw 'found (cadr pp)))
404 (setq props (cdr props))))
405 nil))
406
407 (defun icalendar--get-event-properties (event prop)
408 "For the given EVENT return a list of all values of the property PROP."
409 (let ((props (car (cddr event))) pp result)
410 (while props
411 (setq pp (car props))
412 (if (eq (car pp) prop)
413 (setq result (append (split-string (car (cddr pp)) ",") result)))
414 (setq props (cdr props)))
415 result))
416
417 ;; (defun icalendar--set-event-property (event prop new-value)
418 ;; "For the given EVENT set the property PROP to the value NEW-VALUE."
419 ;; (catch 'found
420 ;; (let ((props (car (cddr event))) pp)
421 ;; (while props
422 ;; (setq pp (car props))
423 ;; (when (eq (car pp) prop)
424 ;; (setcdr (cdr pp) new-value)
425 ;; (throw 'found (car (cddr pp))))
426 ;; (setq props (cdr props)))
427 ;; (setq props (car (cddr event)))
428 ;; (setcar (cddr event)
429 ;; (append props (list (list prop nil new-value)))))))
430
431 (defun icalendar--get-children (node name)
432 "Return all children of the given NODE which have a name NAME.
433 For instance the VCALENDAR node can have VEVENT children as well as VTODO
434 children."
435 (let ((result nil)
436 (children (cadr (cddr node))))
437 (when (eq (car node) name)
438 (setq result node))
439 ;;(message "%s" node)
440 (when children
441 (let ((subresult
442 (delq nil
443 (mapcar (lambda (n)
444 (icalendar--get-children n name))
445 children))))
446 (if subresult
447 (if result
448 (setq result (append result subresult))
449 (setq result subresult)))))
450 result))
451
452 ;; private
453 (defun icalendar--all-events (icalendar)
454 "Return the list of all existing events in the given ICALENDAR."
455 (let ((result '()))
456 (mapc (lambda (elt)
457 (setq result (append (icalendar--get-children elt 'VEVENT)
458 result)))
459 (nreverse icalendar))
460 result))
461
462 (defun icalendar--split-value (value-string)
463 "Split VALUE-STRING at ';='."
464 (let ((result '())
465 param-name param-value)
466 (when value-string
467 (save-current-buffer
468 (set-buffer (get-buffer-create " *icalendar-work*"))
469 (set-buffer-modified-p nil)
470 (erase-buffer)
471 (insert value-string)
472 (goto-char (point-min))
473 (while
474 (re-search-forward
475 "\\([A-Za-z0-9-]+\\)=\\(\\([^;:]+\\)\\|\"\\([^\"]+\\)\"\\);?"
476 nil t)
477 (setq param-name (intern (match-string 1)))
478 (setq param-value (match-string 2))
479 (setq result
480 (append result (list (list param-name param-value)))))))
481 result))
482
483 (defun icalendar--convert-tz-offset (alist dst-p)
484 "Return a cons of two strings representing a timezone start.
485 ALIST is an alist entry from a VTIMEZONE, like STANDARD.
486 DST-P is non-nil if this is for daylight savings time.
487 The strings are suitable for assembling into a TZ variable."
488 (let ((offset (car (cddr (assq 'TZOFFSETTO alist))))
489 (rrule-value (car (cddr (assq 'RRULE alist))))
490 (dtstart (car (cddr (assq 'DTSTART alist)))))
491 ;; FIXME: for now we only handle RRULE and not RDATE here.
492 (when (and offset rrule-value dtstart)
493 (let* ((rrule (icalendar--split-value rrule-value))
494 (freq (cadr (assq 'FREQ rrule)))
495 (bymonth (cadr (assq 'BYMONTH rrule)))
496 (byday (cadr (assq 'BYDAY rrule))))
497 ;; FIXME: we don't correctly handle WKST here.
498 (if (and (string= freq "YEARLY") bymonth)
499 (cons
500 (concat
501 ;; Fake a name.
502 (if dst-p "DST" "STD")
503 ;; For TZ, OFFSET is added to the local time. So,
504 ;; invert the values.
505 (if (eq (aref offset 0) ?-) "+" "-")
506 (substring offset 1 3)
507 ":"
508 (substring offset 3 5))
509 ;; The start time.
510 (let* ((day (icalendar--get-weekday-number (substring byday -2)))
511 (week (if (eq day -1)
512 byday
513 (substring byday 0 -2))))
514 ;; "Translate" the iCalendar way to specify the last
515 ;; (sun|mon|...)day in month to the tzset way.
516 (if (string= week "-1") ; last day as iCalendar calls it
517 (setq week "5")) ; last day as tzset calls it
518 (concat "M" bymonth "." week "." (if (eq day -1) "0"
519 (int-to-string day))
520 ;; Start time.
521 "/"
522 (substring dtstart -6 -4)
523 ":"
524 (substring dtstart -4 -2)
525 ":"
526 (substring dtstart -2)))))))))
527
528 (defun icalendar--parse-vtimezone (alist)
529 "Turn a VTIMEZONE ALIST into a cons (ID . TZ-STRING).
530 Return nil if timezone cannot be parsed."
531 (let* ((tz-id (icalendar--convert-string-for-import
532 (icalendar--get-event-property alist 'TZID)))
533 (daylight (cadr (cdar (icalendar--get-children alist 'DAYLIGHT))))
534 (day (and daylight (icalendar--convert-tz-offset daylight t)))
535 (standard (cadr (cdar (icalendar--get-children alist 'STANDARD))))
536 (std (and standard (icalendar--convert-tz-offset standard nil))))
537 (if (and tz-id std)
538 (cons tz-id
539 (if day
540 (concat (car std) (car day)
541 "," (cdr day) "," (cdr std))
542 (car std))))))
543
544 (defun icalendar--convert-all-timezones (icalendar)
545 "Convert all timezones in the ICALENDAR into an alist.
546 Each element of the alist is a cons (ID . TZ-STRING),
547 like `icalendar--parse-vtimezone'."
548 (let (result)
549 (dolist (zone (icalendar--get-children (car icalendar) 'VTIMEZONE))
550 (setq zone (icalendar--parse-vtimezone zone))
551 (if zone
552 (setq result (cons zone result))))
553 result))
554
555 (defun icalendar--find-time-zone (prop-list zone-map)
556 "Return a timezone string for the time zone in PROP-LIST, or nil if none.
557 ZONE-MAP is a timezone alist as returned by `icalendar--convert-all-timezones'."
558 (let ((id (plist-get prop-list 'TZID)))
559 (if id
560 (cdr (assoc id zone-map)))))
561
562 (defun icalendar--decode-isodatetime (isodatetimestring &optional day-shift
563 zone)
564 "Return ISODATETIMESTRING in format like `decode-time'.
565 Converts from ISO-8601 to Emacs representation. If
566 ISODATETIMESTRING specifies UTC time (trailing letter Z) the
567 decoded time is given in the local time zone! If optional
568 parameter DAY-SHIFT is non-nil the result is shifted by DAY-SHIFT
569 days.
570 ZONE, if provided, is the timezone, in any format understood by `encode-time'.
571
572 FIXME: multiple comma-separated values should be allowed!"
573 (icalendar--dmsg isodatetimestring)
574 (if isodatetimestring
575 ;; day/month/year must be present
576 (let ((year (read (substring isodatetimestring 0 4)))
577 (month (read (substring isodatetimestring 4 6)))
578 (day (read (substring isodatetimestring 6 8)))
579 (hour 0)
580 (minute 0)
581 (second 0))
582 (when (> (length isodatetimestring) 12)
583 ;; hour/minute present
584 (setq hour (read (substring isodatetimestring 9 11)))
585 (setq minute (read (substring isodatetimestring 11 13))))
586 (when (> (length isodatetimestring) 14)
587 ;; seconds present
588 (setq second (read (substring isodatetimestring 13 15))))
589 (when (and (> (length isodatetimestring) 15)
590 ;; UTC specifier present
591 (char-equal ?Z (aref isodatetimestring 15)))
592 ;; if not UTC add current-time-zone offset
593 ;; current-time-zone should be called with actual UTC time
594 ;; (daylight saving at that time may differ to current one)
595 (setq second (+ (car (current-time-zone
596 (encode-time second minute hour day month year
597 0)))
598 second)))
599 ;; shift if necessary
600 (if day-shift
601 (let ((mdy (calendar-gregorian-from-absolute
602 (+ (calendar-absolute-from-gregorian
603 (list month day year))
604 day-shift))))
605 (setq month (nth 0 mdy))
606 (setq day (nth 1 mdy))
607 (setq year (nth 2 mdy))))
608 ;; create the decoded date-time
609 ;; FIXME!?!
610 (condition-case nil
611 (decode-time (encode-time second minute hour day month year zone))
612 (error
613 (message "Cannot decode \"%s\"" isodatetimestring)
614 ;; hope for the best...
615 (list second minute hour day month year 0 nil 0))))
616 ;; isodatetimestring == nil
617 nil))
618
619 (defun icalendar--decode-isoduration (isodurationstring
620 &optional duration-correction)
621 "Convert ISODURATIONSTRING into format provided by `decode-time'.
622 Converts from ISO-8601 to Emacs representation. If ISODURATIONSTRING
623 specifies UTC time (trailing letter Z) the decoded time is given in
624 the local time zone!
625
626 Optional argument DURATION-CORRECTION shortens result by one day.
627
628 FIXME: TZID-attributes are ignored....!
629 FIXME: multiple comma-separated values should be allowed!"
630 (if isodurationstring
631 (save-match-data
632 (string-match
633 (concat
634 "^P[+-]?\\("
635 "\\(\\([0-9]+\\)D\\)" ; days only
636 "\\|"
637 "\\(\\(\\([0-9]+\\)D\\)?T\\(\\([0-9]+\\)H\\)?" ; opt days
638 "\\(\\([0-9]+\\)M\\)?\\(\\([0-9]+\\)S\\)?\\)" ; mand. time
639 "\\|"
640 "\\(\\([0-9]+\\)W\\)" ; weeks only
641 "\\)$") isodurationstring)
642 (let ((seconds 0)
643 (minutes 0)
644 (hours 0)
645 (days 0)
646 (months 0)
647 (years 0))
648 (cond
649 ((match-beginning 2) ;days only
650 (setq days (read (substring isodurationstring
651 (match-beginning 3)
652 (match-end 3))))
653 (when duration-correction
654 (setq days (1- days))))
655 ((match-beginning 4) ;days and time
656 (if (match-beginning 5)
657 (setq days (* 7 (read (substring isodurationstring
658 (match-beginning 6)
659 (match-end 6))))))
660 (if (match-beginning 7)
661 (setq hours (read (substring isodurationstring
662 (match-beginning 8)
663 (match-end 8)))))
664 (if (match-beginning 9)
665 (setq minutes (read (substring isodurationstring
666 (match-beginning 10)
667 (match-end 10)))))
668 (if (match-beginning 11)
669 (setq seconds (read (substring isodurationstring
670 (match-beginning 12)
671 (match-end 12))))))
672 ((match-beginning 13) ;weeks only
673 (setq days (* 7 (read (substring isodurationstring
674 (match-beginning 14)
675 (match-end 14)))))))
676 (list seconds minutes hours days months years)))
677 ;; isodatetimestring == nil
678 nil))
679
680 (defun icalendar--add-decoded-times (time1 time2)
681 "Add TIME1 to TIME2.
682 Both times must be given in decoded form. One of these times must be
683 valid (year > 1900 or something)."
684 ;; FIXME: does this function exist already?
685 (decode-time (encode-time
686 (+ (nth 0 time1) (nth 0 time2))
687 (+ (nth 1 time1) (nth 1 time2))
688 (+ (nth 2 time1) (nth 2 time2))
689 (+ (nth 3 time1) (nth 3 time2))
690 (+ (nth 4 time1) (nth 4 time2))
691 (+ (nth 5 time1) (nth 5 time2))
692 nil
693 nil
694 ;;(or (nth 6 time1) (nth 6 time2)) ;; FIXME?
695 )))
696
697 (defun icalendar--datetime-to-american-date (datetime &optional separator)
698 "Convert the decoded DATETIME to American-style format.
699 Optional argument SEPARATOR gives the separator between month,
700 day, and year. If nil a blank character is used as separator.
701 American format: \"month day year\"."
702 (if datetime
703 (format "%d%s%d%s%d" (nth 4 datetime) ;month
704 (or separator " ")
705 (nth 3 datetime) ;day
706 (or separator " ")
707 (nth 5 datetime)) ;year
708 ;; datetime == nil
709 nil))
710
711 (define-obsolete-function-alias 'icalendar--datetime-to-noneuropean-date
712 'icalendar--datetime-to-american-date "icalendar 0.19")
713
714 (defun icalendar--datetime-to-european-date (datetime &optional separator)
715 "Convert the decoded DATETIME to European format.
716 Optional argument SEPARATOR gives the separator between month,
717 day, and year. If nil a blank character is used as separator.
718 European format: (day month year).
719 FIXME"
720 (if datetime
721 (format "%d%s%d%s%d" (nth 3 datetime) ;day
722 (or separator " ")
723 (nth 4 datetime) ;month
724 (or separator " ")
725 (nth 5 datetime)) ;year
726 ;; datetime == nil
727 nil))
728
729 (defun icalendar--datetime-to-iso-date (datetime &optional separator)
730 "Convert the decoded DATETIME to ISO format.
731 Optional argument SEPARATOR gives the separator between month,
732 day, and year. If nil a blank character is used as separator.
733 ISO format: (year month day)."
734 (if datetime
735 (format "%d%s%d%s%d" (nth 5 datetime) ;year
736 (or separator " ")
737 (nth 4 datetime) ;month
738 (or separator " ")
739 (nth 3 datetime)) ;day
740 ;; datetime == nil
741 nil))
742
743 (defun icalendar--datetime-to-diary-date (datetime &optional separator)
744 "Convert the decoded DATETIME to diary format.
745 Optional argument SEPARATOR gives the separator between month,
746 day, and year. If nil a blank character is used as separator.
747 Call icalendar--datetime-to-*-date according to the current
748 calendar date style."
749 (funcall (intern-soft (format "icalendar--datetime-to-%s-date"
750 calendar-date-style))
751 datetime separator))
752
753 (defun icalendar--datetime-to-colontime (datetime)
754 "Extract the time part of a decoded DATETIME into 24-hour format.
755 Note that this silently ignores seconds."
756 (format "%02d:%02d" (nth 2 datetime) (nth 1 datetime)))
757
758 (defun icalendar--get-month-number (monthname)
759 "Return the month number for the given MONTHNAME."
760 (catch 'found
761 (let ((num 1)
762 (m (downcase monthname)))
763 (mapc (lambda (month)
764 (let ((mm (downcase month)))
765 (if (or (string-equal mm m)
766 (string-equal (substring mm 0 3) m))
767 (throw 'found num))
768 (setq num (1+ num))))
769 calendar-month-name-array))
770 ;; Error:
771 -1))
772
773 (defun icalendar--get-weekday-number (abbrevweekday)
774 "Return the number for the ABBREVWEEKDAY."
775 (if abbrevweekday
776 (catch 'found
777 (let ((num 0)
778 (aw (downcase abbrevweekday)))
779 (mapc (lambda (day)
780 (let ((d (downcase day)))
781 (if (string-equal d aw)
782 (throw 'found num))
783 (setq num (1+ num))))
784 icalendar--weekday-array)))
785 ;; Error:
786 -1))
787
788 (defun icalendar--get-weekday-numbers (abbrevweekdays)
789 "Return the list of numbers for the comma-separated ABBREVWEEKDAYS."
790 (when abbrevweekdays
791 (let* ((num -1)
792 (weekday-alist (mapcar (lambda (day)
793 (progn
794 (setq num (1+ num))
795 (cons (downcase day) num)))
796 icalendar--weekday-array)))
797 (delq nil
798 (mapcar (lambda (abbrevday)
799 (cdr (assoc abbrevday weekday-alist)))
800 (split-string (downcase abbrevweekdays) ","))))))
801
802 (defun icalendar--get-weekday-abbrev (weekday)
803 "Return the abbreviated WEEKDAY."
804 (catch 'found
805 (let ((num 0)
806 (w (downcase weekday)))
807 (mapc (lambda (day)
808 (let ((d (downcase day)))
809 (if (or (string-equal d w)
810 (string-equal (substring d 0 3) w))
811 (throw 'found (aref icalendar--weekday-array num)))
812 (setq num (1+ num))))
813 calendar-day-name-array))
814 ;; Error:
815 nil))
816
817 (defun icalendar--date-to-isodate (date &optional day-shift)
818 "Convert DATE to iso-style date.
819 DATE must be a list of the form (month day year).
820 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days."
821 (let ((mdy (calendar-gregorian-from-absolute
822 (+ (calendar-absolute-from-gregorian date)
823 (or day-shift 0)))))
824 (format "%04d%02d%02d" (nth 2 mdy) (nth 0 mdy) (nth 1 mdy))))
825
826
827 (defun icalendar--datestring-to-isodate (datestring &optional day-shift)
828 "Convert diary-style DATESTRING to iso-style date.
829 If DAY-SHIFT is non-nil, the result is shifted by DAY-SHIFT days
830 -- DAY-SHIFT must be either nil or an integer. This function
831 tries to figure the date style from DATESTRING itself. If that
832 is not possible it uses the current calendar date style."
833 (let ((day -1) month year)
834 (save-match-data
835 (cond ( ;; iso-style numeric date
836 (string-match (concat "\\s-*"
837 "\\([0-9]\\{4\\}\\)[ \t/]\\s-*"
838 "0?\\([1-9][0-9]?\\)[ \t/]\\s-*"
839 "0?\\([1-9][0-9]?\\)")
840 datestring)
841 (setq year (read (substring datestring (match-beginning 1)
842 (match-end 1))))
843 (setq month (read (substring datestring (match-beginning 2)
844 (match-end 2))))
845 (setq day (read (substring datestring (match-beginning 3)
846 (match-end 3)))))
847 ( ;; non-iso numeric date -- must rely on configured
848 ;; calendar style
849 (string-match (concat "\\s-*"
850 "0?\\([1-9][0-9]?\\)[ \t/]\\s-*"
851 "0?\\([1-9][0-9]?\\),?[ \t/]\\s-*"
852 "\\([0-9]\\{4\\}\\)")
853 datestring)
854 (setq day (read (substring datestring (match-beginning 1)
855 (match-end 1))))
856 (setq month (read (substring datestring (match-beginning 2)
857 (match-end 2))))
858 (setq year (read (substring datestring (match-beginning 3)
859 (match-end 3))))
860 (if (eq calendar-date-style 'american)
861 (let ((x month))
862 (setq month day)
863 (setq day x))))
864 ( ;; date contains month names -- iso style
865 (string-match (concat "\\s-*"
866 "\\([0-9]\\{4\\}\\)[ \t/]\\s-*"
867 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
868 "0?\\([123]?[0-9]\\)")
869 datestring)
870 (setq year (read (substring datestring (match-beginning 1)
871 (match-end 1))))
872 (setq month (icalendar--get-month-number
873 (substring datestring (match-beginning 2)
874 (match-end 2))))
875 (setq day (read (substring datestring (match-beginning 3)
876 (match-end 3)))))
877 ( ;; date contains month names -- european style
878 (string-match (concat "\\s-*"
879 "0?\\([123]?[0-9]\\)[ \t/]\\s-*"
880 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
881 "\\([0-9]\\{4\\}\\)")
882 datestring)
883 (setq day (read (substring datestring (match-beginning 1)
884 (match-end 1))))
885 (setq month (icalendar--get-month-number
886 (substring datestring (match-beginning 2)
887 (match-end 2))))
888 (setq year (read (substring datestring (match-beginning 3)
889 (match-end 3)))))
890 ( ;; date contains month names -- american style
891 (string-match (concat "\\s-*"
892 "\\([A-Za-z][^ ]+\\)[ \t/]\\s-*"
893 "0?\\([123]?[0-9]\\),?[ \t/]\\s-*"
894 "\\([0-9]\\{4\\}\\)")
895 datestring)
896 (setq day (read (substring datestring (match-beginning 2)
897 (match-end 2))))
898 (setq month (icalendar--get-month-number
899 (substring datestring (match-beginning 1)
900 (match-end 1))))
901 (setq year (read (substring datestring (match-beginning 3)
902 (match-end 3)))))
903 (t
904 nil)))
905 (if (> day 0)
906 (let ((mdy (calendar-gregorian-from-absolute
907 (+ (calendar-absolute-from-gregorian (list month day
908 year))
909 (or day-shift 0)))))
910 (icalendar--dmsg (format "%04d%02d%02d" (nth 2 mdy) (nth 0 mdy) (nth 1 mdy)))
911 (format "%04d%02d%02d" (nth 2 mdy) (nth 0 mdy) (nth 1 mdy)))
912 nil)))
913
914 (defun icalendar--diarytime-to-isotime (timestring ampmstring)
915 "Convert a time like 9:30pm to an iso-conform string like T213000.
916 In this example the TIMESTRING would be \"9:30\" and the
917 AMPMSTRING would be \"pm\". The minutes may be missing as long
918 as the colon is missing as well, i.e. \"9\" is allowed as
919 TIMESTRING and has the same result as \"9:00\"."
920 (if timestring
921 (let* ((parts (save-match-data (split-string timestring ":")))
922 (h (car parts))
923 (m (if (cdr parts) (cadr parts)
924 (if (> (length h) 2) "" "00")))
925 (starttimenum (read (concat h m))))
926 ;; take care of am/pm style
927 ;; Be sure *not* to convert 12:00pm - 12:59pm to 2400-2459
928 (if (and ampmstring (string= "pm" ampmstring) (< starttimenum 1200))
929 (setq starttimenum (+ starttimenum 1200)))
930 ;; Similar effect with 12:00am - 12:59am (need to convert to 0000-0059)
931 (if (and ampmstring (string= "am" ampmstring) (>= starttimenum 1200))
932 (setq starttimenum (- starttimenum 1200)))
933 (format "T%04d00" starttimenum))
934 nil))
935
936 (defun icalendar--convert-string-for-export (string)
937 "Escape comma and other critical characters in STRING."
938 (icalendar--rris "," "\\\\," string))
939
940 (defun icalendar--convert-string-for-import (string)
941 "Remove escape chars for comma, semicolon etc. from STRING."
942 (icalendar--rris
943 "\\\\n" "\n " (icalendar--rris
944 "\\\\\"" "\"" (icalendar--rris
945 "\\\\;" ";" (icalendar--rris
946 "\\\\," "," string)))))
947
948 ;; ======================================================================
949 ;; Export -- convert emacs-diary to iCalendar
950 ;; ======================================================================
951
952 ;;;###autoload
953 (defun icalendar-export-file (diary-filename ical-filename)
954 "Export diary file to iCalendar format.
955 All diary entries in the file DIARY-FILENAME are converted to iCalendar
956 format. The result is appended to the file ICAL-FILENAME."
957 (interactive "FExport diary data from file: \n\
958 Finto iCalendar file: ")
959 (save-current-buffer
960 (set-buffer (find-file diary-filename))
961 (icalendar-export-region (point-min) (point-max) ical-filename)))
962
963 (define-obsolete-function-alias 'icalendar-convert-diary-to-ical
964 'icalendar-export-file "22.1")
965
966 (defvar icalendar--uid-count 0
967 "Auxiliary counter for creating unique ids.")
968
969 (defun icalendar--create-uid (entry-full contents)
970 "Construct a unique iCalendar UID for a diary entry.
971 ENTRY-FULL is the full diary entry string. CONTENTS is the
972 current iCalendar object, as a string. Increase
973 `icalendar--uid-count'. Returns the UID string."
974 (let ((uid icalendar-uid-format))
975 (if
976 ;; Allow other apps (such as org-mode) to create its own uid
977 (get-text-property 0 'uid entry-full)
978 (setq uid (get-text-property 0 'uid entry-full))
979 (setq uid (replace-regexp-in-string
980 "%c"
981 (format "%d" icalendar--uid-count)
982 uid t t))
983 (setq icalendar--uid-count (1+ icalendar--uid-count))
984 (setq uid (replace-regexp-in-string
985 "%t"
986 (format "%d%d%d" (car (current-time))
987 (cadr (current-time))
988 (car (cddr (current-time))))
989 uid t t))
990 (setq uid (replace-regexp-in-string
991 "%h"
992 (format "%d" (abs (sxhash entry-full))) uid t t))
993 (setq uid (replace-regexp-in-string
994 "%u" (or user-login-name "UNKNOWN_USER") uid t t))
995 (let ((dtstart (if (string-match "^DTSTART[^:]*:\\([0-9]*\\)" contents)
996 (substring contents (match-beginning 1) (match-end 1))
997 "DTSTART")))
998 (setq uid (replace-regexp-in-string "%s" dtstart uid t t))))
999
1000 ;; Return the UID string
1001 uid))
1002
1003 ;;;###autoload
1004 (defun icalendar-export-region (min max ical-filename)
1005 "Export region in diary file to iCalendar format.
1006 All diary entries in the region from MIN to MAX in the current buffer are
1007 converted to iCalendar format. The result is appended to the file
1008 ICAL-FILENAME.
1009 This function attempts to return t if something goes wrong. In this
1010 case an error string which describes all the errors and problems is
1011 written into the buffer `*icalendar-errors*'."
1012 (interactive "r
1013 FExport diary data into iCalendar file: ")
1014 (let ((result "")
1015 (start 0)
1016 (entry-main "")
1017 (entry-rest "")
1018 (entry-full "")
1019 (header "")
1020 (contents-n-summary)
1021 (contents)
1022 (found-error nil)
1023 (nonmarker (concat "^" (regexp-quote diary-nonmarking-symbol)
1024 "?"))
1025 (other-elements nil))
1026 ;; prepare buffer with error messages
1027 (save-current-buffer
1028 (set-buffer (get-buffer-create "*icalendar-errors*"))
1029 (erase-buffer))
1030
1031 ;; here we go
1032 (save-excursion
1033 (goto-char min)
1034 (while (re-search-forward
1035 ;; possibly ignore hidden entries beginning with "&"
1036 (if icalendar-export-hidden-diary-entries
1037 "^\\([^ \t\n#].+\\)\\(\\(\n[ \t].*\\)*\\)"
1038 "^\\([^ \t\n&#].+\\)\\(\\(\n[ \t].*\\)*\\)") max t)
1039 (setq entry-main (match-string 1))
1040 (if (match-beginning 2)
1041 (setq entry-rest (match-string 2))
1042 (setq entry-rest ""))
1043 (setq entry-full (concat entry-main entry-rest))
1044
1045 (condition-case error-val
1046 (progn
1047 (setq cns-cons-or-list
1048 (icalendar--convert-to-ical nonmarker entry-main))
1049 (setq other-elements (icalendar--parse-summary-and-rest
1050 entry-full))
1051 (mapc (lambda (contents-n-summary)
1052 (setq contents (concat (car contents-n-summary)
1053 "\nSUMMARY:"
1054 (cdr contents-n-summary)))
1055 (let ((cla (cdr (assoc 'cla other-elements)))
1056 (des (cdr (assoc 'des other-elements)))
1057 (loc (cdr (assoc 'loc other-elements)))
1058 (org (cdr (assoc 'org other-elements)))
1059 (sta (cdr (assoc 'sta other-elements)))
1060 (sum (cdr (assoc 'sum other-elements)))
1061 (url (cdr (assoc 'url other-elements)))
1062 (uid (cdr (assoc 'uid other-elements))))
1063 (if cla
1064 (setq contents (concat contents "\nCLASS:" cla)))
1065 (if des
1066 (setq contents (concat contents "\nDESCRIPTION:"
1067 des)))
1068 (if loc
1069 (setq contents (concat contents "\nLOCATION:" loc)))
1070 (if org
1071 (setq contents (concat contents "\nORGANIZER:"
1072 org)))
1073 (if sta
1074 (setq contents (concat contents "\nSTATUS:" sta)))
1075 ;;(if sum
1076 ;; (setq contents (concat contents "\nSUMMARY:" sum)))
1077 (if url
1078 (setq contents (concat contents "\nURL:" url)))
1079
1080 (setq header (concat "\nBEGIN:VEVENT\nUID:"
1081 (or uid
1082 (icalendar--create-uid
1083 entry-full contents)))))
1084 (setq result (concat result header contents
1085 "\nEND:VEVENT")))
1086 (if (consp cns-cons-or-list)
1087 (list cns-cons-or-list)
1088 cns-cons-or-list)))
1089 ;; handle errors
1090 (error
1091 (setq found-error t)
1092 (save-current-buffer
1093 (set-buffer (get-buffer-create "*icalendar-errors*"))
1094 (insert (format "Error in line %d -- %s: `%s'\n"
1095 (count-lines (point-min) (point))
1096 error-val
1097 entry-main))))))
1098
1099 ;; we're done, insert everything into the file
1100 (save-current-buffer
1101 (let ((coding-system-for-write 'utf-8))
1102 (set-buffer (find-file ical-filename))
1103 (goto-char (point-max))
1104 (insert "BEGIN:VCALENDAR")
1105 (insert "\nPRODID:-//Emacs//NONSGML icalendar.el//EN")
1106 (insert "\nVERSION:2.0")
1107 (insert result)
1108 (insert "\nEND:VCALENDAR\n")
1109 ;; save the diary file
1110 (save-buffer)
1111 (unless found-error
1112 (bury-buffer)))))
1113 found-error))
1114
1115 (defun icalendar--convert-to-ical (nonmarker entry-main)
1116 "Convert a diary entry to iCalendar format.
1117 NONMARKER is a regular expression matching the start of non-marking
1118 entries. ENTRY-MAIN is the first line of the diary entry."
1119 (or
1120 (unless icalendar-export-sexp-enumerate-all
1121 (or
1122 ;; anniversaries -- %%(diary-anniversary ...)
1123 (icalendar--convert-anniversary-to-ical nonmarker entry-main)
1124 ;; cyclic events -- %%(diary-cyclic ...)
1125 (icalendar--convert-cyclic-to-ical nonmarker entry-main)
1126 ;; diary-date -- %%(diary-date ...)
1127 (icalendar--convert-date-to-ical nonmarker entry-main)
1128 ;; float events -- %%(diary-float ...)
1129 (icalendar--convert-float-to-ical nonmarker entry-main)
1130 ;; block events -- %%(diary-block ...)
1131 (icalendar--convert-block-to-ical nonmarker entry-main)))
1132 ;; other sexp diary entries
1133 (icalendar--convert-sexp-to-ical nonmarker entry-main)
1134 ;; weekly by day -- Monday 8:30 Team meeting
1135 (icalendar--convert-weekly-to-ical nonmarker entry-main)
1136 ;; yearly by day -- 1 May Tag der Arbeit
1137 (icalendar--convert-yearly-to-ical nonmarker entry-main)
1138 ;; "ordinary" events, start and end time given
1139 ;; 1 Feb 2003 blah
1140 (icalendar--convert-ordinary-to-ical nonmarker entry-main)
1141 ;; everything else
1142 ;; Oops! what's that?
1143 (error "Could not parse entry")))
1144
1145 (defun icalendar--parse-summary-and-rest (summary-and-rest)
1146 "Parse SUMMARY-AND-REST from a diary to fill iCalendar properties.
1147 Returns an alist."
1148 (save-match-data
1149 (if (functionp icalendar-import-format)
1150 ;; can't do anything
1151 nil
1152 ;; split summary-and-rest
1153 (let* ((case-fold-search nil)
1154 (s icalendar-import-format)
1155 (p-cla (or (string-match "%c" icalendar-import-format) -1))
1156 (p-des (or (string-match "%d" icalendar-import-format) -1))
1157 (p-loc (or (string-match "%l" icalendar-import-format) -1))
1158 (p-org (or (string-match "%o" icalendar-import-format) -1))
1159 (p-sum (or (string-match "%s" icalendar-import-format) -1))
1160 (p-sta (or (string-match "%t" icalendar-import-format) -1))
1161 (p-url (or (string-match "%u" icalendar-import-format) -1))
1162 (p-uid (or (string-match "%U" icalendar-import-format) -1))
1163 (p-list (sort (list p-cla p-des p-loc p-org p-sta p-sum p-url p-uid) '<))
1164 (ct 0)
1165 pos-cla pos-des pos-loc pos-org pos-sta pos-sum pos-url pos-uid)
1166 (dotimes (i (length p-list))
1167 ;; Use 'ct' to keep track of current position in list
1168 (cond ((and (>= p-cla 0) (= (nth i p-list) p-cla))
1169 (setq ct (+ ct 1))
1170 (setq pos-cla (* 2 ct)))
1171 ((and (>= p-des 0) (= (nth i p-list) p-des))
1172 (setq ct (+ ct 1))
1173 (setq pos-des (* 2 ct)))
1174 ((and (>= p-loc 0) (= (nth i p-list) p-loc))
1175 (setq ct (+ ct 1))
1176 (setq pos-loc (* 2 ct)))
1177 ((and (>= p-org 0) (= (nth i p-list) p-org))
1178 (setq ct (+ ct 1))
1179 (setq pos-org (* 2 ct)))
1180 ((and (>= p-sta 0) (= (nth i p-list) p-sta))
1181 (setq ct (+ ct 1))
1182 (setq pos-sta (* 2 ct)))
1183 ((and (>= p-sum 0) (= (nth i p-list) p-sum))
1184 (setq ct (+ ct 1))
1185 (setq pos-sum (* 2 ct)))
1186 ((and (>= p-url 0) (= (nth i p-list) p-url))
1187 (setq ct (+ ct 1))
1188 (setq pos-url (* 2 ct)))
1189 ((and (>= p-uid 0) (= (nth i p-list) p-uid))
1190 (setq ct (+ ct 1))
1191 (setq pos-uid (* 2 ct)))) )
1192 (mapc (lambda (ij)
1193 (setq s (icalendar--rris (car ij) (cadr ij) s t t)))
1194 (list
1195 ;; summary must be first! because of %s
1196 (list "%s"
1197 (concat "\\(" icalendar-import-format-summary "\\)??"))
1198 (list "%c"
1199 (concat "\\(" icalendar-import-format-class "\\)??"))
1200 (list "%d"
1201 (concat "\\(" icalendar-import-format-description "\\)??"))
1202 (list "%l"
1203 (concat "\\(" icalendar-import-format-location "\\)??"))
1204 (list "%o"
1205 (concat "\\(" icalendar-import-format-organizer "\\)??"))
1206 (list "%t"
1207 (concat "\\(" icalendar-import-format-status "\\)??"))
1208 (list "%u"
1209 (concat "\\(" icalendar-import-format-url "\\)??"))
1210 (list "%U"
1211 (concat "\\(" icalendar-import-format-uid "\\)??"))))
1212 ;; Need the \' regexp in order to detect multi-line items
1213 (setq s (concat "\\`"
1214 (icalendar--rris "%s" "\\(.*?\\)" s nil t)
1215 "\\'"))
1216 (if (string-match s summary-and-rest)
1217 (let (cla des loc org sta sum url uid)
1218 (if (and pos-sum (match-beginning pos-sum))
1219 (setq sum (substring summary-and-rest
1220 (match-beginning pos-sum)
1221 (match-end pos-sum))))
1222 (if (and pos-cla (match-beginning pos-cla))
1223 (setq cla (substring summary-and-rest
1224 (match-beginning pos-cla)
1225 (match-end pos-cla))))
1226 (if (and pos-des (match-beginning pos-des))
1227 (setq des (substring summary-and-rest
1228 (match-beginning pos-des)
1229 (match-end pos-des))))
1230 (if (and pos-loc (match-beginning pos-loc))
1231 (setq loc (substring summary-and-rest
1232 (match-beginning pos-loc)
1233 (match-end pos-loc))))
1234 (if (and pos-org (match-beginning pos-org))
1235 (setq org (substring summary-and-rest
1236 (match-beginning pos-org)
1237 (match-end pos-org))))
1238 (if (and pos-sta (match-beginning pos-sta))
1239 (setq sta (substring summary-and-rest
1240 (match-beginning pos-sta)
1241 (match-end pos-sta))))
1242 (if (and pos-url (match-beginning pos-url))
1243 (setq url (substring summary-and-rest
1244 (match-beginning pos-url)
1245 (match-end pos-url))))
1246 (if (and pos-uid (match-beginning pos-uid))
1247 (setq uid (substring summary-and-rest
1248 (match-beginning pos-uid)
1249 (match-end pos-uid))))
1250 (list (if cla (cons 'cla cla) nil)
1251 (if des (cons 'des des) nil)
1252 (if loc (cons 'loc loc) nil)
1253 (if org (cons 'org org) nil)
1254 (if sta (cons 'sta sta) nil)
1255 ;;(if sum (cons 'sum sum) nil)
1256 (if url (cons 'url url) nil)
1257 (if uid (cons 'uid uid) nil))))))))
1258
1259 ;; subroutines for icalendar-export-region
1260 (defun icalendar--convert-ordinary-to-ical (nonmarker entry-main)
1261 "Convert \"ordinary\" diary entry to iCalendar format.
1262 NONMARKER is a regular expression matching the start of non-marking
1263 entries. ENTRY-MAIN is the first line of the diary entry."
1264 (if (string-match
1265 (concat nonmarker
1266 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)\\s-*" ; date
1267 "\\(\\([0-9][0-9]?\\(:[0-9][0-9]\\)?\\)\\([ap]m\\)?" ; start time
1268 "\\("
1269 "-\\([0-9][0-9]?\\(:[0-9][0-9]\\)?\\)\\([ap]m\\)?\\)?" ; end time
1270 "\\)?"
1271 "\\s-*\\(.*?\\) ?$")
1272 entry-main)
1273 (let* ((datetime (substring entry-main (match-beginning 1)
1274 (match-end 1)))
1275 (startisostring (icalendar--datestring-to-isodate
1276 datetime))
1277 (endisostring (icalendar--datestring-to-isodate
1278 datetime 1))
1279 (endisostring1)
1280 (starttimestring (icalendar--diarytime-to-isotime
1281 (if (match-beginning 3)
1282 (substring entry-main
1283 (match-beginning 3)
1284 (match-end 3))
1285 nil)
1286 (if (match-beginning 5)
1287 (substring entry-main
1288 (match-beginning 5)
1289 (match-end 5))
1290 nil)))
1291 (endtimestring (icalendar--diarytime-to-isotime
1292 (if (match-beginning 7)
1293 (substring entry-main
1294 (match-beginning 7)
1295 (match-end 7))
1296 nil)
1297 (if (match-beginning 9)
1298 (substring entry-main
1299 (match-beginning 9)
1300 (match-end 9))
1301 nil)))
1302 (summary (icalendar--convert-string-for-export
1303 (substring entry-main (match-beginning 10)
1304 (match-end 10)))))
1305 (icalendar--dmsg "ordinary %s" entry-main)
1306
1307 (unless startisostring
1308 (error "Could not parse date"))
1309
1310 ;; If only start-date is specified, then end-date is next day,
1311 ;; otherwise it is same day.
1312 (setq endisostring1 (if starttimestring
1313 startisostring
1314 endisostring))
1315
1316 (when starttimestring
1317 (unless endtimestring
1318 (let ((time
1319 (read (icalendar--rris "^T0?" ""
1320 starttimestring))))
1321 (if (< time 230000)
1322 ;; Case: ends on same day
1323 (setq endtimestring (format "T%06d"
1324 (+ 10000 time)))
1325 ;; Case: ends on next day
1326 (setq endtimestring (format "T%06d"
1327 (- time 230000)))
1328 (setq endisostring1 endisostring)) )))
1329
1330 (cons (concat "\nDTSTART;"
1331 (if starttimestring "VALUE=DATE-TIME:"
1332 "VALUE=DATE:")
1333 startisostring
1334 (or starttimestring "")
1335 "\nDTEND;"
1336 (if endtimestring "VALUE=DATE-TIME:"
1337 "VALUE=DATE:")
1338 endisostring1
1339 (or endtimestring ""))
1340 summary))
1341 ;; no match
1342 nil))
1343
1344 (defun icalendar-first-weekday-of-year (abbrevweekday year)
1345 "Find the first ABBREVWEEKDAY in a given YEAR.
1346 Returns day number."
1347 (let* ((day-of-week-jan01 (calendar-day-of-week (list 1 1 year)))
1348 (result (+ 1
1349 (- (icalendar--get-weekday-number abbrevweekday)
1350 day-of-week-jan01))))
1351 (cond ((<= result 0)
1352 (setq result (+ result 7)))
1353 ((> result 7)
1354 (setq result (- result 7))))
1355 result))
1356
1357 (defun icalendar--convert-weekly-to-ical (nonmarker entry-main)
1358 "Convert weekly diary entry to iCalendar format.
1359 NONMARKER is a regular expression matching the start of non-marking
1360 entries. ENTRY-MAIN is the first line of the diary entry."
1361 (if (and (string-match (concat nonmarker
1362 "\\([a-z]+\\)\\s-+"
1363 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)"
1364 "\\([ap]m\\)?"
1365 "\\(-"
1366 "\\([0-9][0-9]?:[0-9][0-9]\\)"
1367 "\\([ap]m\\)?\\)?"
1368 "\\)?"
1369 "\\s-*\\(.*?\\) ?$")
1370 entry-main)
1371 (icalendar--get-weekday-abbrev
1372 (substring entry-main (match-beginning 1)
1373 (match-end 1))))
1374 (let* ((day (icalendar--get-weekday-abbrev
1375 (substring entry-main (match-beginning 1)
1376 (match-end 1))))
1377 (starttimestring (icalendar--diarytime-to-isotime
1378 (if (match-beginning 3)
1379 (substring entry-main
1380 (match-beginning 3)
1381 (match-end 3))
1382 nil)
1383 (if (match-beginning 4)
1384 (substring entry-main
1385 (match-beginning 4)
1386 (match-end 4))
1387 nil)))
1388 (endtimestring (icalendar--diarytime-to-isotime
1389 (if (match-beginning 6)
1390 (substring entry-main
1391 (match-beginning 6)
1392 (match-end 6))
1393 nil)
1394 (if (match-beginning 7)
1395 (substring entry-main
1396 (match-beginning 7)
1397 (match-end 7))
1398 nil)))
1399 (summary (icalendar--convert-string-for-export
1400 (substring entry-main (match-beginning 8)
1401 (match-end 8)))))
1402 (icalendar--dmsg "weekly %s" entry-main)
1403
1404 (when starttimestring
1405 (unless endtimestring
1406 (let ((time (read
1407 (icalendar--rris "^T0?" ""
1408 starttimestring))))
1409 (setq endtimestring (format "T%06d"
1410 (+ 10000 time))))))
1411 (cons (concat "\nDTSTART;"
1412 (if starttimestring
1413 "VALUE=DATE-TIME:"
1414 "VALUE=DATE:")
1415 ;; Find the first requested weekday of the
1416 ;; start year
1417 (funcall 'format "%04d%02d%02d"
1418 icalendar-recurring-start-year 1
1419 (icalendar-first-weekday-of-year
1420 day icalendar-recurring-start-year))
1421 (or starttimestring "")
1422 "\nDTEND;"
1423 (if endtimestring
1424 "VALUE=DATE-TIME:"
1425 "VALUE=DATE:")
1426 (funcall 'format "%04d%02d%02d"
1427 ;; end is non-inclusive!
1428 icalendar-recurring-start-year 1
1429 (+ (icalendar-first-weekday-of-year
1430 day icalendar-recurring-start-year)
1431 (if endtimestring 0 1)))
1432 (or endtimestring "")
1433 "\nRRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY="
1434 day)
1435 summary))
1436 ;; no match
1437 nil))
1438
1439 (defun icalendar--convert-yearly-to-ical (nonmarker entry-main)
1440 "Convert yearly diary entry to iCalendar format.
1441 NONMARKER is a regular expression matching the start of non-marking
1442 entries. ENTRY-MAIN is the first line of the diary entry."
1443 (if (string-match (concat nonmarker
1444 (if (eq calendar-date-style 'european)
1445 "\\([0-9]+[0-9]?\\)\\s-+\\([a-z]+\\)\\s-+"
1446 "\\([a-z]+\\)\\s-+\\([0-9]+[0-9]?\\)\\s-+")
1447 "\\*?\\s-*"
1448 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1449 "\\("
1450 "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1451 "\\)?"
1452 "\\s-*\\([^0-9]+.*?\\) ?$" ; must not match years
1453 )
1454 entry-main)
1455 (let* ((daypos (if (eq calendar-date-style 'european) 1 2))
1456 (monpos (if (eq calendar-date-style 'european) 2 1))
1457 (day (read (substring entry-main
1458 (match-beginning daypos)
1459 (match-end daypos))))
1460 (month (icalendar--get-month-number
1461 (substring entry-main
1462 (match-beginning monpos)
1463 (match-end monpos))))
1464 (starttimestring (icalendar--diarytime-to-isotime
1465 (if (match-beginning 4)
1466 (substring entry-main
1467 (match-beginning 4)
1468 (match-end 4))
1469 nil)
1470 (if (match-beginning 5)
1471 (substring entry-main
1472 (match-beginning 5)
1473 (match-end 5))
1474 nil)))
1475 (endtimestring (icalendar--diarytime-to-isotime
1476 (if (match-beginning 7)
1477 (substring entry-main
1478 (match-beginning 7)
1479 (match-end 7))
1480 nil)
1481 (if (match-beginning 8)
1482 (substring entry-main
1483 (match-beginning 8)
1484 (match-end 8))
1485 nil)))
1486 (summary (icalendar--convert-string-for-export
1487 (substring entry-main (match-beginning 9)
1488 (match-end 9)))))
1489 (icalendar--dmsg "yearly %s" entry-main)
1490
1491 (when starttimestring
1492 (unless endtimestring
1493 (let ((time (read
1494 (icalendar--rris "^T0?" ""
1495 starttimestring))))
1496 (setq endtimestring (format "T%06d"
1497 (+ 10000 time))))))
1498 (cons (concat "\nDTSTART;"
1499 (if starttimestring "VALUE=DATE-TIME:"
1500 "VALUE=DATE:")
1501 (format "1900%02d%02d" month day)
1502 (or starttimestring "")
1503 "\nDTEND;"
1504 (if endtimestring "VALUE=DATE-TIME:"
1505 "VALUE=DATE:")
1506 ;; end is not included! shift by one day
1507 (icalendar--date-to-isodate
1508 (list month day 1900)
1509 (if endtimestring 0 1))
1510 (or endtimestring "")
1511 "\nRRULE:FREQ=YEARLY;INTERVAL=1;BYMONTH="
1512 (format "%d" month)
1513 ";BYMONTHDAY="
1514 (format "%d" day))
1515 summary))
1516 ;; no match
1517 nil))
1518
1519 (defun icalendar--convert-sexp-to-ical (nonmarker entry-main &optional start)
1520 "Convert sexp diary entry to iCalendar format.
1521 Enumerate the evaluated sexp entry for the next
1522 `icalendar-export-sexp-enumeration-days' days. NONMARKER is a
1523 regular expression matching the start of non-marking entries.
1524 ENTRY-MAIN is the first line of the diary entry.
1525
1526 Optional argument START determines the first day of the
1527 enumeration, given as a time value, in same format as returned by
1528 `current-time' -- used for test purposes."
1529 (cond ((string-match (concat nonmarker
1530 "%%(and \\(([^)]+)\\))\\(\\s-*.*?\\) ?$")
1531 entry-main)
1532 ;; simple sexp entry as generated by icalendar.el: strip off the
1533 ;; unnecessary (and)
1534 (icalendar--dmsg "diary-sexp from icalendar.el %s" entry-main)
1535 (icalendar--convert-to-ical
1536 nonmarker
1537 (concat "%%"
1538 (substring entry-main (match-beginning 1) (match-end 1))
1539 (substring entry-main (match-beginning 2) (match-end 2)))))
1540 ((string-match (concat nonmarker
1541 "%%\\(([^)]+)\\)\\s-*\\(.*\\)")
1542 entry-main)
1543 ;; regular sexp entry
1544 (icalendar--dmsg "diary-sexp %s" entry-main)
1545 (let ((p1 (substring entry-main (match-beginning 1) (match-end 1)))
1546 (p2 (substring entry-main (match-beginning 2) (match-end 2)))
1547 (now (or start (current-time))))
1548 (delete nil
1549 (mapcar
1550 (lambda (offset)
1551 (let* ((day (decode-time (time-add now
1552 (seconds-to-time
1553 (* offset 60 60 24)))))
1554 (d (nth 3 day))
1555 (m (nth 4 day))
1556 (y (nth 5 day))
1557 (se (diary-sexp-entry p1 p2 (list m d y)))
1558 (see (cond ((stringp se) se)
1559 ((consp se) (cdr se))
1560 (t nil))))
1561 (cond ((null see)
1562 nil)
1563 ((stringp see)
1564 (let ((calendar-date-style 'iso))
1565 (icalendar--convert-ordinary-to-ical
1566 nonmarker (format "%4d/%02d/%02d %s" y m d see))))
1567 (;TODO:
1568 (error (format "Unsupported Sexp-entry: %s"
1569 entry-main))))))
1570 (number-sequence
1571 0 (- icalendar-export-sexp-enumeration-days 1))))))
1572 (t
1573 ;; no match
1574 nil)))
1575
1576 (defun icalendar--convert-block-to-ical (nonmarker entry-main)
1577 "Convert block diary entry to iCalendar format.
1578 NONMARKER is a regular expression matching the start of non-marking
1579 entries. ENTRY-MAIN is the first line of the diary entry."
1580 (if (string-match (concat nonmarker
1581 "%%(diary-block \\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\)"
1582 " +\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
1583 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1584 "\\("
1585 "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1586 "\\)?"
1587 "\\s-*\\(.*?\\) ?$")
1588 entry-main)
1589 (let* ((startstring (substring entry-main
1590 (match-beginning 1)
1591 (match-end 1)))
1592 (endstring (substring entry-main
1593 (match-beginning 2)
1594 (match-end 2)))
1595 (startisostring (icalendar--datestring-to-isodate
1596 startstring))
1597 (endisostring (icalendar--datestring-to-isodate
1598 endstring))
1599 (endisostring+1 (icalendar--datestring-to-isodate
1600 endstring 1))
1601 (starttimestring (icalendar--diarytime-to-isotime
1602 (if (match-beginning 4)
1603 (substring entry-main
1604 (match-beginning 4)
1605 (match-end 4))
1606 nil)
1607 (if (match-beginning 5)
1608 (substring entry-main
1609 (match-beginning 5)
1610 (match-end 5))
1611 nil)))
1612 (endtimestring (icalendar--diarytime-to-isotime
1613 (if (match-beginning 7)
1614 (substring entry-main
1615 (match-beginning 7)
1616 (match-end 7))
1617 nil)
1618 (if (match-beginning 8)
1619 (substring entry-main
1620 (match-beginning 8)
1621 (match-end 8))
1622 nil)))
1623 (summary (icalendar--convert-string-for-export
1624 (substring entry-main (match-beginning 9)
1625 (match-end 9)))))
1626 (icalendar--dmsg "diary-block %s" entry-main)
1627 (when starttimestring
1628 (unless endtimestring
1629 (let ((time
1630 (read (icalendar--rris "^T0?" ""
1631 starttimestring))))
1632 (setq endtimestring (format "T%06d"
1633 (+ 10000 time))))))
1634 (if starttimestring
1635 ;; with time -> write rrule
1636 (cons (concat "\nDTSTART;VALUE=DATE-TIME:"
1637 startisostring
1638 starttimestring
1639 "\nDTEND;VALUE=DATE-TIME:"
1640 startisostring
1641 endtimestring
1642 "\nRRULE:FREQ=DAILY;INTERVAL=1;UNTIL="
1643 endisostring)
1644 summary)
1645 ;; no time -> write long event
1646 (cons (concat "\nDTSTART;VALUE=DATE:" startisostring
1647 "\nDTEND;VALUE=DATE:" endisostring+1)
1648 summary)))
1649 ;; no match
1650 nil))
1651
1652 (defun icalendar--convert-float-to-ical (nonmarker entry-main)
1653 "Convert float diary entry to iCalendar format -- partially unsupported!
1654
1655 FIXME! DAY from diary-float yet unimplemented.
1656
1657 NONMARKER is a regular expression matching the start of non-marking
1658 entries. ENTRY-MAIN is the first line of the diary entry."
1659 (if (string-match (concat nonmarker "%%\\((diary-float .+\\) ?$") entry-main)
1660 (with-temp-buffer
1661 (insert (match-string 1 entry-main))
1662 (goto-char (point-min))
1663 (let* ((sexp (read (current-buffer))) ;using `read' here
1664 ;easier than regexp
1665 ;matching, esp. with
1666 ;different forms of
1667 ;MONTH
1668 (month (nth 1 sexp))
1669 (dayname (nth 2 sexp))
1670 (n (nth 3 sexp))
1671 (day (nth 4 sexp))
1672 (summary
1673 (replace-regexp-in-string
1674 "\\(^\s+\\|\s+$\\)" ""
1675 (buffer-substring (point) (point-max)))))
1676
1677 (when day
1678 (progn
1679 (icalendar--dmsg "diary-float %s" entry-main)
1680 (error "Don't know if or how to implement day in `diary-float'")))
1681
1682 (cons (concat
1683 ;;Start today (yes this is an arbitrary choice):
1684 "\nDTSTART;VALUE=DATE:"
1685 (format-time-string "%Y%m%d" (current-time))
1686 ;;BUT remove today if `diary-float'
1687 ;;expression does not hold true for today:
1688 (when
1689 (null (let ((date (calendar-current-date))
1690 (entry entry-main))
1691 (diary-float month dayname n)))
1692 (concat
1693 "\nEXDATE;VALUE=DATE:"
1694 (format-time-string "%Y%m%d" (current-time))))
1695 "\nRRULE:"
1696 (if (or (numberp month) (listp month))
1697 "FREQ=YEARLY;BYMONTH="
1698 "FREQ=MONTHLY")
1699 (when
1700 (listp month)
1701 (mapconcat
1702 (lambda (m)
1703 (number-to-string m))
1704 (cadr month) ","))
1705 (when
1706 (numberp month)
1707 (number-to-string month))
1708 ";BYDAY="
1709 (number-to-string n)
1710 (aref icalendar--weekday-array dayname))
1711 summary)))
1712 ;; no match
1713 nil))
1714
1715 (defun icalendar--convert-date-to-ical (nonmarker entry-main)
1716 "Convert `diary-date' diary entry to iCalendar format -- unsupported!
1717
1718 FIXME!
1719
1720 NONMARKER is a regular expression matching the start of non-marking
1721 entries. ENTRY-MAIN is the first line of the diary entry."
1722 (if (string-match (concat nonmarker
1723 "%%(diary-date \\([^)]+\\))\\s-*\\(.*?\\) ?$")
1724 entry-main)
1725 (progn
1726 (icalendar--dmsg "diary-date %s" entry-main)
1727 (error "`diary-date' is not supported yet"))
1728 ;; no match
1729 nil))
1730
1731 (defun icalendar--convert-cyclic-to-ical (nonmarker entry-main)
1732 "Convert `diary-cyclic' diary entry to iCalendar format.
1733 NONMARKER is a regular expression matching the start of non-marking
1734 entries. ENTRY-MAIN is the first line of the diary entry."
1735 (if (string-match (concat nonmarker
1736 "%%(diary-cyclic \\([^ ]+\\) +"
1737 "\\([^ /]+[ /]+[^ /]+[ /]+[^ ]+\\))\\s-*"
1738 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1739 "\\("
1740 "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1741 "\\)?"
1742 "\\s-*\\(.*?\\) ?$")
1743 entry-main)
1744 (let* ((frequency (substring entry-main (match-beginning 1)
1745 (match-end 1)))
1746 (datetime (substring entry-main (match-beginning 2)
1747 (match-end 2)))
1748 (startisostring (icalendar--datestring-to-isodate
1749 datetime))
1750 (endisostring (icalendar--datestring-to-isodate
1751 datetime))
1752 (endisostring+1 (icalendar--datestring-to-isodate
1753 datetime 1))
1754 (starttimestring (icalendar--diarytime-to-isotime
1755 (if (match-beginning 4)
1756 (substring entry-main
1757 (match-beginning 4)
1758 (match-end 4))
1759 nil)
1760 (if (match-beginning 5)
1761 (substring entry-main
1762 (match-beginning 5)
1763 (match-end 5))
1764 nil)))
1765 (endtimestring (icalendar--diarytime-to-isotime
1766 (if (match-beginning 7)
1767 (substring entry-main
1768 (match-beginning 7)
1769 (match-end 7))
1770 nil)
1771 (if (match-beginning 8)
1772 (substring entry-main
1773 (match-beginning 8)
1774 (match-end 8))
1775 nil)))
1776 (summary (icalendar--convert-string-for-export
1777 (substring entry-main (match-beginning 9)
1778 (match-end 9)))))
1779 (icalendar--dmsg "diary-cyclic %s" entry-main)
1780 (when starttimestring
1781 (unless endtimestring
1782 (let ((time
1783 (read (icalendar--rris "^T0?" ""
1784 starttimestring))))
1785 (setq endtimestring (format "T%06d"
1786 (+ 10000 time))))))
1787 (cons (concat "\nDTSTART;"
1788 (if starttimestring "VALUE=DATE-TIME:"
1789 "VALUE=DATE:")
1790 startisostring
1791 (or starttimestring "")
1792 "\nDTEND;"
1793 (if endtimestring "VALUE=DATE-TIME:"
1794 "VALUE=DATE:")
1795 (if endtimestring endisostring endisostring+1)
1796 (or endtimestring "")
1797 "\nRRULE:FREQ=DAILY;INTERVAL=" frequency
1798 ;; strange: korganizer does not expect
1799 ;; BYSOMETHING here...
1800 )
1801 summary))
1802 ;; no match
1803 nil))
1804
1805 (defun icalendar--convert-anniversary-to-ical (nonmarker entry-main)
1806 "Convert `diary-anniversary' diary entry to iCalendar format.
1807 NONMARKER is a regular expression matching the start of non-marking
1808 entries. ENTRY-MAIN is the first line of the diary entry."
1809 (if (string-match (concat nonmarker
1810 "%%(diary-anniversary \\([^)]+\\))\\s-*"
1811 "\\(\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?"
1812 "\\("
1813 "-\\([0-9][0-9]?:[0-9][0-9]\\)\\([ap]m\\)?\\)?"
1814 "\\)?"
1815 "\\s-*\\(.*?\\) ?$")
1816 entry-main)
1817 (let* ((datetime (substring entry-main (match-beginning 1)
1818 (match-end 1)))
1819 (startisostring (icalendar--datestring-to-isodate
1820 datetime))
1821 (endisostring (icalendar--datestring-to-isodate
1822 datetime 1))
1823 (starttimestring (icalendar--diarytime-to-isotime
1824 (if (match-beginning 3)
1825 (substring entry-main
1826 (match-beginning 3)
1827 (match-end 3))
1828 nil)
1829 (if (match-beginning 4)
1830 (substring entry-main
1831 (match-beginning 4)
1832 (match-end 4))
1833 nil)))
1834 (endtimestring (icalendar--diarytime-to-isotime
1835 (if (match-beginning 6)
1836 (substring entry-main
1837 (match-beginning 6)
1838 (match-end 6))
1839 nil)
1840 (if (match-beginning 7)
1841 (substring entry-main
1842 (match-beginning 7)
1843 (match-end 7))
1844 nil)))
1845 (summary (icalendar--convert-string-for-export
1846 (substring entry-main (match-beginning 8)
1847 (match-end 8)))))
1848 (icalendar--dmsg "diary-anniversary %s" entry-main)
1849 (when starttimestring
1850 (unless endtimestring
1851 (let ((time
1852 (read (icalendar--rris "^T0?" ""
1853 starttimestring))))
1854 (setq endtimestring (format "T%06d"
1855 (+ 10000 time))))))
1856 (cons (concat "\nDTSTART;"
1857 (if starttimestring "VALUE=DATE-TIME:"
1858 "VALUE=DATE:")
1859 startisostring
1860 (or starttimestring "")
1861 "\nDTEND;"
1862 (if endtimestring "VALUE=DATE-TIME:"
1863 "VALUE=DATE:")
1864 endisostring
1865 (or endtimestring "")
1866 "\nRRULE:FREQ=YEARLY;INTERVAL=1"
1867 ;; the following is redundant,
1868 ;; but korganizer seems to expect this... ;(
1869 ;; and evolution doesn't understand it... :(
1870 ;; so... who is wrong?!
1871 ";BYMONTH="
1872 (substring startisostring 4 6)
1873 ";BYMONTHDAY="
1874 (substring startisostring 6 8))
1875 summary))
1876 ;; no match
1877 nil))
1878
1879 ;; ======================================================================
1880 ;; Import -- convert iCalendar to emacs-diary
1881 ;; ======================================================================
1882
1883 ;;;###autoload
1884 (defun icalendar-import-file (ical-filename diary-filename
1885 &optional non-marking)
1886 "Import an iCalendar file and append to a diary file.
1887 Argument ICAL-FILENAME output iCalendar file.
1888 Argument DIARY-FILENAME input `diary-file'.
1889 Optional argument NON-MARKING determines whether events are created as
1890 non-marking or not."
1891 (interactive "fImport iCalendar data from file: \n\
1892 Finto diary file:
1893 P")
1894 ;; clean up the diary file
1895 (save-current-buffer
1896 ;; now load and convert from the ical file
1897 (set-buffer (find-file ical-filename))
1898 (icalendar-import-buffer diary-filename t non-marking)))
1899
1900 ;;;###autoload
1901 (defun icalendar-import-buffer (&optional diary-file do-not-ask
1902 non-marking)
1903 "Extract iCalendar events from current buffer.
1904
1905 This function searches the current buffer for the first iCalendar
1906 object, reads it and adds all VEVENT elements to the diary
1907 DIARY-FILE.
1908
1909 It will ask for each appointment whether to add it to the diary
1910 unless DO-NOT-ASK is non-nil. When called interactively,
1911 DO-NOT-ASK is nil, so that you are asked for each event.
1912
1913 NON-MARKING determines whether diary events are created as
1914 non-marking.
1915
1916 Return code t means that importing worked well, return code nil
1917 means that an error has occurred. Error messages will be in the
1918 buffer `*icalendar-errors*'."
1919 (interactive)
1920 (save-current-buffer
1921 ;; prepare ical
1922 (message "Preparing iCalendar...")
1923 (set-buffer (icalendar--get-unfolded-buffer (current-buffer)))
1924 (goto-char (point-min))
1925 (message "Preparing iCalendar...done")
1926 (if (re-search-forward "^BEGIN:VCALENDAR\\s-*$" nil t)
1927 (let (ical-contents ical-errors)
1928 ;; read ical
1929 (message "Reading iCalendar...")
1930 (beginning-of-line)
1931 (setq ical-contents (icalendar--read-element nil nil))
1932 (message "Reading iCalendar...done")
1933 ;; convert ical
1934 (message "Converting iCalendar...")
1935 (setq ical-errors (icalendar--convert-ical-to-diary
1936 ical-contents
1937 diary-file do-not-ask non-marking))
1938 (when diary-file
1939 ;; save the diary file if it is visited already
1940 (let ((b (find-buffer-visiting diary-file)))
1941 (when b
1942 (save-current-buffer
1943 (set-buffer b)
1944 (save-buffer)))))
1945 (message "Converting iCalendar...done")
1946 ;; return t if no error occurred
1947 (not ical-errors))
1948 (message
1949 "Current buffer does not contain iCalendar contents!")
1950 ;; return nil, i.e. import did not work
1951 nil)))
1952
1953 (define-obsolete-function-alias 'icalendar-extract-ical-from-buffer
1954 'icalendar-import-buffer "22.1")
1955
1956 (defun icalendar--format-ical-event (event)
1957 "Create a string representation of an iCalendar EVENT."
1958 (if (functionp icalendar-import-format)
1959 (funcall icalendar-import-format event)
1960 (let ((string icalendar-import-format)
1961 (case-fold-search nil)
1962 (conversion-list
1963 '(("%c" CLASS icalendar-import-format-class)
1964 ("%d" DESCRIPTION icalendar-import-format-description)
1965 ("%l" LOCATION icalendar-import-format-location)
1966 ("%o" ORGANIZER icalendar-import-format-organizer)
1967 ("%s" SUMMARY icalendar-import-format-summary)
1968 ("%t" STATUS icalendar-import-format-status)
1969 ("%u" URL icalendar-import-format-url)
1970 ("%U" UID icalendar-import-format-uid))))
1971 ;; convert the specifiers in the format string
1972 (mapc (lambda (i)
1973 (let* ((spec (car i))
1974 (prop (cadr i))
1975 (format (car (cddr i)))
1976 (contents (icalendar--get-event-property event prop))
1977 (formatted-contents ""))
1978 (when (and contents (> (length contents) 0))
1979 (setq formatted-contents
1980 (icalendar--rris "%s"
1981 (icalendar--convert-string-for-import
1982 contents)
1983 (symbol-value format)
1984 t t)))
1985 (setq string (icalendar--rris spec
1986 formatted-contents
1987 string
1988 t t))))
1989 conversion-list)
1990 string)))
1991
1992 (defun icalendar--convert-ical-to-diary (ical-list diary-file
1993 &optional do-not-ask
1994 non-marking)
1995 "Convert iCalendar data to an Emacs diary file.
1996 Import VEVENTS from the iCalendar object ICAL-LIST and saves them to a
1997 DIARY-FILE. If DO-NOT-ASK is nil the user is asked for each event
1998 whether to actually import it. NON-MARKING determines whether diary
1999 events are created as non-marking.
2000 This function attempts to return t if something goes wrong. In this
2001 case an error string which describes all the errors and problems is
2002 written into the buffer `*icalendar-errors*'."
2003 (let* ((ev (icalendar--all-events ical-list))
2004 (error-string "")
2005 (event-ok t)
2006 (found-error nil)
2007 (zone-map (icalendar--convert-all-timezones ical-list))
2008 e diary-string)
2009 ;; step through all events/appointments
2010 (while ev
2011 (setq e (car ev))
2012 (setq ev (cdr ev))
2013 (setq event-ok nil)
2014 (condition-case error-val
2015 (let* ((dtstart (icalendar--get-event-property e 'DTSTART))
2016 (dtstart-zone (icalendar--find-time-zone
2017 (icalendar--get-event-property-attributes
2018 e 'DTSTART)
2019 zone-map))
2020 (dtstart-dec (icalendar--decode-isodatetime dtstart nil
2021 dtstart-zone))
2022 (start-d (icalendar--datetime-to-diary-date
2023 dtstart-dec))
2024 (start-t (icalendar--datetime-to-colontime dtstart-dec))
2025 (dtend (icalendar--get-event-property e 'DTEND))
2026 (dtend-zone (icalendar--find-time-zone
2027 (icalendar--get-event-property-attributes
2028 e 'DTEND)
2029 zone-map))
2030 (dtend-dec (icalendar--decode-isodatetime dtend
2031 nil dtend-zone))
2032 (dtend-1-dec (icalendar--decode-isodatetime dtend -1
2033 dtend-zone))
2034 end-d
2035 end-1-d
2036 end-t
2037 (summary (icalendar--convert-string-for-import
2038 (or (icalendar--get-event-property e 'SUMMARY)
2039 "No summary")))
2040 (rrule (icalendar--get-event-property e 'RRULE))
2041 (rdate (icalendar--get-event-property e 'RDATE))
2042 (duration (icalendar--get-event-property e 'DURATION)))
2043 (icalendar--dmsg "%s: `%s'" start-d summary)
2044 ;; check whether start-time is missing
2045 (if (and dtstart
2046 (string=
2047 (cadr (icalendar--get-event-property-attributes
2048 e 'DTSTART))
2049 "DATE"))
2050 (setq start-t nil))
2051 (when duration
2052 (let ((dtend-dec-d (icalendar--add-decoded-times
2053 dtstart-dec
2054 (icalendar--decode-isoduration duration)))
2055 (dtend-1-dec-d (icalendar--add-decoded-times
2056 dtstart-dec
2057 (icalendar--decode-isoduration duration
2058 t))))
2059 (if (and dtend-dec (not (eq dtend-dec dtend-dec-d)))
2060 (message "Inconsistent endtime and duration for %s"
2061 summary))
2062 (setq dtend-dec dtend-dec-d)
2063 (setq dtend-1-dec dtend-1-dec-d)))
2064 (setq end-d (if dtend-dec
2065 (icalendar--datetime-to-diary-date dtend-dec)
2066 start-d))
2067 (setq end-1-d (if dtend-1-dec
2068 (icalendar--datetime-to-diary-date dtend-1-dec)
2069 start-d))
2070 (setq end-t (if (and
2071 dtend-dec
2072 (not (string=
2073 (cadr
2074 (icalendar--get-event-property-attributes
2075 e 'DTEND))
2076 "DATE")))
2077 (icalendar--datetime-to-colontime dtend-dec)
2078 start-t))
2079 (icalendar--dmsg "start-d: %s, end-d: %s" start-d end-d)
2080 (cond
2081 ;; recurring event
2082 (rrule
2083 (setq diary-string
2084 (icalendar--convert-recurring-to-diary e dtstart-dec start-t
2085 end-t))
2086 (setq event-ok t))
2087 (rdate
2088 (icalendar--dmsg "rdate event")
2089 (setq diary-string "")
2090 (mapc (lambda (datestring)
2091 (setq diary-string
2092 (concat diary-string
2093 (format "......"))))
2094 (icalendar--split-value rdate)))
2095 ;; non-recurring event
2096 ;; all-day event
2097 ((not (string= start-d end-d))
2098 (setq diary-string
2099 (icalendar--convert-non-recurring-all-day-to-diary
2100 e start-d end-1-d))
2101 (setq event-ok t))
2102 ;; not all-day
2103 ((and start-t (or (not end-t)
2104 (not (string= start-t end-t))))
2105 (setq diary-string
2106 (icalendar--convert-non-recurring-not-all-day-to-diary
2107 e dtstart-dec dtend-dec start-t end-t))
2108 (setq event-ok t))
2109 ;; all-day event
2110 (t
2111 (icalendar--dmsg "all day event")
2112 (setq diary-string (icalendar--datetime-to-diary-date
2113 dtstart-dec "/"))
2114 (setq event-ok t)))
2115 ;; add all other elements unless the user doesn't want to have
2116 ;; them
2117 (if event-ok
2118 (progn
2119 (setq diary-string
2120 (concat diary-string " "
2121 (icalendar--format-ical-event e)))
2122 (if do-not-ask (setq summary nil))
2123 ;; add entry to diary and store actual name of diary
2124 ;; file (in case it was nil)
2125 (setq diary-file
2126 (icalendar--add-diary-entry diary-string diary-file
2127 non-marking summary)))
2128 ;; event was not ok
2129 (setq found-error t)
2130 (setq error-string
2131 (format "%s\nCannot handle this event:%s"
2132 error-string e))))
2133 ;; FIXME: inform user about ignored event properties
2134 ;; handle errors
2135 (error
2136 (message "Ignoring event \"%s\"" e)
2137 (setq found-error t)
2138 (setq error-string (format "%s\n%s\nCannot handle this event: %s"
2139 error-val error-string e))
2140 (message "%s" error-string))))
2141
2142 ;; insert final newline
2143 (if diary-file
2144 (let ((b (find-buffer-visiting diary-file)))
2145 (when b
2146 (save-current-buffer
2147 (set-buffer b)
2148 (goto-char (point-max))
2149 (insert "\n")))))
2150 (if found-error
2151 (save-current-buffer
2152 (set-buffer (get-buffer-create "*icalendar-errors*"))
2153 (erase-buffer)
2154 (insert error-string)))
2155 (message "Converting iCalendar...done")
2156 found-error))
2157
2158 ;; subroutines for importing
2159 (defun icalendar--convert-recurring-to-diary (e dtstart-dec start-t end-t)
2160 "Convert recurring iCalendar event E to diary format.
2161
2162 DTSTART-DEC is the DTSTART property of E.
2163 START-T is the event's start time in diary format.
2164 END-T is the event's end time in diary format."
2165 (icalendar--dmsg "recurring event")
2166 (let* ((rrule (icalendar--get-event-property e 'RRULE))
2167 (rrule-props (icalendar--split-value rrule))
2168 (frequency (cadr (assoc 'FREQ rrule-props)))
2169 (until (cadr (assoc 'UNTIL rrule-props)))
2170 (count (cadr (assoc 'COUNT rrule-props)))
2171 (interval (read (or (cadr (assoc 'INTERVAL rrule-props)) "1")))
2172 (dtstart-conv (icalendar--datetime-to-diary-date dtstart-dec))
2173 (until-conv (icalendar--datetime-to-diary-date
2174 (icalendar--decode-isodatetime until)))
2175 (until-1-conv (icalendar--datetime-to-diary-date
2176 (icalendar--decode-isodatetime until -1)))
2177 (result ""))
2178
2179 ;; FIXME FIXME interval!!!!!!!!!!!!!
2180
2181 (when count
2182 (if until
2183 (message "Must not have UNTIL and COUNT -- ignoring COUNT element!")
2184 (let ((until-1 0))
2185 (cond ((string-equal frequency "DAILY")
2186 (setq until (icalendar--add-decoded-times
2187 dtstart-dec
2188 (list 0 0 0 (* (read count) interval) 0 0)))
2189 (setq until-1 (icalendar--add-decoded-times
2190 dtstart-dec
2191 (list 0 0 0 (* (- (read count) 1) interval)
2192 0 0)))
2193 )
2194 ((string-equal frequency "WEEKLY")
2195 (setq until (icalendar--add-decoded-times
2196 dtstart-dec
2197 (list 0 0 0 (* (read count) 7 interval) 0 0)))
2198 (setq until-1 (icalendar--add-decoded-times
2199 dtstart-dec
2200 (list 0 0 0 (* (- (read count) 1) 7
2201 interval) 0 0)))
2202 )
2203 ((string-equal frequency "MONTHLY")
2204 (setq until (icalendar--add-decoded-times
2205 dtstart-dec (list 0 0 0 0 (* (- (read count) 1)
2206 interval) 0)))
2207 (setq until-1 (icalendar--add-decoded-times
2208 dtstart-dec (list 0 0 0 0 (* (- (read count) 1)
2209 interval) 0)))
2210 )
2211 ((string-equal frequency "YEARLY")
2212 (setq until (icalendar--add-decoded-times
2213 dtstart-dec (list 0 0 0 0 0 (* (- (read count) 1)
2214 interval))))
2215 (setq until-1 (icalendar--add-decoded-times
2216 dtstart-dec
2217 (list 0 0 0 0 0 (* (- (read count) 1)
2218 interval))))
2219 )
2220 (t
2221 (message "Cannot handle COUNT attribute for `%s' events."
2222 frequency)))
2223 (setq until-conv (icalendar--datetime-to-diary-date until))
2224 (setq until-1-conv (icalendar--datetime-to-diary-date until-1))
2225 ))
2226 )
2227 (cond ((string-equal frequency "WEEKLY")
2228 (let* ((byday (cadr (assoc 'BYDAY rrule-props)))
2229 (weekdays
2230 (icalendar--get-weekday-numbers byday))
2231 (weekday-clause
2232 (when (> (length weekdays) 1)
2233 (format "(memq (calendar-day-of-week date) '%s) "
2234 weekdays))))
2235 (if (not start-t)
2236 (progn
2237 ;; weekly and all-day
2238 (icalendar--dmsg "weekly all-day")
2239 (if until
2240 (setq result
2241 (format
2242 (concat "%%%%(and "
2243 "%s"
2244 "(diary-block %s %s))")
2245 (or weekday-clause
2246 (format "(diary-cyclic %d %s) "
2247 (* interval 7)
2248 dtstart-conv))
2249 dtstart-conv
2250 (if count until-1-conv until-conv)
2251 ))
2252 (setq result
2253 (format "%%%%(and %s(diary-cyclic %d %s))"
2254 (or weekday-clause "")
2255 (if weekday-clause 1 (* interval 7))
2256 dtstart-conv))))
2257 ;; weekly and not all-day
2258 (icalendar--dmsg "weekly not-all-day")
2259 (if until
2260 (setq result
2261 (format
2262 (concat "%%%%(and "
2263 "%s"
2264 "(diary-block %s %s)) "
2265 "%s%s%s")
2266 (or weekday-clause
2267 (format "(diary-cyclic %d %s) "
2268 (* interval 7)
2269 dtstart-conv))
2270 dtstart-conv
2271 until-conv
2272 (or start-t "")
2273 (if end-t "-" "") (or end-t "")))
2274 ;; no limit
2275 ;; FIXME!!!!
2276 ;; DTSTART;VALUE=DATE-TIME:20030919T090000
2277 ;; DTEND;VALUE=DATE-TIME:20030919T113000
2278 (setq result
2279 (format
2280 "%%%%(and %s(diary-cyclic %d %s)) %s%s%s"
2281 (or weekday-clause "")
2282 (if weekday-clause 1 (* interval 7))
2283 dtstart-conv
2284 (or start-t "")
2285 (if end-t "-" "") (or end-t "")))))))
2286 ;; yearly
2287 ((string-equal frequency "YEARLY")
2288 (icalendar--dmsg "yearly")
2289 (if until
2290 (let ((day (nth 3 dtstart-dec))
2291 (month (nth 4 dtstart-dec)))
2292 (setq result (concat "%%(and (diary-date "
2293 (cond ((eq calendar-date-style 'iso)
2294 (format "t %d %d" month day))
2295 ((eq calendar-date-style 'european)
2296 (format "%d %d t" day month))
2297 ((eq calendar-date-style 'american)
2298 (format "%d %d t" month day)))
2299 ") (diary-block "
2300 dtstart-conv
2301 " "
2302 until-conv
2303 ")) "
2304 (or start-t "")
2305 (if end-t "-" "")
2306 (or end-t ""))))
2307 (setq result (format
2308 "%%%%(and (diary-anniversary %s)) %s%s%s"
2309 dtstart-conv
2310 (or start-t "")
2311 (if end-t "-" "") (or end-t "")))))
2312 ;; monthly
2313 ((string-equal frequency "MONTHLY")
2314 (icalendar--dmsg "monthly")
2315 (setq result
2316 (format
2317 "%%%%(and (diary-date %s) (diary-block %s %s)) %s%s%s"
2318 (let ((day (nth 3 dtstart-dec)))
2319 (cond ((eq calendar-date-style 'iso)
2320 (format "t t %d" day))
2321 ((eq calendar-date-style 'european)
2322 (format "%d t t" day))
2323 ((eq calendar-date-style 'american)
2324 (format "t %d t" day))))
2325 dtstart-conv
2326 (if until
2327 until-conv
2328 (if (eq calendar-date-style 'iso) "9999 1 1" "1 1 9999")) ;; FIXME: should be unlimited
2329 (or start-t "")
2330 (if end-t "-" "") (or end-t ""))))
2331 ;; daily
2332 ((and (string-equal frequency "DAILY"))
2333 (if until
2334 (setq result
2335 (format
2336 (concat "%%%%(and (diary-cyclic %s %s) "
2337 "(diary-block %s %s)) %s%s%s")
2338 interval dtstart-conv dtstart-conv
2339 (if count until-1-conv until-conv)
2340 (or start-t "")
2341 (if end-t "-" "") (or end-t "")))
2342 (setq result
2343 (format
2344 "%%%%(and (diary-cyclic %s %s)) %s%s%s"
2345 interval
2346 dtstart-conv
2347 (or start-t "")
2348 (if end-t "-" "") (or end-t ""))))))
2349 ;; Handle exceptions from recurrence rules
2350 (let ((ex-dates (icalendar--get-event-properties e 'EXDATE)))
2351 (while ex-dates
2352 (let* ((ex-start (icalendar--decode-isodatetime
2353 (car ex-dates)))
2354 (ex-d (icalendar--datetime-to-diary-date
2355 ex-start)))
2356 (setq result
2357 (icalendar--rris "^%%(\\(and \\)?"
2358 (format
2359 "%%%%(and (not (diary-date %s)) "
2360 ex-d)
2361 result)))
2362 (setq ex-dates (cdr ex-dates))))
2363 ;; FIXME: exception rules are not recognized
2364 (if (icalendar--get-event-property e 'EXRULE)
2365 (setq result
2366 (concat result
2367 "\n Exception rules: "
2368 (icalendar--get-event-properties
2369 e 'EXRULE))))
2370 result))
2371
2372 (defun icalendar--convert-non-recurring-all-day-to-diary (event start-d end-d)
2373 "Convert non-recurring iCalendar EVENT to diary format.
2374
2375 DTSTART is the decoded DTSTART property of E.
2376 Argument START-D gives the first day.
2377 Argument END-D gives the last day."
2378 (icalendar--dmsg "non-recurring all-day event")
2379 (format "%%%%(and (diary-block %s %s))" start-d end-d))
2380
2381 (defun icalendar--convert-non-recurring-not-all-day-to-diary (event dtstart-dec
2382 dtend-dec
2383 start-t
2384 end-t)
2385 "Convert recurring icalendar EVENT to diary format.
2386
2387 DTSTART-DEC is the decoded DTSTART property of E.
2388 DTEND-DEC is the decoded DTEND property of E.
2389 START-T is the event's start time in diary format.
2390 END-T is the event's end time in diary format."
2391 (icalendar--dmsg "not all day event")
2392 (cond (end-t
2393 (format "%s %s-%s"
2394 (icalendar--datetime-to-diary-date
2395 dtstart-dec "/")
2396 start-t end-t))
2397 (t
2398 (format "%s %s"
2399 (icalendar--datetime-to-diary-date
2400 dtstart-dec "/")
2401 start-t))))
2402
2403 (defun icalendar--add-diary-entry (string diary-file non-marking
2404 &optional summary)
2405 "Add STRING to the diary file DIARY-FILE.
2406 STRING must be a properly formatted valid diary entry. NON-MARKING
2407 determines whether diary events are created as non-marking. If
2408 SUMMARY is not nil it must be a string that gives the summary of the
2409 entry. In this case the user will be asked whether he wants to insert
2410 the entry."
2411 (when (or (not summary)
2412 (y-or-n-p (format "Add appointment for `%s' to diary? "
2413 summary)))
2414 (when summary
2415 (setq non-marking
2416 (y-or-n-p (format "Make appointment non-marking? "))))
2417 (save-window-excursion
2418 (unless diary-file
2419 (setq diary-file
2420 (read-file-name "Add appointment to this diary file: ")))
2421 ;; Note: diary-make-entry will add a trailing blank char.... :(
2422 (funcall (if (fboundp 'diary-make-entry)
2423 'diary-make-entry
2424 'make-diary-entry)
2425 string non-marking diary-file)))
2426 ;; Würgaround to remove the trailing blank char
2427 (with-current-buffer (find-file diary-file)
2428 (goto-char (point-max))
2429 (if (= (char-before) ? )
2430 (delete-char -1)))
2431 ;; return diary-file in case it has been changed interactively
2432 diary-file)
2433
2434 ;; ======================================================================
2435 ;; Examples
2436 ;; ======================================================================
2437 (defun icalendar-import-format-sample (event)
2438 "Example function for formatting an iCalendar EVENT."
2439 (format (concat "SUMMARY=`%s' DESCRIPTION=`%s' LOCATION=`%s' ORGANIZER=`%s' "
2440 "STATUS=`%s' URL=`%s' CLASS=`%s'")
2441 (or (icalendar--get-event-property event 'SUMMARY) "")
2442 (or (icalendar--get-event-property event 'DESCRIPTION) "")
2443 (or (icalendar--get-event-property event 'LOCATION) "")
2444 (or (icalendar--get-event-property event 'ORGANIZER) "")
2445 (or (icalendar--get-event-property event 'STATUS) "")
2446 (or (icalendar--get-event-property event 'URL) "")
2447 (or (icalendar--get-event-property event 'CLASS) "")))
2448
2449 (provide 'icalendar)
2450
2451 ;;; icalendar.el ends here