diff options
author | Peter Stephenson <pws@users.sourceforge.net> | 2010-06-14 13:01:41 +0000 |
---|---|---|
committer | Peter Stephenson <pws@users.sourceforge.net> | 2010-06-14 13:01:41 +0000 |
commit | 14dde084755a8b15004d59bb6be5cc7a3726a8bf (patch) | |
tree | 067f4ebff5e399fb560c710b798a4e3421f771ea | |
parent | 4c1a3a89f0ade5be2330ce688cd3c3c649667f9a (diff) | |
download | zsh-14dde084755a8b15004d59bb6be5cc7a3726a8bf.tar.gz zsh-14dde084755a8b15004d59bb6be5cc7a3726a8bf.tar.xz zsh-14dde084755a8b15004d59bb6be5cc7a3726a8bf.zip |
28038: improved handling of recurring events in calendar system
-rw-r--r-- | ChangeLog | 8 | ||||
-rw-r--r-- | Doc/Zsh/calsys.yo | 69 | ||||
-rw-r--r-- | Functions/Calendar/calendar | 6 | ||||
-rw-r--r-- | Functions/Calendar/calendar_add | 178 | ||||
-rw-r--r-- | Functions/Calendar/calendar_parse | 155 | ||||
-rw-r--r-- | Functions/Calendar/calendar_scandate | 124 |
6 files changed, 416 insertions, 124 deletions
diff --git a/ChangeLog b/ChangeLog index 2ee6f406d..dec583429 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,11 @@ 2010-06-14 Peter Stephenson <pws@csr.com> + * 28038: Index: Completion/Zsh/Command/_zstyle, Doc/Zsh/calsys.yo, + Functions/Calendar/calendar, Functions/Calendar/calendar_add, + Functions/Calendar/calendar_parse, + Functions/Calendar/calendar_scandate: improved handling of + recurring events in calendar system. + * unposted: Doc/Zsh/params.yo: extra note on ZSH_EVAL_CONTEXT. * 28037: Src/exec.c, Src/math.c, Src/module.c, @@ -13291,5 +13297,5 @@ ***************************************************** * This is used by the shell to define $ZSH_PATCHLEVEL -* $Revision: 1.5004 $ +* $Revision: 1.5005 $ ***************************************************** diff --git a/Doc/Zsh/calsys.yo b/Doc/Zsh/calsys.yo index 1201f4dcf..f4bd4eaa4 100644 --- a/Doc/Zsh/calsys.yo +++ b/Doc/Zsh/calsys.yo @@ -408,6 +408,49 @@ 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. +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 the end of the holidays # 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 by the tt(RPT) keyword as a string 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.) diff --git a/Functions/Calendar/calendar b/Functions/Calendar/calendar index e4cdff8e4..48876aa51 100644 --- a/Functions/Calendar/calendar +++ b/Functions/Calendar/calendar @@ -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:]]#} diff --git a/Functions/Calendar/calendar_add b/Functions/Calendar/calendar_add index eded25b2a..c06deda3a 100644 --- a/Functions/Calendar/calendar_add +++ b/Functions/Calendar/calendar_add @@ -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; then 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 diff --git a/Functions/Calendar/calendar_parse b/Functions/Calendar/calendar_parse index e53e97516..b08622a9d 100644 --- a/Functions/Calendar/calendar_parse +++ b/Functions/Calendar/calendar_parse @@ -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 true; do -while (( changed )); do + 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]##[ ]#}" + ;; - (( changed = 0 )) - - # 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 diff --git a/Functions/Calendar/calendar_scandate b/Functions/Calendar/calendar_scandate index 4ae2ae606..b3a583705 100644 --- a/Functions/Calendar/calendar_scandate +++ b/Functions/Calendar/calendar_scandate @@ -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_found) )); then 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_found) )); then "'$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 |