Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

PATCH: improve calendar handling of recurring events



I had this carefully uncommitted, but inevitably it decided to sneak
in with another commit, so I might as well leave it...

The abstracts the function calendar_parse to parse a calendar entry from
the function calendar, and uses it in calendar_add to check whether an
event is recurring.  If it is, then one-off events supplement rather
than replace it.  This is still crude but will at least stop all my
regular meetings infuriatingly disappearing every time one occurrence
moves.  I was hoping to test it a bit more first.

By the way, as I update the various .distfiles I'm (very) gradually
moving them to one entry per line in alphabetical order.  This is much
easier to maintain and actually slightly more compact in storage;
there's absolutely no point in having it prettified.  The change is
pretty much invisible to everyone else anyway.

Index: Doc/Zsh/calsys.yo
===================================================================
RCS file: /cvsroot/zsh/zsh/Doc/Zsh/calsys.yo,v
retrieving revision 1.10
retrieving revision 1.11
diff -u -r1.10 -r1.11
--- Doc/Zsh/calsys.yo	16 Aug 2007 12:04:06 -0000	1.10
+++ Doc/Zsh/calsys.yo	29 Nov 2007 09:49:43 -0000	1.11
@@ -464,6 +464,36 @@
 Hence it should be used to edit the calendar file if there is any
 possibility of a calendar event occurring meanwhile.
 )
