ical2org 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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, 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 = -1;
  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 = "Edd Baxter"
  76. # and to your email address
  77. emailaddress = "eddbaxter@gmx.com"
  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: My Calendar"
  88. print "#+AUTHOR: ", author
  89. print "#+EMAIL: ", emailaddress
  90. print "#+DESCRIPTION: Seriously Funky org-mode calendar"
  91. print "#+CATEGORY: CalDav"
  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. schedule = "print SCHEDULE:"
  112. date = "";
  113. entry = ""
  114. headline = ""
  115. icalentry = "" # the full entry for inspection
  116. id = ""
  117. indescription = 0;
  118. insummary = 0
  119. intfreq = "" # the interval and frequency for repeating org timestamps
  120. lasttimestamp = -1;
  121. location = ""
  122. rrend = ""
  123. status = ""
  124. summary = ""
  125. # if this is the first event, output the preamble from the iCal file
  126. if (first) {
  127. if(preamble) {
  128. print "* COMMENT original iCal preamble"
  129. print gensub("\r", "", "g", icalentry)
  130. }
  131. if (preserve)
  132. icalentry = ""
  133. first = false;
  134. }
  135. }
  136. # any line that starts at the left with a non-space character is a new data field
  137. /^[A-Z]/ {
  138. # we do not copy DTSTAMP lines as they change every time you download
  139. # the iCal format file which leads to a change in the converted
  140. # org file as I output the original input. This change, which is
  141. # really content free, makes a revision control system update the
  142. # repository and confuses.
  143. if (preserve)
  144. if (! index("DTSTAMP", $1))
  145. icalentry = icalentry "\n" $0
  146. # this line terminates the collection of description and summary entries
  147. indescription = 0;
  148. insummary = 0;
  149. }
  150. # this type of entry represents a day entry, not timed, with date stamp YYYYMMDD
  151. /^DTSTART;VALUE=DATE/ {
  152. date = datestring($2);
  153. }
  154. /^DTEND;VALUE=DATE/ {
  155. time2 = datestring($2, 1);
  156. if ( issameday )
  157. time2 = ""
  158. }
  159. # this represents a timed entry with date and time stamp YYYYMMDDTHHMMSS
  160. # we ignore the seconds
  161. /^DTSTART[:;][^V]/ {
  162. date = datetimestring($2);
  163. # print date;
  164. }
  165. # and the same for the end date;
  166. /^DTEND[:;][^V]/ {
  167. time2 = datetimestring($2);
  168. if (substr(date,1,10) == substr(time2,1,10)) {
  169. # timespan within same date, use one date with a time range
  170. date = date "-" substr(time2, length(time2)-4)
  171. time2 = ""
  172. }
  173. }
  174. # repetition rule
  175. /^RRULE:FREQ=(DAILY|WEEKLY|MONTHLY|YEARLY)/ {
  176. # get the d, w, m or y value
  177. freq = tolower(gensub(/.*FREQ=(.).*/, "\\1", $0))
  178. # get the interval, and use 1 if none specified
  179. interval = $2 ~ /INTERVAL=/ ? gensub(/.*INTERVAL=([0-9]+);.*/, "\\1", $2) : 1
  180. # get the enddate of the rule and use "" if none specified
  181. rrend = $2 ~ /UNTIL=/ ? datestring(gensub(/.*UNTIL=([0-9]{8}).*/, "\\1", $2)) : ""
  182. # build the repetitor vale as understood by org
  183. intfreq = " +" interval freq
  184. # if the repetition is daily, and there is an end date, drop the repetitor
  185. # as that is the default
  186. if (intfreq == " +1d" && time2 =="" && rrend != "")
  187. intfreq = ""
  188. }
  189. # The description will the contents of the entry in org-mode.
  190. # this line may be continued.
  191. /^DESCRIPTION/ {
  192. $1 = "";
  193. entry = entry gensub("\r", "", "g", $0);
  194. indescription = 1;
  195. }
  196. # the summary will be the org heading
  197. /^SUMMARY/ {
  198. $1 = "";
  199. summary = gensub("\r", "", "g", $0);
  200. # trim trailing dots if requested by config option
  201. if(trimdots && summary ~ /\.\.\.$/)
  202. sub(/\.\.\.$/, "", summary)
  203. insummary = 1;
  204. }
  205. # the unique ID will be stored as a property of the entry
  206. /^UID/ {
  207. id = gensub("\r", "", "g", $2);
  208. }
  209. /^LOCATION/ {
  210. location = gensub("\r", "", "g", $2);
  211. }
  212. /^STATUS/ {
  213. status = gensub("\r", "", "g", $2);
  214. }
  215. # when we reach the end of the event line, we output everything we
  216. # have collected so far, creating a top level org headline with the
  217. # date/time stamp, unique ID property and the contents, if any
  218. /^END:VEVENT/ {
  219. #output event
  220. if(max_age<0 || ( lasttimestamp>0 && systime()<lasttimestamp+max_age_seconds ) )
  221. {
  222. # build org timestamp
  223. if (intfreq != "")
  224. date = date intfreq
  225. if (time2 != "")
  226. date = date ">--<" time2
  227. else if (rrend != "")
  228. date = date ">--<" rrend
  229. # translate \n sequences to actual newlines and unprotect commas (,)
  230. if (condense)
  231. print "* <" date "> " gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", " ", "g", summary)))
  232. else
  233. print "* " gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", " ", "g", summary))) "\n SCHEDULED: <" date ">"
  234. print ":PROPERTIES:"
  235. print ":ID: " id
  236. if(length(location))
  237. print ":LOCATION: " location
  238. if(length(status))
  239. print ":STATUS: " status
  240. print ":END:"
  241. print ""
  242. # translate \n sequences to actual newlines and unprotect commas (,)
  243. if(length(entry)>1)
  244. print gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", "\n", "g", entry)));
  245. # output original entry if requested by 'original' config option
  246. if (original)
  247. print "** COMMENT original iCal entry\n", gensub("\r", "", "g", icalentry)
  248. }
  249. }
  250. # funtion to convert an iCal time string 'yyyymmddThhmmss[Z]' into a
  251. # date time string as used by org, preferably including the short day
  252. # of week: 'yyyy-mm-dd day hh:mm' or 'yyyy-mm-dd hh:mm' if we cannot
  253. # define the day of the week
  254. function datetimestring(input)
  255. {
  256. # print "________"
  257. # print "input : " input
  258. # convert the iCal Date+Time entry to a format that mktime can understand
  259. 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);
  260. # print "spec :" spec
  261. stamp = mktime(spec);
  262. lasttimestamp = stamp;
  263. if (stamp <= 0) {
  264. # this is a date before the start of the epoch, so we cannot
  265. # use strftime and will deliver a 'yyyy-mm-dd hh:mm' string
  266. # without day of week; this assumes local time, and does not
  267. # attempt UTC offset correction
  268. 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);
  269. # print "==> spec:" spec;
  270. return spec;
  271. }
  272. if (input ~ /[0-9]{8}T[0-9]{6}Z/ ) {
  273. # this is an utc time;
  274. # we need to correct the timestamp by the utc offset for this time
  275. offset = strftime("%z", stamp)
  276. pm = substr(offset,1,1) 1 # define multiplier +1 or -1
  277. hh = substr(offset,2,2) * 3600 * pm
  278. mm = substr(offset,4,2) * 60 * pm
  279. # adjust the timestamp
  280. stamp = stamp + hh + mm
  281. }
  282. return strftime("%Y-%m-%d %a %H:%M", stamp);
  283. }
  284. # function to convert an iCal date into an org date;
  285. # the optional parameter indicates whether this is an end date;
  286. # for single or multiple whole day events, the end date given by
  287. # iCal is the date of the first day after the event;
  288. # if the optional 'isenddate' parameter is non zero, this function
  289. # tries to reduce the given date by one day
  290. function datestring(input, isenddate)
  291. {
  292. #convert the iCal string to a an mktime input string
  293. 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);
  294. # compute the nr of seconds after or before the epoch
  295. # dates before the epoch will have a negative timestamp
  296. # days after the epoch will have a positive timestamp
  297. stamp = mktime(spec);
  298. if (isenddate) {
  299. # subtract 1 day from the timestamp
  300. # note that this also works for dates before the epoch
  301. stamp = stamp - 86400;
  302. # register whether the end date is same as the start date
  303. issameday = lasttimestamp == stamp
  304. }
  305. # save timestamp to allow for check of max_age
  306. lasttimestamp = stamp
  307. if (stamp < 0) {
  308. # this date is before the epoch;
  309. # the returned datestring will not have the short day of week string
  310. # as strftime does not handle negative times;
  311. # we have to construct the datestring directly from the input
  312. if (isenddate) {
  313. # we really should return the date before the input date, but strftime
  314. # does not work with negative timestamp values; so we can not use it
  315. # to obtain the string representation of the corrected timestamp;
  316. # we have to return the date specified in the iCal input and we
  317. # add time 00:00 to clarify this
  318. 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);
  319. } else {
  320. # just generate the desired representation of the input date, without time;
  321. return gensub("([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1-\\2-\\3", "g", input);
  322. }
  323. }
  324. # return the date and day of week
  325. return strftime("%Y-%m-%d %a", stamp);
  326. }
  327. # Local Variables:
  328. # time-stamp-line-limit: 1000
  329. #time-stamp-format: "%04y.%02m.%02d %02H:%02M:%02S"
  330. # time-stamp-active: t
  331. # time-stamp-start: "Last change:[ \t]+"
  332. # time-stamp-end: "$"
  333. # End: