Converts RRULE lines into org recurrent datestamps
[worg.git] / code / awk / ical2org.awk
1 #!/usr/bin/awk -f
2 # awk script for converting an iCal formatted file to a sequence of org-mode headings.
3 # this may not work in general but seems to work for day and timed events from Google's
4 # calendar, which is really all I need right now...
5 #
6 # usage:
7 #   awk -f THISFILE < icalinputfile.ics > orgmodeentries.org
8 #
9 # Note: change org meta information generated below for author and
10 # email entries!
11 #
12 # Caveats:
13 #
14 # - date entries with no time specified are assumed to be local time zone;
15 #   same remark for date entries that do have a time but do not end with Z
16 #   e.g.: 20130101T123456 is local and will be kept as 2013-01-01 12:34
17 #   where 20130223T123422Z is UTC and will be corrected appropriately
18 #
19 # - UTC times are changed into local times, using the time zone of the
20 #   computer that runs the script; it would be very hard in an awk script
21 #   to respect the time zone of a file belonging to another time zone:
22 #   the offsets will be different as well as the switchover time(s);
23 #   (consider a remote shell to a computer with the file's time zone)
24 #
25 # - the UTC conversion entirely relies on the built-in strftime method;
26 #   the author is not responsible for any erroneous conversions nor the
27 #   consequence of such conversions
28 #
29 # - does process RRULE recurring events, except if COUNT specified iso UNTIL
30 #
31 # - does not process EXDATE to exclude date(s) from recurring events
32 #
33 # Eric S Fraga
34 # 20100629 - initial version
35 # 20100708 - added end times to timed events
36 #          - adjust times according to time zone information
37 #          - fixed incorrect transfer for entries with ":" embedded within the text
38 #          - added support for multi-line summary entries (which become headlines)
39 # 20100709 - incorporated time zone identification
40 #          - fixed processing of continuation lines as Google seems to
41 #            have changed, in the last day, the number of spaces at
42 #            the start of the line for each continuation...
43 #          - remove backslashes used to protect commas in iCal text entries
44 # no further revision log after this as the file was moved into a git
45 # repository...
46 #
47 # Updated by: Guido Van Hoecke <guivhoATgmailDOTcom>
48 # Last change: 2013.05.26 13:22:04
49 #----------------------------------------------------------------------------------
50
51 BEGIN {
52     ### config section
53
54     # maximum age in days for entries to be output: set this to -1 to
55     # get all entries or to N>0 to only get enties that start or end
56     # less than N days ago
57     max_age = 7;
58
59     # set to 1 or 0 to yes or not output a header block with TITLE,
60     # AUTHOR, EMAIL etc...
61     header = 1;
62
63     # set to 1 or 0 to yes or not output the original ical preamble as
64     # comment
65     preamble = 1;
66
67     # set to 1 to output time and summary as one line starting with
68     # the time (value 1) or to 0 to output the summary as first line
69     # and the date and time info as a second line
70     condense = 0;
71
72     # set to 1 or 0 to yes or not output the original ical entry as a
73     # comment (mostly useful for debugging purposes)
74     original = 1;
75
76     # google truncates long subjects with ... which is misleading in
77     # an org file: it gives the unfortunate impression that an
78     # expanded entry is still collapsed; value 1 will trim those
79     # ... and value 0 doesn't touch them
80     trimdots = 1;
81
82     # change this to your name
83     author = "Eric S Fraga"
84
85     # and to your email address
86     emailaddress = "e.fraga@ucl.ac.uk"
87
88     ### end config section
89
90     # use a colon to separate the type of data line from the actual contents
91     FS = ":";
92
93     # we only need to preserve the original entry lines if either the
94     # preamble or original options are true
95     preserve = preamble || original
96     first = 1;      # true until an event has been found
97     max_age_seconds = max_age*24*60*60
98
99     if (header) {
100         print "#+TITLE:       Main Google calendar entries"
101         print "#+AUTHOR:     ", author
102         print "#+EMAIL:      ", emailaddress
103         print "#+DESCRIPTION: converted using the ical2org awk script"
104         print "#+CATEGORY:    google"
105         print "#+STARTUP:     hidestars"
106         print "#+STARTUP:     overview"
107         print ""
108     }
109 }
110
111 # continuation lines (at least from Google) start with a space
112 # if the continuation is after a description or a summary, append the entry
113 # to the respective variable
114
115 /^[ ]/ {
116     if (indescription) {
117         entry = entry gensub("\r", "", "g", gensub("^[ ]", "", "", $0));
118     } else if (insummary) {
119         summary = summary gensub("\r", "", "g", gensub("^[ ]", "", "", $0))
120     }
121     if (preserve)
122         icalentry = icalentry "\n" $0
123 }
124
125 /^BEGIN:VEVENT/ {
126     # start of an event: initialize global velues used for each event
127     date = "";
128     entry = ""
129     headline = ""
130     icalentry = ""  # the full entry for inspection
131     id = ""
132     indescription = 0;
133     insummary = 0
134     intfreq = "" # the interval and frequency for repeating org timestamps
135     lasttimestamp = -1;
136     location = ""
137     rrend = ""
138     status = ""
139     summary = ""
140
141     # if this is the first event, output the preamble from the iCal file
142     if (first) {
143         if(preamble) {
144             print "* COMMENT original iCal preamble"
145             print gensub("\r", "", "g", icalentry)
146         }
147         if (preserve)
148             icalentry = ""
149         first = false;
150     }
151 }
152
153 # any line that starts at the left with a non-space character is a new data field
154
155 /^[A-Z]/ {
156     # we do not copy DTSTAMP lines as they change every time you download
157     # the iCal format file which leads to a change in the converted
158     # org file as I output the original input.  This change, which is
159     # really content free, makes a revision control system update the
160     # repository and confuses.
161     if (preserve)
162         if (! index("DTSTAMP", $1))
163             icalentry = icalentry "\n" $0
164     # this line terminates the collection of description and summary entries
165     indescription = 0;
166     insummary = 0;
167 }
168
169 # this type of entry represents a day entry, not timed, with date stamp YYYYMMDD
170
171 /^DTSTART;VALUE=DATE/ {
172     date = datestring($2);
173 }
174
175 /^DTEND;VALUE=DATE/ {
176     time2 = datestring($2, 1);
177     if ( issameday )
178         time2 = ""
179 }
180
181 # this represents a timed entry with date and time stamp YYYYMMDDTHHMMSS
182 # we ignore the seconds
183
184 /^DTSTART[:;][^V]/ {
185     date = datetimestring($2);
186     # print date;
187 }
188
189 # and the same for the end date;
190
191 /^DTEND[:;][^V]/ {
192     time2 = datetimestring($2);
193     if (substr(date,1,10) == substr(time2,1,10)) {
194         # timespan within same date, use one date with a time range
195         date = date "-" substr(time2, length(time2)-4)
196         time2 = ""
197     }
198 }
199
200 # repetition rule
201
202 /^RRULE:FREQ=(DAILY|WEEKLY|MONTHLY|YEARLY)/ {
203     # get the d, w, m or y value
204     freq = tolower(gensub(/.*FREQ=(.).*/, "\\1", $0))
205     # get the interval, and use 1 if none specified
206     interval =  $2 ~ /INTERVAL=/ ? gensub(/.*INTERVAL=([0-9]+);.*/, "\\1", $2) : 1
207     # get the enddate of the rule and use "" if none specified
208     rrend = $2 ~ /UNTIL=/ ? datestring(gensub(/.*UNTIL=([0-9]{8}).*/, "\\1", $2)) : ""
209     # build the repetitor vale as understood by org
210     intfreq =  " +" interval freq
211     # if the repetition is daily, and there is an end date, drop the repetitor
212     # as that is the default
213     if (intfreq == " +1d" && time2 =="" && rrend != "")
214         intfreq = ""
215 }
216
217 # The description will the contents of the entry in org-mode.
218 # this line may be continued.
219
220 /^DESCRIPTION/ {
221     $1 = "";
222     entry = entry gensub("\r", "", "g", $0);
223     indescription = 1;
224 }
225
226 # the summary will be the org heading
227
228 /^SUMMARY/ {
229     $1 = "";
230     summary = gensub("\r", "", "g", $0);
231
232     # trim trailing dots if requested by config option
233     if(trimdots && summary ~ /\.\.\.$/)
234         sub(/\.\.\.$/, "", summary)
235     insummary = 1;
236 }
237
238 # the unique ID will be stored as a property of the entry
239
240 /^UID/ {
241     id = gensub("\r", "", "g", $2);
242 }
243
244 /^LOCATION/ {
245     location = gensub("\r", "", "g", $2);
246 }
247
248 /^STATUS/ {
249     status = gensub("\r", "", "g", $2);
250 }
251
252 # when we reach the end of the event line, we output everything we
253 # have collected so far, creating a top level org headline with the
254 # date/time stamp, unique ID property and the contents, if any
255
256 /^END:VEVENT/ {
257     #output event
258     if(max_age<0 || ( lasttimestamp>0 && systime()<lasttimestamp+max_age_seconds ) )
259     {
260         # build org timestamp
261         if (intfreq != "")
262             date = date intfreq
263         if (time2 != "")
264             date = date ">--<" time2
265         else if (rrend != "")
266             date = date ">--<" rrend
267
268         # translate \n sequences to actual newlines and unprotect commas (,)
269         if (condense)
270             print "* <" date "> " gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", " ", "g", summary)))
271         else
272             print "* " gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", " ", "g", summary))) "\n<" date ">"
273         print ":PROPERTIES:"
274         print     ":ID:       " id
275         if(length(location))
276             print ":LOCATION: " location
277         if(length(status))
278             print ":STATUS:   " status
279         print ":END:"
280
281         print ""
282         # translate \n sequences to actual newlines and unprotect commas (,)
283         if(length(entry)>1)
284             print gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", "\n", "g", entry)));
285
286         # output original entry if requested by 'original' config option
287         if (original)
288             print "** COMMENT original iCal entry\n", gensub("\r", "", "g", icalentry)
289     }
290 }
291
292
293
294 # funtion to convert an iCal time string 'yyyymmddThhmmss[Z]' into a
295 # date time string as used by org, preferably including the short day
296 # of week: 'yyyy-mm-dd day hh:mm' or 'yyyy-mm-dd hh:mm' if we cannot
297 # define the day of the week
298
299 function datetimestring(input)
300 {
301     # print "________"
302     # print "input : " input
303     # convert the iCal Date+Time entry to a format that mktime can understand
304     spec  = gensub("([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1 \\2 \\3 \\4 \\5 \\6", "g", input);
305     # print "spec :" spec
306
307     stamp = mktime(spec);
308     lasttimestamp = stamp;
309
310     if (stamp <= 0) {
311         # this is a date before the start of the epoch, so we cannot
312         # use strftime and will deliver a 'yyyy-mm-dd hh:mm' string
313         # without day of week; this assumes local time, and does not
314         # attempt UTC offset correction
315         spec = gensub("([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1-\\2-\\3 \\4:\\5", "g", input);
316         # print "==> spec:" spec;
317         return spec;
318     }
319
320     if (input ~ /[0-9]{8}T[0-9]{6}Z/ ) {
321         # this is an utc time;
322         # we need to correct the timestamp by the utc offset for this time
323         offset = strftime("%z", stamp)
324         pm = substr(offset,1,1) 1 # define multiplier +1 or -1
325         hh = substr(offset,2,2) * 3600 * pm
326         mm = substr(offset,4,2) * 60 * pm
327
328         # adjust the timestamp
329         stamp = stamp + hh + mm
330     }
331
332     return strftime("%Y-%m-%d %a %H:%M", stamp);
333 }
334
335 # function to convert an iCal date into an org date;
336 # the optional parameter indicates whether this is an end date;
337 # for single or multiple whole day events, the end date given by
338 # iCal is the date of the first day after the event;
339 # if the optional 'isenddate' parameter is non zero, this function
340 # tries to reduce the given date by one day
341
342 function datestring(input, isenddate)
343 {
344     #convert the iCal string to a an mktime input string
345     spec = gensub("([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1 \\2 \\3 00 00 00", "g", input);
346
347     # compute the nr of seconds after or before the epoch
348     # dates before the epoch will have a negative timestamp
349     # days after the epoch will have a positive timestamp
350     stamp = mktime(spec);
351
352     if (isenddate) {
353         # subtract 1 day from the timestamp
354         # note that this also works for dates before the epoch
355         stamp = stamp - 86400;
356
357         # register whether the end date is same as the start date
358         issameday = lasttimestamp == stamp
359     }
360     # save timestamp to allow for check of max_age
361     lasttimestamp = stamp
362
363     if (stamp < 0) {
364         # this date is before the epoch;
365         # the returned datestring will not have the short day of week string
366         # as strftime does not handle negative times;
367         # we have to construct the datestring directly from the input
368         if (isenddate) {
369             # we really should return the date before the input date, but strftime
370             # does not work with negative timestamp values; so we can not use it
371             # to obtain the string representation of the corrected timestamp;
372             # we have to return the date specified in the iCal input and we
373             # add time 00:00 to clarify this
374             return spec = gensub("([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1-\\2-\\3 00:00", "g", input);
375         } else {
376             # just generate the desired representation of the input date, without time;
377             return gensub("([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1-\\2-\\3", "g", input);
378         }
379     }
380
381     # return the date and day of week
382     return strftime("%Y-%m-%d %a", stamp);
383 }
384
385 # Local Variables:
386 # time-stamp-line-limit: 1000
387 # time-stamp-format: "%04y.%02m.%02d %02H:%02M:%02S"
388 # time-stamp-active: t
389 # time-stamp-start: "Last change:[ \t]+"
390 # time-stamp-end: "$"
391 # End: