about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--ChangeLog16
-rw-r--r--Completion/Unix/Type/_list_files8
-rw-r--r--Doc/.distfiles3
-rw-r--r--Doc/Makefile.in5
-rw-r--r--Doc/Zsh/.distfiles2
-rw-r--r--Doc/Zsh/calsys.yo547
-rw-r--r--Doc/Zsh/compsys.yo10
-rw-r--r--Doc/Zsh/intro.yo1
-rw-r--r--Doc/Zsh/manual.yo1
-rw-r--r--Doc/Zsh/modules.yo2
-rw-r--r--Doc/Zsh/tcpsys.yo2
-rw-r--r--Doc/zsh.yo2
-rw-r--r--Doc/zshcalsys.yo3
-rw-r--r--Functions/Calendar/.distfiles6
-rw-r--r--Functions/Calendar/age73
-rw-r--r--Functions/Calendar/calendar356
-rw-r--r--Functions/Calendar/calendar_add69
-rw-r--r--Functions/Calendar/calendar_lockfiles43
-rw-r--r--Functions/Calendar/calendar_read35
-rw-r--r--Functions/Calendar/calendar_scandate519
-rw-r--r--Functions/Calendar/calendar_show24
-rw-r--r--Functions/Calendar/calendar_sort67
-rw-r--r--Src/Modules/datetime.mdd1
23 files changed, 1787 insertions, 8 deletions
diff --git a/ChangeLog b/ChangeLog
index e11ead760..085bbfffc 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,19 @@
+2006-12-01  Peter Stephenson  <pws@csr.com>
+
+	* c.f. 23023: Completion/Unix/Type/_list_files, Doc/.distfiles,
+	Doc/Makefile.in, Doc/zsh.yo, Doc/zshcalsys.yo, Doc/Zsh/.distfiles,
+	Doc/Zsh/calsys.yo, Doc/Zsh/compsys.yo, Doc/Zsh/intro.yo,
+	Doc/Zsh/manual.yo, Doc/Zsh/modules.yo, Doc/Zsh/tcpsys.yo,
+	Functions/Calendar/.distfiles, Functions/Calendar/age,
+	Functions/Calendar/calendar, Functions/Calendar/calendar_add,
+	Functions/Calendar/calendar_lockfiles,
+	Functions/Calendar/calendar_read,
+	Functions/Calendar/calendar_scandate,
+	Functions/Calendar/calendar_show,
+	Functions/Calendar/calendar_sort, Src/Modules/datetime.mdd: new
+	calendar system with age glob qualifier function; files
+	_list_files to be able not to trample over external stat.
+
 2006-11-28  Peter Stephenson  <p.w.stephenson@ntlworld.com>
 
 	* 23022: Test/ztst.zsh: don't allow WORDCHARS to be exported
diff --git a/Completion/Unix/Type/_list_files b/Completion/Unix/Type/_list_files
index 5e745d9d1..3d5281669 100644
--- a/Completion/Unix/Type/_list_files
+++ b/Completion/Unix/Type/_list_files
@@ -48,6 +48,13 @@ done
 
 zmodload -i zsh/stat 2>/dev/null || return 1
 
+# Enable stat temporarily if disabled to avoid clashes.
+integer disable_stat
+if [[ ${builtins[stat]} != defined ]]; then
+  (( disable_stat = 1 ))
+  enable stat
+fi
+
 dir=${2:+$2/}
 dir=${(Q)dir}
 
