about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog7
-rw-r--r--Doc/Zsh/calsys.yo135
-rw-r--r--Functions/Calendar/calendar88
-rw-r--r--Functions/Calendar/calendar_add31
-rw-r--r--Functions/Calendar/calendar_scandate167
-rw-r--r--Functions/Calendar/calendar_show2
-rw-r--r--Functions/Calendar/calendar_showdate48
7 files changed, 397 insertions, 81 deletions
diff --git a/ChangeLog b/ChangeLog
index 2b48e48c1..d24be792d 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,10 @@
+2007-01-31  Peter Stephenson  <pws@csr.com>
+
+	* 23142: Doc/Zsh/calsys.yo, Functions/Calendar/calendar,
+	Functions/Calendar/calendar_add, calendar_scandate,
+	calendar_show, calendar_showdate: enhancements for recurring
+	events and relative times and dates in calendar system.
+
 2007-01-27  Peter Stephenson  <p.w.stephenson@ntlworld.com>
 
 	* 23141: Src/jobs.c: don't refresh the display after
diff --git a/Doc/Zsh/calsys.yo b/Doc/Zsh/calsys.yo
index 109734204..57902cbbb 100644
--- a/Doc/Zsh/calsys.yo
+++ b/Doc/Zsh/calsys.yo
@@ -63,10 +63,14 @@ An example is shown below.
 subsect(Date Format)
 
 The format of the date and time is designed to allow flexibility without
-admitting ambiguity.  Note that there is no localization support; month and
-day names must be in English (though only the first three letters are
-significant) and separator characters are fixed.  Furthermore, time zones
-are not handled; all times are assumed to be local.
+admitting ambiguity.  (The words `date' and `time' are both used in the
+documentation below; except where specifically noted this implies a string
+that may include both a date and a time specification.)  Note that there is
+no localization support; month and day names must be in English and
+separator characters are fixed.  Matching is case insensitive, and only the
+first three letters of the names are significant, although as a special
+case a form beginning "month" does not match "Monday".  Furthermore, time
+zones are not handled; all times are assumed to be local.
 
 It is recommended that, rather than exploring the intricacies of the
 system, users find a date format that is natural to them and stick to it.
@@ -202,24 +206,43 @@ instead a combination of various supported periods are allowed, together
 with an optional time.  The periods must be in order from most to
 least significant.
 
+In some cases, a more accurate calculation is possible when there is an
+anchor date:  offsets of months or years pick the correct day, rather than
+being rounded, and it is possible to pick a particular day in a month as
+`(1st Friday)', etc., as described in more detail below.
+
+Anchors are available in the following cases.  If one or two times are
+passed to the function tt(calendar), the start time acts an anchor for the
+end time when the end time is relative (even if the start time is
+implicit).  When examining calendar files, the scheduled event being
+examined anchors the warning time when it is given explicitly by means of
+the tt(WARN) keyword; likewise, the scheduled event anchors a repitition
+period when given by the tt(RPT) keyword, so that specifications such as
+tt(RPT 2 months, 3rd Thursday) are handled properly.  Finally, the tt(-R)
+argument to tt(calendar_scandate) directly provides an anchor for relative
+calculations.
+
 The periods handled, with possible abbreviations are:
 
 startitem()
 item(Years)(
-tt(years), tt(yrs), tt(ys), tt(year), tt(yr), tt(y).
-Currently a year is 365.25 days, not a calendar year.
+tt(years), tt(yrs), tt(ys), tt(year), tt(yr), tt(y), tt(yearly).
+A year is 365.25 days unless there is an anchor.
 )
 item(Months)(
 tt(months), tt(mons), tt(mnths), tt(mths), tt(month), tt(mon),
-tt(mnth), tt(mth).  Note that tt(m), tt(ms), tt(mn), tt(mns)
-are ambiguous and are em(not) handled.  Currently a month is a period
-of 30 days rather than a calendar month.
+tt(mnth), tt(mth), tt(monthly).  Note that tt(m), tt(ms), tt(mn), tt(mns)
+are ambiguous and are em(not) handled.  A month is a period
+of 30 days rather than a calendar month unless there is an anchor.
 )
 item(Weeks)(
-tt(weeks), tt(wks), tt(ws), tt(week), tt(wk), tt(w)
+tt(weeks), tt(wks), tt(ws), tt(week), tt(wk), tt(w), tt(weekly)
 )
 item(Days)(
-tt(days), tt(dys), tt(ds), tt(day), tt(dy), tt(d)
+tt(days), tt(dys), tt(ds), tt(day), tt(dy), tt(d), tt(daily)
+)
+item(Hours)(
+tt(hours), tt(hrs), tt(hs), tt(hour), tt(hr), tt(h), tt(hourly)
 )
 item(Minutes)(
 tt(minutes), tt(mins), tt(minute), tt(min), but em(not) tt(m),
@@ -233,22 +256,46 @@ enditem()
 Spaces between the numbers are optional, but are required between items,
 although a comma may be used (with or without spaces).
 
+The forms tt(yearly) to tt(hourly) allow the number to be omitted; it is
+assumed to be 1.  For example, tt(1 d) and tt(daily) are equivalent.  Note
+that using those forms with plurals is confusing; tt(2 yearly) is the same
+as tt(2 years), em(not) twice yearly, so it is recommended they only
+be used without numbers.
+
+When an anchor time is present, there is an extension to handle regular
+events in the form of the var(n)th var(some)day of the month.  Such a
+specification must occur immediately after any year and month
+specification, but before any time of day, and must be in the form
+var(n)tt(LPAR()th|st|rd+RPAR()) var(day), for example tt(1st Tuesday) or
+tt(3rd Monday).  As in other places, days are matched case insensitively,
+must be in English, and only the first three letters are significant except
+that a form beginning `month' does not match `Monday'.  No attempt is made
+to sanitize the resulting date; attempts to squeeze too many occurrences
+into a month will push the day into the next month (but in the obvious
+fashion, retaining the correct day of the week).
+
 Here are some examples:
 
 example(30 years 3 months 4 days 3:42:41
 14 days 5 hours
+Monthly, 3rd Thursday
 4d,10hr)
 
 subsect(Example)
 
 Here is an example calendar file.  It uses a consistent date format,
