6004ae2f451c39e6c5e14de41c9b9050c05ccc3c
[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 # Known bugs:
13 # - not so much a bug as a possible assumption: date entries with no time
14 #   specified are assumed to be independent of the time zone.
15 #
16 # Eric S Fraga
17 # 20100629 - initial version
18 # 20100708 - added end times to timed events
19 #          - adjust times according to time zone information
20 #          - fixed incorrect transfer for entries with ":" embedded within the text
21 #          - added support for multi-line summary entries (which become headlines)
22 # 20100709 - incorporated time zone identification
23 #          - fixed processing of continuation lines as Google seems to
24 #            have changed, in the last day, the number of spaces at
25 #            the start of the line for each continuation...
26 #          - remove backslashes used to protect commas in iCal text entries
27 # no further revision log after this as the file was moved into a git
28 # repository...
29 #
30 # Last change: 2011.01.28 16:08:03
31 #----------------------------------------------------------------------------------
32
33 # a function to take the iCal formatted date+time, convert it into an
34 # internal form (seconds since time 0), and adjust according to the
35 # local time zone (specified by +-UTC_offset calculated in the BEGIN
36 # section)
37
38 function datetimestamp(input)
39 {
40     # convert the iCal Date+Time entry to a format that mktime can understand
41     
42     # datespec in UTC, i.e. ending with Z
43     UTC = "no"
44     UTC = 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])Z.*[\r]*", "yes", "g", input);
45     
46     # parse date and time
47     datespec  = 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);
48
49     # print "date spec : " datespec; convert this date+time into
50     # seconds from the beginning of time and include adjustment for
51     # time zone, as determined in the BEGIN section below.  For time
52     # zone adjustment, I have not tested edge effects, specifically
53     # what happens when UTC time is a different day to local time and
54     # especially when an event with a duration crosses midnight in UTC
55     # time.  It should work but...
56     if(UTC == "yes")
57         timestamp = mktime(datespec) + UTC_offset ;
58     else
59         timestamp = mktime(datespec);
60         
61     # print "adjusted    : " timestamp
62     # print "Time stamp  : " strftime("%Y-%m-%d %H:%M", timestamp);
63     return timestamp;
64 }
65
66 BEGIN {
67     ### config section
68     max_age =  7; # in days
69                   # set this to -1 to get all entries or to N>0 to only get 
70                   # that start or end less than N days ago
71     ### end config section
72
73     # use a colon to separate the type of data line from the actual contents
74     FS = ":";
75     
76     # determine the number of seconds to use for adjusting for time
77     # zone difference from UTC.  This is used in the function
78     # datetimestamp above.  The time zone information returned by
79     # strftime() is in hours * 100 so we multiply by 36 to get
80     # seconds.  This does not work for time zones that are not an
81     # integral multiple of hours (e.g. Newfoundland)
82     UTC_offset = gensub("([+-])0", "\\1", "", strftime("%z")) * 36;
83     
84     date = "";
85     entry = ""
86     first = 1;                  # true until an event has been found
87     headline = ""
88     icalentry = ""  # the full entry for inspection
89     id = ""
90     indescription = 0;
91     lasttimestamp=-1;
92     
93     print "#+TITLE:     Main Google calendar entries"
94     print "#+AUTHOR:    Eric S Fraga"
95     print "#+EMAIL:     e.fraga@ucl.ac.uk"
96     print "#+DESCRIPTION: converted using the ical2org awk script"
97     print "#+CATEGORY: google"
98     print "#+STARTUP: hidestars"
99     print "#+STARTUP: overview"
100     print " "
101 }
102
103 # continuation lines (at least from Google) start with two spaces
104 # if the continuation is after a description or a summary, append the entry
105 # to the respective variable
106
107 /^[ ]+/ { 
108     if (indescription) {
109         entry = entry gensub("\r", "", "g", gensub("^[ ]+", "", "", $0));
110     } else if (insummary) {
111         summary = summary gensub("\r", "", "g", gensub("^[ ]+", "", "", $0))
112     }
113     icalentry = icalentry "\n" $0
114 }
115
116 /^BEGIN:VEVENT/ {
117     # start of an event.  if this is the first, output the preamble from the iCal file
118     if (first) {
119         print "* COMMENT original iCal preamble"
120         print gensub("\r", "", "g", icalentry)
121         icalentry = ""
122     }
123     first = false;
124 }
125 # any line that starts at the left with a non-space character is a new data field
126
127 /^[A-Z]/ {
128     # we ignore DTSTAMP lines as they change every time you download
129     # the iCal format file which leads to a change in the converted
130     # org file as I output the original input.  This change, which is
131     # really content free, makes a revision control system update the
132     # repository and confuses.
133     if (! index("DTSTAMP", $1)) icalentry = icalentry "\n" $0
134     # this line terminates the collection of description and summary entries
135     indescription = 0;
136     insummary = 0;
137 }
138
139 # this type of entry represents a day entry, not timed, with date stamp YYYYMMDD
140
141 /^DTSTART;VALUE=DATE/ {
142     datetmp = gensub("([0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9])(.*[\r])", "\\1T000000\\2", "g", $2)
143     date = strftime("%Y-%m-%d %a %H:%M", datetimestamp(datetmp));
144     if(max_age>0)     lasttimestamp = datetimestamp(datetmp);
145 }
146 /^DTEND;VALUE=DATE/ {
147     datetmp = gensub("([0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9])(.*[\r])", "\\1T000000\\2", "g", $2)
148     time2 = strftime("%Y-%m-%d %a %H:%M", datetimestamp(datetmp));
149     date = date ">--<" time2;
150     if(max_age>0)     lasttimestamp = datetimestamp(datetmp);
151 }
152
153 # this represents a timed entry with date and time stamp YYYYMMDDTHHMMSS
154 # we ignore the seconds
155
156 /^DTSTART[:;][^V]/ {
157     date = strftime("%Y-%m-%d %a %H:%M", datetimestamp($2));
158     if(max_age>0)     lasttimestamp = datetimestamp($2);
159     # print date;
160 }
161
162 # and the same for the end date; here we extract only the time and append this to the 
163 # date+time found by the DTSTART entry.  We assume that entry was there, of course.
164 # should probably add some error checking here!  In time...
165
166 /^DTEND[:;][^V]/ {
167     # print $0
168     time2 = strftime("%Y-%m-%d %a %H:%M", datetimestamp($2));
169     date = date ">--<" time2;
170     if(max_age>0)     lasttimestamp = datetimestamp($2);
171 }
172
173 # The description will the contents of the entry in org-mode.
174 # this line may be continued.
175
176 /^DESCRIPTION/ { 
177     $1 = "";
178     entry = entry gensub("\r", "", "g", $0);
179     indescription = 1;
180 }
181
182 # the summary will be the org heading
183
184 /^SUMMARY/ { 
185     $1 = "";
186     summary = gensub("\r", "", "g", $0);
187     insummary = 1;
188 }
189
190 # the unique ID will be stored as a property of the entry
191
192 /^UID/ { 
193     id = gensub("\r", "", "g", $2);
194 }
195
196 /^LOCATION/ {
197     location = gensub("\r", "", "g", $2);
198 }
199
200 /^STATUS/ {
201     status = gensub("\r", "", "g", $2);
202 }
203
204 # when we reach the end of the event line, we output everything we
205 # have collected so far, creating a top level org headline with the
206 # date/time stamp, unique ID property and the contents, if any
207
208 /^END:VEVENT/ {
209     #output event
210     if(max_age<0 || ( lasttimestamp>0 && systime()<lasttimestamp+max_age*24*60*60 ) )
211     {
212     # translate \n sequences to actual newlines and unprotect commas (,)
213     print "* " gensub("\\\\,", ",", "g", gensub("\\\\n", " ", "g", summary))
214     print "  <" date ">"
215     print "  :PROPERTIES:"
216     print "  :ID:       " id
217     if(length(location))
218         print "  :LOCATION: " location
219     if(length(status))
220         print "  :STATUS: " status
221     print "  :END:"
222     # for the entry, convert all embedded "\n" strings to actual newlines
223     print ""
224     # translate \n sequences to actual newlines and unprotect commas (,)
225     if(length(entry)>1)
226     print gensub("\\\\,", ",", "g", gensub("\\\\n", "\n", "g", entry));
227     print "** COMMENT original iCal entry"
228     print gensub("\r", "", "g", icalentry)
229     }
230     summary = ""
231     date = ""
232     location = ""
233     status = ""
234     entry = ""
235     icalentry = ""
236     indescription = 0
237     insummary = 0
238     lasttimestamp = -1
239 }
240
241 # Local Variables:
242 # time-stamp-line-limit: 1000
243 # time-stamp-format: "%04y.%02m.%02d %02H:%02M:%02S"
244 # time-stamp-active: t
245 # time-stamp-start: "Last change:[ \t]+"
246 # time-stamp-end: "$"
247 # End: