Improved date and period handling, includinn for dates before the epoch
authorGuido Van Hoecke <guivho@gmail.com>
Fri, 17 May 2013 17:41:36 +0000 (19:41 +0200)
committerGuido Van Hoecke <guivho@gmail.com>
Fri, 17 May 2013 17:41:36 +0000 (19:41 +0200)
code/awk/ical2org.awk [changed mode: 0644->0755]

old mode 100644 (file)
new mode 100755 (executable)
index 6004ae2..566700e
@@ -9,9 +9,22 @@
 # Note: change org meta information generated below for author and
 # email entries!
 #
-# Known bugs:
-# - not so much a bug as a possible assumption: date entries with no time
-#   specified are assumed to be independent of the time zone.
+# Caveats:
+#
+# - date entries with no time specified are assumed to be local time zone;
+#   same remark for date entries that do have a time but do not end with Z
+#   e.g.: 20130101T123456 is local and will be kept as 2013-01-01 12:34
+#   where 20130223T123422Z is UTC and will be corrected appropriately
+#
+# - UTC times are changed into local times, using the time zone of the
+#   computer that runs the script; it would be very hard in an awk script
+#   to respect the time zone of a file belonging to another time zone:
+#   the offsets will be different as well as the switchover time(s);
+#   (consider a remote shell to a computer with the file's time zone)
+#
+# - the UTC conversion entirely relies on the built-in strftime method;
+#   the author is not responsible for any erroneous conversions nor the
+#   consequence of such conversions
 #
 # Eric S Fraga
 # 20100629 - initial version
 # no further revision log after this as the file was moved into a git
 # repository...
 #
-# Last change: 2011.01.28 16:08:03
+# Updated by: Guido Van Hoecke <guivhoATgmailDOTcom>
+# Last change: 2013.05.17 19:34:27
 #----------------------------------------------------------------------------------
 
-# a function to take the iCal formatted date+time, convert it into an
-# internal form (seconds since time 0), and adjust according to the
-# local time zone (specified by +-UTC_offset calculated in the BEGIN
-# section)
-
-function datetimestamp(input)
-{
-    # convert the iCal Date+Time entry to a format that mktime can understand
-    
-    # datespec in UTC, i.e. ending with Z
-    UTC = "no"
-    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);
-    
-    # parse date and time
-    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);
-
-    # print "date spec : " datespec; convert this date+time into
-    # seconds from the beginning of time and include adjustment for
-    # time zone, as determined in the BEGIN section below.  For time
-    # zone adjustment, I have not tested edge effects, specifically
-    # what happens when UTC time is a different day to local time and
-    # especially when an event with a duration crosses midnight in UTC
-    # time.  It should work but...
-    if(UTC == "yes")
-       timestamp = mktime(datespec) + UTC_offset ;
-    else
-       timestamp = mktime(datespec);
-       
-    # print "adjusted    : " timestamp
-    # print "Time stamp  : " strftime("%Y-%m-%d %H:%M", timestamp);
-    return timestamp;
-}
-
 BEGIN {
     ### config section
-    max_age =  7; # in days
-                  # set this to -1 to get all entries or to N>0 to only get 
-                  # that start or end less than N days ago
+
+    # maximum age in days for entries to be output: set this to -1 to
+    # get all entries or to N>0 to only get enties that start or end
+    # less than N days ago
+    max_age = -1;
+    max_age =  7;
+
+    # set to 1 or 0 to yes or not output a header block with TITLE,
+    # AUTHOR, EMAIL etc...
+    header = 1;
+
+    # set to 1 or 0 to yes or not output the original ical preamble as
+    # comment
+    preamble = 1;
+
+    # set to 1 to output time and summary as one line starting with
+    # the time (value 1) or to 0 to output the summary as first line
+    # and the date and time info as a second line
+    condense = 0;
+
+    # set to 1 or 0 to yes or not output the original ical entry as a
+    # comment (mostly useful for debugging purposes)
+    original = 1;
+
+    # google truncates long subjects with ... which is misleading in
+    # an org file: it gives the unfortunate impression that an
+    # expanded entry is still collapsed; value 1 will trim those
+    # ... and value 0 doesn't touch them
+    trimdots = 0;
+    trimdots = 1;
+
+    # change this to your name
+    author = "Eric S Fraga"
+
+    # and to your email address
+    emailaddress = "e.fraga@ucl.ac.uk"
+
     ### end config section
 
     # use a colon to separate the type of data line from the actual contents
     FS = ":";
-    
-    # determine the number of seconds to use for adjusting for time
-    # zone difference from UTC.  This is used in the function
-    # datetimestamp above.  The time zone information returned by
-    # strftime() is in hours * 100 so we multiply by 36 to get
-    # seconds.  This does not work for time zones that are not an
-    # integral multiple of hours (e.g. Newfoundland)
-    UTC_offset = gensub("([+-])0", "\\1", "", strftime("%z")) * 36;
-    
+
+    # we only need to preserve the original entry lines if either the
+    # preamble or original options are true
+    preserve = preamble || original
+
     date = "";
     entry = ""
-    first = 1;                 # true until an event has been found
+    first = 1;      # true until an event has been found
     headline = ""
     icalentry = ""  # the full entry for inspection
     id = ""
     indescription = 0;
-    lasttimestamp=-1;
-    
-    print "#+TITLE:     Main Google calendar entries"
-    print "#+AUTHOR:    Eric S Fraga"
-    print "#+EMAIL:     e.fraga@ucl.ac.uk"
-    print "#+DESCRIPTION: converted using the ical2org awk script"
-    print "#+CATEGORY: google"
-    print "#+STARTUP: hidestars"
-    print "#+STARTUP: overview"
-    print " "
-}
-
-# continuation lines (at least from Google) start with two spaces
+    lasttimestamp = -1;
+
+    if (header) {
+        print "#+TITLE:       Main Google calendar entries"
+        print "#+AUTHOR:     ", author
+        print "#+EMAIL:      ", emailaddress
+        print "#+DESCRIPTION: converted using the ical2org awk script"
+        print "#+CATEGORY:    google"
+        print "#+STARTUP:     hidestars"
+        print "#+STARTUP:     overview"
+        print ""
+    }
+}
+
+# continuation lines (at least from Google) start with a space
 # if the continuation is after a description or a summary, append the entry
 # to the respective variable
 
