about summary refs log tree commit diff
path: root/Functions/Calendar
diff options
context:
space:
mode:
Diffstat (limited to 'Functions/Calendar')
-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
5 files changed, 270 insertions, 66 deletions
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