From 483810a525b138f91dcb11f5864817a6e9ba6699 Mon Sep 17 00:00:00 2001 From: Peter Stephenson Date: Wed, 31 Jan 2007 16:53:31 +0000 Subject: 23142: calendar enhancements: relative times, recurring events --- Functions/Calendar/calendar | 88 ++++++++++++------ Functions/Calendar/calendar_add | 31 +++++-- Functions/Calendar/calendar_scandate | 167 ++++++++++++++++++++++++++++------- Functions/Calendar/calendar_show | 2 + Functions/Calendar/calendar_showdate | 48 ++++++++++ 5 files changed, 270 insertions(+), 66 deletions(-) create mode 100644 Functions/Calendar/calendar_showdate (limited to 'Functions') 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 -- cgit 1.4.1