-/^[ ]+/ { 
+/^[ ]/ {
     if (indescription) {
-       entry = entry gensub("\r", "", "g", gensub("^[ ]+", "", "", $0));
+        entry = entry gensub("\r", "", "g", gensub("^[ ]", "", "", $0));
     } else if (insummary) {
-       summary = summary gensub("\r", "", "g", gensub("^[ ]+", "", "", $0))
+        summary = summary gensub("\r", "", "g", gensub("^[ ]", "", "", $0))
     }
-    icalentry = icalentry "\n" $0
+    if (preserve)
+        icalentry = icalentry "\n" $0
 }
 
 /^BEGIN:VEVENT/ {
-    # start of an event.  if this is the first, output the preamble from the iCal file
+    # start of an event.
     if (first) {
-       print "* COMMENT original iCal preamble"
-       print gensub("\r", "", "g", icalentry)
-       icalentry = ""
+        # if this is the first event, output the preamble from the iCal file
+        if(preamble) {
+            print "* COMMENT original iCal preamble"
+            print gensub("\r", "", "g", icalentry)
+        }
+        if (preserve)
+            icalentry = ""
     }
     first = false;
 }
+
 # any line that starts at the left with a non-space character is a new data field
 
 /^[A-Z]/ {
@@ -130,7 +149,9 @@ BEGIN {
     # org file as I output the original input.  This change, which is
     # really content free, makes a revision control system update the
     # repository and confuses.
-    if (! index("DTSTAMP", $1)) icalentry = icalentry "\n" $0
+    if (preserve)
+        if (! index("DTSTAMP", $1))
+            icalentry = icalentry "\n" $0
     # this line terminates the collection of description and summary entries
     indescription = 0;
     insummary = 0;
@@ -139,41 +160,39 @@ BEGIN {
 # this type of entry represents a day entry, not timed, with date stamp YYYYMMDD
 
 /^DTSTART;VALUE=DATE/ {
-    datetmp = gensub("([0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9])(.*[\r])", "\\1T000000\\2", "g", $2)
-    date = strftime("%Y-%m-%d %a %H:%M", datetimestamp(datetmp));
-    if(max_age>0)     lasttimestamp = datetimestamp(datetmp);
+    date = datestring($2);
 }
+
 /^DTEND;VALUE=DATE/ {
-    datetmp = gensub("([0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9])(.*[\r])", "\\1T000000\\2", "g", $2)
-    time2 = strftime("%Y-%m-%d %a %H:%M", datetimestamp(datetmp));
-    date = date ">--<" time2;
-    if(max_age>0)     lasttimestamp = datetimestamp(datetmp);
+    time2 = datestring($2, 1);
+    if ( ! issameday ) # && substr(date,1,10) != substr(time2,1,10))
+        date = date ">--<" time2;
 }
 
 # this represents a timed entry with date and time stamp YYYYMMDDTHHMMSS
 # we ignore the seconds
 
 /^DTSTART[:;][^V]/ {
-    date = strftime("%Y-%m-%d %a %H:%M", datetimestamp($2));
-    if(max_age>0)     lasttimestamp = datetimestamp($2);
+    date = datetimestring($2);
     # print date;
 }
 
-# and the same for the end date; here we extract only the time and append this to the 
-# date+time found by the DTSTART entry.  We assume that entry was there, of course.
-# should probably add some error checking here!  In time...
+# and the same for the end date;
 
 /^DTEND[:;][^V]/ {
-    # print $0
-    time2 = strftime("%Y-%m-%d %a %H:%M", datetimestamp($2));
-    date = date ">--<" time2;
-    if(max_age>0)     lasttimestamp = datetimestamp($2);
+    time2 = datetimestring($2);
+    if (substr(date,1,10) == substr(time2,1,10))
+        # timespan within same date, use one date with a time range
+        date = date "-" substr(time2, length(time2)-4)
+    else
+        # timespan extends over at least two dates
+        date = date ">--<" time2;
 }
 
 # The description will the contents of the entry in org-mode.
 # this line may be continued.
 
-/^DESCRIPTION/ { 
+/^DESCRIPTION/ {
     $1 = "";
     entry = entry gensub("\r", "", "g", $0);
     indescription = 1;
@@ -181,15 +200,19 @@ BEGIN {
 
 # the summary will be the org heading
 
-/^SUMMARY/ { 
+/^SUMMARY/ {
     $1 = "";
     summary = gensub("\r", "", "g", $0);
+
+    # trim trailing dots if requested by config option
+    if(trimdots && summary ~ /\.\.\.$/)
+        sub(/\.\.\.$/, "", summary)
     insummary = 1;
 }
 
 # the unique ID will be stored as a property of the entry
 
-/^UID/ { 
+/^UID/ {
     id = gensub("\r", "", "g", $2);
 }
 
@@ -209,23 +232,27 @@ BEGIN {
     #output event
     if(max_age<0 || ( lasttimestamp>0 && systime()<lasttimestamp+max_age*24*60*60 ) )
     {
-    # translate \n sequences to actual newlines and unprotect commas (,)
-    print "* " gensub("\\\\,", ",", "g", gensub("\\\\n", " ", "g", summary))
-    print "  <" date ">"
-    print "  :PROPERTIES:"
-    print "  :ID:       " id
-    if(length(location))
-       print "  :LOCATION: " location
-    if(length(status))
-       print "  :STATUS: " status
-    print "  :END:"
-    # for the entry, convert all embedded "\n" strings to actual newlines
-    print ""
-    # translate \n sequences to actual newlines and unprotect commas (,)
-    if(length(entry)>1)
-    print gensub("\\\\,", ",", "g", gensub("\\\\n", "\n", "g", entry));
-    print "** COMMENT original iCal entry"
-    print gensub("\r", "", "g", icalentry)
+        # translate \n sequences to actual newlines and unprotect commas (,)
+        if (condense)
+            print "* <" date "> " gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", " ", "g", summary)))
+        else
+            print "* " gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", " ", "g", summary))) "\n<" date ">"
+        print ":PROPERTIES:"
+        print     ":ID:       " id
+        if(length(location))
+            print ":LOCATION: " location
+        if(length(status))
+            print ":STATUS:   " status
+        print ":END:"
+
+        print ""
+        # translate \n sequences to actual newlines and unprotect commas (,)
+        if(length(entry)>1)
+            print gensub("^[ ]+", "", "", gensub("\\\\,", ",", "g", gensub("\\\\n", "\n", "g", entry)));
+
+        # output original entry if requested by 'original' config option
+        if (original)
+            print "** COMMENT original iCal entry\n", gensub("\r", "", "g", icalentry)
     }
     summary = ""
     date = ""
@@ -238,6 +265,98 @@ BEGIN {
     lasttimestamp = -1
 }
 
+# funtion to convert an iCal time string 'yyyymmddThhmmss[Z]' into a
+# date time string as used by org, preferably including the short day
+# of week: 'yyyy-mm-dd day hh:mm' or 'yyyy-mm-dd hh:mm' if we cannot
+# define the day of the week
+
+function datetimestring(input)
+{
+    # print "________"
+    # print "input : " input
+    # convert the iCal Date+Time entry to a format that mktime can understand
+    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);
+    # print "spec :" spec
+
+    stamp = mktime(spec);
+    lasttimestamp = stamp;
+
+    if (stamp <= 0) {
+        # this is a date before the start of the epoch, so we cannot
+        # use strftime and will deliver a 'yyyy-mm-dd hh:mm' string
+        # without day of week; this assumes local time, and does not
+        # attempt UTC offset correction
+        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);
+        # print "==> spec:" spec;
+        return spec;
+    }
+
+    if (input ~ /[0-9]{8}T[0-9]{6}Z/ ) {
+        # this is an utc time;
+        # we need to correct the timestamp by the utc offset for this time
+        offset = strftime("%z", stamp)
+        pm = substr(offset,1,1) 1 # define multiplier +1 or -1
+        hh = substr(offset,2,2) * 3600 * pm
+        mm = substr(offset,4,2) * 60 * pm
+
+        # adjust the timestamp
+        stamp = stamp + hh + mm
+    }
+
+    return strftime("%Y-%m-%d %a %H:%M", stamp);
+}
+
+# function to convert an iCal date into an org date;
+# the optional parameter indicates whether this is an end date;
+# for single or multiple whole day events, the end date given by
+# iCal is the date of the first day after the event;
+# if the optional 'isenddate' parameter is non zero, this function
+# tries to reduce the given date by one day
+
+function datestring(input, isenddate)
+{
+    #convert the iCal string to a an mktime input string
+    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);
+
+    # compute the nr of seconds after or before the epoch
+    # dates before the epoch will have a negative timestamp
+    # days after the epoch will have a positive timestamp
+    stamp = mktime(spec);
+
+    if (isenddate) {
+        # subtract 1 day from the timestamp
+        # note that this also works for dates before the epoch
+        stamp = stamp - 86400;
+
+        # register whether the end date is same as the start date
+        issameday = lasttimestamp == stamp
+    } else {
+        # save timestamp of start date to allow for check of end date
+        lasttimestamp = stamp
+    }
+
+    if (stamp < 0) {
+        # this date is before the epoch;
+        # the returned datestring will not have the short day of week string
+        # as strftime does not handle negative times;
+        # we have to construct the datestring directly from the input
+        if (isenddate) {
+            # we really should return the date before the input date, but strftime
+            # does not work with negative timestamp values; so we can not use it
+            # to obtain the string representation of the carrected timestamp;
+            # we have to return the date specified in the iCal input and we
+            # add time 00:00 to clarify this
+            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);
+        } else {
+            # just generate the desired representation of the input date, without time;
+            return gensub("([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9]).*[\r]*", "\\1-\\2-\\3", "g", input);
+        }
+    }
+
+    # return the date and day of week
+    return strftime("%Y-%m-%d %a", stamp);
+}
+
 # Local Variables:
 # time-stamp-line-limit: 1000
 # time-stamp-format: "%04y.%02m.%02d %02H:%02M:%02S"