ical2org.awk 13 KB

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