@@ -66,4 +73,5 @@ done
 
 (( ${#listfiles} )) && listopts=(-d listfiles -l -o)
 
+(( disable_stat )) && disable stat
 return 0
diff --git a/Doc/.distfiles b/Doc/.distfiles
index 6a0c44e3d..6175aa8d6 100644
--- a/Doc/.distfiles
+++ b/Doc/.distfiles
@@ -2,7 +2,8 @@ DISTFILES_SRC='
     .cvsignore .distfiles Makefile.in
     META-FAQ.yo intro.ms
     version.yo zmacros.yo zman.yo ztexi.yo
-    zsh.yo zshbuiltins.yo zshcompctl.yo zshcompsys.yo zshcompwid.yo
+    zsh.yo zshbuiltins.yo zshcalsys.yo
+    zshcompctl.yo zshcompsys.yo zshcompwid.yo
     zshexpn.yo zshmisc.yo zshmodules.yo zshoptions.yo zshparam.yo
     zshroadmap.yo zshzftpsys.yo zshzle.yo zshcontrib.yo zshtcpsys.yo
     zsh.texi
diff --git a/Doc/Makefile.in b/Doc/Makefile.in
index fea6cb8b6..c87f79cef 100644
--- a/Doc/Makefile.in
+++ b/Doc/Makefile.in
@@ -45,7 +45,7 @@ TEXI2HTML = texi2html --output . --ifinfo --split=chapter
 .SUFFIXES: .yo .1
 
 # man pages to install
-MAN = zsh.1 zshbuiltins.1 zshcompctl.1 zshcompwid.1 zshcompsys.1 \
+MAN = zsh.1 zshbuiltins.1 zshcalsys.1 zshcompctl.1 zshcompwid.1 zshcompsys.1 \
 zshcontrib.1 zshexpn.1 zshmisc.1 zshmodules.1 \
 zshoptions.1 zshparam.1 zshroadmap.1 zshtcpsys.1 zshzftpsys.1 zshzle.1 \
 zshall.1
@@ -70,6 +70,7 @@ Zsh/mod_zprof.yo Zsh/mod_zpty.yo Zsh/mod_zselect.yo \
 Zsh/mod_zutil.yo
 
 YODLSRC = zmacros.yo zman.yo ztexi.yo Zsh/arith.yo Zsh/builtins.yo \
+Zsh/calsys.yo \
 Zsh/compat.yo Zsh/compctl.yo Zsh/compsys.yo Zsh/compwid.yo Zsh/cond.yo \
 Zsh/contrib.yo Zsh/exec.yo Zsh/expn.yo \
 Zsh/filelist.yo Zsh/files.yo Zsh/func.yo Zsh/grammar.yo Zsh/manual.yo \
@@ -175,6 +176,8 @@ zsh.1 zshall.1: Zsh/intro.yo Zsh/metafaq.yo Zsh/invoke.yo Zsh/files.yo \
 
 zshbuiltins.1: Zsh/builtins.yo
 
+zshcalsys.1: Zsh/calsys.yo
+
 zshcompctl.1: Zsh/compctl.yo
 
 zshcompwid.1: Zsh/compwid.yo
diff --git a/Doc/Zsh/.distfiles b/Doc/Zsh/.distfiles
index 5961f6201..2dd39b20b 100644
--- a/Doc/Zsh/.distfiles
+++ b/Doc/Zsh/.distfiles
@@ -1,6 +1,6 @@
 DISTFILES_SRC='
     .cvsignore .distfiles
-    arith.yo builtins.yo compat.yo compctl.yo compsys.yo compwid.yo
+    arith.yo builtins.yo calsys.yo compat.yo compctl.yo compsys.yo compwid.yo
     cond.yo exec.yo expn.yo filelist.yo files.yo func.yo grammar.yo
     index.yo intro.yo invoke.yo jobs.yo manual.yo metafaq.yo mod_cap.yo
     mod_clone.yo mod_compctl.yo mod_complete.yo mod_complist.yo
diff --git a/Doc/Zsh/calsys.yo b/Doc/Zsh/calsys.yo
new file mode 100644
index 000000000..61226d39d
--- /dev/null
+++ b/Doc/Zsh/calsys.yo
@@ -0,0 +1,547 @@
+texinode(Calendar Function System)(TCP Function System)(Zsh Modules)(Top)
+chapter(Calendar Function System)
+cindex(calendar function system)
+cindex(zsh/datetime, function system based on)
+sect(Description)
+
+The shell is supplied with a series of functions to replace and enhance the
+traditional Unix tt(calendar) programme, which warns the user of imminent
+or future events, details of which are stored in a text file (typically
+tt(calendar) in the user's home directory).  The version provided here
+includes a mechanism for alerting the user when an event is due.
+
+In addition a function tt(age) is provided that can be used in a glob
+qualifier; it allows files to be selected based on their modification
+times.
+
+The format of the tt(calendar) file and the dates used there in and in
+the tt(age) function are described first, then the functions that can
+be called to examine and modify the tt(calendar) file.
+
+The functions here depend on the availability of the tt(zsh/datetime)
+module which is usually installed with the shell.  The library function
+tt(strptime+LPAR()RPAR()) must be available; it is present on most recent
+operating systems.
+
+startmenu()
+menu(Calendar File and Date Formats)
+menu(Calendar System User Functions)
+menu(Calendar Styles)
+menu(Calendar Utility Functions)
+menu(Calendar Bugs)
+endmenu()
+
+
+texinode(Calendar File and Date Formats)(Calendar System User Functions)()(Calendar Function System)
+sect(File and Date Formats)
+
+subsect(Calendar File Format)
+
+The calendar file is by default tt(~/calendar).  This can be configured
+by the tt(calendar-file) style, see
+ifzman(the section STYLES below)\
+ifnzman(noderef(Calendar Styles)).  The basic format consists
+of a series of separate lines, with no indentation, each including
+a date and time specification followed by a description of the event.
+
+Various enhancements to this format are supported, based on the syntax
+of Emacs calendar mode.  An indented line indicates a continuation line
+that continues the description of the event from the preceeding line
+(note the date may not be continued in this way).  An initial ampersand
+(tt(&)) is ignored for compatibility.
+
+The Emacs extension that a date with no description may refer to a number
+of succeeding events at different times is not supported.
+
+Unless the tt(done-file) style has been altered, any events which
+have been processed are appended to the file with the same name as the
+calendar file with the suffix tt(.done), hence tt(~/calendar.done) by
+default.
+
+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.
+
+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.
+This will avoid unexpected effects.  Various key facts should be noted.
+
+startitemize()
+itemiz(In particular, note the confusion between
+var(month)tt(/)var(day)tt(/)var(year) and
+var(day)tt(/)var(month)tt(/)var(year) when the month is numeric; these
+formats should be avoided if at all possible.  Many alternatives are
+available.)
+itemiz(The year must be given in full to avoid confusion, and only years
+from 1900 to 2099 inclusive are matched.)
+enditemize()
+
+The following give some obvious examples; users finding here
+a format they like and not subject to vagaries of style may skip
+the full description.  As dates and times are matched separately
+(even though the time may be embedded in the date), any date format
+may be mixed with any format for the time of day provide the
+separators are clear (whitespace, colons, commas).
+
+example(2007/04/03 13:13
+2007/04/03:13:13
+2007/04/03 1:13 pm
+3rd April 2007, 13:13
+April 3rd 2007 1:13 p.m.
+Apr 3, 2007 13:13
+Tue Apr 03 13:13:00 2007
+13:13 2007/apr/3)
+
+More detailed rules follow.
+
+Times are parsed and extracted before dates.  They must use colons
+to separate hours and minutes, though a dot is allowed before seconds
+if they are present.  This limits time formats to the following:
+
+startitemize()
+itemiz(var(HH)tt(:)var(MM)[tt(:)var(SS)[tt(.)var(FFFFF)]] [tt(am)|tt(pm)|tt(a.m.)|tt(p.m.)])
+itemiz(var(HH)tt(:)var(MM)tt(.)var(SS)[tt(.)var(FFFFF)] [tt(am)|tt(pm)|tt(a.m.)|tt(p.m.)])
+enditemize()
+
+Here, square brackets indicate optional elements, possibly with
+alternatives.  Fractions of a second are recognised but ignored.  For
+absolute times (the normal format require by the tt(calendar) file and the
+tt(age) function) a date is mandatory but a time of day is not; the time
+returned is at the start of the date.  One variation is allowed: if
+tt(a.m.) or tt(p.m.) or one of their variants is present, an hour without a
+minute is allowed, e.g. tt(3 p.m.).
+
+Time zones are not handled, though if one is matched following a time
+specification it will be removed to allow a surrounding date to be
+parsed.  This only happens if the format of the timezone is not too
+unusual.  The following are examples of forms that are understood:
+
+example(+0100
+GMT
+GMT-7
+CET+1CDT)
+
+Any part of the timezone that is not numeric must have exactly three
+capital letters in the name.
+
+Dates suffer from the ambiguity between var(DD)tt(/)var(MM)tt(/)var(YYYY)
+and var(MM)tt(/)var(DD)tt(/)var(YYYY).  It is recommended this form is
+avoided with purely numeric dates, but use of ordinals,
+eg. tt(3rd/04/2007), will resolve the ambiguity as the ordinal is always
+parsed as the day of the month.  Years must be four digits (and the first
+two must be tt(19) or tt(20)); tt(03/04/08) is not recognised.  Other
+numbers may have leading zeroes, but they are not required.  The following
+are handled:
+
+startitemize()
+itemiz(var(YYYY)tt(/)var(MM)tt(/)var(DD))
+itemiz(var(YYYY)tt(-)var(MM)tt(-)var(DD))
+itemiz(var(YYYY)tt(/)var(MNM)tt(/)var(DD))
+itemiz(var(YYYY)tt(-)var(MNM)tt(-)var(DD))
+itemiz(var(DD)[tt(th)|tt(st)|tt(rd)] var(MNM)[tt(,)] [ var(YYYY) ])
+itemiz(var(MNM) var(DD)[tt(th)|tt(st)|tt(rd)][tt(,)] [ var(YYYY) ])
+itemiz(var(DD)[tt(th)|tt(st)|tt(rd)]tt(/)var(MM)[tt(,)] var(YYYY))
+itemiz(var(DD)[tt(th)|tt(st)|tt(rd)]tt(/)var(MM)tt(/)var(YYYY))
+itemiz(var(MM)tt(/)var(DD)[tt(th)|tt(st)|tt(rd)][tt(,)] var(YYYY))
+itemiz(var(MM)tt(/)var(DD)[tt(th)|tt(st)|tt(rd)]tt(/)var(YYYY))
+enditemize()
+
+Here, var(MNM) is at least the first three letters of a month name,
+matched case-insensitively.  The remainder of the month name may appear but
+its contents are irrelevant, so janissary, febrile, martial, apricot,
+maybe, junta, etc. are happily handled.
+
+Where the year is shown as optional, the current year is assumed.  There
+are only two such cases, the form tt(Jun 20) or tt(14 September) (the only
+two commonly occurring forms, apart from a "the" in some forms of English,
+which isn't currently supported).  Such dates will of course become
+ambiguous in the future, so should ideally be avoided.
+
+Times may follow dates with a colon, e.g. tt(1965/07/12:09:45); this is in
+order to provide a format with no whitespace.  A comma and whitespace are
+allowed, e.g. tt(1965/07/12, 09:45).  Currently the order of these
+separators is not checked, so illogical formats such as tt(1965/07/12, :
+,09:45) will also be matched.  For simplicity such variations are not shown
+in the list above.  Otherwise, a time is only recognised as being
+associated with a date if there is only whitespace in between, or if the
+time was embedded in the date.
+
+Days of the week are not scanned, but will be ignored if they occur
+at the start of the date pattern only.
+
+For example, the standard date format:
+
+example(Fri Aug 18 17:00:48 BST 2006)
+
+is handled by matching var(HH)tt(:)var(MM)tt(:)var(SS) and removing it
+together with the matched (but unused) time zone.  This leaves the following:
+
+example(Fri Aug 18 2006)
+
+tt(Fri) is ignored and the rest is matched according to the standard rules.
+
+subsect(Relative Time Format)
+
+In certain places relative times are handled.  Here, a date is not allowed;
+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.
+
+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.
+)
+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.
+)
+item(Weeks)(
+tt(weeks), tt(wks), tt(ws), tt(week), tt(wk), tt(w)
+)
+item(Days)(
+tt(days), tt(dys), tt(ds), tt(day), tt(dy), tt(d)
+)
+item(Minutes)(
+tt(minutes), tt(mins), tt(minute), tt(min), but em(not) tt(m),
+tt(ms), tt(mn) or tt(mns)
+)
+item(Seconds)(
+tt(seconds), tt(secs), tt(ss), tt(second), tt(sec), tt(s)
+)
+enditem()
+
+Spaces between the numbers are optional, but are required between items,
+although a comma may be used (with or without spaces).
+
+Here are some examples:
+
+example(30 years 3 months 4 days 3:42:41
+14 days 5 hours
+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.
+
+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)
+
+
+texinode(Calendar System User Functions)(Calendar Styles)(Calendar File and Date Formats)(Calendar Function System)
+sect(User Functions)
+
+This section describes functions that are designed to be called
+directly by the user.  The first part describes those functions
+associated with the user's calendar; the second part describes
+the use in glob qualifiers.
+
+subsect(Calendar system functions)
+
+startitem()
+findex(calendar)
+xitem(tt(calendar) [ tt(-dDsv) ] [ tt(-C) var(calfile) ] [ -n var(num) ] [ tt(-S) var(showprog) ] [ [ var(start) ] var(end) ])(
+item(tt(calendar -r) [ tt(-dDrsv) ] [ tt(-C) var(calfile) ] [ -n var(num) ] [ tt(-S) var(showprog) ] [ var(start) ])(
+Show events in the calendar.
+
+With no arguments, show events from the start of today until the end of
+the next working day after today.  In other words, if today is Friday,
+Saturday, or Sunday, show up to the end of the following Monday, otherwise
+show today and tomorrow.
+
+If var(end) is given, show events from the start of today up to the time
+and date given, which is in the format described in the previous section.
+Note that if this is a date the time is assumed to be midnight at the
+start of the date, so that effectively this shows all events before
+the given date.
+
+var(end) may start with a tt(+), in which case the remainder of the
+specification is a relative time format as described in the previous
+section indicating the range of time from the start time that is to
+be included.
+
+If var(start) is also given, show events starting from that time and date.
+The word tt(now) can be used to indicate the current time.
+
+To implement an alert when events are due, include tt(calendar -s) in your
+tt(~/.zshrc) file.
+
+Options:
+
+startitem()
+item(tt(-C) var(calfile))(
+Explicitly specify a calendar file instead of the value of
+the tt(calendar-file) style or the the default tt(~/calendar).
+)
+item(tt(-d))(
+Move any events that have passed from the calendar file to the
+"done" file, as given by the tt(done-file) style or the default
+which is the calendar file with tt(.done) appended.  This option
+is implied by the tt(-s) option.
+)
+item(tt(-D))(
+Turns off the option tt(-d), even if the tt(-s) option is also present.
+)
+item(tt(-n) var(num), tt(-)var(num))(
+Show at least var(num) events, if present in the calendar file, regardless
+of the tt(start) and tt(end).
+)
+item(tt(-r))(
+Show all the remaining options in the calendar, ignoring the given tt(end)
+time.  The tt(start) time is respected; any argument given is treated
+as a tt(start) time.
+)
+item(tt(-s))(
+Use the shell's tt(sched) command to schedule a timed event that
+will warn the user when an event is due.  Note that the tt(sched) command
+only runs if the shell is at an interactive prompt; a foreground taks
+blocks the scheduled task from running until it is finished.
+
+The timed event usually runs the programme tt(calendar_show) to show
+the event, as described in
+ifzman(the section UTILITY FUNCTIONS below)\
+ifnzman(noderef(Calendar Utility Functions)).
+
+By default, a warning of the event is shown five minutes before it is due.
+The warning period can be configured by the style tt(warn-time) or
+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.
+
+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
+is locked when in use.
+
+By default, expired events are moved to the "done" file; see the tt(-d)
+option.  Use tt(-D) to prevent this.
+)
+item(tt(-S) var(showprog))(
+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.
+)
+enditem()
+)
+findex(calendar_add)
+item(tt(calendar_add) 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).
+)
+findex(calendar_sort)
+item(tt(calendar_sort))(
+Sorts the calendar file into date and time order.    The old calendar is
+left in a file with the suffix tt(.old).
+)
+enditem()
+
+subsect(Glob qualifiers)
+findex(age)
+
+The function tt(age) can be autoloaded and use separately from
+the calendar system, although it uses the function tt(calendar_scandate)
+for date formatting.  It requires the tt(zsh/stat) builtin, which
+makes available the builtin tt(stat).  This may conflict with an
+external programme of the same name; if it does, the builtin may be
+disabled for normal operation by including the following code in
+an initialization file:
+
+example(zmodload -i zsh/stat
+disable stat)
+
+tt(age) selects files having a given modification time for use
+as a glob qualifer.  The format of the date is the same as that
+understood by the calendar system, described in
+ifzman(the section FILE AND DATE FORMATS above)\
+ifnzman(noderef(Calendar File and Date Formats)).
+
+The function can take one or two arguments, which can be supplied either
+directly as command or arguments, or separately as shell parameters.
+
+example(print *+LPAR()e:age 2006/10/04 2006/10/09:+RPAR())
+
+The example above matches all files modified between the start of those
+dates.
+
+example(print *+LPAR()e:age 2006/10/04+RPAR())
+
+The example above matches all files modified on that date.  If the second
+argument is omitted it is taken to be exactly 24 hours after the first
+argument (even if the first argument contains a time).
+
+example(print *+LPAR()e-age 2006/10/04:10:15 2006/10/04:10:45-RPAR())
+
+The example above supplies times.  Note that whitespace within the time and
+date specification must be quoted to ensure tt(age) receives the correct
+arguments, hence the use of the additional colon to separate the date and
+time.
+
+example(AGEREF1=2006/10/04:10:15
+AGEREF2=2006/10/04:10:45
+print *+LPAR()PLUS()age+RPAR())
+
+This shows the same example before using another form of argument
+passing.  The dates and times in the parameters tt(AGEREF1) and tt(AGEREF2)
+stay in effect until unset, but will be overridden if any argument is
+passed as an explicit argument to age.  Any explicit argument
+causes both parameters to be ignored.
+
+
+texinode(Calendar Styles)(Calendar Utility Functions)(Calendar System User Functions)(Calendar Function System)
+sect(Styles)
+
+The zsh style mechanism using the tt(zstyle) command is describe in
+ifzman(zmanref(zshmodules))\
+ifnzman(noderef(The zsh/zutil Module)).  This is the same mechanism
+used in the completion system.
+
+The styles below are all examined in the context
+tt(:datetime:)var(function)tt(:), for example tt(:datetime:calendar:).
+
+startitem()
+kindex(calendar-file)
+item(tt(calendar-file))(
+The location of the main calendar.  The default is tt(~/calendar).
+)
+kindex(done-file)
+item(tt(done-file))(
+The location of the file to which events which have passed are appended.
+The default is the calendar file location with the suffix tt(.done).
+The style may be set to an empty string in which case a "done" file
+will not be maintained.
+)
+kindex(show-prog)
+item(tt(show-prog))(
+The programme run by tt(calendar) for showing events.  It will
+be passed the start time and stop time of the events requested in seconds
+since the epoch followed by the event text.  Note that tt(calendar -s) uses
+a start time and stop time equal to one another to indicate alerts
+for specific events.
+
+The default is the function tt(calendar_show).
+)
+kindex(warn-time)
+item(tt(warn-time))(
+The time before an event at which a warning will be displayed, if the
+first line of the event does not include the text tt(EVENT) var(reltime).
+The default is 5 minutes.
+)
+enditem()
+
+
+texinode(Calendar Utility Functions)(Calendar Bugs)(Calendar Styles)(Calendar Function System)
+sect(Utility functions)
+
+startitem()
+findex(calendar_lockfiles)
+item(tt(calendar_lockfiles))(
+Attempt to lock the files given in the argument.  To prevent
+problems with network file locking this is done in an ad hoc fashion
+by attempting to create a symbolic link to the file with the name
+var(file)tt(.lockfile).  Otherwise, however, the function is not
+specific to the calendar system.  Three attempts are made to lock
+the file before giving up.
+
+The files locked are appended to the array tt(lockfiles), which should
+be local to the caller.
+
+If all files were successully, status zero is returned, else status one.
+)
+findex(calendar_read)
+item(tt(calendar_read))(
+This is a backend used by various other functions to parse the
+calendar file, which is passed as the only argument.  The array
+tt(calendar_entries) is set to the list of events in the file; no
+pruning is done except that ampersands are removed from the start of
+the line.  Each entry may contain multiple lines.
+)
+findex(calendar_scandate)
+item(tt(calendar_scandate))(
+This is a generic function to parse dates and times that may be
+used separately from the calendar system.  The argument is a date
+or time specification as described in
+ifzman(the section FILE AND DATE FORMATS above)\
+ifnzman(noderef(Calendar File and Date Formats).  The parameter tt(REPLY)
+is set to the number of seconds since the epoch corresponding to that date
+or time.  By default, the date and time may occur anywhere within the given
+argument.
+
+Returns status zero if the date and time were successfully parsed,
+else one.
+
+Options:
+startitem()
+item(tt(-a))(
+The date and time are anchored to the start of the argument; they
+will not be matched if there is preceeding text.
+)
+item(tt(-A))(
+The date and time are anchored to both the start and end of the argument;
+they will not be matched if the is any other text in the argument.
+)
+item(tt(-d))(
+Enable additional debugging output.
+)
+item(tt(-r))(
+The arguments passed is to be parsed as a relative time.
+)
+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
+empty if the option tt(-A) was given.
+)
+enditem()
+)
+findex(calendar_show)
+item(tt(calendar_show))(
+The function used by default to display events.  It accepts a start time
+and end time for events, both in epoch seconds, and an event description.
+
+The event is always printed to standard output.  If the command line editor
+is active (which will usually be the case) the command line will be
+redisplayed after the output.
+
+If the parameter tt(DISPLAY) is set and the start and end times are
+the same (indicating a scheduled event), the function uses the
+command tt(xmessage) to display a window with the event details.
+)
+enditem()
+
+texinode(Calendar Bugs)(Calendar Utility Functions)()(Calendar Function System)
+sect(Bugs)
+
+There is no tt(calendar_delete) function.
+
+There is no localization support for dates and times, nor any support
+for the use of time zones.
+
+Relative periods of months and years do not take into account the variable
+number of days.
+
+Recurrent events are not yet supported.
+
+The tt(calendar_show) function is currently hardwired to use tt(xmessage)
+for displaying alerts on X Window System displays.  This should be
+configurable and ideally integrate better with the desktop.
+
+tt(calendar_lockfiles) hangs the shell while waiting for a lock on a file.
+If called from a scheduled task, it should instead reschedule the event
+that caused it.
diff --git a/Doc/Zsh/compsys.yo b/Doc/Zsh/compsys.yo
index de8f18094..d8e43ea77 100644
--- a/Doc/Zsh/compsys.yo
+++ b/Doc/Zsh/compsys.yo
@@ -1361,10 +1361,14 @@ specified will always be completed.
 kindex(file-list, completion style)
 item(tt(file-list))(
 This style controls whether files completed using the standard builtin
-mechanism are to be listed with a long list similar to tt(ls -l)
-(although note that this feature actually uses the shell module
+mechanism are to be listed with a long list similar to tt(ls -l).
+Note that this feature uses the shell module
 tt(zsh/stat) for file information; this loads the builtin tt(stat)
-which will replace any external tt(stat) executable).
+which will replace any external tt(stat) executable.  To avoid
+this the following code can be included in an initialization file:
+
+example(zmodload -i zsh/stat
+disable stat)
 
 The style may either be set to a true value (or `tt(all)'), or
 one of the values `tt(insert)' or `tt(list)', indicating that files
diff --git a/Doc/Zsh/intro.yo b/Doc/Zsh/intro.yo
index c56dcaedd..510a7bda5 100644
--- a/Doc/Zsh/intro.yo
+++ b/Doc/Zsh/intro.yo
@@ -26,6 +26,7 @@ list(em(zshcompwid)   Zsh completion widgets)
 list(em(zshcompsys)   Zsh completion system)
 list(em(zshcompctl)   Zsh completion control)
 list(em(zshmodules)   Zsh loadable modules)
+list(em(zshcalsys)    Zsh built-in calendar functions)
 list(em(zshtcpsys)    Zsh built-in TCP functions)
 list(em(zshzftpsys)   Zsh built-in FTP client)
 list(em(zshcontrib)   Additional zsh functions and utilities)
diff --git a/Doc/Zsh/manual.yo b/Doc/Zsh/manual.yo
index 4bf9f6bcd..ff6ecae41 100644
--- a/Doc/Zsh/manual.yo
+++ b/Doc/Zsh/manual.yo
@@ -34,6 +34,7 @@ menu(Completion Widgets)
 menu(Completion System)
 menu(Completion Using compctl)
 menu(Zsh Modules)
+menu(Calendar Function System)
 menu(TCP Function System)
 menu(Zftp Function System)
 menu(User Contributions)
diff --git a/Doc/Zsh/modules.yo b/Doc/Zsh/modules.yo
index cbec478e3..3f9986096 100644
--- a/Doc/Zsh/modules.yo
+++ b/Doc/Zsh/modules.yo
@@ -1,4 +1,4 @@
-texinode(Zsh Modules)(TCP Function System)(Completion Using compctl)(Top)
+texinode(Zsh Modules)(Calendar Function System)(Completion Using compctl)(Top)
 chapter(Zsh Modules)
 cindex(modules)
 sect(Description)
diff --git a/Doc/Zsh/tcpsys.yo b/Doc/Zsh/tcpsys.yo
index f1586b602..c93736a37 100644
--- a/Doc/Zsh/tcpsys.yo
+++ b/Doc/Zsh/tcpsys.yo
@@ -1,4 +1,4 @@
-texinode(TCP Function System)(Zftp Function System)(Zsh Modules)(Top)
+texinode(TCP Function System)(Zftp Function System)(Calendar Function System)(Top)
 chapter(TCP Function System)
 cindex(TCP function system)
 cindex(ztcp, function system based on)
diff --git a/Doc/zsh.yo b/Doc/zsh.yo
index 0517c8748..0d815c2e0 100644
--- a/Doc/zsh.yo
+++ b/Doc/zsh.yo
@@ -64,6 +64,7 @@ ifnzman(includefile(Zsh/compwid.yo))
 ifnzman(includefile(Zsh/compsys.yo))
 ifnzman(includefile(Zsh/compctl.yo))
 ifnzman(includefile(Zsh/modules.yo))
+ifnzman(includefile(Zsh/calsys.yo))
 ifnzman(includefile(Zsh/tcpsys.yo))
 ifnzman(includefile(Zsh/zftpsys.yo))
 ifnzman(includefile(Zsh/contrib.yo))
@@ -81,6 +82,7 @@ source(zshcompwid)
 source(zshcompsys)
 source(zshcompctl)
 source(zshmodules)
+source(zshcalsys)
 source(zshtcpsys)
 source(zshzftpsys)
 source(zshcontrib)
diff --git a/Doc/zshcalsys.yo b/Doc/zshcalsys.yo
new file mode 100644
index 000000000..70c800db0
--- /dev/null
+++ b/Doc/zshcalsys.yo
@@ -0,0 +1,3 @@
+manpage(ZSHCALSYS)(1)(date())(zsh version())
+manpagename(zshcalsys)(zsh calendar system)
+includefile(Zsh/calsys.yo)
diff --git a/Functions/Calendar/.distfiles b/Functions/Calendar/.distfiles
new file mode 100644
index 000000000..5d96b5706
--- /dev/null
+++ b/Functions/Calendar/.distfiles
@@ -0,0 +1,6 @@
+DISTFILES_SRC='
+    .distfiles
+    age
+    calendar calendar_add calendar_lockfiles calendar_read
+    calendar_scandate calendar_show calendar_sort
+'
diff --git a/Functions/Calendar/age b/Functions/Calendar/age
new file mode 100644
index 000000000..4ed3bd8c2
--- /dev/null
+++ b/Functions/Calendar/age
@@ -0,0 +1,73 @@
+# Match the age of a file, for use as a glob qualifer.  Can
+# take one or two arguments, which can be supplied by one of two
+# ways (always the same for both arguments):
+#
+#   print *(e:age 2006/10/04 2006/10/09:)
+#
+# Match all files modified between the start of those dates.
+#
+#   print *(e:age 2006/10/04)
+#
+# Match all files modified on that date.  If the second argument is
+# omitted it is taken to be exactly 24 hours after the first argument
+# (even if the first argument contains a time).
+#
+#   print *(e:age 2006/10/04:10:15 2006/10/04:10:45)
+#
+# Supply times.  All the time and formats handled by calendar_scandate
+# are allowed, but whitespace must be quoted to ensure age receives
+# the correct arguments.
+#
+#   AGEREF1=2006/10/04:10:15
+#   AGEREF2=2006/10/04:10:45
+#   print *(+age)
+#
+# The same example using the other form of argument passing.  The
+# dates stay in effect until unset, but will be overridden if
+# any argument is passed in the first format.
+
+emulate -L zsh
+integer mystat disable_stat
+
+zmodload -i zsh/stat
+# Allow the builtin stat to be hidden.
+zmodload -i zsh/parameter
+if [[ $builtins[stat] != defined ]]; then
+  (( disable_stat = 1 ))
+  enable stat
+fi
+
+autoload -U calendar_scandate
+
+local -a vals
+
+[[ -e $REPLY ]] || return 1
+stat -A vals +mtime $REPLY || return 1
+
+if (( $# >= 1 )); then
+  local AGEREF=$1
+  # if 1 argument given, never use globally defined AGEREF2
+  local AGEREF2=$2
+fi
+
+integer mtime=$vals[1] date1 date2
+local REPLY
+
+if calendar_scandate $AGEREF; then
+  date1=$REPLY
+
+  if [[ -n $AGEREF2 ]] && calendar_scandate $AGEREF2; then
+    date2=$REPLY
+  else
+    (( date2 = date1 + 24 * 60 * 60 ))
+  fi
+
+  (( date1 <= mtime && mtime <= date2 ))
+else
+  mystat=1
+fi
+
+# If the builtin stat was previously disabled, disable it again.
+(( disable_stat )) && disable stat
+
+return $mystat
diff --git a/Functions/Calendar/calendar b/Functions/Calendar/calendar
new file mode 100644
index 000000000..124fd9786
--- /dev/null
+++ b/Functions/Calendar/calendar
@@ -0,0 +1,356 @@
+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
+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
+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}
+
+# Read the calendar file from the calendar-file style
+zstyle -s ':datetime:calendar:' calendar-file calendar || calendar=~/calendar
+newfile=$calendar.new.$HOST.$$
+zstyle -s ':datetime:calendar:' done-file donefile || donefile="$calendar.done"
+# Read the programme to show the message from the show-prog style.
+zstyle -a ':datetime:calendar:' show-prog showprog ||
+  showprog=(calendar_show)
+# 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"
+
+if [[ -n $warnstr ]]; then
+  if [[ $warnstr = <-> ]]; then
+    (( warntime = warnstr ))
+  elif ! calendar_scandate -ar $warnstr; then
+    print >&2 \
+      "warn-time value '$warnstr' not understood; using default 5 minutes"
+    warnstr="5 mins"
+    (( warntime = 5 * 60 ))
+  else
+    (( warntime = REPLY ))
+  fi
+fi
+
+[[ -f $calendar ]] || return 1
+
+# We're not using getopts because we want +... to refer to a
+# relative time, not an option, and allow some other additions
+# like handling -<->.
+integer opti=0
+local opt optrest optarg
+
+while [[ ${argv[opti+1]} = -* ]]; do
+  (( opti++ ))
+  opt=${argv[opti][2]}
+  optrest=${argv[opti][3,-1]}
+  [[ -z $opt || $opt = - ]] && break
+  while [[ -n $opt ]]; do
+    case $opt in
+      ########################
+      # Options with arguments
+      ########################
+      ([CnS])
+      if [[ -n $optrest ]]; then
+	optarg=$optrest
+	optrest=
+      elif (( opti < $# )); then
+	optarg=$argv[++opti]
+	optrest=
+      else
+	print -r "$0: option -$opt requires an argument." >&2
+	return 1
+      fi
+      case $opt in
+	(C)
+	# Pick the calendar file, overriding style and default.
+	calendar=$optarg
+	;;
+
+	(n)
+	# Show this many remaining events regardless of date.
+	showcount=$optarg
+	if (( showcount <= 0 )); then
+	  print -r "$0: option -$opt requires a positive integer." >&2
+	  return 1
+	fi
+	;;
+
+	(S)
+	# Explicitly specify a show programme, overriding style and default.
+	# Colons in the argument are turned into space.
+	showprog=(${(s.:.)optarg})
+	;;
+      esac
+      ;;
+
+      ###########################
+      # Options without arguments
+      ###########################
+      (d)
+      # Move out of date items to the done file.
+      (( done = 1 ))
+      ;;
+
+      (D)
+      # Don't use done; needed with sched
+      (( nodone = 1 ))
+      ;;
+
+      (r)
+      # Show all remaining options in the calendar, i.e.
+      # respect start time but ignore end time.
+      # Any argument is treated as a start time.
+      (( remaining = 1 ))
+      ;;
+
+      (s)
+      # Use the "sched" builtin to scan at the appropriate time.
+      sched=sched
+      (( done = 1 ))
+      ;;
+
+      (v)
+      # Verbose
+      verbose=1
+      ;;
+
+      (<->)
+      # Shorthand for -n <->
+      showcount=$opt
+      ;;
+
+      (*)
+      print "$0: unrecognised option: -$opt" >&2
+      return 1
+      ;;
+    esac
+    opt=$optrest[1]
+    optrest=$optrest[2,-1]
+  done
+done
+calopts=($argv[1,opti])
+shift $(( opti ))
+
+# Use of donefile requires explicit or implicit option request, plus
+# no explicit -D.  It may already be empty because of the style.
+(( done && !nodone )) || donefile=
+
+if (( $# > 1 || ($# == 1 && remaining) )); then
+  if [[ $1 = now ]]; then
+    start=$EPOCHSECONDS
+  elif [[ $1 = <-> ]]; then
+    start=$1
+  else
+    if ! calendar_scandate -a $1; then
+      print "$0: failed to parse date/time: $1" >&2
+      return 1
+    fi
+    start=$REPLY
+  fi
+  shift
+else
+  # Get the time at which today started.
+  y=${(%):-"%D{%Y}"} m=${(%):-"%D{%m}"} d=${(%):-"%D{%d}"}
+  strftime -s today -r "%Y/%m/%d" "$y/$m/$d"
+  start=$today
+fi
+# day of week of start time
+strftime -s wd "%u" $start
+
+if (( $# && !remaining )); then
+  if [[ $1 = +* ]]; then
+    if ! calendar_scandate -ar ${1[2,-1]}; then
+      print "$0: failed to parse relative time: $1" >&2
+      return 1
+    fi
+    (( stop = start + REPLY ))
+  elif [[ $1 = <-> ]]; then
+    stop=$1
+  else
+    if ! calendar_scandate -a $1; then
+      print "$0: failed to parse date/time: $1" >&2
+      return 1
+    fi
+    stop=$REPLY
+  fi
+  if (( stop < start )); then
+    strftime -s REPLY $ctime $start
+    strftime -s REPLY2 $ctime $stop
+    print "$0: requested end time is before start time:
+  start: $REPLY
+  end: $REPLY2" >&2
+    return 1
+  fi
+  shift
+else
+  # By default, show 2 days.  If it's Friday (5) show up to end
+  # of Monday (4) days; likewise on Saturday show 3 days.
+  # If -r, this is calculated but not used.  This is paranoia,
+  # to avoid an unusable value of stop; but it shouldn't get used.
+  case $wd in
+    (5)
+    ndays=4
+    ;;
+
+    (6)
+    ndays=3
+    ;;
+
+    (*)
+    ndays=2
+    ;;
+  esac
+  stop=$(( start + ndays * 24 * 60 * 60 ))
+fi
+
+if (( $# )); then
+  print "Usage: $0 [ start-date-time stop-date-time ]" >&2
+  return 1
+fi
+
+autoload -Uz matchdate
+
+[[ -n $donefile ]] && rm -f $newfile
+
+if (( verbose )); then
+  print -n "start: "
+  strftime $ctime $start
+  print -n "stop: "
+  if (( remaining )); then
+    print "none"
+  else
+    strftime $ctime $stop
+  fi
+fi
+
+# start of block for following always to clear up lockfiles.
+{
+  if [[ -n $donefile ]]; then
+    # Attempt to lock both $donefile and $calendar.
+    # Don't lock $newfile; we've tried our best to make
+    # the name unique.
+    calendar_lockfiles $calendar $donefile || return 1
+  fi
+
+  calendar_read $calendar
+  for line in $calendar_entries; do
+    # This call sets REPLY to the date and time in seconds since the epoch,
+    # REPLY2 to the line with the date and time removed.
+    calendar_scandate -as $line || continue
+    (( t = REPLY ))
+
+    # Look for specific warn time.
+    pruned=${REPLY2#(|*[[:space:],])WARN[[:space:]]}
+    (( mywarntime = warntime ))
+    mywarnstr=$warnstr
+    if [[ $pruned != $REPLY2 ]]; then
+      if calendar_scandate -ars $pruned; then
+	(( mywarntime = REPLY ))
+	mywarnstr=${pruned%%"$REPLY2"}
+      fi
+    fi
+
+    if (( verbose )); then
+      print "Examining: $line"
+      print -n "  Date/time: "
+      strftime $ctime $t
+      if [[ -n $sched ]]; then
+	print "  Warning $mywarntime seconds ($mywarnstr) before"
+      fi
+    fi
+    (( shown = 0 ))
+    if (( t >= start && (remaining || t <= stop || icount < showcount) ))
+    then
+      $showprog $start $stop "$line"
+      (( shown = 1, icount++ ))
+    elif [[ -n $sched ]]; then
+      (( tsched = t - mywarntime ))
+      if (( tsched >= start && tsched <= stop)); then
+	$showprog $start $stop "due in ${mywarnstr}: $line"
+      fi
+    fi
+    if [[ -n $sched ]]; then
+      if (( t - mywarntime > EPOCHSECONDS )); then
+	# schedule for a warning
+	(( tsched = t - mywarntime ))
+      else
+	# schedule for event itself
+	(( tsched = t ))
+      fi
+      if (( (tsched > EPOCHSECONDS || ! shown) &&
+	    (next < 0 || tsched < next) )); then
+	(( next = tsched ))
+      fi
+    fi
+    if [[ -n $donefile ]]; then
+      if (( t <= EPOCHSECONDS && shown )); then
+	# Done and dusted.
+	# TODO: handle repeated times from REPLY2.
+	if ! print -r $line >>$donefile; then
+	  if (( done != 3 )); then
+	    (( done = 3 ))
+	    print "Failed to append to $donefile" >&2
+	  fi
+	elif (( done != 3 )); then
+	  (( done = 2 ))
+	fi
+      else
+	# Still not over.
+	if ! print -r $line >>$newfile; then
+	  if (( done != 3 )); then
+	    (( done = 3 ))
+	    print "Failed to append to $newfile" >&2
+	  fi
+	elif (( done != 3 )); then
+	  (( done = 2 ))
+	fi
+      fi
+    fi
+  done
+
+  if [[ -n $sched ]]; then
+    if [[ $next -ge 0 ]]; then
+      # Remove any existing calendar scheduling.
+      # Luckily sched doesn't delete its schedule in a subshell.
+      sched | while read line; do
+	if [[ $line = (#b)[[:space:]]#(<->)[[:space:]]##*[[:space:]]'calendar -s'* ]]; then
+	  # End of pipeline run in current shell, so delete directly.
+	  sched -1 $match[1]
+	fi
+      done
+      $sched $next calendar "${calopts[@]}" $next $next
+    else
+      $showprog $start $stop \
+"No more calendar events: calendar not rescheduled.
+Run \"calendar -s\" again if you add to it."
+    fi
+  fi
+
+  if (( done == 2 )); then
+    if ! mv $calendar $calendar.old; then
+      print "Couldn't back up $calendar to $calendar.old.
+New calendar left in $newfile." >&2
+      (( rstat = 1 ))
+    elif ! mv $newfile $calendar; then
+      print "Failed to rename $newfile to $calendar.
+Old calendar left in $calendar.old." >&2
+      (( rstat = 1 ))
+    fi
+  elif [[ -n $donefile ]]; then
+    rm -f $newfile
+  fi
+} always {
+  (( ${#lockfiles} )) && rm -f $lockfiles
+}
+
+return $rstat
diff --git a/Functions/Calendar/calendar_add b/Functions/Calendar/calendar_add
new file mode 100644
index 000000000..2a00811fd
--- /dev/null
+++ b/Functions/Calendar/calendar_add
@@ -0,0 +1,69 @@
+#!/bin/env zsh
+
+# All arguments are joined with spaces and inserted into the calendar
+# file at the appropriate point.
+#
+# While the function compares the date of the new entry with dates in the
+# existing calendar file, it does not do any sorting; it inserts the new
+# entry before the first existing entry with a later date and time.
+
+emulate -L zsh
+setopt extendedglob
+
+local calendar newfile REPLY lastline
+local -a calendar_entries lockfiles
+integer newdate done rstat
+
+autoload -U calendar_{read,lockfiles}
+
+# Read the calendar file from the calendar-file style
+zstyle -s ':datetime:calendar_add:' calendar-file calendar ||
+  calendar=~/calendar
+newfile=$calendar.new.$HOST.$$
+
+if ! calendar_scandate -a "$*"; then
+  print "$0: failed to parse date/time" >&2
+  return 1
+fi
+(( newdate = $REPLY ))
+
+# $calendar doesn't necessarily exist yet.
+
+# start of block for following always to clear up lockfiles.
+{
+  calendar_lockfiles $calendar || return 1
+
+  if [[ -f $calendar ]]; then
+    calendar_read $calendar
+
+    {
+      for line in $calendar_entries; do
+	if (( ! done )) && calendar_scandate -a $line && (( REPLY > newdate )); then
+	  print -r -- "$*"
+	  (( done = 1 ))
+	elif [[ $REPLY -eq $newdate && $line = "$*" ]]; then
+	  (( done = 1 ))
+	fi
+	print -r -- $line
+	done
+	(( done )) || print -r -- "$*"
+    } >$newfile
+    if ! mv $calendar $calendar.old; then
+      print "Couldn't back up $calendar to $calendar.old.
+New calendar left in $newfile." >&2
+      (( rstat = 1 ))
+    fi
+  else
+    print -r -- $line >$newfile
+  fi
+
+  if (( !rstat )) && ! mv $newfile $calendar; then
+    print "Failed to rename $newfile to $calendar.
+Old calendar left in $calendar.old." >&2
+    (( rstat = 1 ))
+  fi
+} always {
+  (( ${#lockfiles} )) && rm -f $lockfiles
+}
+
+return $rstat
diff --git a/Functions/Calendar/calendar_lockfiles b/Functions/Calendar/calendar_lockfiles
new file mode 100644
index 000000000..58ee42114
--- /dev/null
+++ b/Functions/Calendar/calendar_lockfiles
@@ -0,0 +1,43 @@
+# Lock the given files.
+# Append the names of lockfiles to the array lockfiles.
+
+local file lockfile msgdone
+# Number of attempts to lock a file.  Probably not worth stylising.
+integer lockattempts=3
+
+# The lockfile name is not stylised: it has to be a fixed
+# derivative of the main fail.
+for file; do
+  lockfile=$file.lockfile
+  for (( i = 0; i < lockattempts; i++ )); do
+    if ln -s $file $lockfile >/dev/null 2>&1; then
+      lockfiles+=($lockfile)
+      break
+    fi
+    if zle && [[ -z $msgdone ]]; then
+      msgdone="${lockfile}: waiting to acquire lock"
+      zle -M $msgdone
+    fi
+    sleep 1
+  done
+  if [[ -n $msgdone ]]; then
+    zle -M ${msgdone//?/ }
+    msgdone=
+  fi
+  if [[ ${lockfiles[-1]} != $lockfile ]]; then
+    msgdone="Failed to lock $file; giving up after $lockattempts attempts.
+Another instance of calendar may be using it.
+Delete $lockfiles if you believe this to be an error."
+    if zle; then
+      zle -M $msgdone
+    else
+      print $msgdone >&2
+    fi
+    # The parent should take action to delete any lockfiles
+    # already locked.  Typically this won't be necessary, since
+    # we will always lock the main calendar file first.
+    return 1
+  fi
+done
+
+return 0
diff --git a/Functions/Calendar/calendar_read b/Functions/Calendar/calendar_read
new file mode 100644
index 000000000..ed163887f
--- /dev/null
+++ b/Functions/Calendar/calendar_read
@@ -0,0 +1,35 @@
+# Utility for "calendar" to read entries into the array calendar_entries.
+# This should be local to the caller.
+# The only argument is the file to read.  We expect options etc. to
+# be correct.
+#
+# This is based on Emacs calendar syntax, which has two implications:
+#  - Lines beginning with whitespace are continuation lines.
+#    Hence we have to read the entire file first to determine entries.
+#  - Lines beginning with "&" are inhibited from producing marks in
+#    Emacs calendar window.  This is irrelevant to us, so we
+#    we simply remove leading ampersands.  This is necessary since
+#    we expect the date to start at the beginning of the line.
+#
+# TODO: Emacs has some special handling for entries where the first line
+# has only the date and continuation lines indicate times.  Actually,
+# it doesn't parse the times as far as I can see, but if we want to
+# handle that format sensibly we would need to here.  It could
+# be tricky to get right.
+
+local calendar=$1 line
+local -a lines
+
+lines=(${(f)"$(<$calendar)"})
+
+calendar_entries=()
+# ignore blank lines
+for line in $lines; do
+  if [[ $line = [[:space:]]* ]]; then
+    if (( ${#calendar_entries} )); then
+      calendar_entries[-1]+=$'\n'$line
+    fi
+  else
+    calendar_entries+=(${line##\&})
+  fi
+done
diff --git a/Functions/Calendar/calendar_scandate b/Functions/Calendar/calendar_scandate
new file mode 100644
index 000000000..f0024b89a
--- /dev/null
+++ b/Functions/Calendar/calendar_scandate
@@ -0,0 +1,519 @@
+# Scan a line for various common date and time formats.
+# Set REPLY to the number of seconds since the epoch at which that
+# time occurs.  The time does not need to be matched; this will
+# produce midnight at the start of the date.
+#
+# Absolute times
+#
+# The rules below are fairly complicated, to allow any natural (and
+# some highly unnatural but nonetheless common) combination of
+# time and date used by English speakers.  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.  This
+# will avoid unexpected effects.  Various key facts should be noted,
+# explained in more detail below:
+#
+# - In particular, note the confusion between month/day/year and
+#   day/month/year when the month is numeric; this format should be
+#   avoided if at all possible.  Many alternatives are available.
+# - However, there is currently no localization support, so month
+#   names must be English (though only the first three letters are required).
+#   The same applies to days of the week if they occur (they are not useful).
+# - The year must be given in full to avoid confusion, and only years
+#   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.
+#
+# The following give some obvious examples; users finding here
+# a format they like and not subject to vagaries of style may skip
+# the full description.  As dates and times are matched separately
+# (even though the time may be embedded in the date), any date format
+# may be mixed with any format for the time of day provide the
+# separators are clear (whitespace, colons, commas).
+#   2007/04/03 13:13
+#   2007/04/03:13:13
+#   2007/04/03 1:13 pm
+#   3rd April 2007, 13:13
+#   April 3rd 2007 1:13 p.m.
+#   Apr 3, 2007 13:13
+#   Tue Apr 03 13:13:00 2007
+#   13:13 2007/apr/3
+#
+# Times are parsed and extracted before dates.  They must use colons
+# to separate hours and minutes, though a dot is allowed before seconds
+# if they are present.  This limits time formats to
+#   HH:MM[:SS[.FFFFF]] [am|pm|a.m.|p.m.]
+#   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
+# not; the time returned is at the start of the date.
+#
+# Time zones are not handled, though if one is matched following a time
+# specification it will be removed to allow a surrounding date to be
+# parsed.  This only happens if the format of the timezone is not too
+# wacky:
+#   +0100
+#   GMT
+#   GMT-7
+#   CET+1CDT
+# etc. are all understood, but any part of the timezone that is not numeric
+# must have exactly three capital letters in the name.
+#
+# Dates suffer from the ambiguity between DD/MM/YYYY and MM/DD/YYYY.  It is
+# recommended this form is avoided with purely numeric dates, but use of
+# ordinals, eg. 3rd/04/2007, will resolve the ambiguity as the ordinal is
+# always parsed as the day of the month.  Years must be four digits (and
+# the first two must be 19 or 20); 03/04/08 is not recognised.  Other
+# numbers may have leading zeroes, but they are not required.  The
+# following are handled:
+#   YYYY/MM/DD
+#   YYYY-MM-DD
+#   YYYY/MNM/DD
+#   YYYY-MNM-DD
+#   DD[th|st|rd] MNM[,] YYYY
+#   DD[th|st|rd] MNM[,]            current year assumed
+#   MNM DD[th|st|rd][,] YYYY
+#   MNM DD[th|st|rd][,]            current year assumed
+#   DD[th|st|rd]/MM[,] YYYY
+#   DD[th|st|rd]/MM/YYYY
+#   MM/DD[th|st|rd][,] YYYY
+#   MM/DD[th|st|rd]/YYYY
+# Here, MNM is at least the first three letters of a month name,
+# matched case-insensitively.  The remainder of the month name may appear but
+# its contents are irrelevant, so janissary, febrile, martial, apricot,
+# etc. are happily handled.
+#
+# Note there are only two cases that assume the current year, the
+# form "Jun 20" or "14 September" (the only two commonly occurring
+# forms, apart from a "the" in some forms of English, which isn't
+# currently supported).  Such dates will of course become ambiguous
+# in the future, so should ideally be avoided.
+#
+# Times may follow dates with a colon, e.g. 1965/07/12:09:45; this
+# is in order to provide a format with no whitespace.  A comma
+# and whitespace are allowed, e.g. "1965/07/12, 09:45".
+# Currently the order of these separators is not checked, so
+# illogical formats such as "1965/07/12, : ,09:45" will also
+# be matched.  Otherwise, a time is only recognised as being associated
+# with a date if there is only whitespace in between, or if the time
+# was embedded in the date.
+#
+# Days of the week are not scanned, but will be ignored if they occur
+# at the start of the date pattern only.
+#
+# For example, the standard date format:
+#   Fri Aug 18 17:00:48 BST 2006
+# is handled by matching HH:MM:SS and removing it together with the
+# matched (but unused) time zone.  This leaves the following:
+#   Fri Aug 18 2006
+# "Fri" is ignored and the rest is matched according to the sixth of
+# the standard rules.
+#
+# Relative times
+# ==============
+#
+# The option -r allows a relative time.  Years (or ys, yrs, or without s),
+# months (or mths, mons, mnths, months, or without s --- "m", "ms" and
+# "mns" are ambiguous and are not handled), weeks (or ws, wks, or without
+# s) and days (or ds, dys, days, or without s), hours (or hs, hrs, with or
+# without s), minutes (or mins, with or without s) and seconds (or ss,
+# secs, with or without s) are understood.  Spaces between the numbers
+# 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!)
+#
+# This allows forms like:
+#   30 years 3 months 4 days 3:42:41
+#   14 days 5 hours
+#   4d,10hr
+# In this case absolute dates are ignored.
+
+emulate -L zsh
+setopt extendedglob
+
+zmodload -i zsh/datetime || return 1
+
+# separator characters before time or between time and date
+# allow , - or : before the time: this allows spaceless but still
+# 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:]]"
+# start pattern for time when anchored
+local tspat_anchor="(${tschars}#)"
+# ... when not anchored
+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}#"
+# 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.
+# We need to be able to ignore the day here, although (for consistency
+# with the unanchored case) we don't remove it until later.
+# (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}#)"
+# Date start pattern when not anchored at the start.
+local dspat_noanchor="(|*${schars})"
+# end pattern for relative times: similar remark about use of $schars.
+local repat="(|s)(|${schars}*)"
+# not locale-dependent!  I don't know how to get the months out
+# 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 year month day hour minute second
+local opt line orig_line mname MATCH MBEGIN MEND tz
+local -a match mbegin mend
+# Flags that we found a date or a time (maybe a relative time)
+integer date_found time_found
+# Indices of positions of start and end of time and dates found.
+# 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
+
+while getopts "aAdrs" opt; do
+  case $opt in
+    (a)
+    # anchor
+    (( anchor = 1 ))
+    ;;
+
+    (A)
+    # anchor at end, too
+    (( anchor = 1, anchor_end = 1 ))
+    ;;
+
+    (d)
+    # enable debug output
+    (( debug = 1 ))
+    ;;
+
+    (r)
+    (( relative = 1 ))
+    ;;
+
+    (s)
+    (( setvar = 1 ))
+    ;;
+
+    (*)
+    return 1
+    ;;
+  esac
+done
+shift $(( OPTIND - 1 ))
+
+line=$1 orig_line=$1
+
+local dspat tspat
+if (( anchor )); then
+  # Anchored at the start.
+  dspat=$dspat_anchor
+  if (( relative )); then
+    tspat=$tspat_anchor
+  else
+    # We'll test later if the time is associated with the date.
+    tspat=$tspat_noanchor
+  fi
+else
+  dspat=$dspat_noanchor
+  tspat=$tspat_noanchor
+fi
+
+# Look for a time separately; we need colons for this.
+case $line in
+  # with seconds, am/pm: don't match / in front.
+  ((#ibm)${~tspat}(<0-12>):(<0-59>)[.:]((<0-59>)(.<->|))[[:space:]]#([ap])(|.)[[:space:]]#m(.|[[:space:]]|(#e))(*))
+  hour=$match[2]
+  minute=$match[3]
+  second=$match[5]
+  [[ $match[7] = (#i)p ]] && (( hour <= 12 )) && (( hour += 12 ))
+  time_found=1
+  ;;
+
+  # no seconds, am/pm
+  ((#ibm)${~tspat}(<0-12>):(<0-59>)[[:space:]]#([ap])(|.)[[:space:]]#m(.|[[:space:]]|(#e))(*))
+  hour=$match[2]
+  minute=$match[3]
+  [[ $match[4] = (#i)p ]] && (( hour <= 12 )) && (( hour += 12 ))
+  time_found=1
+  ;;
+
+  # no colon, even, but a.m./p.m. indicator
+  ((#ibm)${~tspat}(<0-12>)[[:space:]]#([ap])(|.)[[:space:]]#m(.|[[:space:]]|(#e))(*))
+  hour=$match[2]
+  minute=0
+  [[ $match[3] = (#i)p ]] && (( hour <= 12 )) && (( hour += 12 ))
+  time_found=1
+  ;;
+
+  # 24 hour clock, with seconds
+  ((#ibm)${~tspat}(<0-24>):(<0-59>)[.:]((<0-59>)(.<->|))(*))
+  hour=$match[2]
+  minute=$match[3]
+  second=$match[5]
+  time_found=1
+  ;;
+
+  # 24 hour clock, no seconds
+  ((#ibm)${~tspat}(<0-24>):(<0-59>)(*))
+  hour=$match[2]
+  minute=$match[3]
+  time_found=1
+  ;;
+esac
+
+(( hour == 24 )) && hour=0
+
+if (( time_found )); then
+  # time was found
+  time_start=$mbegin[2]
+  time_end=$mend[-2]
+  # Remove the timespec because it may be in the middle of
+  # the date (as in the output of "date".
+  # There may be a time zone, too, which we don't yet handle.
+  # (It's not in POSIX strptime() and libraries don't support it well.)
+  # This attempts to remove some of the weirder forms.
+  if [[ $line[$time_end+1,-1] = (#b)[[:space:]]#([A-Z][A-Z][A-Z]|[-+][0-9][0-9][0-9][0-9])([[:space:]]|(#e))* || \
+        $line[$time_end+1,-1] = (#b)[[:space:]]#([A-Z][A-Z][A-Z](|[-+])<0-12>)([[:space:]]|(#e))*  || \
+        $line[$time_end+1,-1] = (#b)[[:space:]]#([A-Z][A-Z][A-Z](|[-+])<0-12>[A-Z][A-Z][A-Z])([[:space:]]|(#e))* ]]; then
+     (( time_end += ${mend[-1]} ))
+     tz=$match[1]
+  fi
+  line=$line[1,time_start-1]$line[time_end+1,-1]
+  (( debug )) && print "line after time: $line"
+fi
+
+if (( relative == 0 )); then
+  # Date.
+  case $line in
+  # Look for YEAR[-/.]MONTH[-/.]DAY
+  ((#bi)${~dspat}((19|20)[0-9][0-9])[-/](<1-12>)[-/](<1-31>)*)
+  year=$match[2]
+  month=$match[4]
+  day=$match[5]
+  date_start=$mbegin[2] date_end=$mend[5]
+  date_found=1
+  ;;
+
+  # Same with month name
+  ((#bi)${~dspat}((19|20)[0-9][0-9])[-/]${~monthpat}[-/](<1-31>)*)
+  year=$match[2]
+  mname=$match[4]
+  day=$match[5]
+  date_start=$mbegin[2] date_end=$mend[5]
+  date_found=1
+  ;;
+
+  # Look for DAY[th/st/rd] MNAME[,] YEAR
+  ((#bi)${~dspat}(<1-31>)(|th|st|rd)[[:space:]]##${~monthpat}(|,)[[:space:]]##((19|20)[0-9][0-9])*)
+  day=$match[2]
+  mname=$match[4]
+  year=$match[6]
+  date_start=$mbegin[2] date_end=$mend[6]
+  date_found=1
+  ;;
+
+  # Look for MNAME DAY[th/st/rd][,] YEAR
+  ((#bi)${~dspat}${~monthpat}[[:space:]]##(<1-31>)(|th|st|rd)(|,)[[:space:]]##((19|20)[0-9][0-9])*)
+  mname=$match[2]
+  day=$match[3]
+  year=$match[6]
+  date_start=$mbegin[2] date_end=$mend[6]
+  date_found=1
+  ;;
+
+  # Look for DAY[th/st/rd] MNAME; assume current year
+  ((#bi)${~dspat}(<1-31>)(|th|st|rd)[[:space:]]##${~monthpat}(|,)([[:space:]]##*|))
+  day=$match[2]
+  mname=$match[4]
+  strftime -s year "%Y" $EPOCHSECONDS
+  date_start=$mbegin[2] date_end=$mend[5]
+  date_found=1
+  ;;
+
+  # Look for MNAME DAY[th/st/rd]; assume current year
+  ((#bi)${~dspat}${~monthpat}[[:space:]]##(<1-31>)(|th|st|rd)(|,)([[:space:]]##*|))
+  mname=$match[2]
+  day=$match[3]
+  strftime -s year "%Y" $EPOCHSECONDS
+  date_start=$mbegin[2] date_end=$mend[5]
+  date_found=1
+  ;;
+
+  # Now it gets a bit ambiguous.
+  # Look for DAY[th/st/rd][/]MONTH[/ ,]YEAR
+  ((#bi)${~dspat}(<1-31>)(|th|st|rd)/(<1-12>)((|,)[[:space:]]##|/)((19|20)[0-9][0-9])*)
+  day=$match[2]
+  month=$match[4]
+  year=$match[7]
+  date_start=$mbegin[2] date_end=$mend[7]
+  date_found=1
+  ;;
+
+  # Look for MONTH[/]DAY[th/st/rd][/ ,]YEAR
+  ((#bi)${~dspat}(<1-12>)/(<1-31>)(|th|st|rd)((|,)[[:space:]]##|/)((19|20)[0-9][0-9])*)
+  month=$match[2]
+  day=$match[3]
+  year=$match[7]
+  date_start=$mbegin[2] date_end=$mend[7]
+  date_found=1
+  ;;
+  esac
+fi
+
+if (( date_found )); then
+  # date found
+  # see if there's a day at the start
+  if [[ ${line[1,$date_start-1]} = (#bi)${~daypat} ]]; then
+    date_start=$mbegin[1]
+  fi
+  line=${line[1,$date_start-1]}${line[$date_end+1,-1]}
+  if (( time_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
+    # string to find the date, however, it's complicated to
+    # know where both were found.  Reconstruct the date indices of
+    # the original string.
+    if (( time_start <= date_start )); then
+      # Time came before start of date; add length in.
+      (( date_start += time_end - time_start + 1 ))
+    fi
+    if (( time_start <= date_end )); then
+      (( date_end += time_end - time_start + 1 ))
+    fi
+
+    if (( time_end + 1 < date_start )); then
+      # If time wholly before date, OK if only separator characters
+      # in between.  (This allows some illogical stuff with commas
+      # but that's probably not important.)
+      if [[ ${orig_line[time_end+1,date_start-1]} != ${~schars}# ]]; then
+	# Clearly this can't work if anchor is set.  In principle,
+	# we could match the date and ignore the time if it wasn't.
+	# However, that seems dodgy.
+	return 1
+      else
+	# Form massaged line by removing the entire date/time chunk.
+	line="${orig_line[1,time_start-1]}${orig_line[date_end+1,-1]}"
+      fi
+    elif (( date_end + 1 < time_start )); then
+      # If date wholly before time, OK if only time separator characters
+      # in between.  This allows 2006/10/12:13:43 etc.
+      if [[ ${orig_line[date_end+1,time_start-1]} != ${~tschars}# ]]; then
+	# Here, we assume the time is associated with something later
+	# in the line.  This is pretty much inevitable for the sort
+	# of use we are expecting.  For example,
+	#   2006/10/24  Meeting from early, may go on till 12:00.
+	# or with some uses of the calendar system,
+	#   2006/10/24 MR 1 Another pointless meeting WARN 01:00
+	# The 01:00 says warn an hour before, not that the meeting starts
+	# at 1 am.  About the only safe way round would be to force
+	# a time to be present, but that's not how the traditional
+	# calendar programme works.
+	#
+	# Hence we need to reconstruct.
+	(( time_found = 0, hour = 0, minute = 0, second = 0 ))
+	line="${orig_line[1,date_start-1]}${orig_line[date_end+1,-1]}"
+      else
+	# As above.
+	line="${orig_line[1,date_start-1]}${orig_line[time_end+1,-1]}"
+      fi
+    fi
+    if (( debug )); then
+      print "Time string: $time_start,$time_end:" \
+	"'$orig_line[time_start,time_end]'"
+      print "Date string: $date_start,$date_end:" \
+	"'$orig_line[date_start,date_end]'"
+      print "Remaining line: '$line'"
+    fi
+  fi
+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 ))
+    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]} ))
+     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]} ))
+     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]} ))
+     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]} ))
+     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]} ))
+     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]} ))
+     line=${line[1,$mbegin[2]-1]}${line[$mend[4]+1,-1]}
+     time_found=1
+  fi
+fi
+
+if (( relative )); then
+  # If no date was found, we're in trouble unless we found a time.
+  if (( time_found )); then
+    if (( anchor_end )); then
+      # must be left with only separator characters
+      if [[ $line != ${~schars}# ]]; then
+	return 1
+      fi
+    fi
+    (( REPLY = reladd + (hour * 60 + minute) * 60 + second ))
+    [[ -n $setvar ]] && REPLY2=$line
+    return 0
+  fi
+  return 1
+elif (( ! date_found )); then
+  return 1
+fi
+
+if (( anchor_end )); then
+  # must be left with only separator characters
+  if [[ $line != ${~schars}# ]]; then
+    return 1
+  fi
+fi
+
+local fmt nums
+if [[ -n $mname ]]; then
+  fmt="%Y %b %d %H %M %S"
+  nums="$year $mname $day $hour $minute $second"
+else
+  fmt="%Y %m %d %H %M %S"
+  nums="$year $month $day $hour $minute $second"
+fi
+
+strftime -s REPLY -r $fmt $nums
+
+[[ -n $setvar ]] && REPLY2=$line
+
+return 0
diff --git a/Functions/Calendar/calendar_show b/Functions/Calendar/calendar_show
new file mode 100644
index 000000000..f731d07a5
--- /dev/null
+++ b/Functions/Calendar/calendar_show
@@ -0,0 +1,24 @@
+integer start=$1 stop=$2
+shift 2
+
+[[ -o zle ]] && zle -I
+print -r "$*"
+
+local -a cmd
+zmodload -i zsh/parameter || return
+
+# Use xmessage to display the message if the start and stop time
+# are the same, indicating we have been scheduled to display it.
+# Don't do this if there's already an xmessage for the same user.
+# HERE: this should be configurable and we should be able to do
+# better if xmessage isn't available, e.g. wish.
+if [[ -n $DISPLAY &&  $start -eq $stop ]]; then
+  if [[ -n ${commands[xmessage]} ]]; then
+    cmd=(xmessage -center)
+  fi
+  if [[ -n $cmd[0] ]] &&
+    ! ps -u$UID | grep $cmd[0] >/dev/null 2>&1; then
+    # turn off job control for this
+    ($cmd "$*" &)
+  fi
+fi
diff --git a/Functions/Calendar/calendar_sort b/Functions/Calendar/calendar_sort
new file mode 100644
index 000000000..7d346efc1
--- /dev/null
+++ b/Functions/Calendar/calendar_sort
@@ -0,0 +1,67 @@
+emulate -L zsh
+setopt extendedglob
+
+autoload -U calendar_{read,scandate,lockfiles}
+
+local calendar line REPLY new lockfile
+local -a calendar_entries
+local -a times lines_sorted lines_unsorted lines_failed lockfiles
+integer i
+
+# Read the calendar file from the calendar-file style
+zstyle -s ':datetime:calendar:' calendar-file calendar || calendar=~/calendar
+
+# Start block for "always" to handle lockfile
+{
+  calendar_lockfiles $calendar || return 1
+
+  new=$calendar.new.$$
+  calendar_read $calendar
+  if [[ ${#calendar_entries} -eq 0 || \
+    ( ${#calendar_entries} -eq 1 && -z $calendar_entries[1] ) ]]; then
+    return 0
+  fi
+
+  for line in $calendar_entries; do
+    if calendar_scandate -a $line; then
+      lines_unsorted+=("${(l.16..0.)REPLY}:$line")
+    else
+      lines_failed+=($line)
+    fi
+  done
+
+  if (( ${#lines_unsorted} )); then
+    lines_sorted=(${${(o)lines_unsorted}##[0-9]##:})
+  fi
+
+  {
+    for line in "${lines_failed[@]}"; do
+      print "$line # BAD DATE"
+    done
+    (( ${#lines_sorted} )) && print -l "${lines_sorted[@]}"
+  } > $new
+
+  if [[ ! -s $new ]]; then
+    print "Writing to $new failed."
+    return 1
+  elif (( ${#lines_failed} )); then
+    print "Warning: lines with date that couldn't be parsed.
+Output (with unparseable dates marked) left in $new"
+    return 1
+  fi
+
+  if ! mv $calendar $calendar.old; then
+    print "Couldn't back-up $calendar to $calendar.old.
+New calendar left in $new"
+    return 1
+  fi
+  if ! mv $new $calendar; then
+    print "Failed to rename $new to $calendar.
+Old calendar left in $calendar.old"
+    return 1
+  fi
+
+  print "Old calendar left in $calendar.old"
+} always {
+  (( ${#lockfiles} )) && rm -rf $lockfiles
+}
diff --git a/Src/Modules/datetime.mdd b/Src/Modules/datetime.mdd
index fbe7e0356..853b3bc79 100644
--- a/Src/Modules/datetime.mdd
+++ b/Src/Modules/datetime.mdd
@@ -3,6 +3,7 @@ name=zsh/datetime
 link=either
 load=no
 
+functions='Functions/Calendar/*'
 autobins="strftime"
 
 objects="datetime.o"