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