-as recommended above.  The second entry has a continuation line.
+as recommended above.
 
 example(Feb 1, 2006 14:30 Pointless bureaucratic meeting
 Mar 27, 2006 11:00 Mutual recrimination and finger pointing
   Bring water pistol and waterproofs
-Apr 10, 2006 13:30 Even more pointless blame assignment exercise)
+Apr 10, 2006 13:30 Even more pointless blame assignment exercise WARN 30 mins
+May 18, 2006 16:00 Regular moaning session RPT monthly, 3rd Thursday)
 
+The second entry has a continuation line.  The third entry will produce
+a warning 30 minutes before the event (to allow you to equip yourself
+appropriately).  The fourth entry repeats after a month on the 3rd
+Thursday, i.e. June 15, 2006, at the same time.
 
 texinode(Calendar System User Functions)(Calendar Styles)(Calendar File and Date Formats)(Calendar Function System)
 sect(User Functions)
@@ -330,6 +377,14 @@ for a single calendar entry by including tt(WARN) var(reltime) in the first
 line of the entry, where var(reltime) is one of the usual relative time
 formats.
 
+A repeated event may be indicated by including tt(RPT) var(reldate) in the
+first line of the entry.  After the scheduled event has been displayed
+it will be re-entered into the calendar file at a time var(reldate)
+after the existing event.  Note that this is currently the only use
+made of the repeat count, so that it is not possible to query the schedule
+for a recurrence of an event in the calendar until the previous event
+has passed.
+
 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
@@ -343,17 +398,37 @@ Explicitly specify a programme to be used for showing events instead
 of the value of the tt(show-prog) style or the default tt(calendar_show).
 )
 item(tt(-v))(
-Verbose:  show more information about stages of processing.
+Verbose:  show more information about stages of processing.  This
+is useful for confirming that the function has successfully parsed
+the dates in the calendar file.
 )
 enditem()
 )
 findex(calendar_add)
