summary refs log tree commit diff
path: root/Functions/Calendar/calendar_scandate
diff options
context:
space:
mode:
Diffstat (limited to 'Functions/Calendar/calendar_scandate')
-rw-r--r--Functions/Calendar/calendar_scandate167
1 files changed, 135 insertions, 32 deletions
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