+findex(calendar_parse)
+item(tt(calendar_parse) var(calendar-entry))(
+This is the internal function that analyses the parts of a calendar
+entry, which is passed as the only argument.  The function returns
+status 1 if the argument could not be parsed as a calendar entry
+and status 2 if the wrong number of arguments were passed; it also sets the
+parameter tt(reply) to an empty associative array.  Otherwise,
+it returns status 0 and sets elements of the associative
+array tt(reply) as follows:
+startsitem()
+sitem(time)(The time as a string of digits in the same units as
+tt($EPOCHSECONDS))
+sitem(text1)(The text from the line not including the date and time of the
+event, but including any tt(WARN) or tt(RPT) keywords and values.)
+sitem(warntime)(Any warning time given by the tt(WARN) keyword as a string
+of digits containing the time at which to warn in the same units as
+tt($EPOCHSECONDS).  (Note this is an absolute time, not the relative time
+passed down.)  Not set no tt(WARN) keyword and value were
+matched.)
+sitem(warnstr)(The raw string matched after the tt(WARN) keyword, else unset.)
+sitem(rpttime)(Any recurrence time given by the tt(RPT) keyword as a string
+of digits containing the time of the recurrenced in the same units
+as tt($EPOCHSECONDS).  (Note this is an absolute time.)  Not set if
+no tt(RPT) keyword and value were matched.)
+sitem(rptstr)(The raw string matched after the tt(RPT) keyword, else unset.)
+sitem(text2)(The text from the line after removal of the date and any
+keywords and values.)
+)
+endsitem()
+)
 findex(calendar_showdate)
 item(tt(calendar_showdate) [ tt(-r) ] [ tt(-f) var(fmt) ] var(date-spec ...))(
 The given var(date-spec) is interpreted and the corresponding date and
Index: Functions/Calendar/.distfiles
===================================================================
RCS file: /cvsroot/zsh/zsh/Functions/Calendar/.distfiles,v
retrieving revision 1.2
retrieving revision 1.3
diff -u -r1.2 -r1.3
--- Functions/Calendar/.distfiles	16 Apr 2007 13:21:26 -0000	1.2
+++ Functions/Calendar/.distfiles	29 Nov 2007 09:49:43 -0000	1.3
@@ -1,13 +1,14 @@
 DISTFILES_SRC='
-    .distfiles
-    age
-    calendar
-    calendar_add
-    calendar_edit
-    calendar_lockfiles
-    calendar_read
-    calendar_scandate
-    calendar_show
-    calendar_showdate
-    calendar_sort
+.distfiles
+age
+calendar
+calendar_add
+calendar_edit
+calendar_lockfiles
+calendar_parse
+calendar_read
+calendar_scandate
+calendar_show
+calendar_showdate
+calendar_sort
 '
Index: Functions/Calendar/calendar
===================================================================
RCS file: /cvsroot/zsh/zsh/Functions/Calendar/calendar,v
retrieving revision 1.7
retrieving revision 1.8
diff -u -r1.7 -r1.8
--- Functions/Calendar/calendar	5 Sep 2007 08:34:27 -0000	1.7
+++ Functions/Calendar/calendar	29 Nov 2007 09:49:43 -0000	1.8
@@ -1,18 +1,19 @@
 emulate -L zsh
 setopt extendedglob
 
-local line showline restline REPLY REPLY2 userange pruned nobackup datefmt
+local line showline restline REPLY REPLY2 userange nobackup datefmt
 local calendar donefile sched newfile warnstr mywarnstr newdate
 integer time start stop today ndays y m d next=-1 shown done nodone
 integer verbose warntime mywarntime t tcalc tsched i rstat remaining
 integer showcount icount repeating repeattime resched showall brief
 local -a calendar_entries calendar_addlines
 local -a times calopts showprog lockfiles match mbegin mend
+local -A reply
 
 zmodload -i zsh/datetime || return 1
 zmodload -i zsh/zutil || return 1
 
-autoload -U calendar_{add,read,scandate,show,lockfiles}
+autoload -U calendar_{add,parse,read,scandate,show,lockfiles}
 
 # Read the calendar file from the calendar-file style
 zstyle -s ':datetime:calendar:' calendar-file calendar || calendar=~/calendar
@@ -254,31 +255,29 @@
 
   calendar_read $calendar
   for line in $calendar_entries; do
-    # This call sets REPLY to the date and time in seconds since the epoch,
-    # REPLY2 to the line with the date and time removed.
-    calendar_scandate -as $line || continue
-    (( t = REPLY ))
-    restline=$REPLY2
+    calendar_parse $line  ||  continue
 
+    # Extract returned parameters from $reply
+    # Time of event
+    (( t = ${reply[time]} ))
+    # Remainder of line including RPT and WARN stuff:  we need
+    # to keep these for rescheduling.
+    restline=$reply[text1]
     # Look for specific warn time.
-    pruned=${restline#(|*[[:space:],])WARN[[:space:]]}
-    (( mywarntime = warntime ))
-    mywarnstr=$warnstr
-    if [[ $pruned != $restline ]]; then
-      if calendar_scandate -asm -R $t $pruned; then
-	(( mywarntime = t - REPLY ))
-	mywarnstr=${pruned%%"$REPLY2"}
-      fi
+    if [[ -n ${reply[warntime]} ]]; then
+      (( mywarntime = t - ${reply[warntime]} ))
+      mywarnstr=${reply[warnstr]}
+    else
+      (( mywarntime = warntime ))
+      mywarnstr=$warnstr
     fi
-
     # Look for a repeat time.
-    (( repeating = 0 ))
-    pruned=${restline#(|*[[:space:],])RPT[[:space:]]}
-    if [[ $pruned != $restline ]]; then
-      if calendar_scandate -a -R $t $pruned; then
-	(( repeattime = REPLY, repeating = 1 ))
-      fi
+    if [[ -n ${reply[rpttime]} ]]; then
+      (( repeattime = ${reply[rpttime]}, repeating = 1 ))
+    else
+      (( repeating = 0 ))
     fi
+    # Finished extracting parameters from $reply
 
     if (( verbose )); then
       print "Examining: $line"
Index: Functions/Calendar/calendar_add
===================================================================
RCS file: /cvsroot/zsh/zsh/Functions/Calendar/calendar_add,v
retrieving revision 1.4
retrieving revision 1.5
diff -u -r1.4 -r1.5
--- Functions/Calendar/calendar_add	16 Aug 2007 12:04:08 -0000	1.4
+++ Functions/Calendar/calendar_add	29 Nov 2007 09:49:43 -0000	1.5
@@ -11,10 +11,11 @@
 setopt extendedglob
 
 local calendar newfile REPLY lastline opt
-local -a calendar_entries lockfiles
-integer my_date done rstat nolock nobackup
+local -a calendar_entries lockfiles reply
+integer my_date done rstat nolock nobackup new_recurring old_recurring
+local -A reply parse_new parse_old recurring_uids
 
-autoload -U calendar_{read,lockfiles,scandate}
+autoload -U calendar_{parse,read,lockfiles}
 
 while getopts "BL" opt; do
   case $opt in
@@ -38,11 +39,13 @@
   calendar=~/calendar
 newfile=$calendar.new.$HOST.$$
 
-if ! calendar_scandate -a "$*"; then
+if ! calendar_parse "$*"; then
   print "$0: failed to parse date/time" >&2
   return 1
 fi
-(( my_date = $REPLY ))
+parse_new=("${(@kv)reply}")
+(( my_date = $parse_new[time] ))
+[[ -n $parse_new[rpttime] ]] && (( new_recurring = 1 ))
 
 # $calendar doesn't necessarily exist yet.
 
@@ -53,7 +56,7 @@
 # text/calendar format.
 local uidpat='(|*[[:space:]])UID[[:space:]]##(#b)([[:xdigit:]]##)(|[[:space:]]*)'
 if [[ "$*" = ${~uidpat} ]]; then
-  my_uid=$match[1]
+  my_uid=${(U)match[1]}
 fi
 
 # start of block for following always to clear up lockfiles.
@@ -63,16 +66,55 @@
   if [[ -f $calendar ]]; then
     calendar_read $calendar
 
+    if [[ -n $my_uid ]]; then
+      # Pre-scan to find recurring events with a UID
+      for line in $calendar_entries; do
+	calendar_parse $line  ||  continue
+	# Recurring with a UID?
+	if [[ -n $reply[rpttime] && $line = ${~uidpat} ]]; then
+	  # Yes, so record this as a recurring event.
+	  their_uid=${(U)match[1]}
+	  recurring_uids[$their_uid]=1
+	fi
+      done
+    fi
+
     {
       for line in $calendar_entries; do
-	if (( ! done )) && calendar_scandate -a $line && (( REPLY > my_date )); then
+	calendar_parse $line  ||  continue
+	parse_old=("${(@kv)reply}")
+	if (( ! done && ${parse_old[time]} > my_date )); then
 	  print -r -- "$*"
 	  (( done = 1 ))
 	fi
-	# Don't save this entry if it has the same UID as the new one.
+	if [[ -n $parse_old[rpttime] ]]; then
+	  (( old_recurring = 1 ))
+	else
+	  (( old_recurring = 0 ))
+	fi
 	if [[ -n $my_uid && $line = ${~uidpat} ]]; then
-	  their_uid=$match[1]
-	  [[ ${(U)my_uid} = ${(U)their_uid} ]] && continue
+	  their_uid=${(U)match[1]}
+	  if [[ $my_uid = $their_uid ]]; then
+	    # Deal with recurrences, being careful in case there
+	    # are one-off variants that don't replace recurrences.
+	    #
+	    # Bug 1: "calendar" still doesn't know about one-off variants.
+	    # Bug 2: neither do I; how do we know which occurrence
+	    # it replaces?
+	    # Bug 3: the code for calculating recurrences is awful anyway.
+
+	    if (( old_recurring && new_recurring )); then
+	      # Replacing a recurrence; there can be only one.
+	      continue
+	    elif (( ! new_recurring )); then
+	      # Not recurring.  See if we have previously found
+	      # a recurrent version
+	      [[ -n $recurring_uids[$their_uid] ]] && (( old_recurring = 1 ))
+	      # No, so assume this is a straightforward replacement
+	      # of a non-recurring event.
+	      (( ! old_recurring )) && continue
+	    fi
+	  fi
 	fi
 	if [[ $REPLY -eq $my_date && $line = "$*" ]]; then
 	  (( done )) && continue # paranoia: shouldn't happen
Index: Functions/Calendar/calendar_parse
===================================================================
RCS file: Functions/Calendar/calendar_parse
diff -N Functions/Calendar/calendar_parse
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ Functions/Calendar/calendar_parse	29 Nov 2007 09:49:43 -0000	1.1
@@ -0,0 +1,83 @@
+# Parse the line passed down in the first argument as a calendar entry.
+# Sets the values parsed into the associative array reply, consisting of:
+# time  The time as an integer (as per EPOCHSECONDS)
+# text1 The text from the the line not including the date/time, but
+#       including any WARN or RPT text.  This is useful for rescheduling
+#       events, since the keywords need to be retained in this case.
+# warntime  Any warning time (WARN keyword) as an integer, else an empty
+#       string.  This is the time of the warning in units of EPOCHSECONDS,
+#       not the parsed version of the original number (which was a time
+#       difference).
+# warnstr  Any warning time as the original string (e.g. "5 mins"), not
+#       including the WARN keyword.
+# rpttime  Any repeat/recurrence time (RPT keyword) as an integer, else empty.
+#       This is the time of the recurrence itself in EPOCHSECONDS units
+#       (as with a warning---not the difference between the events).
+# rptstr   Any repeat/recurrence time as the original string.
+# text2 The text from the line with the date and keywords and values removed.
+#
+# Note that here an "integer" is a string of digits, not an internally
+# formatted integer.
+#
+# Return status 1 if parsing failed.  reply is set to an empty
+# in this case.  Note the caller is responsible for
+# making reply local.
+
+emulate -L zsh
+setopt extendedglob
+
+local REPLY REPLY2
+local -a match mbegin mend
+
+autoload -U calendar_scandate
+
+typeset -gA reply
+
+reply=()
+
+if (( $# != 1 )); then
+  print "Usage: $0 calendar-entry" >&2
+  return 2
+fi
+
+# This call sets REPLY to the date and time in seconds since the epoch,
+# REPLY2 to the line with the date and time removed.
+calendar_scandate -as $1 || return 1
+reply[time]=$(( REPLY ))
+reply[text1]=${REPLY2##[[:space:]]#}
+
+reply[text2]=$reply[text1]
+
+integer changed=1
+
+while (( changed )); do
+
+  (( changed = 0 ))
+
+  # Look for specific warn time.
+  if [[ $reply[text2] = (#b)(|*[[:space:],])WARN[[:space:]](*) ]]; then
+    if calendar_scandate -asm -R $reply[time] $match[2]; then
+      reply[warntime]=$REPLY
+      reply[warnstr]=${match[2]%%"$REPLY2"}
+      reply[text2]="${match[1]}${REPLY2##[[:space:]]#}"
+    else
+      # Just remove the keyword for further parsing
+      reply[text2]="${match[1]}${match[2]##[[:space:]]#}"
+    fi
+    (( changed = 1 ))
+  elif [[ $reply[text2] = (#b)(|*[[:space:],])RPT[[:space:]](*) ]]; then
+    if calendar_scandate -a -R $reply[time] $match[2]; then
+      reply[rpttime]=$REPLY
+      reply[rptstr]=${match[2]%%"$REPLY2"}
+      reply[text2]="${match[1]}${REPLY2##[[:space:]]#}"
+    else
+      # Just remove the keyword for further parsing
+      reply[text2]="${match[1]}${match[2]##[[:space:]]#}"
+    fi
+    (( changed = 1 ))
+  fi
+done
+
+reply[text2]="${reply[text2]##[[:space:],]#}"
+
+return 0


-- 
Peter Stephenson <pws@xxxxxxx>                  Software Engineer
CSR PLC, Churchill House, Cambridge Business Park, Cowley Road
Cambridge, CB4 0WZ, UK                          Tel: +44 (0)1223 692070



Messages sorted by: Reverse Date, Date, Thread, Author