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

PATCH: repeated events in calendar system



I've improved the calendar system so that it handles repeating events in
a slightly more sophisticated fashion.  This has had some reasonable
testing although I know there are some glitches and I think there are
others I haven't recorded.  It still doesn't handle end dates for
repeated events.  However, I'm not likely to be spending much
more time on it soon.  You'd better not abandon your GUI calendar
front-end yet.

Index: Doc/Zsh/calsys.yo
===================================================================
RCS file: /cvsroot/zsh/zsh/Doc/Zsh/calsys.yo,v
retrieving revision 1.19
diff -p -u -r1.19 calsys.yo
--- Doc/Zsh/calsys.yo	16 Mar 2010 22:53:32 -0000	1.19
+++ Doc/Zsh/calsys.yo	14 Jun 2010 11:59:20 -0000
@@ -408,6 +408,49 @@ made of the repeat count, so that it is 
 for a recurrence of an event in the calendar until the previous event
 has passed.
 
+If tt(RPT) is used, it is also possible to specify that certain
+recurrences of an event are rescheduled or cancelled.  This is
+done with the tt(OCCURRENCE) keyword, followed by whitespace and the
+date and time of the occurrence in the regular sequence, followed by
+whitespace and either the date and time of the rescheduled event or
+the exact string tt(CANCELLED).  In this case the date and time must
+be in exactly the "date with local time" format used by the
+tt(text/calendar) MIME type (RFC 2445),
+var(<YYYY><MM><DD>)tt(T)var(<hh><mm><ss>) (note the presence of the literal
+character tt(T)).  The first word (the regular recurrence) may be
+something other than a proper date/time to indicate that the event
+is additional to the normal sequence; a convention that retains
+the formatting appearance is tt(XXXXXXXXTXXXXXX).
+
+Furthermore, it is useful to record the next regular recurrence
+(as then the displayed date may be for a rescheduled event so cannot
+be used for calculating the regular sequence).  This is specified by
+tt(RECURRENCE) and a time or date in the same format.  tt(calendar_add)
+adds such an indication when it encounters a recurring event that does not
+include one, based on the headline date/time.
+
+If tt(calendar_add) is used to update occurrences the tt(UID) keyword
+described there should be present in both the existing entry and the added
+occurrence in order to identify recurring event sequences.
+
+For example,
+
+example(Thu May 6, 2010 11:00 Informal chat RPT 1 week
+  # RECURRENCE 20100506T110000
+  # OCCURRENCE 20100513T110000 20100513T120000
+  # OCCURRENCE 20100520T110000 CANCELLED)
+
+The event that occurs at 11:00 on 13th May 2010 is rescheduled an hour
+later.  The event that occurs a week later is cancelled.  The occurrences
+are given on a continuation line starting with a tt(#) character so will
+not usually be displayed as part of the event.  As elsewhere, no account of
+time zones is taken with the times. After the next event occurs the headline
+date/time will be `tt(Thu May 13, 2010 12:00)' while the tt(RECURRENCE)
+date/time will be `tt(20100513T110000)' (note that cancelled and
+moved events are not taken account of in the tt(RECURRENCE), which
+records what the next regular recurrence is, but they are accounted for in
+the headline date/time).
+
 It is safe to run tt(calendar -s) to reschedule an existing event
 (if the calendar file has changed, for example), and also to have it
 running in multiples instances of the shell since the calendar file
@@ -460,6 +503,24 @@ example(Aug 31, 2007 09:30  Celebrate th
   # UID 045B78A0)
 
 The second line will not be shown by the tt(calendar) function.
+
+It is possible to specify the tt(RPT) keyword followed by tt(CANCELLED)
+instead of a relative time.  This causes any matched event or series
+of events to be cancelled (the original event does not have to be marked
+as recurring in order to be cancelled by this method).  A tt(UID) is
+required in order to match an existing event in the calendar.
+
+tt(calendar_add) will attempt to manage recurrences and occurrences of
+repeating events as described for event scheduling by tt(calendar -s)
+above.  To reschedule or cancel a single event tt(calendar_add) should be
+called with an entry that includes the correct tt(UID) but does em(not)
+include the tt(RPT) keyword as this is taken to mean the entry applies to a
+series of repeating events and hence replaces all existing information.
+Each rescheduled or cancelled occurrence must have an tt(OCCURRENCE)
+keyword in the entry passed to tt(calendar_add) which will be merged into
+the calendar file.  Any existing reference to the occurrence is replaced.
+An occurrence that does not refer to a valid existing event is added as a
+one-off occurrence to the same calendar entry.
 )
 findex(calendar_edit)
 item(tt(calendar_edit))(
@@ -489,6 +550,10 @@ array tt(reply) as follows:
 startsitem()
 sitem(time)(The time as a string of digits in the same units as
 tt($EPOCHSECONDS))
+sitem(schedtime)(The regularly scheduled time.  This may differ from
+the actual event time tt(time) if this is a recurring event and the next
+occurrence has been rescheduled.  Then tt(time) gives the actual time
+and tt(schedtime) the time of the regular recurrence before modification.)
 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
@@ -501,6 +566,10 @@ sitem(rpttime)(Any recurrence time given
 of digits containing the time of the recurrence 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(schedrpttime)(The next regularly scheduled occurrence of a recurring
+event before modification.  This may differ from tt(rpttime), which is the
+actual time of the event that may have been rescheduled from the regular
+time.)
 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.)
Index: Functions/Calendar/calendar
===================================================================
RCS file: /cvsroot/zsh/zsh/Functions/Calendar/calendar,v
retrieving revision 1.16
diff -p -u -r1.16 calendar
--- Functions/Calendar/calendar	13 Apr 2010 16:28:21 -0000	1.16
+++ Functions/Calendar/calendar	14 Jun 2010 11:59:20 -0000
@@ -296,7 +296,9 @@ chmod 600 $mycmds
     fi
     # Look for a repeat time.
     if [[ -n ${reply[rpttime]} ]]; then
-      (( repeattime = ${reply[rpttime]}, repeating = 1 ))
+      # The actual time of the next event, which appears as text
+      (( repeattime = ${reply[rpttime]} ))
+      (( repeating = 1 ))
     else
       (( repeating = 0 ))
     fi
@@ -320,7 +322,7 @@ chmod 600 $mycmds
     match=()
     # Strip continuation lines starting " #".
     while [[ $showline = (#b)(*$'\n')[[:space:]]##\#[^$'\n']##(|$'\n'(*)) ]]; do
-	  showline="$match[1]$match[3]"
+      showline="$match[1]$match[3]"
     done
     # Strip trailing empty lines
     showline=${showline%%[[:space:]]#}
Index: Functions/Calendar/calendar_add
===================================================================
RCS file: /cvsroot/zsh/zsh/Functions/Calendar/calendar_add,v
retrieving revision 1.12
diff -p -u -r1.12 calendar_add
--- Functions/Calendar/calendar_add	13 Apr 2010 16:28:21 -0000	1.12
+++ Functions/Calendar/calendar_add	14 Jun 2010 11:59:20 -0000
@@ -7,14 +7,19 @@
 # entry before the first existing entry with a later date and time.
 
 emulate -L zsh
-setopt extendedglob
+setopt extendedglob # xtrace
 
 local context=":datetime:calendar_add:"
+local vdatefmt="%Y%m%dT%H%M%S"
+local d='[[:digit:]]'
 
-local calendar newfile REPLY lastline opt
-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
+local calendar newfile REPLY lastline opt text occur
+local -a calendar_entries lockfiles reply occurrences
+integer my_date done rstat nolock nobackup new_recurring
+integer keep_my_uid
+local -A reply parse_new parse_old
+local -a match mbegin mend
+local my_uid their_uid
 
 autoload -U calendar_{parse,read,lockfiles}
 
@@ -47,7 +52,6 @@ if ! calendar_parse $addline; then
 fi
 parse_new=("${(@kv)reply}")
 (( my_date = $parse_new[time] ))
-[[ -n $parse_new[rpttime] ]] && (( new_recurring = 1 ))
 if zstyle -t $context reformat-date; then
   local datefmt
   zstyle -s $context date-format datefmt ||
@@ -55,12 +59,24 @@ if zstyle -t $context reformat-date; the
   strftime -s REPLY $datefmt $parse_new[time]
   addline="$REPLY $parse_new[text1]"
 fi
+if [[ -n $parse_new[rptstr] ]]; then
+  (( new_recurring = 1 ))
+  if [[ $parse_new[rptstr] = CANCELLED ]]; then
+    (( done = 1 ))
+  elif [[ $addline = (#b)(*[[:space:]\#]RECURRENCE[[:space:]]##)([^[:space:]]##)([[:space:]]*|) ]]; then
+    # Use the updated recurrence time
+    strftime -s REPLY $vdatefmt ${parse_new[schedrpttime]}
+    addline="${match[1]}$REPLY${match[3]}"
+  else
+    # Add a recurrence time
+    [[ $addline = *$'\n' ]] || addline+=$'\n'
+    strftime -s REPLY $vdatefmt ${parse_new[schedrpttime]}
+    addline+="  # RECURRENCE $REPLY"
+  fi
+fi
 
 # $calendar doesn't necessarily exist yet.
 
-local -a match mbegin mend
-local my_uid their_uid
-
 # Match a UID, a unique identifier for the entry inherited from
 # text/calendar format.
 local uidpat='(|*[[:space:]])UID[[:space:]]##(#b)([[:xdigit:]]##)(|[[:space:]]*)'
@@ -87,14 +103,112 @@ fi
     calendar_read $calendar
 
     if [[ -n $my_uid ]]; then
-      # Pre-scan to find recurring events with a UID
+      # Pre-scan to events with the same UID
       for line in $calendar_entries; do
 	calendar_parse $line  ||  continue
+	parse_old=("${(@kv)reply}")
 	# Recurring with a UID?
-	if [[ -n $reply[rpttime] && $line = ${~uidpat} ]]; then
-	  # Yes, so record this as a recurring event.
+	if [[ $line = ${~uidpat} ]]; then
 	  their_uid=${(U)match[1]}
-	  recurring_uids[$their_uid]=$reply[time]
+	  if [[ $their_uid = $my_uid ]]; then
+	    # Deal with recurrences and also some add some
+	    # extra magic for cancellation.
+
+	    # See if we have found a recurrent version
+	    if [[ -z $parse_old[rpttime] ]]; then
+	      # No, so assume this is a straightforward replacement
+	      # of a non-recurring event.
+
+	      # Is this a cancellation of a non-recurring event?
+	      # Look for an OCCURRENCE in the form
+	      #   OCCURRENCE 20100513T110000 CANCELLED
+	      # although we don't bother looking at the date/time---
+	      # it's one-off, so this should already be unique.
+	      if [[ $new_recurring -eq 0 && \
+		$parse_new[text1] = (|*[[:space:]\#])"OCCURRENCE"[[:space:]]##([^[:space:]]##[[:space:]]##CANCELLED)(|[[:space:]]*) ]]; then
+		# Yes, so skip both the old and new events.
+		(( done = 1 ))
+	      fi
+	      # We'll skip this UID when we encounter it again.
+	      continue
+	    fi
+	    if (( new_recurring )); then
+	      # Replacing a recurrence; there can be only one.
+	      # TBD: do we replace existing occurrences of the
+	      # replaced recurrent event?  I'm guessing not, but
+	      # if we keep the UID then maybe we should.
+	      #
+	      # TBD: ick, suppose we're cancelling an even that
+	      # we added to a recurring sequence but didn't replace
+	      # the recurrence.  We might get RPT CANCELLED for this.
+	      # That would be bad.  Should invent better way of
+	      # cancelling non-recurring event.
+	      continue
+	    else
+	      # The recorded event is recurring, but the new one is a
+	      # one-off event. If there are embedded OCCURRENCE declarations,
+	      # use those.
+	      #
+	      # TBD: We could be clever about text associated
+	      # with the occurrence.  Copying the entire text
+	      # of the meeting seems like overkill but people often
+	      # add specific remarks about why this occurrence was
+	      # moved/cancelled.
+	      #
+	      # TBD: one case we don't yet handle is cancelling
+	      # something that isn't replacing a recurrence, i.e.
+	      # something we added as OCCURRENCE XXXXXXXXTXXXXXX <when>.
+	      # If we're adding a CANCELLED occurrence we should
+	      # look to see if it matches <when> and if so simply
+	      # delete that occurrence.
+	      #
+	      # TBD: one nasty case is if the new occurrence
+	      # occurs before the current scheduled time.  As we
+	      # never look backwards we'll miss it.
+	      text=$addline
+	      occurrences=()
+	      while [[ $text = (#b)(|*[[:space:]\#])"OCCURRENCE"[[:space:]]##([^[:space:]]##[[:space:]]##[^[:space:]]##)(|[[:space:]]*) ]]; do
+		occurrences+=($match[2])
+		text="$match[1] $match[3]"
+	      done
+	      if (( ! ${#occurrences} )); then
+		# No embedded occurrences.  We'll manufacture one
+		# that doesn't refer to an original recurrence.
+		strftime -s REPLY $vdatefmt $my_date
+		occurrences=("XXXXXXXXTXXXXXX $REPLY")
+	      fi
+	      # Add these occurrences, checking if they replace
+	      # an existing one.
+	      for occur in ${(o)occurrences}; do
+		REPLY=${occur%%[[:space:]]*}
+		# Only update occurrences that refer to genuine
+		# recurrences.
+		if [[ $REPLY = [[:digit:]](#c8)T[[:digit:]](#c6) && $line = (#b)(|*[[:space:]\#])(OCCURRENCE[[:space:]]##)${REPLY}[[:space:]]##[^[:space:]]##(|[[:space:]]*) ]]; then
+		  # Yes, update in situ
+		  line="${match[1]}${match[2]}$occur${match[3]}"
+		else
+		  # No, append.
+		  [[ $line = *$'\n' ]] || line+=$'\n'
+		  line+="  # OCCURRENCE $occur"
+		fi
+	      done
+	      # The line we're adding now corresponds to the
+	      # original event.  We'll skip the matching UID
+	      # in the file below, however.
+	      addline=$line
+	      # We need to work out which event is next, so
+	      # reparse.
+	      if calendar_parse $addline; then
+		parse_new=("${(@kv)reply}")
+		(( my_date = ${parse_new[time]} ))
+		if zstyle -t $context reformat-date; then
+		  zstyle -s $context date-format datefmt
+		  strftime -s REPLY $datefmt $parse_new[time]
+		  addline="$REPLY $parse_new[text1]"
+		fi
+	      fi
+	    fi
+	  fi
 	fi
       done
     fi
@@ -107,39 +221,11 @@ fi
 	  print -r -- $addline
 	  (( done = 1 ))
 	fi
-	if [[ -n $parse_old[rpttime] ]]; then
-	  (( old_recurring = 1 ))
-	else
-	  (( old_recurring = 0 ))
-	fi
-	if [[ -n $my_uid && $line = ${~uidpat} ]]; then
+	# We've already merged any information on the same UID
+	# with our new text, probably.
+	if [[ $keep_my_uid -eq 0 && -n $my_uid && $line = ${~uidpat} ]]; then
 	  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
-	      # It's recurring, but if this is a one-off at the
-	      # same time as the previous one, replace anyway.
-	      [[ -z $parse_old[$rpttime] ]] &&
-	        (( ${parse_new[time]} == ${parse_old[time]} )) &&
-		continue
-	    fi
-	  fi
+	  [[ $my_uid = $their_uid ]] && continue
 	fi
 	if [[ $parse_old[time] -eq $my_date && $line = $addline ]]; then
 	  (( done )) && continue # paranoia: shouldn't happen
@@ -157,7 +243,7 @@ New calendar left in $newfile." >&2
       fi
     fi
   else
-    print -r -- $line >$newfile
+    (( done )) || print -r -- $addline >$newfile
   fi
 
   if (( !rstat )) && ! mv $newfile $calendar; then
Index: Functions/Calendar/calendar_parse
===================================================================
RCS file: /cvsroot/zsh/zsh/Functions/Calendar/calendar_parse,v
retrieving revision 1.2
diff -p -u -r1.2 calendar_parse
--- Functions/Calendar/calendar_parse	20 Nov 2008 18:12:32 -0000	1.2
+++ Functions/Calendar/calendar_parse	14 Jun 2010 11:59:20 -0000
@@ -1,6 +1,6 @@
 # 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)
+# time  The time as an integer (as per EPOCHSECONDS) of the (next) event.
 # 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.
@@ -10,11 +10,16 @@
 #       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).
+# schedrpttime The next scheduled recurrence (which may be cancelled
+#              or rescheduled).
+# rpttime The actual occurrence time:  the event may have been rescheduled,
+#         in which case this is the time of the actual event (for use in
+#         programming warnings etc.) rather than that of the normal
+#         recurrence (which is recorded by calendar_add as RECURRENCE).
+#
 # rptstr   Any repeat/recurrence time as the original string.
-# text2 The text from the line with the date and keywords and values removed.
+# text2    The text from the line with the date and other keywords and
+#          values removed.
 #
 # Note that here an "integer" is a string of digits, not an internally
 # formatted integer.
@@ -26,9 +31,14 @@
 emulate -L zsh
 setopt extendedglob
 
-local REPLY REPLY2
+local vdatefmt="%Y%m%dT%H%M%S"
+
+local REPLY REPLY2 timefmt occurrence skip try_to_recover before after
 local -a match mbegin mend
-integer now
+integer now then replaced firstsched schedrpt
+# Any text matching "OCCURRENCE <timestamp> <disposition>"
+# may occur multiple times.  We set occurrences[<timestamp>]=disposition.
+local -A occurrences
 
 autoload -U calendar_scandate
 
@@ -45,51 +55,122 @@ fi
 # REPLY2 to the line with the date and time removed.
 calendar_scandate -as $1 || return 1
 reply[time]=$(( REPLY ))
+schedrpt=${reply[time]}
 reply[text1]=${REPLY2##[[:space:]]#}
+reply[text2]=${reply[text1]}
 
-reply[text2]=$reply[text1]
-
-integer changed=1
-
-while (( changed )); do
+while true; do
 
-  (( changed = 0 ))
+  case ${reply[text2]} in
+    # First check for a scheduled repeat time.  If we don't find one
+    # we'll use the normal time.
+    ((#b)(*[[:space:]\#])RECURRENCE[[:space:]]##([^[:space:]]##)([[:space:]]*|))
+    strftime -rs then $vdatefmt ${match[2]} ||
+    print "format: $vdatefmt, string ${match[2]}" >&2
+    schedrpt=$then
+    reply[text2]="${match[1]}${match[3]##[ 	]#}"
+    ;;
 
-  # Look for specific warn time.
-  if [[ $reply[text2] = (#b)(|*[[:space:],])WARN[[:space:]](*) ]]; then
+    # Look for specific warn time.
+    ((#b)(|*[[:space:],])WARN[[:space:]](*))
     if calendar_scandate -asm -R $reply[time] $match[2]; then
       reply[warntime]=$REPLY
       reply[warnstr]=${match[2]%%"$REPLY2"}
-      reply[text2]="${match[1]}${REPLY2##[[:space:]]#}"
+      # Remove spaces and tabs but not newlines from trailing text,
+      # else the formatting looks funny.
+      reply[text2]="${match[1]}${REPLY2##[ 	]#}"
     else
       # Just remove the keyword for further parsing
-      reply[text2]="${match[1]}${match[2]##[[:space:]]#}"
+      reply[text2]="${match[1]}${match[2]##[ 	]#}"
     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:]]#}"
-      (( now = EPOCHSECONDS ))
-      while (( ${reply[rpttime]} < now )); do
-	# let's hope the original appointment wasn't in 44 B.C.
-	if calendar_scandate -a -R ${reply[rpttime]} ${reply[rptstr]}; then
-	  if (( REPLY <= ${reply[rpttime]} )); then
-	    # pathological case
-	    break;
-	  fi
-	  reply[rpttime]=$REPLY
-	fi
-      done
+    ;;
+
+    ((#b)(|*[[:space:],])RPT[[:space:]](*))
+    before=${match[1]}
+    after=${match[2]}
+    if [[ $after = CANCELLED(|[[:space:]]*) ]]; then
+      reply[text2]="$before${match[2]##[ 	]#}"
+      reply[rptstr]=CANCELLED
+      reply[rpttime]=CANCELLED
+      reply[schedrpttime]=CANCELLED
+    elif calendar_scandate -a -R $schedrpt $after; then
+      # It's possible to calculate a recurrence, however we don't
+      # do that yet.  For now just keep the current time as
+      # the recurrence.  Hence we ignore REPLY.
+      reply[text2]="$before${REPLY2##[	]#}"
+      reply[rptstr]=${after%%"$REPLY2"}
+      # Until we find an individual occurrence, the actual time
+      # of the event is the regular one.
+      reply[rpttime]=$schedrpt
     else
       # Just remove the keyword for further parsing
-      reply[text2]="${match[1]}${match[2]##[[:space:]]#}"
+      reply[text2]="$before${after##[[:space:]]#}"
     fi
-    (( changed = 1 ))
-  fi
+    ;;
+
+    ((#b)(|*[[:space:]\#])OCCURRENCE[[:space:]]##([^[:space:]]##)[[:space:]]##([^[:space:]]##)(*))
+    occurrences[${match[2]}]="${match[3]}"
+    # as above
+    reply[text2]="${match[1]}${match[4]##[ 	]#}"
+    ;;
+
+    (*)
+    break
+    ;;
+  esac
 done
 
+if [[ -n ${reply[rpttime]} && ${reply[rptstr]} != CANCELLED ]]; then
+  # Recurring event.  We need to find out when it recurs.
+  (( now = EPOCHSECONDS ))
+
+  # First find the next recurrence.
+  replaced=0
+  reply[schedrpttime]=$schedrpt
+  if (( schedrpt >= now )); then
+    firstsched=$schedrpt
+  fi
+  while (( ${reply[schedrpttime]} < now || replaced )); do
+    if ! calendar_scandate -a -R ${reply[schedrpttime]} ${reply[rptstr]}; then
+      break
+    fi
+    if (( REPLY <= ${reply[schedrpttime]} )); then
+      # going backwards --- pathological case
+      break;
+    fi
+    reply[schedrpttime]=$REPLY
+    reply[rpttime]=$REPLY
+    if (( ${reply[schedrpttime]} > now && firstsched == 0 )); then
+      firstsched=$REPLY
+    fi
+    replaced=0
+    # do we have an occurrence to compare against?
+    if (( ${#occurrences} )); then
+      strftime -s timefmt $vdatefmt ${reply[schedrpttime]}
+      occurrence=$occurrences[$timefmt]
+      if [[ -n $occurrence ]]; then
+	# Yes, this replaces the scheduled one.
+	replaced=1
+      fi
+    fi
+  done
+  # Now look through occurrences (values only) and see which are (i) still
+  # to happen (ii) early than the current rpttime.
+  for occurrence in $occurrences; do
+    if [[ $occurrence != CANCELLED ]]; then
+      strftime -rs then $vdatefmt $occurrence ||
+      print "format: $vdatefmt, string $occurrence" >&2
+      if (( then > now && then < ${reply[rpttime]} )); then
+	reply[rpttime]=$then
+      fi
+    fi
+  done
+  # Finally, update the scheduled repeat time to the earliest
+  # possible value.  This is so that if an occurrence replacement is
+  # cancelled we pick up the regular one.  Can this happen?  Dunno.
+  reply[schedrpttime]=$firstsched
+fi
+
 reply[text2]="${reply[text2]##[[:space:],]#}"
 
 return 0
Index: Functions/Calendar/calendar_scandate
===================================================================
RCS file: /cvsroot/zsh/zsh/Functions/Calendar/calendar_scandate,v
retrieving revision 1.8
diff -p -u -r1.8 calendar_scandate
--- Functions/Calendar/calendar_scandate	8 Feb 2010 11:53:56 -0000	1.8
+++ Functions/Calendar/calendar_scandate	14 Jun 2010 11:59:21 -0000
@@ -23,6 +23,19 @@
 #   from 1900 to 2099 inclusive are matched.
 # - Although timezones are parsed (complicated formats may not be recognized),
 #   they are then ignored; no time adjustment is made.
+# - Embedding of times within dates (e.g. "Wed Jun 16 09:30:00 BST 2010")
+#   causes horrific problems because of the combination of the many
+#   possible date and time formats to match.  The approach taken
+#   here is to match the time, remove it, and see if the nearby text
+#   looks like a date.  The problem is that the time matched may not
+#   be that associated with the date, in which case the time will be
+#   ignored.  To minimise this, when the argument "-a" is given to
+#   anchor the date/time to the start of the line, we never look
+#   beyond a newline.  So if any date/time strings in the text
+#   are on separate lines the problem is avoided.
+# - If you feel sophisticated enough and wish to avoid any ambiguity,
+#   you can use RFC 2445 date/time strings, for example 20100601T170000.
+#   These are parsed in one go.
 #
 # The following give some obvious examples; users finding here
 # a format they like and not subject to vagaries of style may skip
@@ -136,7 +149,7 @@
 # In this case absolute dates are ignored.
 
 emulate -L zsh
-setopt extendedglob
+setopt extendedglob # xtrace
 
 zmodload -i zsh/datetime || return 1
 
@@ -145,7 +158,7 @@ zmodload -i zsh/datetime || return 1
 # relatively logical dates like 2006/09/19:14:27
 # don't allow / before time !  the above
 # is not 19 hours 14 mins and 27 seconds after anything.
-local tschars="[-,:[:space:]]"
+local tschars="[-,:[:blank:]]"
 # start pattern for time when anchored
 local tspat_anchor="(${tschars}#)"
 # ... when not anchored
@@ -175,9 +188,10 @@ local repat="(|s)(|${schars}*)"
 # We may need some completely different heuristic.
 local monthpat="(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]#"
 integer daysecs=$(( 24 * 60 * 60 ))
+local d="[[:digit:]]"
 
 integer year year2 month month2 day day2 hour minute second then nth wday wday2
-local opt line orig_line mname MATCH MBEGIN MEND tz test
+local opt line orig_line mname MATCH MBEGIN MEND tz test rest_line
 local -a match mbegin mend
 # Flags that we found a date or a time (maybe a relative time)
 integer date_found time_found
@@ -237,7 +251,7 @@ while getopts "aAdmrR:st" opt; do
 done
 shift $(( OPTIND - 1 ))
 
-line=$1 orig_line=$1
+line=$1
 
 local dspat dspat_noday tspat
 if (( anchor )); then
@@ -250,11 +264,20 @@ if (( anchor )); then
     # We'll test later if the time is associated with the date.
     tspat=$tspat_noanchor
   fi
+  # We can save a huge amount of grief (I've discovered) if when
+  # we're anchored to the start we ignore anything after a newline.
+  # However, don't do this if we're anchored to the end.  The
+  # match should fail if there are extra lines in that case.
+  if [[ anchor_end -eq 0 && $line = (#b)([^$'\n']##)($'\n'*) ]]; then
+    line=$match[1]
+    rest_line=$match[2]
+  fi
 else
   dspat=$dspat_noanchor
   dspat_noday=$dspat_noanchor
   tspat=$tspat_noanchor
 fi
+orig_line=$line
 
 # Look for a time separately; we need colons for this.
 # We want to look for the first time to ensure it's associated
@@ -268,6 +291,7 @@ fi
 # To use a case statement we'd need to be able to request non-greedy
 # matching for a pattern.
 local rest
+# HH:MM:SECONDS am/pm with optional decimal seconds
 rest=${line#(#ibm)${~tspat}(<0-12>):(<0-59>)[.:]((<0-59>)(.<->|))[[:space:]]#([ap])(|.)[[:space:]]#m(.|[[:space:]]|(#e))}
 if [[ $rest != $line ]]; then
   hour=$match[2]
@@ -275,7 +299,8 @@ if [[ $rest != $line ]]; then
   second=$match[5]
   [[ $match[7] = (#i)p ]] && (( hour <= 12 )) && (( hour += 12 ))
   time_found=1
-else
+fi
+if (( time_found == 0 )); then
   # no seconds, am/pm
   rest=${line#(#ibm)${~tspat}(<0-12>):(<0-59>)[[:space:]]#([ap])(|.)[[:space:]]#m(.|[[:space:]]|(#e))}
   if [[ $rest != $line ]]; then
@@ -283,37 +308,60 @@ else
     minute=$match[3]
     [[ $match[4] = (#i)p ]] && (( hour <= 12 )) && (( hour += 12 ))
     time_found=1
-  else
-    # no colon, even, but a.m./p.m. indicator
-    rest=${line#(#ibm)${~tspat}(<0-12>)[[:space:]]#([ap])(|.)[[:space:]]#m(.|[[:space:]]|(#e))}
-    if [[ $rest != $line ]]; then
-      hour=$match[2]
-      minute=0
-      [[ $match[3] = (#i)p ]] && (( hour <= 12 )) && (( hour += 12 ))
-      time_found=1
-    else
-      # 24 hour clock, with seconds
-      rest=${line#(#ibm)${~tspat}(<0-24>):(<0-59>)[.:]((<0-59>)(.<->|))(.|[[:space:]]|(#e))}
-      if [[ $rest != $line ]]; then
-	hour=$match[2]
-	minute=$match[3]
-	second=$match[5]
-	time_found=1
-      else
-	rest=${line#(#ibm)${~tspat}(<0-24>):(<0-59>)(.|[[:space:]]|(#e))}
-	if [[ $rest != $line ]]; then
-	  hour=$match[2]
-	  minute=$match[3]
-	  time_found=1
-	fi
-      fi
-    fi
+  fi
+fi
+if (( time_found == 0 )); then
+  # no colon, even, but a.m./p.m. indicator
+  rest=${line#(#ibm)${~tspat}(<0-12>)[[:space:]]#([ap])(|.)[[:space:]]#m(.|[[:space:]]|(#e))}
+  if [[ $rest != $line ]]; then
+    hour=$match[2]
+    minute=0
+    [[ $match[3] = (#i)p ]] && (( hour <= 12 )) && (( hour += 12 ))
+    time_found=1
+  fi
+fi
+if (( time_found == 0 )); then
+  # 24 hour clock, with seconds
+  rest=${line#(#ibm)${~tspat}(<0-24>):(<0-59>)[.:]((<0-59>)(.<->|))(.|[[:space:]]|(#e))}
+  if [[ $rest != $line ]]; then
+    hour=$match[2]
+    minute=$match[3]
+    second=$match[5]
+    time_found=1
+  fi
+fi
+if (( time_found == 0 )); then
+  rest=${line#(#ibm)${~tspat}(<0-24>):(<0-59>)(.|[[:space:]]|(#e))}
+  if [[ $rest != $line ]]; then
+    hour=$match[2]
+    minute=$match[3]
+    time_found=1
+  fi
+fi
+if (( time_found == 0 )); then
+  # Combined date and time formats:  here we can use an anchor because
+  # we know the complete format.
+  (( anchor )) && tspat=$tspat_anchor
+  # RFC 2445
+  rest=${line#(#ibm)${~tspat}(|\"[^\"]##\":)($~d$~d$~d$~d)($~d$~d)($~d$~d)T($~d$~d)($~d$~d)($~d$~d)([[:space:]]#|(#e))}
+  if [[ $rest != $line ]]; then
+    year=$match[3]
+    month=$match[4]
+    day=$match[5]
+    hour=$match[6]
+    minute=$match[7]
+    second=$match[8]
+    # signal don't need to take account of time in date...
+    time_found=2
+    date_found=1
+    date_start=$mbegin[3]
+    date_end=$mend[-1]
   fi
 fi
 (( hour == 24 )) && hour=0
 
-if (( time_found )); then
-  # time was found
+if (( time_found && ! date_found )); then
+  # time was found; if data also found already, process below.
   time_start=$mbegin[2]
   time_end=$mend[-1]
   # Remove the timespec because it may be in the middle of
@@ -331,7 +379,7 @@ if (( time_found )); then
   (( debug )) && print "line after time: $line"
 fi
 
-if (( relative == 0 )); then
+if (( relative == 0 && date_found == 0 )); then
   # Date.
   case $line in
   # Look for YEAR[-/.]MONTH[-/.]DAY
@@ -468,7 +516,7 @@ if (( date_found || (time_ok && time_fou
     fi
     line=${line[1,$date_start-1]}${line[$date_end+1,-1]}
   fi
-  if (( time_found )); then
+  if (( time_found == 1 )); then
     if (( date_found )); then
       # If we found a time, it must be associated with the date,
       # or we can't use it.  Since we removed the time from the
@@ -540,7 +588,7 @@ if (( date_found || (time_ok && time_fou
 	"'$orig_line[time_start,time_end]'"
       (( date_ok )) && print "Date string: $date_start,$date_end:" \
 	"'$orig_line[date_start,date_end]'"
-      print "Remaining line: '$line'"
+      print "Remaining line: '$line$rest_line'"
     fi
   fi
 fi
@@ -722,11 +770,11 @@ if (( relative )); then
     (( reladd += (hour * 60 + minute) * 60 + second ))
     typeset -g REPLY
     (( REPLY = relative_start + reladd  ))
-    [[ -n $setvar ]] && typeset -g REPLY2="$line"
+    [[ -n $setvar ]] && typeset -g REPLY2="$line$rest_line"
     return 0
   fi
   return 1
-elif (( ! date_found )); then
+elif (( date_found == 0 )); then
   return 1
 fi
 
@@ -748,6 +796,6 @@ fi
 
 strftime -s REPLY -r $fmt $nums
 
-[[ -n $setvar ]] && typeset -g REPLY2="$line"
+[[ -n $setvar ]] && typeset -g REPLY2="$line$rest_line"
 
 return 0

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


Member of the CSR plc group of companies. CSR plc registered in England and Wales, registered number 4187346, registered office Churchill House, Cambridge Business Park, Cowley Road, Cambridge, CB4 0WZ, United Kingdom



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