-item(tt(calendar_add) var(event ...))(
+item(tt(calendar_add) [ tt(-BL) ] var(event ...))(
 Adds a single event to the calendar in the appropriate location.
 Using this function ensures that the calendar file is sorted in date
 and time order.  It also makes special arrangments for locking
 the file will it is altered.  The old calendar is left in a file
 with the suffix tt(.old).
+
+The option tt(-B) indicates that backing up the calendar file will be
+handled by the caller and should not be performed by tt(calendar_add).  The
+option tt(-L) indicates that tt(calendar_add) does not need to lock the
+calendar file up the old one as it is already locked.  These options will
+not usually be needed by users.
+)
+findex(calendar_showdate)
+item(tt(calendar_showdate) [ tt(-r) ] var(date-spec ...))(
+The given var(date-spec) is interpreted and the corresponding date and
+time printed.  If the initial var(date-spec) begins with a tt(PLUS()) or
+tt(-) it is treated as relative to the current time; var(date-spec)s after
+the first are treated as relative to the date calculated so far and
+a leading tt(PLUS()) is optional in that case.  This allows one to
+use the system as a date calculator.  For example, tt(calendar_showdate '+1
+month, 1st Friday') shows the date of the first Friday of next month.
+With the option tt(-r) nothing is printed but the value of the date and
+timein seconds since the epoch is stored in the parameter tt(REPLY).
 )
 findex(calendar_sort)
 item(tt(calendar_sort))(
@@ -443,6 +518,21 @@ kindex(calendar-file)
 item(tt(calendar-file))(
 The location of the main calendar.  The default is tt(~/calendar).
 )
+kindex(date-format)
+item(tt(date-format))(
+A tt(strftime) format string (see manref(strftime)(3)) with the zsh
+extensions tt(%f) for a day of the month with no leading zero or space
+for single digits, and tt(%k) or tt(%l) for the hour of the day on the 24-
+or 12-hour clock, again with no leading zero or space for single digits.
+
+This is used for outputting dates in tt(calendar), both to support
+the tt(-v) option and when adding recurring events back to the calendar
+file, and in tt(calendar_showdate) as the final output format.
+
+If the style is not set, the default used is similar the standard system
+format as output by the tt(date) command (also known as `ctime format'):
+`tt(%a %b %d %H:%M:%S %Z %Y)'.
+)
 kindex(done-file)
 item(tt(done-file))(
 The location of the file to which events which have passed are appended.
@@ -522,9 +612,24 @@ they will not be matched if the is any other text in the argument.
 item(tt(-d))(
 Enable additional debugging output.
 )
+item(tt(-m))(
+Minus.  When tt(-R) var(anchor_time) is also given the relative time is
+calculated backwards from var(anchor_time).
+)
 item(tt(-r))(
 The argument passed is to be parsed as a relative time.
 )
+item(tt(-R) var(anchor_time))(
+The argument passed is to be parsed as a relative time.  The time is
+relative to var(anchor_time), a time in seconds since the epoch,
+and the returned value is the absolute time corresponding to advancing
+var(anchor_time) by the relative time given.
+This allows lengths of months to be correctly taken into account.  If
+the final day does not exist in the given month, the last day of the
+final month is given.  For example, if the anchor time is during 31st
+January 2007 and the relative time is 1 month, the final time is the
+same time of day during 28th February 2007.
+)
 item(tt(-s))(
 In addition to setting tt(REPLY), set tt(REPLY2) to the remainder of
 the argument after the date and time have been stripped.  This is
diff --git a/Functions/Calendar/calendar b/Functions/Calendar/calendar
index 124fd9786..ea81c7ae7 100644
--- a/Functions/Calendar/calendar
+++ b/Functions/Calendar/calendar
@@ -1,21 +1,18 @@
 emulate -L zsh
 setopt extendedglob
 
-# standard ctime date/time format
-local ctime="%a %b %d %H:%M:%S %Z %Y"
-
-local line REPLY REPLY2 userange pruned
-local calendar donefile sched newfile warnstr mywarnstr
+local line restline REPLY REPLY2 userange pruned 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 tsched i rstat remaining
-integer showcount icount
-local -a calendar_entries
+integer verbose warntime mywarntime t tcalc tsched i rstat remaining
+integer showcount icount repeating repeattime resched
+local -a calendar_entries calendar_addlines
 local -a times calopts showprog lockfiles match mbegin mend
 
 zmodload -i zsh/datetime || return 1
 zmodload -i zsh/zutil || return 1
 
-autoload -U calendar_{read,scandate,show,lockfiles}
+autoload -U calendar_{add,read,scandate,show,lockfiles}
 
 # Read the calendar file from the calendar-file style
 zstyle -s ':datetime:calendar:' calendar-file calendar || calendar=~/calendar
@@ -27,6 +24,9 @@ zstyle -a ':datetime:calendar:' show-prog showprog ||
 # Amount of time before an event when it should be flagged.
 # May be overridden in individual entries
 zstyle -s ':datetime:calendar:' warn-time warnstr || warnstr="0:05"
+# default to standard ctime date/time format
+zstyle -s ':datetime:calendar:' date-format datefmt ||
+  datefmt="%a %b %d %H:%M:%S %Z %Y"
 
 if [[ -n $warnstr ]]; then
   if [[ $warnstr = <-> ]]; then
@@ -169,11 +169,11 @@ strftime -s wd "%u" $start
 
 if (( $# && !remaining )); then
   if [[ $1 = +* ]]; then
-    if ! calendar_scandate -ar ${1[2,-1]}; then
+    if ! calendar_scandate -a -R $start ${1[2,-1]}; then
       print "$0: failed to parse relative time: $1" >&2
       return 1
     fi
-    (( stop = start + REPLY ))
+    (( stop = REPLY ))
   elif [[ $1 = <-> ]]; then
     stop=$1
   else
@@ -184,8 +184,8 @@ if (( $# && !remaining )); then
     stop=$REPLY
   fi
   if (( stop < start )); then
-    strftime -s REPLY $ctime $start
-    strftime -s REPLY2 $ctime $stop
+    strftime -s REPLY $datefmt $start
+    strftime -s REPLY2 $datefmt $stop
     print "$0: requested end time is before start time:
   start: $REPLY
   end: $REPLY2" >&2
@@ -224,12 +224,12 @@ autoload -Uz matchdate
 
 if (( verbose )); then
   print -n "start: "
-  strftime $ctime $start
+  strftime $datefmt $start
   print -n "stop: "
   if (( remaining )); then
     print "none"
   else
-    strftime $ctime $stop
+    strftime $datefmt $stop
   fi
 fi
 
@@ -248,22 +248,32 @@ fi
     # REPLY2 to the line with the date and time removed.
     calendar_scandate -as $line || continue
     (( t = REPLY ))
+    restline=$REPLY2
 
     # Look for specific warn time.
-    pruned=${REPLY2#(|*[[:space:],])WARN[[:space:]]}
+    pruned=${restline#(|*[[:space:],])WARN[[:space:]]}
     (( mywarntime = warntime ))
     mywarnstr=$warnstr
-    if [[ $pruned != $REPLY2 ]]; then
-      if calendar_scandate -ars $pruned; then
-	(( mywarntime = REPLY ))
+    if [[ $pruned != $restline ]]; then
+      if calendar_scandate -asm -R $t $pruned; then
+	(( mywarntime = t - REPLY ))
 	mywarnstr=${pruned%%"$REPLY2"}
       fi
     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
+    fi
+
     if (( verbose )); then
       print "Examining: $line"
       print -n "  Date/time: "
-      strftime $ctime $t
+      strftime $datefmt $t
       if [[ -n $sched ]]; then
 	print "  Warning $mywarntime seconds ($mywarnstr) before"
       fi
@@ -272,7 +282,9 @@ fi
     if (( t >= start && (remaining || t <= stop || icount < showcount) ))
     then
       $showprog $start $stop "$line"
-      (( shown = 1, icount++ ))
+      (( icount++ ))
+      # Doesn't count as "shown" unless the event has now passed.
+      (( t <= EPOCHSECONDS )) && (( shown = 1 ))
     elif [[ -n $sched ]]; then
       (( tsched = t - mywarntime ))
       if (( tsched >= start && tsched <= stop)); then
@@ -280,22 +292,36 @@ fi
       fi
     fi
     if [[ -n $sched ]]; then
-      if (( t - mywarntime > EPOCHSECONDS )); then
+      if (( shown && repeating )); then
+	# Done and dusted, but a repeated event is due.
+	strftime -s newdate $datefmt $repeattime
+	calendar_addlines+=("$newdate$restline")
+
+	# We'll add this back in below, but we check in case the
+	# repeated event is the next one due.  It's not
+	# actually a disaster if there's an error and we fail
+	# to add the time.  Always try to reschedule this event.
+	(( tcalc = repeattime, resched = 1 ))
+      else
+	(( tcalc = t ))
+      fi
+
+      if (( tcalc - mywarntime > EPOCHSECONDS )); then
 	# schedule for a warning
-	(( tsched = t - mywarntime ))
+	(( tsched = tcalc - mywarntime, resched = 1 ))
       else
 	# schedule for event itself
-	(( tsched = t ))
+	(( tsched = tcalc ))
+	# but don't schedule unless the event has not yet been shown.
+	(( !shown )) && (( resched = 1 ))
       fi
-      if (( (tsched > EPOCHSECONDS || ! shown) &&
-	    (next < 0 || tsched < next) )); then
+      if (( resched && (next < 0 || tsched < next) )); then
 	(( next = tsched ))
       fi
     fi
     if [[ -n $donefile ]]; then
-      if (( t <= EPOCHSECONDS && shown )); then
+      if (( shown )); then
 	# Done and dusted.
-	# TODO: handle repeated times from REPLY2.
 	if ! print -r $line >>$donefile; then
 	  if (( done != 3 )); then
 	    (( done = 3 ))
@@ -346,9 +372,15 @@ New calendar left in $newfile." >&2
 Old calendar left in $calendar.old." >&2
       (( rstat = 1 ))
     fi
+    nobackup=-B
   elif [[ -n $donefile ]]; then
     rm -f $newfile
   fi
+
+  # Reschedule repeating events.
+  for line in $calendar_addlines; do
+    calendar_add -L $nobackup $line
+  done
 } always {
   (( ${#lockfiles} )) && rm -f $lockfiles
 }
diff --git a/Functions/Calendar/calendar_add b/Functions/Calendar/calendar_add
index f7f60e136..8e6eca8b6 100644
--- a/Functions/Calendar/calendar_add
+++ b/Functions/Calendar/calendar_add
@@ -10,12 +10,29 @@
 emulate -L zsh
 setopt extendedglob
 
-local calendar newfile REPLY lastline
+local calendar newfile REPLY lastline opt
 local -a calendar_entries lockfiles
-integer newdate done rstat
+integer newdate done rstat nolock nobackup
 
 autoload -U calendar_{read,lockfiles,scandate}
 
+while getopts "BL" opt; do
+  case $opt in
+    (B)
+    nobackup=1
+    ;;
+
+    (L)
+    nolock=1
+    ;;
+
+    (*)
+    return 1
+    ;;
+  esac
+done
+shift $(( OPTIND - 1 ))
+
 # Read the calendar file from the calendar-file style
 zstyle -s ':datetime:calendar_add:' calendar-file calendar ||
   calendar=~/calendar
@@ -31,7 +48,7 @@ fi
 
 # start of block for following always to clear up lockfiles.
 {
-  calendar_lockfiles $calendar || return 1
+  (( nolock )) || calendar_lockfiles $calendar || return 1
 
   if [[ -f $calendar ]]; then
     calendar_read $calendar
@@ -48,10 +65,12 @@ fi
 	done
 	(( done )) || print -r -- "$*"
     } >$newfile
-    if ! mv $calendar $calendar.old; then
-      print "Couldn't back up $calendar to $calendar.old.
+    if (( ! nobackup )); then
+      if ! mv $calendar $calendar.old; then
+	print "Couldn't back up $calendar to $calendar.old.
 New calendar left in $newfile." >&2
-      (( rstat = 1 ))
+	(( rstat = 1 ))
+      fi
     fi
   else
     print -r -- $line >$newfile
diff --git a/Functions/Calendar/calendar_scandate b/Functions/Calendar/calendar_scandate
index 825aaf65b..53d0a9edf 100644
--- a/Functions/Calendar/calendar_scandate
+++ b/Functions/Calendar/calendar_scandate
@@ -46,7 +46,7 @@
 #   HH:MM.SS[.FFFFF] [am|pm|a.m.|p.m.]
 # in which square brackets indicate optional elements, possibly with
 # alternatives.  Fractions of a second are recognised but ignored.
-# Unless -r is given (see below), a date is mandatory but a time of day is
+# Unless -r or -R are given (see below), a date is mandatory but a time of day is
 # not; the time returned is at the start of the date.
 #
 # Time zones are not handled, though if one is matched following a time
@@ -122,10 +122,12 @@
 # are optional, but are required between items, although a comma
 # may be used (with or without spaces).
 #
-# Note that a year here is 365.25 days and a month is 30 days.  TODO:
-# improve this by passing down base time and adjusting.  (This will
-# be crucial for events repeating monthly.)  TODO: it then makes
-# sense to make PERIODly = 1 PERIOD (also for PERIOD = dai!)
+# Note that a year here is 365.25 days and a month is 30 days.
+#
+# With -R start_time, a relative time is parsed and start_time is treated
+# as the start of the period.  This allows months and years to be calculated
+# accurately.  If the option -m (minus) is also given the relative time is
+# taken backwards from the start time.
 #
 # This allows forms like:
 #   30 years 3 months 4 days 3:42:41
@@ -151,7 +153,9 @@ local tspat_noanchor="(|*${tschars})"
 # separator characters between elements.  comma is fairly
 # natural punctuation; otherwise only allow whitespace.
 local schars="[.,[:space:]]"
-local daypat="${schars}#(sun|mon|tue|wed|thu|fri|sat)[a-z]#${schars}#"
+local -a dayarr
+dayarr=(sun mon tue wed thu fri sat)
+local daypat="${schars}#((#B)(${(j.|.)dayarr})[a-z]#~month*)"
 # Start pattern for date: treat , as space for simplicity.  This
 # is illogical at the start but saves lots of minor fiddling later.
 # Date start pattern when anchored at the start.
@@ -160,7 +164,7 @@ local daypat="${schars}#(sun|mon|tue|wed|thu|fri|sat)[a-z]#${schars}#"
 # (The problem in the other case is that matching anything before
 # the day of the week is greedy, so the day of the week gets ignored
 # if it's optional.)
-local dspat_anchor="(|(#B)${daypat}(#b)${schars}#)"
+local dspat_anchor="(|(#B)(${daypat}|)(#b)${schars}#)"
 local dspat_anchor_noday="(|${schars}#)"
 # Date start pattern when not anchored at the start.
 local dspat_noanchor="(|*${schars})"
@@ -170,10 +174,10 @@ local repat="(|s)(|${schars}*)"
 # of the system for the purpose of finding out where they occur.
 # We may need some completely different heuristic.
 local monthpat="(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]#"
-# days, not handled but we need to ignore them. also not localized.
+integer daysecs=$(( 24 * 60 * 60 ))
 
-integer year month day hour minute second then
-local opt line orig_line mname MATCH MBEGIN MEND tz
+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 -a match mbegin mend
 # Flags that we found a date or a time (maybe a relative time)
 integer date_found time_found
@@ -183,9 +187,10 @@ integer time_ok
 # These are actual character indices as zsh would normally use, i.e.
 # line[time_start,time_end] is the string for the time.
 integer time_start time_end date_start date_end
-integer anchor anchor_end debug relative reladd setvar
+integer anchor anchor_end debug setvar
+integer relative relative_start reladd reldate relsign=1
 
-while getopts "aAdrst" opt; do
+while getopts "aAdmrR:st" opt; do
   case $opt in
     (a)
     # anchor
@@ -202,10 +207,21 @@ while getopts "aAdrst" opt; do
     (( debug = 1 ))
     ;;
 
+    (m)
+    # relative with negative offsets
+    (( relsign = -1 ))
+    ;;
+
     (r)
+    # relative with no fixed start
     (( relative = 1 ))
     ;;
 
+    (R)
+    # relative with fixed start supplied
+    (( relative_start = OPTARG, relative = 2 ))
+    ;;
+
     (s)
     (( setvar = 1 ))
     ;;
@@ -381,7 +397,7 @@ if (( relative == 0 )); then
   ;;
 
   # Look for WEEKDAY
-  ((#bi)${~dspat_noday}(${~daypat})*)
+  ((#bi)${~dspat_noday}(${~daypat})(|${~schars})*)
   integer wday_now wday
   local wdaystr=${(L)match[3]}
   date_start=$mbegin[2] date_end=$mend[2]
@@ -405,15 +421,22 @@ if (( relative == 0 )); then
   ;;
 
   # Look for "today", "yesterday", "tomorrow"
-  ((#bi)${~dspat_noday}(yesterday|today|tomorrow)(|${schars})*)
+  ((#bi)${~dspat_noday}(yesterday|today|tomorrow|now)(|${~schars})*)
   (( then = EPOCHSECONDS ))
   case ${(L)match[2]} in
     (yesterday)
-    (( then -= 24 * 60 * 60 ))
+    (( then -= daysecs ))
     ;;
 
     (tomorrow)
-    (( then += 24 * 60 * 60 ))
+    (( then += daysecs ))
+    ;;
+
+    (now)
+    time_found=1 time_end=0 time_start=1
+    strftime -s hour "%H" $then
+    strftime -s minute "%M" $then
+    strftime -s second "%S" $then
     ;;
   esac
   strftime -s year "%Y" $then
@@ -429,7 +452,7 @@ if (( date_found || (time_ok && time_found) )); then
   # date found
   # see if there's a day at the start
   if (( date_found )); then
-    if [[ ${line[1,$date_start-1]} = (#bi)${~daypat} ]]; then
+    if [[ ${line[1,$date_start-1]} = (#bi)${~daypat}${~schars}# ]]; then
 	    date_start=$mbegin[1]
     fi
     line=${line[1,$date_start-1]}${line[$date_end+1,-1]}
@@ -512,38 +535,117 @@ if (( date_found || (time_ok && time_found) )); then
 fi
 
 if (( relative )); then
-  if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(y|yr|year)${~repat} ]]; then
-    (( reladd += ((365*4+1) * 24 * 60 * 60 * ${match[2]} + 1) / 4 ))
+  if (( relative == 2 )); then
+    # Relative years and months are variable, and we may need to
+    # be careful about days.
+    strftime -s year "%Y" $relative_start
+    strftime -s month "%m" $relative_start
+    strftime -s day "%d" $relative_start
+    strftime -rs then "%Y:%m:%d" "${year}:${month}:${day}"
+  fi
+  if [[ $line = (#bi)${~dspat}(<->|)[[:space:]]#(y|yr|year|yearly)${~repat} ]]; then
+    [[ -z $match[2] ]] && match[2]=1
+    if (( relative == 2 )); then
+      # We need the difference between relative_start & the
+      # time ${match[2]} years later.  This means keeping the month and
+      # day the same and changing the year.
+      (( year2 = year + relsign * ${match[2]} ))
+      strftime -rs reldate "%Y:%m:%d" "${year2}:${month}:${day}"
+
+      # If we've gone from a leap year to a non-leap year, go back a day.
+      strftime -s month2 "%m" $reldate
+      (( month2 != month )) && (( reldate -= daysecs ))
+
+      # Keep this as a difference for now since we may need to add in other stuff.
+      (( reladd += reldate - then ))
+    else
+      (( reladd += relsign * ((365*4+1) * daysecs * ${match[2]} + 1) / 4 ))
+    fi
     line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
     time_found=1
   fi
-  if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(mth|mon|mnth|month)${~repat} ]]; then
-     (( reladd += 30 * 24 * 60 * 60 * ${match[2]} ))
+  if [[ $line = (#bi)${~dspat}(<->|)[[:space:]]#(mth|mon|mnth|month|monthly)${~repat} ]]; then
+     [[ -z $match[2] ]] && match[2]=1
+     if (( relative == 2 )); then
+       # Need to add on ${match[2]} months as above.
+       (( month2 = month + relsign * ${match[2]} ))
+       if (( month2 <= 0 )); then
+	 # going backwards beyond start of given year
+	 (( year2 = year + month2 / 12 - 1, month2 = month2 + (year-year2)*12 ))
+       else
+	 (( year2 = year + (month2 - 1)/ 12, month2 = (month2 - 1) % 12 + 1 ))
+       fi
+       strftime -rs reldate "%Y:%m:%d" "${year2}:${month2}:${day}"
+
+       # If we've gone past the end of the month because it was too short,
+       # we have two options (i) get the damn calendar fixed (ii) wind
+       # back to the end of the previous month.  (ii) is easier for now.
+       if (( day > 28 )); then
+	 while true; do
+	   strftime -s day2 "%d" $reldate
+	   # There are only up to 3 days in it, so just wind back one at a time.
+	   # Saves counting.
+	   (( day2 >= 28 )) && break
+	   (( reldate -= daysecs ))
+	 done
+       fi
+
+       (( reladd += reldate - then ))
+     else
+       (( reladd += relsign * 30 * daysecs * ${match[2]} ))
+     fi
      line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
      time_found=1
   fi
-  if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(w|wk|week)${~repat} ]]; then
-     (( reladd += 7 * 24 * 60 * 60 * ${match[2]} ))
+  if [[ $relative = 2 && $line = (#bi)${~dspat_noday}(<->)(th|rd|st)(${~daypat})(|${~schars}*) ]]; then
+     nth=$match[2]
+     test=${(L)${${match[4]##${~schars}#}%%${~schars}#}[1,3]}
+     wday=${dayarr[(I)$test]}
+     if (( wday )); then
+       line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
+       time_found=1
+       # We want weekday 0 to 6
+       (( wday-- ))
+       (( reldate = relative_start + reladd ))
+       strftime -s year2 "%Y" $reldate
+       strftime -s month2 "%m" $reldate
+       # Find day of week of the first of the month we've landed on.
+       strftime -rs then "%Y:%m:%d" "${year2}:${month2}:1"
+       strftime -s wday2 "%w" $then
+       # Calculate day of month
+       (( day = 1 + (wday - wday2) + (nth - 1) * 7 ))
+       (( wday < wday2 )) && (( day += 7 ))
+       # whereas the day of the month calculated so far is...
+       strftime -s day2 "%d" $reldate
+       # so we need to compensate by...
+       (( reladd += (day - day2) * daysecs ))
+     fi
+  fi
+  if [[ $line = (#bi)${~dspat}(<->|)[[:space:]]#(w|wk|week|weekly)${~repat} ]]; then
+     [[ -z $match[2] ]] && match[2]=1
+     (( reladd += relsign * 7 * daysecs * ${match[2]} ))
      line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
      time_found=1
   fi
-  if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(d|dy|day)${~repat} ]]; then
-     (( reladd += 24 * 60 * 60 * ${match[2]} ))
+  if [[ $line = (#bi)${~dspat}(<->|)[[:space:]]#(d|dy|day|daily)${~repat} ]]; then
+     [[ -z $match[2] ]] && match[2]=1
+     (( reladd += relsign * daysecs * ${match[2]} ))
      line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
      time_found=1
   fi
-  if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(h|hr|hour)${~repat} ]]; then
-     (( reladd += 60 * 60 * ${match[2]} ))
+  if [[ $line = (#bi)${~dspat}(<->|)[[:space:]]#(h|hr|hour|hourly)${~repat} ]]; then
+     [[ -z $match[2] ]] && match[2]=1
+     (( reladd += relsign * 60 * 60 * ${match[2]} ))
      line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
      time_found=1
   fi
-  if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(min|minute)${~repat} ]]; then
-     (( reladd += 60 * ${match[2]} ))
+  if [[ $line = (#bi)${~dspat}(<->)[[:space:]]#(min|minute)${~repat} ]]; then
+     (( reladd += relsign * 60 * ${match[2]} ))
      line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
      time_found=1
   fi
-  if [[ $line = (#bi)${~dspat}(<->)[[:blank:]]#(s|sec|second)${~repat} ]]; then
-     (( reladd += ${match[2]} ))
+  if [[ $line = (#bi)${~dspat}(<->)[[:space:]]#(s|sec|second)${~repat} ]]; then
+     (( reladd += relsign * ${match[2]} ))
      line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
      time_found=1
   fi
@@ -558,7 +660,8 @@ if (( relative )); then
 	return 1
       fi
     fi
-    (( REPLY = reladd + (hour * 60 + minute) * 60 + second ))
+    # relative_start is zero if we're not using it
+    (( REPLY = relative_start + reladd + (hour * 60 + minute) * 60 + second ))
     [[ -n $setvar ]] && REPLY2=$line
     return 0
   fi
diff --git a/Functions/Calendar/calendar_show b/Functions/Calendar/calendar_show
index f731d07a5..77f025ec5 100644
--- a/Functions/Calendar/calendar_show
+++ b/Functions/Calendar/calendar_show
@@ -22,3 +22,5 @@ if [[ -n $DISPLAY &&  $start -eq $stop ]]; then
     ($cmd "$*" &)
   fi
 fi
+
+return 0
diff --git a/Functions/Calendar/calendar_showdate b/Functions/Calendar/calendar_showdate
new file mode 100644
index 000000000..b35a0a91f
--- /dev/null
+++ b/Functions/Calendar/calendar_showdate
@@ -0,0 +1,48 @@
+emulate -L zsh
+setopt extendedglob
+
+local optm datefmt
+integer optr replyset
+
+zstyle -s ':datetime:calendar_showdate:' date-format datefmt ||
+  datefmt="%a %b %d %H:%M:%S %Z %Y"
+
+if [[ $1 = -r ]]; then
+  shift
+  REPLY=0
+  optr=1
+else
+  local REPLY
+fi
+
+if (( ! $# )); then
+  print "Usage: $0 datespec [ ... ]" >&2
+  return 1
+fi
+
+while (( $# )); do
+  optm=
+  if [[ $1 = [-+]* ]]; then
+    # relative
+    [[ $1 = -* ]] && optm=-m
+    1=${1[2,-1]}
+    # if this is the first argument, use current time
+    # don't make assumptions about type of reply in case global
+    if (( ! replyset )); then
+      REPLY=$EPOCHSECONDS
+      replyset=1
+    fi
+  fi
+
+  if (( replyset )); then
+    calendar_scandate $optm -R $REPLY -aA $1 || return 1
+    replyset=1
+  else
+    calendar_scandate -aA $1 || return 1
+  fi
+
+  shift
+done
+
+(( optr )) && return
+strftime $datefmt $REPLY