Pristine
authorMahmud <bigthingist@gmail.com>
Wed, 18 Nov 2009 09:34:03 +0000 (20:34 +1100)
committerMahmud <bigthingist@gmail.com>
Wed, 18 Nov 2009 09:34:03 +0000 (20:34 +1100)
README [new file with mode: 0644]
README-time-and-zone.text [new file with mode: 0644]
time-test.lisp [new file with mode: 0644]
time.lisp [new file with mode: 0644]

diff --git a/README b/README
new file mode 100644 (file)
index 0000000..70bc071
--- /dev/null
+++ b/README
@@ -0,0 +1,29 @@
+From: =?ISO-8859-1?Q?Far=E9?= <fahree@[domain-name-of-msg-id-below].com>
+Date: Mon, 26 Oct 2009 16:46:31 -0400
+Message-ID: <653bea160910261346m43ca20e7tc93b4b9d1d491d34@mail.gmail.com>
+Subject: itasoftware.com/quux/time
+To: bigthingist@[domain-name-of-msg-id-above].com
+Content-Type: multipart/mixed; boundary=0016e6d96d204135b60476dca966
+
+--0016e6d96d204135b60476dca966
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: quoted-printable
+
+Good opportunity to publish some free software.
+
+Please make the initial commit with unmodified files, so we have an
+easier time tracking changes you make.
+
+Copyright and license are MIT style. You can override any proprietary
+notice in a second commit, and copy the license from XCVB.
+
+The README may or may not be useful. I let you judge how useful it is or is=
+n't.
+
+If you feel this software is no good for your purposes, just don't
+bother publishing it.
+
+Thanks!
+
+[ Fran=E7ois-Ren=E9 =D0VB Rideau | Reflection&Cybernethics | http://fare.tu=
+nes.org ]
diff --git a/README-time-and-zone.text b/README-time-and-zone.text
new file mode 100644 (file)
index 0000000..4898309
--- /dev/null
@@ -0,0 +1,140 @@
+
+QReS Timezone Policy
+--------------------
+
+"Time", and its representation in software, has a lot of tricky subtleties.
+If you write any QReS code which involves temporal quantities (and chances
+are good that you will --- it's a reservation system), please adhere to the
+following guidelines.  
+
+Try to use the utilities in "quux/time.lisp" as much as possible.  Avoid
+writing time-manipulation code from scratch, and even avoid adding new
+functionality to time.lisp without review.  (There are things which have
+been purposefully omitted because, although they would seem like handy
+functions, they actually are not.)
+
+  -matto m.
+
+
+Some Guidelines:  UTC or Local time?  A tricky question.
+--------------------------------------------------------
+
+Rule #0A:  Policy or not, all time-related variables (or DB columns) must
+           have names which explicitly declare whether the quantity is
+           'local' or 'UTC', or a 'zoned-time' object.
+
+           How?  Simple; use the suffixes "-local", "-utc" and "-zul":
+
+                                Lisp          SQL
+             o local:        foobar-local   foobar_local
+             o UTC:          foobar-utc     foobar_utc
+             o zoned:        foobar-zul     foobar_zul
+
+
+Rule #0B:  All time-related variables must also have names which explicitly
+           declare whether the quantity is used as just a 'date', just a
+           'tofd' (time-of-day), or a complete 'time' (i.e. date+tofd).
+
+
+Rule #1:  If both the UTC and local-time perspectives are ever relevant for
+          some quantity, then use a 'zoned-time' object, so that the local
+          TZ-offset is stored along with the time.  
+
+          This gives the system a chance of catching and dealing with
+          changes/errors in TZ, and avoids the need to recalculate the
+          TZ every time the quantity is used.
+
+
+Rule #2:  If the UTC perspective is ever relevant, then store UTC in
+          the database.  In other words, 'zoned-time' should be stored
+          as a UTC time and a local time.  (An alternate combination
+          is UTC and TZ-offset, but often it is more useful to be able
+          to perform a SQL query on the local time than on the tz-offset.)
+          This allows DB operators to make intelligent calculations
+          of physical order and duration.
+
+
+Rule #3:  NEVER use lisp's 'decode-universal-time' or 'encode-universal-time'.
+          (Use quux's 'decode-integer-time', 'encode-integer-time', and their
+           relatives instead.)
+
+
+Rule #4:  ALWAYS supply a timezone of "0" to 'decode-universal-time' 
+          and to 'encode-universal-time'.  (The local TZ of the machine
+          on which the server is running is *never* useful.)
+
+
+Rule #5:  Temporal quantities are opaque.
+          Use the symbolic constants such as "$1hour" or "$24hours" to 
+           generate temporal quantities.
+          Use the functions exported from quux/time.lisp to manipulate,
+           parse, write, compare, etc. times.
+
+
+Rule #6:  Temporal quantities which have physical meaning (e.g. which
+           label the moments at which events occur and/or which will
+           be compared to one another to sort those events) must use
+           UTC.
+
+
+
+A Taxonomy of Time
+------------------
+
+o Time vs Date vs Time-of-Day
+
+     "Time" refers to a fully-specified 'moment'.  A "Time" quantity
+     can be broken into two major components, "Date" and "Time of Day".
+     
+     "Time of Day" is the duration elapsed since the last midnight,
+     typically broken down further into hours, minutes, and seconds.
+
+     "Date" is a kind of a label for the most recent midnight, broken
+     down further into year, month, and day.
+
+
+o Local vs UTC
+
+     A UTC 'time' is a fully-specified moment in the spacetime fabric
+      of the universe (i.e. date + tofd).  It is a real, physical quantity.
+
+     A local 'time' is more or less just a facsimile representation of a
+      bunch of fields (month, day, hour...).  It has dubious physical
+      meaning beyond that.  It is not necessarily meaningful to compare
+      local times from different locales; nor is it always meaningful
+      to compare local times from the *same* locale!
+
+     Local 'tofd' and local 'date' are useful concepts, as they really
+      name subsets of the full set of time components (year, month, day...).
+     UTC 'tofd' and UTC 'date' are not generally useful by themselves.
+
+
+o Date (and Time) as an Interval
+
+     Although temporal quantities label moments, they typically *mean*
+     intervals.  Temporal intervals are expressed as half-open intervals
+     [m1, m2) between moment m1 and m2.
+
+     A 'time' based in seconds is the interval [s, s+1) from a particular
+     moment s to the next moment, one second later.  Since all seconds
+     are the same duration, 'time' quantities are well-formed in both
+     UTC and local times.  The meanings of 'time' as a moment or an
+     interval are compatible (i.e. specifying any moment s uniquely
+     identifies the s+1 which defines the interval).
+
+     A 'date' based in days is the interval [d, d+1) from a particular
+     midnight moment d to the next, one "day" later.  However, days
+     are not all the same duration; the duration of a day is affected
+     by timezone changes (e.g. daylight saving time).  For this reason:
+
+         o A 'day' is only meaningful when treated as an interval,
+            i.e. both start and end times are required.
+         o A 'day' is an inherently local quantity --- its duration
+            is determined by the timezone rules in effect for a 
+            particular location.
+         o 'UTC date' is meaningless, since timezone transformations
+            cannot guarantee translation of a "UTC day" interval into
+            "local day" interval.
+
diff --git a/time-test.lisp b/time-test.lisp
new file mode 100644 (file)
index 0000000..1d74760
--- /dev/null
@@ -0,0 +1,430 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;                                                                  ;;;
+;;; Confidential and proprietary information of ITA Software, Inc.   ;;;
+;;;                                                                  ;;;
+;;; Copyright (c) 2006 ITA Software, Inc.  All rights reserved.      ;;;
+;;;                                                                 ;;;
+;;; Original author: Adam Worrall                                    ;;;
+;;;                                                                 ;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+;; This module provides xUnit tests for rigged & cached clocks
+
+(in-package :quux-test)
+
+(defun showtime (time &optional (message ""))
+  (format t "~A: ~A (" message time)
+  (cl-user::loc%YYYY-MM-DD-HH-MM-SS *standard-output* time nil nil)
+  (format t ")~%"))
+
+(defun wait-until-midsecond ()
+  "Busyloop until the fractional seconds of the system clock are inside (0.4, 0.6)."
+  (loop while t do
+        (let* ((rt-now  (get-internal-real-time))             ;; e.g. 4431428
+               (rt-base-sec (* internal-time-units-per-second ;; e.g. 4431000
+                               (truncate (/ rt-now internal-time-units-per-second))))
+               (fractional-second (/ (- rt-now rt-base-sec)   ;; e.g      428/1000
+                                     internal-time-units-per-second))
+               )
+          (process-sleep 0.02)
+          (when (and (> fractional-second 0.4)
+                     (< fractional-second 0.6))
+            (return)))))
+
+(define-test-function time-flows-normally (seconds message)
+  "Check that if we snooze for SECONDS seconds, then current-time-utc increases
+by SECONDS seconds."
+  ;; Consider what the real time T could be as this executes ...
+  ;; T =  8.99s
+  ;;              t-start := current_time_utc     (e.g. := 8)
+  ;; T =  9.01s
+  ;;              process-sleep 1
+  ;; T = 10.01s
+  ;;              t-end   := current_time_utc     (e.g. := 10)
+  ;;
+  ;; So, it's possible that the time diff will be 2, and not 1. Bah!
+  ;; (The diff could even be 0, if process-sleep is under a second, and T=9.01 when t_start is
+  ;;  measured ...)
+  ;;
+  ;; Bodge fix: use the real, underlying timer to busy-loop until we're approximately
+  ;;  halfway between seconds. Then we can be pretty sure that the two measurements will
+  ;;  occur in separate seconds.
+  ;; To avoid those embarrassing test failure messages, make the test <=
+  (wait-until-midsecond)
+  (let ((t-start (current-time-utc)))
+    (process-sleep seconds)
+    (let ((t-end (current-time-utc)))
+      (assert-equal seconds (- t-end t-start) :message message
+                                              :test #'<=))))
+
+(defmacro define-time-test (test args &body body)
+  `(define-test ,test ,args
+    (without-current-time-trickery ,@body)))
+
+(define-time-test test-normal-time ()
+  "Checks the normal behavior: no caching, no skewing of time."
+  (let ((seconds 1))
+    (call-test-function time-flows-normally seconds "normal-ticks")))
+
+(defparameter $dur-snooze 1 "Process-Sleep for one wallclock second")
+
+(define-time-test test-cached-time ()
+  "Tests freezing (and thawing) of current time using the macros
+with/without-current-time-cached."
+
+  (let ((t-start (current-time-utc)))
+
+    ;; freeze the current time (at t-start)
+    (with-current-time-cached ()
+
+      ;; sleep a second
+      (process-sleep $dur-snooze)
+
+      ;; frozen time hasn't changed
+      (assert-equal (current-time-utc) t-start)
+
+      ;; Check that the time can be unfrozen in-scope ...
+      (without-current-time-cached ()
+        (call-test-function time-flows-normally $dur-snooze
+                           "without-current-time-cached"))
+
+      ;; ... and that things continue afterwards
+      (let ((t-start (current-time-utc)))
+       (process-sleep $dur-snooze)
+       (assert-equal (current-time-utc) t-start)))
+
+    ;; freeze current time at fake-time
+    (let ((fake-time 17))
+      (with-current-time-cached (fake-time)
+        (assert-equal (current-time-utc) fake-time :message "faked time before")
+        (process-sleep $dur-snooze)
+        (assert-equal (current-time-utc) fake-time :message "faked time after")))
+
+    ;; Check things are back to normal
+    (call-test-function time-flows-normally $dur-snooze "outside of cached scope")))
+
+(define-time-test test-accelerated-time ()
+  "Tests acceleration of (fake) time using the time-speeder-upper time function."
+  (let ((speedup-rate 67))
+    (with-current-time-function ((time-speeder-upper speedup-rate))
+      (let ((t-start (current-time-utc)))
+        (process-sleep $dur-snooze)
+        (let ((t-end (current-time-utc)))
+          (assert-equal (- t-end t-start) (* $dur-snooze speedup-rate)
+                       :message "time fast-forward"
+                        :test #'>=))))
+
+    ;; Check things are back to normal
+    (call-test-function time-flows-normally $dur-snooze
+                       "outside of rigged scope")))
+
+(define-time-test test-cached-accelerated-time ()
+  "Tests combination of accelerated time and cached time."
+  (let ((speedup-rate 67))
+    ;; first speed up
+    (with-current-time-function ((time-speeder-upper speedup-rate))
+      (let ((t-start (current-time-utc)))
+        (process-sleep $dur-snooze)
+        (let ((t-pre-cache (current-time-utc)))
+
+         ;; then cache
+          (with-current-time-cached ()
+            (process-sleep $dur-snooze)
+            (let ((t-mid-cache (current-time-utc)))
+              (assert-equal t-pre-cache t-mid-cache
+                           :message "cache works inside rigging")))
+
+         ;; go back to accelerated time
+          (process-sleep $dur-snooze)
+          (let ((t-end (current-time-utc)))
+            (assert-equal (* $dur-snooze speedup-rate 3) (- t-end t-start)
+                         :message "rigging restored"
+                          :test #'<=)))))))
+
+(define-time-test test-nested-accelerated-time ()
+  "Tests nesting of accelerated time."
+  (let ((outer-rate 7)
+        (inner-rate 67))
+
+    ;; This nested call should trigger a SIMPLE-ERROR condition
+    (assert-error simple-error
+      (with-current-time-function ((time-speeder-upper 3))
+       (with-current-time-function ((time-speeder-upper 7)))))
+
+    ;; This nested call should work.
+    (let ((t-start (current-time-utc)))
+      (with-current-time-function ((time-speeder-upper outer-rate))
+        (with-current-time-function
+           ((time-speeder-upper inner-rate) :allow-nesting? t)
+         (process-sleep $dur-snooze)
+         (let ((t-end (current-time-utc)))
+           (assert-equal (- t-end t-start)
+                         (* $dur-snooze inner-rate outer-rate)
+                         :message "nested rigs correct"
+                          :test #'>=)))))))
+
+(define-time-test test-accelerated-cached-time ()
+  "Checks that the outer scope caching the time wins over an inner acceleration."
+  (let ((cached-time 86400)   ; 2nd Jan, 1900
+        (rate 7))
+
+    ;; freeze the time
+    (with-current-time-cached (cached-time)
+
+      ;; This nested call should trigger a SIMPLE-ERROR condition
+      (assert-error simple-error
+       (with-current-time-function ((time-speeder-upper rate))))
+
+      ;; This nested call should be OK
+      (with-current-time-function ((time-speeder-upper rate) :allow-nesting? t)
+        (let ((t-start (current-time-utc)))
+          (assert-equal cached-time t-start)
+         (process-sleep $dur-snooze)
+         (assert-equal (current-time-utc) t-start))))))
+
+(define-time-test test-accelerated-time-increments ()
+  "Ensure sped-up time has finer increments than one wallclock second."
+  (with-current-time-function ((time-speeder-upper-hires 1000))
+    (let ((t-start (current-time-utc)))
+      (process-sleep 0.1)
+      (let ((t-end (current-time-utc)))
+       (assert-not-equal t-start t-end)))))
+
+(define-test test-roll-time ()
+  "Test the ROLL-TIME function"
+
+  (flet ((ymd (year month &optional (day 1))
+           (encode-integer-time 0 0 0 day month year)))
+    (assert-equal (ymd 2006 1 1)
+                  (roll-time (ymd 2005 1 1) :years 1))
+    (assert-equal (ymd 2006 2 28)
+                  (roll-time (ymd 2006 3 31) :months -1))
+    (assert-equal (ymd 2006 2 28)
+                  (roll-time (ymd 2006 3 30) :months -1))
+    (assert-equal (ymd 2006 2 28)
+                  (roll-time (ymd 2006 3 29) :months -1))
+    (assert-equal (ymd 2006 2 28)
+                  (roll-time (ymd 2006 3 28) :months -1))
+    (assert-equal (ymd 2006 2 27)
+                  (roll-time (ymd 2006 3 27) :months -1))
+    (assert-equal (ymd 2000 1 1)
+                  (roll-time (ymd 1996 1 1) :years 4)))
+  
+  
+  (dotimes (i 100)
+    (let* ((direction (case (random 2)
+                        (0 +1)
+                        (1 -1)))
+           (second (random 60))
+           (second-delta (* direction (random 1000)))
+           (minute (random 60))
+           (minute-delta (* direction (random 1000)))
+           (hour (random 24))
+           (hour-delta (* direction (random 1000)))
+           (year (+ 2000 (random 100)))
+           (year-delta (* direction (random 100)))
+           (month (1+ (random 12)))
+           ;; Avoid problematical February 29
+           (day (1+ (random (days-per-month month 2007))))
+           (day-delta (* direction (random 1000)))
+           (start-time (encode-integer-time second minute hour day month year)))
+      (assert-equal start-time
+                    (roll-time
+                     (roll-time start-time
+                                :seconds second-delta
+                                :minutes minute-delta
+                                :hours hour-delta)
+                     :seconds (- second-delta)
+                     :minutes (- minute-delta)
+                     :hours (- hour-delta))
+                    :message (format nil "Failed to roll (encode-integer-time ~D ~D ~D ~D ~D ~D) by ~D ~D ~D 0 0 0"
+                                     second minute hour day month year
+                                     second-delta minute-delta hour-delta))
+      (assert-equal start-time
+                    (roll-time (roll-time start-time :days day-delta) :days (- day-delta))
+                    :message (format nil "Failed to roll (encode-integer-time ~D ~D ~D ~D ~D ~D) by ~D days "
+                                     second minute hour day month year day-delta))
+      (assert-equal start-time
+                    (roll-time (roll-time start-time :years year-delta) :years (- year-delta))
+                    :message (format nil "Failed to roll (encode-integer-time ~D ~D ~D ~D ~D ~D) by ~D years "
+                                     second minute hour day month year year-delta)))))
+
+
+(define-test test-parse-local-time ()
+  (debugf "test-parse-local-time current-time-utc: ~S" (current-time-utc))
+  (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                (parse-local-time "2006-05-04 03:02:01"))
+  (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                (parse-local-time "junk2006-05-04 03:02:01morejunk" :start 4 :end 23))
+  (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                (parse-local-time "junk2006-05-04 03:02:01" :start 4))
+  (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                (parse-local-time "2006-05-04 03:02:01junk" :end 19))
+  (with-current-time-cached ((encode-integer-time 0 0 0 3 5 2006))
+    (debugf "test-parse-local-time current-time-utc: ~S" (current-time-utc))
+    (assert-equal (encode-integer-time 1 2 3 4 5 1983)
+                  (parse-local-time "MAY4 03:02:01" :guess-year 1983))
+    (assert-equal (encode-integer-time 1 2 3 4 5 1983)
+                  (parse-local-time "4MAY 03:02:01" :guess-year 1983))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "MAY4 03:02:01" :guess-year :utc-future))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "4MAY 03:02:01" :guess-year :utc-future))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "MAY4 03:02:01" :guess-year :utc-current))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "4MAY 03:02:01" :guess-year :utc-current))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2005)
+                  (parse-local-time "MAY4 03:02:01" :guess-year :utc-past))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2005)
+                  (parse-local-time "4MAY 03:02:01" :guess-year :utc-past))
+    (assert-equal nil
+                  (parse-local-time "MAY4 03:02:01" :guess-year nil))
+    (assert-equal nil
+                  (parse-local-time "4MAY 03:02:01" :guess-year nil))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "04MAY06 03:02:01" :guess-century :utc-nearest))
+    (assert-equal (encode-integer-time 1 2 3 4 5 1906)
+                  (parse-local-time "04MAY06 03:02:01" :guess-century :utc-past))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "04MAY06 03:02:01" :guess-century :utc-future)))
+  (with-current-time-cached ((encode-integer-time 0 0 0 4 5 2006))
+    (debugf "test-parse-local-time current-time-utc: ~S" (current-time-utc))
+    (assert-equal (encode-integer-time 1 2 3 4 5 1983)
+                  (parse-local-time "MAY4 03:02:01" :guess-year 1983))
+    (assert-equal (encode-integer-time 1 2 3 4 5 1983)
+                  (parse-local-time "4MAY 03:02:01" :guess-year 1983))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "MAY4 03:02:01" :guess-year :utc-future))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "4MAY 03:02:01" :guess-year :utc-future))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "MAY4 03:02:01" :guess-year :utc-current))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "4MAY 03:02:01" :guess-year :utc-current))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "MAY4 03:02:01" :guess-year :utc-past))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "4MAY 03:02:01" :guess-year :utc-past))
+    (assert-equal nil
+                  (parse-local-time "MAY4 03:02:01" :guess-year nil))
+    (assert-equal nil
+                  (parse-local-time "4MAY 03:02:01" :guess-year nil))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "04MAY06 03:02:01" :guess-century :utc-nearest))
+    (assert-equal (encode-integer-time 1 2 3 4 5 1906)
+                  (parse-local-time "04MAY06 03:02:01" :guess-century :utc-past))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "04MAY06 03:02:01" :guess-century :utc-future)))
+
+  (with-current-time-cached ((encode-integer-time 0 0 0 5 5 2006))
+    (debugf "test-parse-local-time current-time-utc: ~S" (current-time-utc))
+    (assert-equal (encode-integer-time 1 2 3 4 5 1983)
+                  (parse-local-time "MAY4 03:02:01" :guess-year 1983))
+    (assert-equal (encode-integer-time 1 2 3 4 5 1983)
+                  (parse-local-time "4MAY 03:02:01" :guess-year 1983))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2007)
+                  (parse-local-time "MAY4 03:02:01" :guess-year :utc-future))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2007)
+                  (parse-local-time "4MAY 03:02:01" :guess-year :utc-future))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "MAY4 03:02:01" :guess-year :utc-current))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "4MAY 03:02:01" :guess-year :utc-current))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "MAY4 03:02:01" :guess-year :utc-past))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "4MAY 03:02:01" :guess-year :utc-past))
+    (assert-equal nil
+                  (parse-local-time "MAY4 03:02:01" :guess-year nil))
+    (assert-equal nil
+                  (parse-local-time "4MAY 03:02:01" :guess-year nil))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "04MAY06 03:02:01" :guess-century :utc-nearest))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                  (parse-local-time "04MAY06 03:02:01" :guess-century :utc-past))
+    (assert-equal (encode-integer-time 1 2 3 4 5 2106)
+                  (parse-local-time "04MAY06 03:02:01" :guess-century :utc-future)))
+  (debugf "test-parse-local-time current-time-utc: ~S" (current-time-utc))
+  (assert-equal (encode-integer-time 1 2 3 4 5 1970)
+                (parse-local-time "04MAY70 03:02:01" :guess-century :unix-y2k))
+  (assert-equal (encode-integer-time 1 2 3 4 5 2069)
+                (parse-local-time "04MAY69 03:02:01" :guess-century :unix-y2k))
+  ;; non-existent date
+  (assert-equal nil
+                (parse-local-time "2006-02-29 03:02:01"))
+  (assert-equal nil
+                (parse-local-time "2006-06-31 03:02:01"))
+  ;; there was a bug having to do with 12am and 12pm
+  (assert-equal (encode-integer-time 0 0 0 4 5 2006)
+                (parse-local-time "2006-05-04 00:00:00"))
+  (assert-equal (encode-integer-time 0 0 12 4 5 2006)
+                (parse-local-time "2006-05-04 12:00:00"))
+  (assert-equal (encode-integer-time 0 0 0 5 5 2006)
+                (parse-local-time "2006-05-04 24:00:00"))
+  (assert-equal (encode-integer-time 0 0 0 4 5 2006)
+                (parse-local-time "2006-05-04 12:00:00AM"))
+  (assert-equal (encode-integer-time 1 0 0 4 5 2006)
+                (parse-local-time "2006-05-04 12:00:01AM"))
+  (assert-equal (encode-integer-time 0 59 0 4 5 2006)
+                (parse-local-time "2006-05-04 12:59AM"))
+  (assert-equal (encode-integer-time 0 0 1 4 5 2006)
+                (parse-local-time "2006-05-04 1:00AM"))
+  (assert-equal (encode-integer-time 0 0 12 4 5 2006)
+                (parse-local-time "2006-05-04 12:00:00PM"))
+  (assert-equal (encode-integer-time 1 0 12 4 5 2006)
+                (parse-local-time "2006-05-04 12:00:01PM"))
+  (assert-equal (encode-integer-time 0 59 12 4 5 2006)
+                (parse-local-time "2006-05-04 12:59PM"))
+  (assert-equal (encode-integer-time 0 0 13 4 5 2006)
+                (parse-local-time "2006-05-04 1:00PM"))
+  ;; various formats
+  (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                (parse-local-time "4MAY2006 030201"))
+  (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                (parse-local-time "4MAY06T030201"))
+  (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                (parse-local-time "4-MAY-06 030201"))
+  (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                (parse-local-time "04052006T030201"))
+  (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                (parse-local-time "04-05-2006 03:02:01"))
+  (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                (parse-local-time "2006MAY4 3:02:01"))
+  (assert-equal (encode-integer-time 0 2 3 4 5 2006)
+                (parse-local-time "4-5-2006 0302"))
+  (assert-equal (encode-integer-time 0 2 15 31 12 2006)
+                (parse-local-time "31-12-2006 0302pm"))
+  (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                (parse-local-time "4/5/2006 03:02:01"))
+  (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                (parse-local-time "04/05/2006 03:02:01"))
+  (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                (parse-local-time "2006/5/4 03:02:01"))
+  (assert-equal (encode-integer-time 1 2 3 4 5 2006)
+                (parse-local-time "2006/05/04 03:02:01"))
+  ;; unparsable date
+  (assert-equal nil
+                (parse-local-time "452006 03:02:01"))
+  ;; unparsable time
+  (assert-equal nil
+                (parse-local-time "2006-05-04 03pm"))
+  (assert-equal nil
+                (parse-local-time "2006-05-04 302"))
+  (assert-equal nil
+                (parse-local-time "2006-05-04 03.02.01")))
+  
+
+;; These tests are a tad brittle (see 
+(define-test-suite test-time ()
+    (test-normal-time
+     test-cached-time
+     test-accelerated-time
+     test-cached-accelerated-time
+     test-nested-accelerated-time
+     test-accelerated-cached-time
+     test-accelerated-time-increments
+     test-roll-time
+     test-parse-local-time))
+
+(register-test 'test-time)
diff --git a/time.lisp b/time.lisp
new file mode 100644 (file)
index 0000000..3d634b4
--- /dev/null
+++ b/time.lisp
@@ -0,0 +1,2626 @@
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;                                                                  ;;;
+;;; Confidential and proprietary information of ITA Software, Inc.   ;;;
+;;;                                                                  ;;;
+;;; Copyright (c) 2005-2008 ITA Software, Inc.  All rights reserved. ;;;
+;;;                                                                  ;;;
+;;; Original author: Matt Marjanovic                                 ;;;
+;;;                                                                  ;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(in-package "QUUX")
+
+
+;;; Date and time functions
+
+;; These are specific aliases for representing time quantities as integers,
+;; whether local or UTC.  The basic form is as a Lisp 'universal time',
+;; i.e. seconds from the Lisp epoch.
+;;
+;; Although they are all interchangeable here in Lisp-world, these types
+;; are stored differently (e.g. truncated) in the DB-world!
+;; (See instances of 'define-db-class' for examples.)
+
+;; 'time' means fully-specified moment (i.e. "date + tofd").
+(deftype integer-time () 'integer)
+
+;; 'date' means just year, month, day (tofd components zero or ignored).
+(deftype integer-date () 'integer)
+
+;; 'tofd' means just hour, minute, second (date components zero or ignored).
+(deftype integer-tofd () 'integer)
+
+;; Number of seconds.
+(deftype integer-duration () 'integer)
+
+(eval-when (:compile-toplevel :load-toplevel :execute)
+
+(defconstant-equal +400-years-of-seconds+ 
+  (- (cl:encode-universal-time 0 0 0 1 4 (+ 400 2010) 0)
+     (cl:encode-universal-time 0 0 0 1 4 2010 0))
+  "The number of seconds in four hundred years")
+
+(defun encode-universal-time (second minute hour date month year &optional time-zone)
+  "Extend to return negative values for years < 1900"
+  (check-type year (integer 0))
+  (if (< 99 year 1900)
+    (- (encode-universal-time second minute hour date month (+ year 400) time-zone)
+       +400-years-of-seconds+)
+    (cl:encode-universal-time second minute hour date month year time-zone)))
+
+(defun decode-universal-time (universal-time &optional time-zone)
+  "Extend to decode negative numbers"
+  (if (< universal-time 0)
+    (multiple-value-bind (second minute hour date month year day daylight timezone)
+        (decode-universal-time (+ universal-time +400-years-of-seconds+) time-zone)
+      (values second minute hour date month (- year 400) day daylight timezone))
+    (cl:decode-universal-time universal-time time-zone)))
+
+(defconstant $1week #.(* 24 60 60 7)
+  "The duration of one week, in integer-time.")
+(defconstant $24hours #.(* 24 60 60)
+  "The duration of one day, in integer-time.")
+(defconstant $12hours #.(* 12 60 60)
+  "The duration of one half-day, in integer-time.")
+(defconstant $4hours #.(* 4 60 60)
+  "The duration of four hours, in integer-time.")
+(defconstant $1hour #.(* 1 60 60)
+  "The duration of one hour, in integer-time.")
+(defconstant $1minute #.(* 1 60)
+  "The duration of one minute, in integer-time.")
+(defconstant $1second 1
+  "The duration of one second, in integer-time.")
+
+(defconstant $largest-tz-offset #.(* 14 60 60)
+  "The largest possible timezone offset, in integer-time.")
+
+(defconstant $first-minute-of-day 0
+  "The first minute of a day (i.e. 00:00, midnight), in integer-time.")
+
+)      ;eval-when
+
+
+(eval-when (:compile-toplevel :load-toplevel :execute)
+
+;; If you liked this constant, you might also want to try our related
+;; function, last-minute-of-local-date (see below).
+(defconstant $last-minute-of-day #.(- $24hours $1minute)
+  "The last minute of a day (i.e. 23:59), in integer-time.")
+
+(defconstant $time-in-distant-past 0
+  "A time in the distant past.")
+
+(defconstant $time-in-distant-future
+  #.(encode-universal-time 0 0 0 1 1 2525 0)
+  "A time in the distant future.")
+
+)      ;eval-when
+
+
+(defun-inline local-tofd-only (time)
+  "Zero the date bits of a integer-time, leaving the tofd bits alone."
+  (check-types (integer-time time))
+  (mod time $24hours))
+
+(defun-inline local-date-only (time)
+  "Zero the time bits of a integer-time, leaving the date bits alone."
+  (check-types (integer-time time))
+  (- time (local-tofd-only time)))
+
+
+(defun-inline first-minute-of-local-date (time)
+  "Return the integer-time representing the first minute on the given date."
+  (local-date-only time))
+
+(defun last-minute-of-local-date (time)
+  "Return the integer-time representing the last minute on the given date."
+  (check-types (integer-time time))
+  (+ (local-date-only time) $last-minute-of-day))
+
+(defun roll-time (time &key (seconds 0)
+                            (minutes 0)
+                            (hours 0)
+                            (days 0) 
+                            (months 0)
+                            (years 0))
+  "Return an integer-time before/after TIME by SECONDS, MINUTES,
+HOURS, DAYS, MONTHS or YEARS.
+
+TIME - an integer-time (number of seconds since epoch).
+
+SECONDS, MINUTES, HOURS, DAYS, MONTHS, YEARS - integers.
+
+ROLL-TIME operates by adding/subtracting SECONDS, MINUTES, HOURS,
+DAYS, MONTHS and YEARS (in that order) to the facsimile time
+TIME. ROLL-TIME treats TIME simply as the number of seconds since
+epoch ignoring DST, time zones and anything else which isn't
+passed as an argument to `encode-integer-time'.
+
+Notes:
+
+1) The length of months, expressed as a number of days,
+varies. Therefore it does not always hold that:
+
+  (= x (roll-time (roll-time x :months M :years Y)
+       :months -M :years -Y))
+
+2) If the result of rolling, even in an interedmiate step, ends
+   up before 1900-01-01T00:00:00 we lose. Example:
+
+ (roll-time (encode-universal-time 0 0 0 1 1 1900)
+            :days -31 :months 3)
+ ==> ERROR"
+  (check-type time integer-time)
+  (check-type seconds integer)
+  (check-type minutes integer)
+  (check-type hours integer)
+  (check-type days integer)
+  (check-type months integer)
+  (check-type years integer)
+  ;; seconds
+  (setf time (+ time seconds))
+  ;; minutes
+  (setf time (+ time (* $1minute minutes)))
+  ;; hours
+  (setf time (+ time (* $1hour hours)))
+  ;; days
+  (setf time (+ time (* $24hours days)))
+  ;; months & years
+  (multiple-value-bind (sec min hour day start-month start-year)
+      (decode-integer-time time)
+    (let* ((month-delta (mod months 12))
+           (year-delta (+ years (floor months 12)))
+           (new-month (+ start-month month-delta))
+           (new-year (+ start-year year-delta)))
+      (cond
+        ((> new-month 12)
+         (setf new-month (- new-month 12)
+               new-year (+ new-year 1))))
+      (setf time (encode-integer-time sec min hour
+                                      (min day (days-per-month new-month new-year))
+                                      new-month new-year))))
+  time)
+
+
+
+;;---!!!mm  Write unit-tests and documentation for this.
+(defun advance-local-time (time &key (seconds 0) (minutes 0) (hours 0) (days 0))
+  (check-types (integer-time time)
+               (integer seconds minutes hours days))
+  (+ time
+     (* $1second seconds)
+     (* $1minute minutes)
+     (* $1hour hours)
+     (* $24hours days)))
+
+
+;;; Zoned times
+
+;; A zoned-time has readers, not accessors, on purpose -- it needs to
+;;  be an atomic, immutable object.  Otherwise, DB and other consistency
+;;  becomes a maintenance/engineering nightmare.
+;;
+;; Note that variables which contain a zoned-time will have the suffix "-zul"
+;;  (which is the short acronym for "zone, utc, local").
+(defclass zoned-time ()
+  ((time-utc :type integer-time
+            :reader utc-time
+            :initarg :utc
+            :documentation "UTC integer-time representation of moment")
+   (tz-offset :type integer-time
+             :reader tzoffset
+             :initarg :tzoffset
+             :documentation "Offset (seconds) to local timezone (from UTC)"))
+  (:documentation
+   "An immutable time object which includes timezone information.
+   If both local and UTC views are ever relevant for some absolute temporal
+   quantity, then a zoned-time should be used to represent that quantity."))
+
+
+(defmethod print-object ((zul zoned-time) stream)
+  "Pretty-print a zoned-time object."
+  (print-unprintable-object (zul stream :type t :identity t)
+    (write-zoned-time zul stream
+                     :date-as :yyyy-mm-dd :time-as :hh-mm-ss
+                     :show-timezone t)))
+
+(defun make-zoned-time (&key utc local tzoffset)
+  (assert (and (not (and utc local tzoffset))
+               (if tzoffset
+                 (or utc local)
+                 (and utc local)))
+          () "Exactly two of time, local, tzoffset must be specified.")
+  (make-instance 'zoned-time
+    :utc (or utc
+             (- local tzoffset))
+    :tzoffset (or tzoffset
+                  (- local utc))))
+
+(defgeneric zoned-time (time-designator)
+  (:documentation
+   "Transform the argument into a ZONED-TIME instance.
+ A string is parse  in ISO8601 syntax, a ZONED-TIME instance is returned, and an integer is used as
+ the utc argument to construct a new ZONED-TIME instance with offset 0."))
+
+(defmethod zoned-time ((time-designator string))
+  (parse-iso8601-zoned time-designator))
+
+(defmethod zoned-time ((zoned-time zoned-time))
+  zoned-time)
+
+(defmethod zoned-time ((utc-time integer))
+  (make-zoned-time :utc utc-time :tzoffset 0))
+
+
+
+(defmethod local-time ((zul zoned-time))
+  "Return the local time of a zoned-time object.
+
+  Returns an 'integer-time', i.e. seconds since the beginning of the
+  lisp epoch."
+  (+ (utc-time zul) (tzoffset zul)))
+
+(defun get-local-time (time)
+  (etypecase time
+    (integer-time time)
+    (zoned-time (local-time time))))
+
+(defmethod duration ((a zoned-time) (b zoned-time))
+  "Compute the signed duration (in seconds) from moment 'a' to moment 'b'.
+
+  'a' and 'b' are 'zoned-time' quantities.
+   If b is later than a, then the result is positive."
+  (- (utc-time b) (utc-time a)))
+
+(defmethod zoned-time-equal-p ((a zoned-time) (b zoned-time))
+  "Return T if zoned-times A and B are equal (same time and tzoffset)"
+  (and (eql (utc-time a) (utc-time b))
+       (eql (tzoffset a) (tzoffset b))))
+
+(defun utc-is-before-utc-p (a b)
+  "Return T if 'a' precedes 'b' in absolute (UTC) time."
+  (check-type a zoned-time)
+  (check-type b zoned-time)
+  (< (utc-time a) (utc-time b)))
+
+(defun utc-is-after-utc-p (a b)
+  "Return T if 'a' succeeds 'b' in absolute (UTC) time."
+  (check-type a zoned-time)
+  (check-type b zoned-time)
+  (> (utc-time a) (utc-time b)))
+
+
+(defmethod make-zoned-date ((z zoned-time))
+  "Create a zoned-time matching input, but with zero'ed time components."
+  (make-instance 'zoned-time
+    :tzoffset (tzoffset z)
+    :utc (local-date-only (utc-time z))))
+
+
+;;---!!!mm  WHO ADDED THIS??  IT IS BROKEN.                             
+(defmethod add-days ((z zoned-time) days)
+  "Add some number of days to a zoned-time."
+  (if (zerop days)
+    z
+    (make-instance 'zoned-time
+      :tzoffset (tzoffset z)
+      :utc (+ (* days $24hours) (utc-time z)))))
+
+
+;;; Time caching & rigging
+
+(defun-inline server-system-time-utc ()
+  "Return the server's true UTC system time (as an integer-time).
+
+  This function returns the real UTC time as seen by the server's clock,
+  and should only be used in situations such as logging, where the true
+  physical clock time is important.
+
+  Otherwise, one should always use CURRENT-TIME-UTC."
+  (get-universal-time))
+
+(defvar *current-time-function* #'get-universal-time)
+
+(defun current-time-utc ()
+  "Return the 'current' UTC time (as an integer-time).
+
+  The 'current' time may have been cached by 'with-current-time-cached',
+  or it may be skewed by 'with-current-time-function'."
+  (funcall *current-time-function*))
+
+(let ((last-overridden-time-utc nil)
+      (last-overridden-time-utc-string nil))
+
+  (defun current-time-utc-db-override ()
+    (if (eq *current-time-function* #'get-universal-time)
+      nil
+      (let ((now (current-time-utc)))
+       (cond ((eql now last-overridden-time-utc)
+              last-overridden-time-utc-string)
+             (t
+              (setq last-overridden-time-utc now
+                    last-overridden-time-utc-string
+                    (with-output-to-string (str)
+                      (write-iso8601-utc now str)))))))))
+
+(defun current-time-zoned ()
+  "Return the 'current' UTC time (as a zoned time).
+
+  The 'current' time may have been cached by 'with-current-time-cached',
+  or it may be skewed by 'with-current-time-function'."
+  (make-instance 'zoned-time :utc (current-time-utc) :tzoffset 0))
+
+(defmacro with-current-time-cached ((&optional override-time-utc) &body body)
+  "Create a scope which caches/freezes the current UTC time.
+
+  This scope caches the result of (current-time-utc).  Evaluations of
+  (current-time-utc) within the scope will return the cached value.
+  If the optional 'override-time-utc' is provided, then the scope will
+  cache that value instead."
+  `(let* ((now (or ,override-time-utc
+                   (current-time-utc)))
+          (*current-time-function* (lambda () now)))
+    ,@body))
+
+(defmacro without-current-time-trickery (&body body)
+  "Create a scope that undoes any of the current-time caches/functions."
+  `(let ((*current-time-function* #'get-universal-time))
+    ,@body))
+
+(defmacro without-current-time-cached (&body body)
+  "DEPRECATED; use 'without-current-time-trickery' instead."
+  `(without-current-time-trickery
+   ,@body))
+
+
+(defmacro with-current-time-function ((new-time-function
+                                       &key allow-nesting?) &body body)
+  "Create a scope in which the current time is generated by a function.
+
+  Within this scope, evaluations of (current-time-utc) will report the
+  time as generated by 'new-time-function', instead of the time as
+  reported by the operating system.
+
+  For example, to create a scope where time runs 5x faster than normal:
+    (with-current-time-function ((time-speeder-upper 5))
+      (process-sleep 1)
+      (current-time-utc))   ; This returns a time 4s in the future
+
+  These scopes may be nested. Nesting is disabled by default, since you're
+  probably shooting yourself in the foot by it.
+
+  The inner-most function is first executed; if it refers to
+  (current-time-utc), which (time-speeder-upper) does, then the function in the
+  outer scope will provide that value. E.g. nesting two (tims-speeder-upper)s
+  will have the effect of speeding time by the product of their rates."
+  (with-gensyms (original-time-function-v new-time-function-v)
+    `(let* ((,original-time-function-v *current-time-function*)
+            (,new-time-function-v ,new-time-function)
+            (*current-time-function*
+             (lambda ()
+               (let ((*current-time-function* ,original-time-function-v))
+                 (funcall ,new-time-function-v)))))
+      ,@(unless allow-nesting?
+                `((unless (eq ,original-time-function-v #'get-universal-time)
+                    (error "Nested 'with-current-time-function'."))))
+      ,@body)))
+
+(defmacro with-current-integer-time-fixed ((integer-time) &body body)
+  `(let ((*current-time-function* (constantly ,integer-time)))
+     ,@body))
+
+(defmacro with-current-time-fixed ((second minute hour day month year) &body body)
+  `(with-current-integer-time-fixed ((encode-integer-time ,second ,minute ,hour ,day ,month ,year))
+     ,@body))
+
+(defmacro with-virtual-sleep ((&key (days 0) (hours 0) (mins 0) (secs 0)) &body body)
+  `(let* ((previous-time-function *current-time-function*)
+          (*current-time-function* (lambda ()
+                                     (roll-time (funcall previous-time-function)
+                                                :days    ,days
+                                                :hours   ,hours
+                                                :minutes ,mins
+                                                :seconds ,secs))))
+     ,@body))
+
+(defun time-speeder-upper (rate)
+  "Returns a lambda that generates time at 'rate' x normal rate.
+
+  For use in the (with-current-time-function) macro.
+
+  Note that this clock still 'ticks' once per wallclock second; it is just that each tick moves
+  the time forward a large amount. If 'current-time-utc' is called multiple times within that
+  second, time will appear to be frozen.
+
+  See 'time-speeder-upper-hires' for a clock that ticks more often."
+  (let ((now (current-time-utc)))
+    (lambda ()
+      (+ now (* rate
+                (- (current-time-utc) now))))))
+
+(defun time-speeder-upper-hires (rate)
+  "Returns a lambda that generates time at 'rate' x normal rate, at high resolution.
+
+  For use in the (with-current-time-function) macro.
+
+  This clock ticks at the rate given by 'internal-time-units-per-second', typically 100.
+
+  Note: this clock breaks nesting, since it only relies on the outer scope to set
+  the initial time for the clock."
+  (let ((now (current-time-utc))
+        (base-realtime (get-internal-real-time)))
+    (lambda ()
+      (truncate
+       (+ now (* (/ rate internal-time-units-per-second)
+                 (- (get-internal-real-time) base-realtime)))))))
+
+
+;;; Date and time manipulation
+
+(defun-inline round-time-to-next-minute (time)
+  "Round up an integer-time to the nearest larger minute.
+
+   Returns an integer number of minutes, which, when multiplied by $1minute, will be 0-60 greater
+   than the input."
+  (check-types (integer-time time))
+  (ceiling time $1minute))
+
+
+;; Lisp's encode-universal-time and decode-universal-time functions
+;; are swell, but slow.  (They also have this annoying "tz" argument,
+;; which should *always* be zero in QRS's usage.)  The issue of bignum
+;; arithmetic is unavoidable if the timebase is in seconds, until we
+;; have a 64-bit lisp.  However, we can speed up these functions by
+;; a factor of 2-3 by precomputing most of the results over a range
+;; of dates.  Hence, the encode- and decode- functions defined below.
+;;
+;; The date range is [1900,2100] (all months/days inclusive),
+;; set immediately below.
+
+(eval-when (:compile-toplevel :load-toplevel :execute)
+
+(defconstant $it-cache-first-year 1900
+  "The first year of precomputed integer-time en-/de-coding")
+(defconstant $it-cache-last-year  2100
+  "The last year of precomputed integer-time en-/de-coding")
+
+)      ;eval-when
+
+
+(eval-when (:compile-toplevel :load-toplevel :execute)
+
+(defconstant $it-cache-first-day
+  (floor (encode-universal-time 0 0 0 1 1 $it-cache-first-year 0)
+         $24hours)
+  "The first day (since epoch) of precomputed integer-time en-/de-coding.")
+
+(defconstant $it-cache-last-day
+  (floor (encode-universal-time 0 0 0 31 12 $it-cache-last-year 0)
+         $24hours)
+  "The last day (since epoch) of precomputed integer-time en-/de-coding.")
+
+)      ;eval-when
+
+
+(eval-when (:compile-toplevel :load-toplevel :execute)
+
+(defconstant-equalp $days-until-year-month
+  (let ((v (make-array (i* 13 (i+ 1 (i- $it-cache-last-year
+                                        $it-cache-first-year))))))
+    (do ((year $it-cache-first-year (1+ year)))
+        ((> year $it-cache-last-year) v)
+      (declare (type fixnum year))
+      (do ((month 0 (1+ month)))
+          ((> month 12))
+        (declare (type fixnum month))
+        (setf (svref v (i+ month (i* 13 (i- year $it-cache-first-year))))
+              (let* ((month (if (i= month 0) 1 month))
+                     (time (encode-universal-time 0 0 0 1 month year 0)))
+                (floor time $24hours))))))
+  "An array which maps (year,month) --> (days preceding that year/month)
+   where 'days' starts at the lisp epoch.
+   The map starts with the first month of year '$it-cache-first-year'
+   and ends with the last month of year '$it-cache-last-year'.
+   And, there are 13 months in a year -- i.e. [0,12] are valid months,
+   and month 0 is the same as month 1.")
+
+(defun-inline %days-until-year-month (year month)
+  "Return the number of days since the lisp epoch which precede the
+   given (year, month).  For example:  (1900 2) --> 31 days."
+  (svref $days-until-year-month
+         (i+ month (i* 13 (i- year $it-cache-first-year)))))
+
+)      ;eval-when
+
+
+(eval-when (:compile-toplevel :load-toplevel :execute)
+
+(defconstant-equalp $days-per-february-year
+  (let ((v (make-array (i+ 1 (i- $it-cache-last-year
+                                 $it-cache-first-year)))))
+    (do ((year $it-cache-first-year (1+ year)))
+        ((> year $it-cache-last-year) v)
+      (declare (type fixnum year))
+      (setf (svref v (i- year $it-cache-first-year))
+            (i- (%days-until-year-month year 3)
+                (%days-until-year-month year 2)))))
+  "An array which maps (year) --> (days in february that year)
+   The map starts with the first month of year '$it-cache-first-year'
+   and ends with the last month of year '$it-cache-last-year'.")
+
+(defun-inline %days-per-february-year (year)
+  "Return the number of days in February of the given year
+   For example:  1900 --> 28 days."
+  (svref $days-per-february-year
+         (i- year $it-cache-first-year)))
+
+)      ;eval-when
+
+
+(defconstant-equalp $days-per-month
+  (make-array 12 :initial-contents '(31 28 31 30 31 30 31 31 30 31 30 31))
+  "An array mapping (month) --> (days in month)
+   The value for February is, of course, incorrect during leap years,
+   hence the need for %days-per-february-year defined above.")
+
+
+;; The following few definitions are defparameter instead of a
+;; defconstant because the latter is always (in SBCL and CMCL)
+;; evaluated at compile time.  We don't need these at compile time,
+;; and they slow down the build by around 30 seconds for no good
+;; reason. -- DLW 10/6/06
+
+;; See note above for why this is a defparameter instead of a defconstant.
+(defparameter $days-until-month
+  (let ((v (make-array 12 :initial-element 0)))
+    (loop for m from 0 below 12
+          for total = 0 then (+ total next)
+          for next = (svref $days-per-month m)
+          doing (setf (svref v m) total))
+    v)
+  "An array mapping (month) --> (days before beginning of month)
+   The value for months after February is, of course, incorrect
+   during leap years.")
+
+
+;; See note above for why this is a defparameter instead of a defconstant.
+(defparameter $year-from-days
+  (let ((v (make-array (- $it-cache-last-day $it-cache-first-day -1))))
+    (do ((days $it-cache-first-day (1+ days)))
+        ((> days $it-cache-last-day) v)
+      (multiple-value-bind (s m h day month year)
+          (decode-universal-time (* days $24hours) 0)
+        (declare (ignore s m h day month))
+        (setf (svref v days) year))))
+  "An array which maps (days since epoch) --> (year)
+   The zeroth entry in the array is for $it-cache-first-day,
+   the last entry is for $it-cache-last-day.")
+
+;; See note above for why this is a defparameter instead of a defconstant.
+(defparameter $month-from-days
+  (let ((v (make-array (- $it-cache-last-day $it-cache-first-day -1))))
+    (do ((days $it-cache-first-day (1+ days)))
+        ((> days $it-cache-last-day) v)
+      (multiple-value-bind (s m h day month year)
+          (decode-universal-time (* days $24hours) 0)
+        (declare (ignore s m h day year))
+        (setf (svref v days) month))))
+  "An array which maps (days since epoch) --> (month-of-year),
+   where month-of-year is in the range [1,12].
+   The zeroth entry in the array is for $it-cache-first-day,
+   the last entry is for $it-cache-last-day.")
+
+;; See note above for why this is a defparameter instead of a defconstant.
+(defparameter $day-from-days
+  (let ((v (make-array (- $it-cache-last-day $it-cache-first-day -1))))
+    (do ((days $it-cache-first-day (1+ days)))
+        ((> days $it-cache-last-day) v)
+      (multiple-value-bind (s m h day month year)
+          (decode-universal-time (* days $24hours) 0)
+        (declare (ignore s m h month year))
+        (setf (svref v days) day))))
+  "An array which maps (days since epoch) --> (day-of-month)
+   where day-of-month is in the range [1,N].
+   The zeroth entry in the array is for $it-cache-first-day,
+   the last entry is for $it-cache-last-day.")
+
+;; See note above for why this is a defparameter instead of a defconstant.
+(defparameter $dofw-from-days
+  (let ((v (make-array (- $it-cache-last-day $it-cache-first-day -1))))
+    (do ((days $it-cache-first-day (1+ days)))
+        ((> days $it-cache-last-day) v)
+      (multiple-value-bind (s m h day month year weekday)
+          (decode-universal-time (* days $24hours) 0)
+        (declare (ignore s m h day month year))
+        (setf (svref v days) weekday))))
+  "An array which maps (days since epoch) --> (day-of-week)
+   where day-of-week is in the range [0,6] for [mon,sun].
+   The zeroth entry in the array is for $it-cache-first-day,
+   the last entry is for $it-cache-last-day.")
+
+
+(defun-inline %year-from-2digit-year (2digit-year)
+  "Convert an ambiguous 2-digit year into an unambiguous 4+-digit year.
+
+  Years before '70 are assumed to be in the 2000's.
+  '70 and later are assumed to be in the 1900's.
+  This function doesn't check that input *is* a 2-digit year."
+  (if (i>= 2digit-year 70)
+    (i+ 2digit-year 1900)
+    (i+ 2digit-year 2000)))
+
+(defun days-per-month (month year)
+  "Return the number of days in a given month in a given year.
+
+  'month' is in the range [1,12] (where 1 == January).
+  'year' is the year -- which only affects the result for February."
+  (check-types (fixnum month year))
+  (if (i= month 2)
+    (if (and (i>= year $it-cache-first-year)
+            (i<= year $it-cache-last-year))
+      (%days-per-february-year year)
+      (let ((feb-first (encode-universal-time 0 0 0 1 2 year 0))
+           (mar-first (encode-universal-time 0 0 0 1 3 year 0)))
+       (floor (- mar-first feb-first) $24hours)))
+    (svref $days-per-month (i- month 1))))
+
+(defun encode-integer-time (seconds minutes hours day month year)
+  "Encode a broken-down time specification into integer-time.
+  Input is the seconds, minutes, hours, day, month, and year.
+  Result is an integer-time (seconds since lisp epoch).
+
+  This function is equivalent to (encode-univeral-time ... 0),
+  but is precomputed and thus faster for years in the range
+  [$it-cache-first-year,$it-cache-last-year]."
+  (check-types (fixnum seconds minutes hours day month year))
+  (when (i< year 100)
+    (setq year (%year-from-2digit-year year)))
+  (if (and (i>= year $it-cache-first-year)
+           (i<= year $it-cache-last-year))
+    (let ((days (i+ (%days-until-year-month year month)
+                   (i- day 1)))
+         (seconds (i+ seconds
+                      (i+ (i* minutes $1minute)
+                          (i* hours $1hour)))))
+      (+ (* days $24hours) seconds))
+    (encode-universal-time seconds minutes hours day month year 0)))
+
+
+(defun encode-integer-date (day month year)
+  "Encode a broken-down date specification into integer-time.
+  Input is the day, month, and year.
+  Result is an integer-time (seconds since lisp epoch).
+
+  This function is equivalent to (encode-integer-time 0 0 0 ...),
+  but can be 3x as fast."
+  (check-types (fixnum day month year))
+  (when (i< year 100)
+    (setq year (%year-from-2digit-year year)))
+  (if (and (i>= year $it-cache-first-year)
+           (i<= year $it-cache-last-year))
+    (* (i+ (%days-until-year-month year month) (i- day 1)) $24hours)
+    (encode-universal-time 0 0 0 day month year 0)))
+
+;; check to make sure it's not time to revisit the function "%year-from-2digit-year" above
+(assert (utc-is-before-utc-p
+         (make-instance 'zoned-time :utc (server-system-time-utc))
+         (make-instance 'zoned-time :utc (encode-integer-date 01 01 2030))))
+
+
+(defun encode-integer-tofd (seconds minutes hours)
+  "Encode a broken-down time-of-day specification into integer-time.
+  Input is the seconds, minutes, and hours.
+  Result is an integer-tofd (seconds since midnight).
+
+  This function is equivalent to (encode-integer-time ... 0 0 0),
+  but is 7.5x as fast and does almost no consing (since a tofd always
+  fits in a fixnum."
+  (check-types (fixnum seconds minutes hours))
+  (i+ seconds
+      (i+ (i* minutes $1minute)
+          (i* hours $1hour))))
+
+
+(defun decode-integer-time (time)
+  "Decode an integer-time into its broken-down components.
+
+  Input is an integer-time.
+  Result is the values
+      <seconds> <minutes> <hours> <day> <month> <year> <dofw>.
+  This function is equivalent to (decode-universal-time ... 0),
+  but is precomputed and thus faster for years in the range
+  [$it-cache-first-year,$it-cache-last-year]."
+  (check-types (integer-time time))
+  (multiple-value-bind (days tofd) (floor time $24hours)
+    (declare (type fixnum days tofd))
+    (if (and (i>= days $it-cache-first-day)
+             (i<= days $it-cache-last-day))
+      (multiple-value-bind (hours tofd-min) (floor tofd $1hour)
+       (declare (type fixnum hours tofd-min))
+       (multiple-value-bind (minutes seconds) (floor tofd-min $1minute)
+         (declare (type fixnum minutes seconds))
+         (values seconds minutes hours
+                 (svref $day-from-days days)
+                 (svref $month-from-days days)
+                 (svref $year-from-days days)
+                 (svref $dofw-from-days days))))
+      (multiple-value-bind (seconds minutes hours day month year weekday)
+         (decode-universal-time time 0)
+       (values seconds minutes hours day month year weekday)))))
+
+
+(defun decode-integer-date (date)
+  "Decode an integer-time into only its broken-down date components.
+
+  Input is an integer-time.
+  Result is the values <day> <month> <year> <dofw>.
+  This function is equivalent to (decode-universal-time ... 0),
+  but is precomputed and thus faster for years in the range
+  [$it-cache-first-year,$it-cache-last-year]."
+  (check-types (integer-date date))
+  (let ((days (floor date $24hours)))
+    (declare (type fixnum days))
+    (if (and (i>= days $it-cache-first-day)
+             (i<= days $it-cache-last-day))
+      (values (svref $day-from-days days)
+             (svref $month-from-days days)
+             (svref $year-from-days days)
+             (svref $dofw-from-days days))
+      (multiple-value-bind (seconds minutes hours day month year weekday)
+         (decode-universal-time date 0)
+       (declare (ignore seconds minutes hours))
+       (values day month year weekday)))))
+
+
+(defun decode-integer-tofd (tofd)
+  "Decode an integer-time into only its broken-down time-of-day components.
+
+  Input is an integer-time.
+  Result is the values <seconds> <minutes> <hours>.
+  This function is equivalent to (decode-universal-time ... 0),
+  but is precomputed and thus faster for years in the range
+  [$it-cache-first-year,$it-cache-last-year]."
+  (check-types (integer-tofd tofd))
+  (let ((tofd (mod tofd $24hours)))
+    (declare (type fixnum tofd))
+    (multiple-value-bind (hours tofd-min) (floor tofd $1hour)
+      (declare (type fixnum hours tofd-min))
+      (multiple-value-bind (minutes seconds) (floor tofd-min $1minute)
+        (declare (type fixnum minutes seconds))
+        (values seconds minutes hours)))))
+
+
+(defun-inline %days-until-month (month year)
+  "Return the number of days preceding the given month in the given year.
+   For example:  March, 2004 --> (3 2004) --> 60 days"
+  (i+ (svref $days-until-month (i1- month))
+      (if (and (i= 29 (%days-per-february-year year))
+               (> month 2))
+          1
+          0)))
+
+
+(defun day-of-year (time)
+  "Extract the 1-based day-of-year from an integer-time.
+    E.g. January 1 (of any year) --> 1
+
+  'time' is an integer-time.
+  Result is an integer in the range [1,366]."
+  (check-types (integer-time time))
+  (let ((days (floor time $24hours)))
+    (declare (type fixnum days))
+    (if (and (i>= days $it-cache-first-day)
+             (i<= days $it-cache-last-day))
+      ;; Using cached time constants
+      (let ((day   (svref $day-from-days days))
+            (month (svref $month-from-days days))
+            (year  (svref $year-from-days days)))
+        ;; (Days from epoch til 'time') - (Days from epoch til January)
+        (i+ day (%days-until-month month year)))
+      ;; Outside of time cache
+      (multiple-value-bind (s m h day month year)
+          (decode-universal-time time 0)
+        (declare (ignore s m h))
+        (i+ day
+            (loop for m from 1 below month
+                  sum (days-per-month m year)))))))
+
+
+(defun local-date-offset (time1 time2)
+  "Return the (integer) difference in _date_ between two local times.
+
+  This is essentially the difference in 'printed days', not the
+  difference in '24-hour periods'.
+  For example, '2005-03-05 23:00' and '2005-03-06 01:00' will yield
+  a 1 day difference in date, even though they are only 2 hours apart.
+
+  'time1' and 'time2' are local 'integer-times'.
+  The result is positive if time2 is later than time1."
+  (check-types (integer-time time1 time2))
+  (- (floor time2 $24hours) (floor time1 $24hours)))
+
+
+(defun-inline local-time-to-utc (time-local tzoffset)
+  "Given a local integer-time and a TZ offset, return the UTC integer-time."
+  (- time-local tzoffset))
+
+(defun-inline utc-time-to-local (time-utc tzoffset)
+  "Given a UTC integer-time and a TZ offset, return the local integer-time."
+  (+ time-utc tzoffset))
+
+
+(defun local-date-equal (date1 date2)
+  "Return T if 'date1' and 'date2' have equal day, month, and year.
+
+  'date1' and 'date2' are integer-time's."
+  (= (local-date-only date1) (local-date-only date2)))
+
+
+(defun merge-date-and-tofd (date tofd)
+  "Merge a 'date' with a 'tofd'.
+
+   Take the date components of 'date' (an integer-time)
+   and merge them with the time components of 'tofd' (another
+   integer-time), generating a new integer-time.
+
+   ('date' and 'time' should be local to the same TZ, although
+   the result is effectively in the same relative TZ as 'tofd'.)"
+  (check-types (integer date tofd))
+  (+ (local-date-only date) (local-tofd-only tofd)))
+
+
+(defun local-date-to-utc (local-date local-tofd tzoffset)
+  "Convert the given LOCAL-DATE/LOCAL-TOFD to UTC and return the date part.
+   For example: 1-MAR 23:00 EST -> 2-MAR"
+  (local-date-only (local-time-to-utc (merge-date-and-tofd local-date local-tofd) tzoffset)))
+
+(defun utc-date-to-local (utc-date local-tofd tzoffset)
+  "Return the local date for the given local time and UTC date.
+   For example: 2-MAR 23:00 EST -> 1-MAR"
+  (local-date-only (utc-time-to-local (merge-date-and-tofd utc-date (local-time-to-utc local-tofd tzoffset)) tzoffset)))
+
+(defun override-local-time (time &key seconds minutes hours day month year)
+  "Override specific date/tofd components of an integer-time.
+
+  Returns the new integer-time constructed by replacing the specified
+  components of the original integer-time 'time'."
+  (check-types (integer-time time)
+              ((or integer null) seconds minutes hours day month year))
+  (multiple-value-bind (_seconds _minutes _hours _day _month _year)
+      (decode-integer-time time)
+    (encode-integer-time (or seconds _seconds)
+                         (or minutes _minutes)
+                         (or hours   _hours)
+                         (or day     _day)
+                         (or month   _month)
+                         (or year    _year))))
+
+;;!!!TZ
+(defun advance-to-next-day-of-week (time
+                                   weekday ; 0-6 corresponds to mon-sun
+                                   &key keep-tofd-p)
+  "Return the time corresponding to the next WEEKDAY calculated from TIME.
+   WEEKDAY is an integer with 0 corresponding to Monday, 6 to Sunday.
+   KEEP-TOFD-P, if non-NIL, indicates that the time portion of TIME should be
+   carried over to the generated time.  By default, 0:00h is returned for the
+   time potions.  Returns a timezone agnostic universal time."
+  (check-types (integer-time time)
+              (fixnum weekday))
+  (multiple-value-bind (seconds minutes hours day month year old-weekday)
+      (decode-universal-time time 0)
+    (+ (encode-universal-time (if keep-tofd-p seconds 0)
+                               (if keep-tofd-p minutes 0)
+                               (if keep-tofd-p hours 0)
+                               day month year
+                               0)
+       (* $24hours (let ((v (- weekday old-weekday)))
+                     (if (< v 1)
+                       (+ v 7)
+                       v))))))
+
+;;; Time and date parsing
+
+(defconstant-equalp $uppercase-months
+  '#("JAN" "FEB" "MAR" "APR" "MAY" "JUN" "JUL" "AUG" "SEP" "OCT" "NOV" "DEC"))
+
+(defconstant-equalp $mixedcase-months
+  '#("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec"))
+
+(defconstant-equalp $lowercase-months
+  '#("jan" "feb" "mar" "apr" "may" "jun" "jul" "aug" "sep" "oct" "nov" "dec"))
+
+(defconstant-equalp $uppercase-months-long
+  #("JANUARY" "FEBRUARY" "MARCH" "APRIL" "MAY" "JUNE"
+    "JULY" "AUGUST" "SEPTEMBER" "OCTOBER" "NOVEMBER" "DECEMBER"))
+
+(defconstant-equalp $mixedcase-months-long
+  #("January" "February" "March" "April" "May" "June"
+    "July" "August" "September" "October" "November" "December"))
+
+(defconstant-equalp $lowercase-months-long
+  #("january" "february" "march" "april" "may" "june"
+    "july" "august" "september" "october" "november" "december"))
+
+
+(defconstant-equalp $uppercase-days
+  '#("MON" "TUE" "WED" "THU" "FRI" "SAT" "SUN"))
+
+(defconstant-equalp $mixedcase-days
+  '#("Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun"))
+
+(defconstant-equalp $lowercase-days
+  '#("mon" "tue" "wed" "thu" "fri" "sat" "sun"))
+
+(defconstant-equalp $uppercase-days-ws
+  '#("MON " "TUE " "WED " "THU " "FRI " "SAT " "SUN "))
+
+(defconstant-equalp $mixedcase-days-ws
+  '#("Mon " "Tue " "Wed " "Thu " "Fri " "Sat " "Sun "))
+
+(defconstant-equalp $lowercase-days-ws
+  '#("mon " "tue " "wed " "thu " "fri " "sat " "sun "))
+
+(defconstant-equalp $uppercase-days-long
+  '#("MONDAY" "TUESDAY" "WEDNESDAY" "THURSDAY" "FRIDAY" "SATURDAY" "SUNDAY"))
+
+(defconstant-equalp $mixedcase-days-long
+  '#("Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday" "Sunday"))
+
+(defconstant-equalp $lowercase-days-long
+  '#("monday" "tuesday" "wednesday" "thursday" "friday" "saturday" "sunday"))
+
+
+(defun month-name (month &key (size :short) (case :upper))
+  "Return a string containing the English name of a numeric month.
+
+  'month' belongs to the range [1,12] (1==January).
+  'size' is one of :short (default) or :long, specifying size of name.
+  'case' is one of :upper (default), :mixed or :lower.
+  Return value is a string, or NIL if the month is not in [1,12]."
+  (check-types (fixnum month))
+  (when (and (>= month 1) (<= month 12))
+    (svref (ecase size
+             (:short (ecase case
+                       (:upper $uppercase-months)
+                       (:mixed $mixedcase-months)
+                       (:lower $lowercase-months)))
+             (:long (ecase case
+                      (:upper $uppercase-months-long)
+                      (:mixed $mixedcase-months-long)
+                      (:lower $lowercase-months-long))))
+           (i1- month))))
+
+(defun dofw-name (dofw &key (size :short) (case :upper))
+  "Return a string containing the English name of a numeric day-of-week.
+
+  'dofw' belongs to the range [0,6] (0==Monday).
+  'size' is one of :short (default) or :long, specifying size of name.
+  'case' is one of :upper (default), :mixed or :lower.
+  Return value is a string, or NIL if the dofw is not in [0,6]."
+  (check-types (fixnum dofw))
+  (when (and (>= dofw 0) (<= dofw 6))
+    (svref (ecase size
+             (:short (ecase case
+                       (:upper $uppercase-days)
+                       (:mixed $mixedcase-days)
+                       (:lower $lowercase-days)))
+             (:long (ecase case
+                      (:upper $uppercase-days-long)
+                      (:mixed $mixedcase-days-long)
+                      (:lower $lowercase-days-long))))
+           dofw)))
+
+(defun local-date-long-dofw-name (time &key (case :upper))
+  "Return a string with the full English name of the day-of-week."
+  (check-types (integer-time time))
+  (multiple-value-bind (day month year dofw)
+      (decode-integer-date time)
+    (declare (ignore day month year))
+    (svref (ecase case
+             (:upper $uppercase-days-long)
+             (:lower $lowercase-days-long)
+             (:mixed $mixedcase-days-long))
+           dofw)))
+
+
+(defun explode-local-tofd (string &key (start 0) (end (length string)))
+  "Parse a string (or substring) as a local time-of-day.
+  Valid formats are:    HHMM      HHMMSS
+                     H..H:MM  H..H:MM:SS
+  Returns multiple-values:  <seconds> <minutes> <hours>"
+  (check-types (string string)
+              (integer start end))
+  (multiple-value-let*
+      (((end additional-hours twelve-is-zero)
+        (cond ((ends-with string "AM" :end end)
+               (values (- end 2) 0 t))
+              ((ends-with string "PM" :end end)
+               (values (- end 2) 12 t))
+              (t (values end 0 nil))))
+       (len (i- end start))
+       (all-digits t)
+       c1 c2
+       hours minutes seconds)
+    ;; find colons, check for digits
+    (loop for i fixnum from start below end
+          for c = (char string i)
+          doing
+          (unless (ascii-digit-p c) (setq all-digits nil))
+          (when (eql c #\:)
+            (cond ((not c1) (setq c1 i))
+                  ((not c2) (setq c2 i))
+                  (t (return-from explode-local-tofd (values nil nil nil))))))
+    (if (and all-digits
+             (or (i= len 4) (i= len 6)))
+      ;; HHMM or HHMMSS
+      (setq hours   (string-to-integer string
+                                       :start start :end (i+ start 2))
+            minutes (string-to-integer string
+                                       :start (i+ start 2) :end (i+ start 4))
+            seconds (and (i= len 6)
+                         (string-to-integer string
+                                            :start (i+ start 4) :end end)))
+      ;; H..H:MM or H..H:MM:SS
+      (setq hours   (and c1
+                         (string-to-integer string :start start :end c1))
+            minutes (and c1
+                         (string-to-integer string
+                                            :start (i1+ c1) :end (or c2 end)))
+            seconds (and c2
+                         (string-to-integer string :start (i1+ c2) :end end))))
+    (let ((hours (if (and hours twelve-is-zero (= hours 12))
+                   0
+                   hours)))
+      ;; S requires H and M, H and M require each other
+      (values (and minutes hours seconds)
+              (and hours minutes)
+              (and minutes hours (i+ hours additional-hours))))))
+
+
+(defun explode-local-time (string &key (start 0) (end (length string))
+                          default-year)
+  "Explode a string into (local) date and tofd components.
+   Valid input formats are (maybe?):
+
+   [D]D-MTH[-YY[YY]]
+   [D]D-MM-YYYY
+   [D]D[-]M[M][-]YYYY
+   YYYY-MM-D[D]
+   YYYY[-]M[M][-]D[D]
+   DDMMYY
+
+   MM-YY  - day is set to last day of month
+   MM-YYYY  - day is set to last day of month
+
+
+
+
+   Return values are:  <seconds> <minutes> <hours> <day> <month> <year>.
+   'year' may be NIL if no year was supplied."
+  (check-types (string string)
+              (fixnum start end))
+  (let ((point start)
+       (seconds  0)
+       (minutes  0)
+       (hours 0)
+       (day   nil)
+       (month nil)
+       (year  nil))
+    (labels ((punct (&optional (optional-p t))
+              (or (when (i<= point (i- end 1))
+                    (let ((ch (char string point)))
+                      (when (or (eql ch #\.) (eql ch #\-) (eql ch #\/))
+                        (iincf point)
+                        t)))
+                  optional-p))
+            (parse-day1 ()
+              (when (i<= point (1- end))
+                (let ((d (ascii-digit-p (char string point))))
+                  (when (and d (i> d 0))
+                    (iincf point)
+                    (setq day d)
+                    t))))
+            (parse-day2 ()
+              (when (i<= point (i- end 2))
+                (let ((d1 (ascii-digit-p (char string point)))
+                      (d2 (ascii-digit-p (char string (i+ point 1)))))
+                  (when (and d1 d2)
+                    (let ((d12 (i+ (i* d1 10) d2)))
+                      (when (or (i= d12 0)
+                                (i= d12 99)
+                                (and (i<= d12 31) (i> d12 0)))
+                        (iincf point 2)
+                        (setq day d12)
+                        t))))))
+            (parse-month1 ()
+              (when (i<= point (1- end))
+                (let ((d (ascii-digit-p (char string point))))
+                  (when (and d (i> d 0))
+                    (iincf point 1)
+                    (setq month d)
+                    t))))
+            (parse-month2 ()
+              (when (i<= point (i- end 2))
+                (let ((d1 (ascii-digit-p (char string point)))
+                      (d2 (ascii-digit-p (char string (i+ point 1)))))
+                  (when (and d1 d2)
+                    (let ((d12 (i+ (i* d1 10) d2)))
+                      (when (or (i= d12 0)
+                                (i= d12 99)
+                                (and (i<= d12 12) (i> d12 0)))
+                        (iincf point 2)
+                        (setq month d12)
+                        t))))))
+            (parse-month3 ()
+              (when (i<= point (i- end 3))
+                (let ((c1 (char string point))
+                      (c2 (char string (i+ point 1)))
+                      (c3 (char string (i+ point 2))))
+                  (when (and (ascii-letter-p c1) (ascii-letter-p c2) (ascii-letter-p c3))
+                    (loop for m fixnum from 0 below 12
+                          as s = (svref #("JAN" "FEB" "MAR" "APR" "MAY" "JUN"
+                                          "JUL" "AUG" "SEP" "OCT" "NOV" "DEC") m)
+                          when (and (char-equal c1 (char s 0))
+                                    (char-equal c2 (char s 1))
+                                    (char-equal c3 (char s 2)))
+                          do (iincf point 3)
+                          (setq month (i+ m 1))
+                          (return t))))))
+            (parse-year2 ()
+              (when (i<= point (i- end 2))
+                (let ((d1 (ascii-digit-p (char string point)))
+                      (d2 (ascii-digit-p (char string (i+ point 1)))))
+                  (when (and d1 d2)
+                    (iincf point 2)
+                    (setq year (i+ (i* d1 10) d2))
+                    t))))
+            (parse-year4 ()
+              (when (i<= point (i- end 4))
+                (let ((d1 (ascii-digit-p (char string point)))
+                      (d2 (ascii-digit-p (char string (i+ point 1))))
+                      (d3 (ascii-digit-p (char string (i+ point 2))))
+                      (d4 (ascii-digit-p (char string (i+ point 3)))))
+                  (when (and d1 d2 d3 d4)
+                    (let ((y (i+ (i+ (i* d1 1000) (i* d2 100))
+                                 (i+ (i* d3 10) d4))))
+                      (when (or (i= y 0)
+                                (i= y 9999)
+                                (and (i>= y 1300)
+                                      (i<= y 2600))) ; year must be after $time-in-distant-future
+                        (iincf point 4)
+                        (setq year y)
+                        t)))))))
+      (declare (dynamic-extent #'punct
+                              #'parse-day1 #'parse-day2
+                              #'parse-month1 #'parse-month2 #'parse-month3
+                              #'parse-year2 #'parse-year4))
+      (macrolet ((end ()
+                  '(when (or (i= point end)
+                          (char-equal (char string point) #\H)
+                          (char-equal (char string point) #\T)     ; XML dateTime type
+                          (eql (char string point) #\space))
+                    (unless (i= point end)
+                      ;; Parse the time
+                      (multiple-value-setq (seconds minutes hours)
+                        (explode-local-tofd string
+                                            :start (i+ point 1) :end end)))
+                    (return-from explode-local-time
+                       (values seconds minutes hours day month (or year default-year)))))
+                (and+ (&rest args)
+                  `(block end-of-and
+                     (let ((p point))
+                       ,@(mapcar (lambda (arg)
+                                   `(unless ,arg
+                                      (setq point p)
+                                      (return-from end-of-and nil)))
+                                 args)
+                       t))))
+       (or (and+ (or (parse-day2) (parse-day1))
+                 (punct)
+                 (parse-month3)
+                 (or (end)
+                     (and+ (punct)
+                           (or (parse-year4) (parse-year2))
+                           (end))))
+           (and+ (or (and+ (parse-year4) (punct)) t)
+                 (parse-month3)
+                 (punct)
+                 (or (parse-day2) (parse-day1))
+                 (end))
+           (and+ (parse-day2) (punct) (parse-month2) (punct) (parse-year4) (end))
+           (and+ (parse-day1) (punct) (parse-month2) (punct) (parse-year4) (end))
+           (and+ (or (parse-day2) (parse-day1)) (punct nil)
+                 (or (parse-month2) (parse-month1)) (punct nil)
+                 (parse-year4) (end))
+           (and+ (parse-year4) (punct) (parse-month2) (punct)
+                 (or (parse-day2) (parse-day1)) (end))
+           (and+ (parse-year4) (punct nil)
+                 (or (parse-month2) (parse-month1)) (punct nil)
+                 (or (parse-day2) (parse-day1)) (end))
+           (and (i= (length string) 6) ;ddmmyy, *not* mmddyy
+                (every #'ascii-digit-p string)
+                (and+ (parse-day2) (parse-month2) (parse-year2) (end)))
+           (and (i<= (length string) 5) ;mmyy or mm/yy
+                (and+ (parse-month2) (punct) (parse-year2)
+                      (setq day (days-per-month month year))
+                      (end)))
+           (and (i<= (length string) 7) ;mmyyyy or mm/yyyy
+                (and+ (parse-month2) (punct) (parse-year4)
+                      (setq day (days-per-month month year))
+                      (end))))))))
+
+
+(defun explode-iso8601-date (string
+                       &key (start 0) (end (length string)))
+  "Convert an ISO-8601 date string into its numerical components.
+  'string' may have one of precisely must be YYYY-MM-DD syntax.
+  Return values are:
+      <day> <month> <year>
+  If parsing fails, all NILs are returned."
+  (check-types (string string)
+              (fixnum start end))
+  ;; basic syntax check:  year, month, day, hour, minute pieces
+  (unless (and (eql (- end start) 10)
+               (eql (char string (i+ start 4)) #\-)
+               (eql (char string (i+ start 7)) #\-))
+    (return-from explode-iso8601-date
+      (values nil nil nil)))
+  (flet ((parse2 (string posn)
+          (let ((d1 (ascii-digit-p (char string posn)))
+                (d0 (ascii-digit-p (char string (i+ posn 1)))))
+            (and d1 d0
+                 (i+ (i* d1 10) d0))))
+        (parse4 (string posn)
+          (let ((d3 (ascii-digit-p (char string posn)))
+                (d2 (ascii-digit-p (char string (i+ posn 1))))
+                (d1 (ascii-digit-p (char string (i+ posn 2))))
+                (d0 (ascii-digit-p (char string (i+ posn 3)))))
+            (and d3 d2 d1 d0
+                 (+ (i* d3 1000) (i* d2 100) (i* d1 10) d0)))))
+    (declare (dynamic-extent #'parse2 #'parse4))
+    (let ((year   (parse4 string start))
+         (month  (parse2 string (i+ start 5)))
+         (day    (parse2 string (i+ start 8))))
+      ;; year, month, day, hour, minute are the bare-bones pieces.
+      (unless (and year month day)
+       (return-from explode-iso8601-date
+         (values nil nil nil nil nil nil nil nil nil nil)))
+        (values day month year))))
+
+(defun explode-iso8601-tofd (string
+                            &key (start 0) (end (length string)))
+  "Convert an ISO-8601 tofd string into its numerical components.
+  'string' may have one of these syntaxes:
+
+     hh:mm[:ss[.ffff...]]Z
+     hh:mm[:ss[.ffff...]]Saa[:bb]
+     hh:mm[:ss[.ffff...]]
+
+  where:   hh = hour
+           mm = minute
+           ss = second
+        fff.. = fractional seconds
+            Z = literal 'Z', meaning a UTC time
+            S = timezone offset sign:  either '-' or '+'
+           aa = timezone offset hour
+           bb = timezone offset minute
+
+  The return values are the components:
+      <seconds>             [optional]
+      <minutes> <hours>     [required]
+      <tz-offset minutes> <tz-offset hours> <tz-offset sign> [optional]
+      <seconds fraction>    [optional]
+
+  All values are integers, except for <seconds fraction> (a rational),
+   or missing optional values (NIL).
+
+  <tz-offset sign> is zero iff the timestring is in the UTC 'Z' form;
+   if any timezone is specified (even 00:00), it will be +1 or -1.
+   <tz-offset minutes/hours> are zero if <tz-offset sign> is zero.
+   All tz-offset values are NIL if timezone fields are not supplied.
+
+  If parsing fails, all NILs are returned."
+  (check-types (string string)
+              (fixnum start end))
+  ;; basic syntax check:  hour, minute pieces
+  (unless (eql (char string (i+ start 2)) #\:)
+    (return-from explode-iso8601-tofd
+      (values nil nil nil nil nil nil nil)))
+  (let* ((size   (- end start))
+         (has-seconds (and (> size 5)
+                           (eql (char string (i+ start 5)) #\:)))
+         (has-fraction (and has-seconds
+                            (> size 8)
+                            (eql (char string (i+ start 8)) #\.)))
+         (end-of-tofd (cond (has-fraction (or (position-if-not #'ascii-digit-p
+                                                               string
+                                                               :start (i+ start 9)
+                                                               :end end)
+                                              end))
+                            (has-seconds (i+ start 8))
+                            (t (i+ start 5))))
+         (tz-sign (and (> end end-of-tofd)
+                       (cond ((eql (char string end-of-tofd) #\Z) 0)
+                             ((eql (char string end-of-tofd) #\+) +1)
+                             ((eql (char string end-of-tofd) #\-) -1)
+                             (t (return-from explode-iso8601-tofd
+                                  (values nil nil nil nil nil nil nil)))))))
+    (flet ((parse2 (string posn)
+            (let ((d1 (ascii-digit-p (char string posn)))
+                  (d0 (ascii-digit-p (char string (i+ posn 1)))))
+              (and d1 d0
+                   (i+ (i* d1 10) d0))))
+          (parse-frac (string start end)
+             (loop with place-value = (/ 1 10)
+                   for spot from start below end
+                   summing (let ((d (ascii-digit-p (char string spot))))
+                             (assert d () "~A is not a digit as expected" (char string spot))
+                             (* place-value d))
+                   doing (setq place-value (/ place-value 10)))))
+      (declare (dynamic-extent #'parse2 #'parse-frac))
+      (let ((hour   (parse2 string start))
+           (minute (parse2 string (i+ start 3)))
+            (second (and has-seconds
+                         (parse2 string (i+ start 6))))
+            (fraction (and has-fraction
+                           (parse-frac string (i+ start 9) end-of-tofd))))
+       ;; hour, minute are the bare-bones pieces.
+        (unless (and hour minute)
+          (return-from explode-iso8601-tofd
+            (values nil nil nil nil nil nil nil)))
+        (multiple-value-bind (tz-hour tz-minute)
+            (cond ((not tz-sign)   (values nil nil))
+                  ((zerop tz-sign) (values 0 0))
+                  ((and (i= (i+ end-of-tofd 6) end)
+                       (eql (char string (i+ end-of-tofd 3)) #\:))
+                  (values (parse2 string (i+ end-of-tofd 1))
+                          (parse2 string (i+ end-of-tofd 4))))
+                 ((i= (i+ end-of-tofd 3) end)
+                  (values (parse2 string (i+ end-of-tofd 1))
+                          nil))
+                 (t
+                   (return-from explode-iso8601-tofd nil)))
+          (values second minute hour
+                  tz-minute tz-hour tz-sign
+                  fraction))))))
+
+(defun explode-iso8601 (string
+                       &key (start 0) (end (length string)))
+  "Convert an ISO-8601 date+tofd string into its numerical components.
+  'string' may have one of these syntaxes:
+
+     YYYY-MM-DDThh:mm[:ss[.ffff...]]Z
+     YYYY-MM-DDThh:mm[:ss[.ffff...]]Saa[:bb]
+     YYYY-MM-DDThh:mm[:ss[.ffff...]]
+
+  where: YYYY = year
+           MM = month
+           DD = day/date
+            T = literal 'T'
+           hh = hour
+           mm = minute
+           ss = second
+        fff.. = fractional seconds
+            Z = literal 'Z', meaning a UTC time
+            S = timezone offset sign:  either '-' or '+'
+           aa = timezone offset hour
+           bb = timezone offset minute
+
+  The return values are the components:
+      <seconds>             [optional]
+      <minutes> <hours>     [required]
+      <day> <month> <year>  [required]
+      <tz-offset minutes> <tz-offset hours> <tz-offset sign> [optional]
+      <seconds fraction>    [optional]
+
+  All values are integers, except for <seconds fraction> (a rational),
+   or missing optional values (NIL).
+
+  <tz-offset sign> is zero iff the timestring is in the UTC 'Z' form;
+   if any timezone is specified (even 00:00), it will be +1 or -1.
+   <tz-offset minutes/hours> are zero if <tz-offset sign> is zero.
+   All tz-offset values are NIL if timezone fields are not supplied.
+
+  If parsing fails, all NILs are returned."
+  (check-types (string string)
+              (fixnum start end))
+  ;; basic syntax check:  year, month, day, hour, minute pieces
+  (unless (and (>= (- end start) 16)
+              (eql (char string (i+ start 10)) #\T)
+               (eql (char string (i+ start 13)) #\:))
+    (return-from explode-iso8601
+      (values nil nil nil nil nil nil nil nil nil nil)))
+  (multiple-value-bind (day month year)
+      (explode-iso8601-date string :start start :end (+ start 10))
+    (multiple-value-bind (second minute hour
+                                tz-minute tz-hour tz-sign
+                                fraction)
+       (explode-iso8601-tofd string :start (+ start 11) :end  end)
+      ;; year, month, day, hour, minute are the bare-bones pieces.
+      (unless (and year month day hour minute)
+       (return-from explode-iso8601
+         (values nil nil nil nil nil nil nil nil nil nil)))
+      (values second minute hour day month year
+             tz-minute tz-hour tz-sign
+             fraction))))
+
+(defun parse-duration (string &key (start 0) (end (length string)))
+  "Convert the duration 'string' to a number of seconds.  'string' may take
+  one of the following forms:
+    HH:MM   HH:MM:SS   HHMM   HHMMSS
+   +HH:MM  +HH:MM:SS  +HHMM  +HHMMSS
+   -HH:MM  -HH:MM:SS  -HHMM  -HHMMSS
+  The sign of the result reflects the sign of the duration."
+  (check-types (string string)
+              (integer start end))
+  (let ((sign  +1))
+    (cond ((char-equal (char string start) #\+)
+          (iincf start))
+         ((char-equal (char string start) #\-)
+          (setf sign -1)
+          (iincf start)))
+    (multiple-value-bind (seconds minutes hours)
+       (explode-local-tofd string :start start :end end)
+      (if (and minutes hours)
+        (* sign (+ (* (or seconds 0) $1second)
+                   (* minutes $1minute)
+                   (* hours $1hour)))
+        NIL))))
+
+
+(defun parse-local-tofd (string &key (start 0) (end (length string)))
+  "Parse a string (or substring) as a local tofd.
+   Valid formats are:  HHMM   HHMMSS
+                       HH:MM  HH:MM:SS
+   Returns the number of seconds since (local) midnight."
+  (check-types (string string)
+              (fixnum start end))
+  (multiple-value-bind (seconds minutes hours)
+      (explode-local-tofd string :start start :end end)
+    (if (and minutes hours)
+      (+ (or seconds 0) (* minutes $1minute) (* hours $1hour))
+      NIL)))
+
+
+;;!!!TZ Need a better way to deal with inferring a missing YEAR.
+
+(defun parse-local-time (string &key (start 0) (end (length string))
+                                    (guess-year :utc-future)
+                                     (guess-year-offset 0)
+                                     (guess-century :unix-y2k))
+
+  "Parse a (sub)string as a local time (date+tofd).
+
+  STRING is the time string to parse.
+
+  START and END are optional bounds.
+
+  GUESS-YEAR specifies how the year should be inferred if it is
+  missing from STRING.  The options for GUESS-YEAR are:
+
+        A number - The given number is used when STRING does not specify a year.
+     :utc-future - Choose the year such that the time is in the future (or same date), relative to
+                   the current printed UTC date.  [default]
+    :utc-current - Use the year from the current printed UTC date.
+             NIL - Return NIL if no year is specified.
+       :utc-past - Choose the year such that the time is in the past (or same date), relative to the
+                   current printed UTC date.
+
+  GUESS-YEAR-OFFSET specifies an offset (in seconds) which is added
+  to the current UTC time before inferring the year for a yearless
+  STRING.  It is applicable only when GUESS-YEAR is :UTC-FUTURE or
+  :UTC-PAST.  GUESS-YEAR-OFFSET defaults to zero.
+
+  GUESS-CENTURY specifies how the year should be interpreted if
+  it is specified as (exactly) two digits.  The options are:
+
+    :utc-nearest - Choose a date within 50 years of the current UTC date.
+       :utc-past - Choose the latest century giving a past UTC date.
+     :utc-future - Choose the earliest century not giving a past UTC date.
+       :unix-y2k - Calls %year-from-2digit-year, resulting in a year
+                   in the range 1970 to 2069 [default].
+
+  The function returns a local datetime as an integer-time, or NIL."
+  
+  (check-types (string string)
+              (fixnum start end))
+  (multiple-value-bind (seconds minutes hours day month year)
+      (explode-local-time string :start start :end end)
+    (when (and minutes hours day month
+               (>= month 1) (<= month 12)
+               (> day 0))
+      (cond
+       ((null year)
+        (setq year (cond
+                     ((eq guess-year :utc-future)
+                      (multiple-value-bind (curday curmon curyear)
+                          (decode-integer-date (i+ (current-time-utc) guess-year-offset))
+                        (cond ((i< curmon month) curyear)
+                              ((and (i= curmon month)
+                                    (i<= curday day)) curyear)
+                              (t (i+ curyear 1)))))
+                     ((eq guess-year :utc-past)
+                      (multiple-value-bind (curday curmon curyear)
+                          (decode-integer-date (i+ (current-time-utc) guess-year-offset))
+                        (cond ((i< month curmon) curyear)
+                              ((and (i= curmon month)
+                                    (i<= day curday)) curyear)
+                              (t (i- curyear 1)))))
+                     ((eq guess-year :utc-current)
+                      (nth-value 2 (decode-integer-date (current-time-utc))))
+                     ((numberp guess-year) guess-year)
+                     (t (assert (null guess-year) ()
+                                "Invalid :guess-year option '~A'" guess-year)))))
+       ((i< year 100)
+        (setq year (multiple-value-bind (curday curmon curyear)
+                       (decode-integer-date (current-time-utc))
+                     (let* ((adjusted-curyear (cond ((i< curmon month) curyear)
+                                                     ((and (i= curmon month)
+                                                           (i<= curday day)) curyear)
+                                                     (t (i+ curyear 1))))
+                            ;; Fenceposting: year-offset 0 denotes a date between now and a year hence.
+                            (year-offset (ecase guess-century
+                                           (:unix-y2k
+                                            (i- (%year-from-2digit-year year) adjusted-curyear))
+                                           (:utc-nearest
+                                            (i+ -50 (mod (i- year adjusted-curyear 50) 100)))
+                                           (:utc-past
+                                            (i+ -100 (mod (i- year adjusted-curyear) 100)))
+                                           (:utc-future
+                                            (mod (i- year adjusted-curyear) 100)))))
+                       (i+ adjusted-curyear year-offset))))))
+
+      (when (and year
+                 (<= day (days-per-month month year)))
+       (encode-integer-time (or seconds 0) minutes hours day month year)))))
+
+
+(defun parse-local-date (string &key (start 0) (end (length string))
+                                    (guess-year :utc-future)
+                                     (guess-year-offset 0)
+                                     (guess-century :unix-y2k))
+
+  "Parse a (sub)string as a local date, neglecting tofd components.
+
+  STRING is the time string to parse.
+
+  START and END are optional bounds.
+
+  GUESS-YEAR specifies how the year should be inferred if it is
+  missing from STRING.  The options for GUESS-YEAR are:
+
+        a number - The given number is used when STRING does not
+                   specify a year.
+     :utc-future - Choose the year such that the time is in the
+                   future, relative to the current printed UTC
+                   time.  [default]
+    :utc-current - Use the year from the current printed UTC time.
+             NIL - Return NIL if no year is specified.
+       :utc-past - Choose the year such that the time is in the
+                   past, relative to the current printed UTC time.
+
+  GUESS-YEAR-OFFSET specifies an offset (in seconds) which is added
+  to the current UTC time before inferring the year for a yearless
+  STRING. It is applicable only when GUESS-YEAR is :UTC-FUTURE or
+  :UTC-PAST.  GUESS-YEAR-OFFSET defaults to zero.
+
+  GUESS-CENTURY specifies how the year should be interpreted if
+  it is specified as (exactly) two digits.  The options are:
+
+    :utc-nearest - Choose a date within 50 years of the current UTC date.
+       :utc-past - Choose the latest century giving a past UTC date.
+     :utc-future - Choose the earliest century not giving a past UTC date.
+       :unix-y2k - Calls %year-from-2digit-year, resulting in a year
+                   in the range 1970 to 2069 [default].
+
+  The function returns a local date represented as an integer-time, or NIL."
+  
+  (let ((time (parse-local-time string :start start :end end
+                                :guess-year guess-year
+                                :guess-year-offset guess-year-offset
+                                :guess-century guess-century)))
+    (and time (local-date-only time))))
+
+
+(defun parse-iso8601-date (string &key (start 0) (end (length string)))
+  "Convert an ISO-8601 date (sub)string into an integer-date.
+   Must be in YYYY-MM-DD syntax.
+   If parsing fails, NIL is returned."
+  (check-types (string string)
+              (fixnum start end))
+  (multiple-value-bind (day month year)
+      (explode-iso8601-date string :start start :end end)
+    (when (and day month year)
+      (encode-integer-date day month year))))
+
+(defun parse-iso8601-utc (string &key (start 0) (end (length string)))
+  "Convert an ISO-8601 date+tofd (sub)string into a UTC integer-time.
+  (For allowed syntaxes, see 'explode-iso8601'.)
+  Any fractional seconds are truncated.
+  If parsing fails, NIL is returned."
+  (check-types (string string)
+              (fixnum start end))
+  (multiple-value-bind (second minute hour day month year
+                       tz-minute tz-hour tz-sign)
+      (explode-iso8601 string :start start :end end)
+    (when (and minute hour day month year)
+      (if (and tz-sign tz-hour)
+       (- (encode-integer-time (or second 0) minute hour day month year)
+          (i* tz-sign (i+ (* $1hour tz-hour) (* $1minute (or tz-minute 0)))))
+       (encode-integer-time (or second 0) minute hour day month year)))))
+
+(defun parse-iso8601-local (string &key (start 0) (end (length string)))
+  "Convert an ISO-8601 date+tofd (sub)string into an integer-time.
+  (For allowed syntaxes, see 'explode-iso8601'.)
+  Any fractional seconds are truncated.
+  Any time zone specification is ignored.
+  If parsing fails, NIL is returned."
+  (check-types (string string)
+              (fixnum start end))
+  (multiple-value-bind (second minute hour day month year
+                       tz-minute tz-hour tz-sign)
+      (explode-iso8601 string :start start :end end)
+    (declare (ignore tz-minute tz-hour tz-sign))
+    (when (and minute hour day month year)
+      (encode-integer-time (or second 0) minute hour day month year))))
+
+(defun parse-iso8601-tofd-zoned (string &key (start 0) (end (length string)))
+  "Convert an ISO-8601 tofd (sub)string into a zoned-time.
+  (For allowed syntaxes, see 'explode-iso8601-tofd'.)
+  Any fractional seconds are truncated.
+  If parsing fails, NIL is returned."
+  (check-types (string string)
+              (fixnum start end))
+  (multiple-value-bind (second minute hour
+                        tz-minute tz-hour tz-sign)
+      (explode-iso8601-tofd string :start start :end end)
+    (when (and minute hour tz-sign tz-hour)
+      (let ((offset (* tz-sign (+ (* $1hour tz-hour) (* $1minute (or tz-minute 0))))))
+       (make-instance 'zoned-time
+         :utc (- (encode-integer-time (or second 0) minute hour
+                                      01 01 2000)
+                 offset)
+         :tzoffset offset)))))
+
+(defun parse-iso8601-zoned (string &key (start 0) (end (length string)))
+  "Convert an ISO-8601 date+tofd (sub)string into a zoned-time.
+  (The allowed syntax is YYYY-MM-DDThh:mm[:ss[.ffff...]]Saa:bb;
+  see 'explode-iso8601' for an explanation.)
+  Any fractional seconds are truncated.
+  If parsing fails, NIL is returned."
+  (check-types (string string)
+              (fixnum start end))
+  (multiple-value-bind (second minute hour day month year
+                        tz-minute tz-hour tz-sign)
+      (explode-iso8601 string :start start :end end)
+    (when (and minute hour day month year tz-sign tz-hour tz-minute)
+      (let ((offset (* tz-sign (+ (* $1hour tz-hour) (* $1minute tz-minute)))))
+       (make-instance 'zoned-time
+         :utc (- (encode-integer-time (or second 0) minute hour
+                                      day month year)
+                 offset)
+         :tzoffset offset)))))
+
+(defun merge-date-and-tofd-zoned (date zul &optional (local-date-offset 0))
+  "Combines an integer-date and a the time portion of a zoned-time,
+   and optionally a day offset for the local-date."
+  (let* ((tzoffset (tzoffset zul))
+        (local-time (+ (utc-time zul) tzoffset)))
+    (make-instance 'zoned-time
+      ;; combine local date with local time, then convert back to UTC
+      :utc (- (+ (local-date-only date) (* local-date-offset $24hours) (local-tofd-only local-time)) tzoffset)
+      :tzoffset tzoffset)))
+
+;;; Time and date printing
+
+(defun write-duration (duration &optional (stream *standard-output*)
+                      &key (format :hh-mm)
+                           (show-plus nil))
+  ;; Note: default format :hh-mm ONLY valid for durations under 100 hrs
+  ;; The current code will use cryptic ASCII characters under 790 hrs or so,
+  ;; then may use unicode characters under a few million hours, then overflow
+  ;; and cause a Lisp error. This suggests a code review at some point,
+  ;; including a robustification of many such functions and their callers.
+  ;; I filed bug 46257 for such a review. --fare 2008-08-15
+  (let ((sign (cond ((< duration 0) "-")
+                   (show-plus "+")
+                   (t nil)))
+       (duration (abs duration)))
+    (multiple-value-bind (hrs rem) (floor duration $1hour)
+      (multiple-value-bind (mins secs) (floor rem $1minute)
+       (macrolet ((write-nn (i)
+                    `(multiple-value-bind (d2 d1) (floor ,i 10)
+                      (write-char (code-char (i+ d2 #.(char-code #\0))) stream)
+                      (write-char (code-char (i+ d1 #.(char-code #\0))) stream))))
+         (when sign
+           (write-string sign stream))
+         (ecase format
+           (:hhmm ;ONLY valid for durations under 100 hrs
+            (write-nn hrs)
+            (write-nn mins))
+           (:hh-mm ;ONLY valid for durations under 100 hrs
+            (write-nn hrs)
+            (write-string ":" stream)
+            (write-nn mins))
+           (:h*-mm
+             (format stream "~2,'0D" hrs)
+            (write-string ":" stream)
+            (write-nn mins))
+           (:hh-mm-ss ;ONLY valid for durations under 100 hrs
+            (write-nn hrs)
+            (write-string ":" stream)
+            (write-nn mins)
+            (write-string ":" stream)
+            (write-nn secs))
+           (:h*-mm-ss
+             (format stream "~D" hrs)
+            (write-string "h" stream)
+            (write-nn mins)
+            (write-string "m" stream)
+            (write-nn secs)
+            (write-string "s" stream))
+            )))))
+  nil)
+
+
+(defun write-exploded-date (day month year weekday
+                           &optional (stream *standard-output*)
+                           &key (format :yyyy-mm-dd)
+                                 (show-weekday nil)
+                                 (use-uppercase t))
+  "Write an exploded local date to a stream; input is the component fixnums
+    day, month, year, weekday.
+   If 'weekday' is not NIL, ':show-weekday' specifies where to write an
+    English weekday:
+       :before, :before-dash - before the date
+       :after,  :dash-after  - after the date
+           NIL - do not display weekday
+   If 'use-uppercase' is NIL, mixed-case is used for English months and
+    weekdays.
+
+   Possible output formats are:
+
+    English Month:   :ddmth        '07JAN'
+                     :ddmthyy      '07JAN99'
+                     :ddmthyyyy    '07JAN1999'
+                     :dd-mth-yyyy  '07-JAN-1999'
+
+    Numeric Month:   :ddmmyy       '070199'
+                     :mmddyy       '010799'
+                     :ddmmyyyy     '07011999'
+                     :yyyymmdd     '19990107'
+                     :yymmdd       '990107'
+                     :yyyy-mm-dd   '1999-01-07'
+                     :nu/mm/yy     '01/99'
+                     :nu/mm/yyyy   '01/1999'
+                     :nu/mmyy      '0199'
+
+   Return value is NIL."
+  (check-types (fixnum day month year)
+              ((or fixnum null) weekday))
+  (macrolet ((write-nn (i)
+              `(multiple-value-bind (d2 d1) (floor ,i 10)
+                (write-char (code-char (i+ d2 #.(char-code #\0))) stream)
+                (write-char (code-char (i+ d1 #.(char-code #\0))) stream)))
+            (write-mth (m)
+              `(write-string (svref (if use-uppercase
+                                        $uppercase-months
+                                        $mixedcase-months) (i- ,m 1)) stream))
+            (write-weekday-before ()
+              `(when (and weekday (or (eq show-weekday :before) (eq show-weekday :before-dash)))
+                (write-string (svref (if use-uppercase
+                                         $uppercase-days-ws
+                                         $mixedcase-days) weekday) stream)
+                 (when (eq show-weekday :before-dash)
+                   (write-char #\- stream))))
+            (write-weekday-after ()
+              `(when (and weekday (or (eq show-weekday :after) (eq show-weekday :dash-after)))
+                 (when (eq show-weekday :dash-after)
+                   (write-char #\- stream))
+                (write-string (svref (if use-uppercase
+                                         $uppercase-days-ws
+                                         $mixedcase-days) weekday) stream))))
+    (ecase format
+      ;; English Month formats
+      (:ddmth                          ;DDMTH
+       (write-weekday-before)
+       (write-nn day)
+       (write-mth month)
+       (write-weekday-after))
+      (:ddmthyy                                ;DDMTHYY
+       (write-weekday-before)
+       (write-nn day)
+       (write-mth month)
+       (write-nn (mod year 100))
+       (write-weekday-after))
+      (:ddmthyyyy                      ;DDMTHYYYY
+       (write-weekday-before)
+       (write-nn day)
+       (write-mth month)
+       (write-integer year stream)
+       (write-weekday-after))
+      (:dd-mth-yyyy                    ;DD-MTH-YYYY
+       (write-weekday-before)
+       (write-nn day)
+       (write-string "-" stream)
+       (write-mth month)
+       (write-string "-" stream)
+       (write-integer year stream)
+       (write-weekday-after))
+      ;; Numeric Month formats
+      (:ddmmyy                         ;DDMMYY
+       (write-weekday-before)
+       (write-nn day)
+       (write-nn month)
+       (write-nn (mod year 100))
+       (write-weekday-after))
+      (:mmddyy                         ;MMDDYY
+       (write-weekday-before)
+       (write-nn month)
+       (write-nn day)
+       (write-nn (mod year 100))
+       (write-weekday-after))
+      (:ddmmyyyy                       ;DDMMYYYY
+       (write-weekday-before)
+       (write-nn day)
+       (write-nn month)
+       (write-integer year stream)
+       (write-weekday-after))
+      (:mmddyyyy                       ;MMDDYYYY
+       (write-weekday-before)
+       (write-nn month)
+       (write-nn day)
+       (write-integer year stream)
+       (write-weekday-after))
+      (:yyyymmdd                       ;YYYYMMDD - for TCN reporting
+       (write-weekday-before)
+       (write-integer year stream)
+       (write-nn month)
+       (write-nn day)
+       (write-weekday-after))
+      (:yyyymm                         ;YYYYMM - for BIDT reporting
+       (write-weekday-before)
+       (write-integer year stream)
+       (write-nn month)
+       (write-weekday-after))
+      (:yymmdd                        ;YYMMDD - used in edifact's UNB+IATA... header
+       (write-nn (mod year 100))
+       (write-nn month)
+       (write-nn day))
+      (:yyyy-mm-dd                     ;YYYY-MM-DD
+       (write-weekday-before)
+       (write-integer year stream)
+       (write-string "-" stream)
+       (write-nn month)
+       (write-string "-" stream)
+       (write-nn day)
+       (write-weekday-after))
+      (:yyyy-mm                                ;YYYY-MM
+       (write-integer year stream)
+       (write-string "-" stream)
+       (write-nn month))
+      ;; was :mmyy
+      (:nu/mm/yy                       ;MM/YY, for expiration dates
+       (write-nn month)
+       (write-string "/" stream)
+       (write-nn (mod year 100)))
+      (:nu/mm/yyyy                     ;MM/YYYY, for expiration dates
+       (write-nn month)
+       (write-string "/" stream)
+       (write-integer year stream))
+      ;; Some special-purpose formats
+      (:d                              ;With :hhmm below, for AIRIMP
+       (write-nn day))
+      ;; was :ccexp
+      (:nu/mmyy                                ;MMYY, for expiration dates
+       (write-nn month)
+       (write-nn (mod year 100)))
+      )))
+
+
+(defun write-exploded-tofd (seconds minutes hours
+                           &optional (stream *standard-output*)
+                           &key (format :hh-mm))
+  "Write an exploded local tofd to a stream; input is the component
+    seconds, minutes, and hours.
+   Possible output formats are:
+      :hh-mm = HH:MM  :hh-mm-ss = HH:MM:SS   :hh-mm-ap = HH:MMAM, HH:MMPM
+      :_h-mm =  H:MM                         :_h-mm-ap =  H:MMa,   H:MMp
+      :hhmm  = HHMM   :hhmmss   = HHMMSS     :hhmmap   = HHMMA, HHMMP
+                                             :_hmmap   =  HMMA,  HMMP
+
+   The \"_h\" formats use a leading space instead of a leading zero.
+   Return value is NIL."
+  (check-types (fixnum seconds minutes hours))
+  ;; Avoid using 'format' since it conses so much
+  (flet ((write-nn (i)
+          (declare (type fixnum i))
+          (multiple-value-bind (d2 d1) (floor i 10)
+              (write-char (code-char (+ d2 #.(char-code #\0))) stream)
+              (write-char (code-char (+ d1 #.(char-code #\0))) stream)))
+         (write-_n (i) ; space padded to two digits
+           (declare (type fixnum i))
+           (multiple-value-bind (d2 d1) (floor i 10)
+             (write-char (if (> d2 0)
+                             (code-char (i+ d2 #.(char-code #\0)))
+                             #\SPACE) stream)
+             (write-char (code-char (i+ d1 #.(char-code #\0))) stream)))
+        (ampm-hours (hours)
+          (declare (type fixnum hours))
+          (cond ((= hours 0) 12)
+                ((= hours 24) 0)
+                ((> hours 12) (- hours 12))
+                (t hours))))
+    (declare (inline write-nn ampm-hours)
+            (dynamic-extent #'write-nn #'ampm-hours))
+    (ecase format
+      (:hh-mm                          ;hh:mm
+       (write-nn hours)
+       (write-string ":" stream)
+       (write-nn minutes))
+      (:_h-mm                          ;_h:mm
+       (write-_n hours)
+       (write-string ":" stream)
+       (write-nn minutes))
+      (:hh-mm-ss                       ;hh:mm:ss
+       (write-nn hours)
+       (write-string ":" stream)
+       (write-nn minutes)
+       (write-string ":" stream)
+       (write-nn seconds))
+      (:hhmm                           ;hhmm
+       (write-nn hours)
+       (write-nn minutes))
+      (:hhmmss                         ;hhmmss
+       (write-nn hours)
+       (write-nn minutes)
+       (write-nn seconds))
+      (:hhmmap                         ;hhmmA or hhmmP
+       (write-nn (ampm-hours hours))
+       (write-nn minutes)
+       (write-string (if (>= hours 12) "P" "A") stream))
+      (:_hmmap
+       (write-_n (ampm-hours hours))
+       (write-nn minutes)
+       (write-string (if (>= hours 12) "P" "A") stream))
+      (:hh-mm-ap                       ;hh:mmAM or hh:mmPM
+       (write-nn (ampm-hours hours))
+       (write-string ":" stream)
+       (write-nn minutes)
+       (write-string (if (>= hours 12) " PM" " AM") stream))
+      (:_h-mm-ap                       ;_H:MMa or _H:MMp
+       (write-_n (ampm-hours hours))
+       (write-string ":" stream)
+       (write-nn minutes)
+       (write-char (if (>= hours 12) #\p #\a) stream))
+      (:h-mm-ap-word ;H:MMa or HH:MMa or H:MMp or midnight or noon or ...
+       (cond
+         ((and (i= hours 0) (i= minutes 0))  (write-string "midnight" stream))
+         ((and (i= hours 12) (i= minutes 0)) (write-string "noon" stream))
+         (t
+          (write-integer (ampm-hours hours) stream)
+          (write-string ":" stream)
+          (write-nn minutes)
+          (write-char (if (>= hours 12) #\p #\a) stream))))
+      ))
+  nil)
+
+
+(defun write-zoned-time (zul &optional (stream *standard-output*)
+                        &key (date-as :yyyy-mm-dd)
+                             (time-as :hh-mm time-as-supplied-p)
+                             (show-weekday nil)
+                             (as-utc nil)
+                             (show-timezone t show-tz-supplied-p)
+                             (use-uppercase t))
+  "Write the zoned-time ZUL to the STREAM.  If DATE-AS is specified as :ISO8601,
+   use ISO-8601 format with possible variations as specified by DATE-AS and
+   TIME-AS.  AS-UTC, if non-NIL, indicates that the the time should be considered
+   as being UTC, with no timezone information output.  The DATE-AS and TIME-AS
+   arguments accept format arguments as accepted by WRITE-EXPLODED-DATE and
+   WRITE-EXPLODED-TOFD."
+  (check-types (zoned-time zul))
+  (multiple-value-bind (seconds minutes hours day month year weekday)
+      (if as-utc
+       (decode-integer-time (utc-time zul))
+       (decode-integer-time (local-time zul)))
+
+    (case date-as
+      (:iso8601
+       (write-exploded-date day month year weekday stream
+                           :format :yyyy-mm-dd
+                           :show-weekday nil
+                           :use-uppercase use-uppercase)
+       (write-string "T" stream)
+       (unless time-as-supplied-p (setf time-as :hh-mm-ss))
+       (write-exploded-tofd seconds minutes hours stream
+                           :format time-as)
+       (if as-utc
+        (write-string "Z" stream)
+        (write-duration (tzoffset zul) stream
+                        :format :hh-mm :show-plus t)))
+      (otherwise
+       (when date-as
+        (write-exploded-date day month year weekday stream
+                             :format date-as
+                             :show-weekday show-weekday
+                             :use-uppercase use-uppercase)
+         (when time-as
+           (write-string " " stream)))
+       (when time-as
+        (write-exploded-tofd seconds minutes hours stream
+                             :format time-as))
+       (when (and (or show-tz-supplied-p time-as)
+                 show-timezone)
+        (write-duration (tzoffset zul) stream
+                        :format (case time-as
+                                  ((:hhmm :hhmmss :hhmmap) :hhmm)
+                                  (otherwise :hh-mm))
+                         :show-plus t)))
+      ))
+  nil)
+
+(defun cl-user::zoned-time (stream zul colon-p atsign-p)
+  "Format function for displaying a zoned time, with defaulted write-zoned-time parameters."
+  (declare (ignore colon-p atsign-p))
+  (write-zoned-time zul stream))
+
+
+(defun write-local-tofd (tofd &optional (stream *standard-output*)
+                        &key (format :hh-mm))
+  "Write a local tofd to a stream.  'tofd' is an integer-tofd,
+    i.e. seconds from midnight.
+   Possible output formats are:
+      :hh-mm = HH:MM  :hh-mm-ss = HH:MM:SS   :hh-mm-ap = HH:MMAM, HH:MMPM
+      :hhmm  = HHMM   :hhmmss   = HHMMSS     :hhmmap   = HHMMA, HHMMP
+   Return value is NIL."
+  (declare (type integer-tofd tofd)
+          (type stream stream)
+          (type keyword format))
+  (multiple-value-bind (seconds minutes hours)
+      (decode-integer-tofd tofd)
+    (write-exploded-tofd seconds minutes hours stream :format format)))
+
+
+(defun write-local-date (date &optional (stream *standard-output*)
+                        &key (format :yyyy-mm-dd)
+                             (show-weekday nil)
+                             (use-uppercase t))
+  "Write the given local integer-date DATE to STREAM according to the FORMAT,
+   SHOW-WEEKDAY and USE-UPPERCASE arguments as expected by WRITE-EXPLODED-DATE."
+  (multiple-value-bind (day month year weekday)
+      (decode-integer-date date)
+    (write-exploded-date day month year weekday stream
+                        :format format
+                        :show-weekday show-weekday
+                        :use-uppercase use-uppercase)))
+
+(defun local-date-to-string-format (date format) 
+  "Return the given local integer-date DATE as formatted string according to the FORMAT,
+   SHOW-WEEKDAY and USE-UPPERCASE arguments as expected by WRITE-EXPLODED-DATE."
+  (check-types (integer-time date))
+  (with-output-to-string (stream)
+    (write-local-date date stream 
+      :format format :show-weekday nil :use-uppercase t)))
+
+(defun local-date-to-iso8601-string (date)
+  "Return the given local integer-date DATE as a string formatted as YYYY-MM-DDD."
+  (local-date-to-string-format date :yyyy-mm-dd))
+
+;; ---???hh I wonder why no error is signaled for unknown FORMAT arguments here?
+(defun local-date-to-string (date &optional format)
+  "Return the given local integer-date DATE as string.  FORMAT specifies how the
+   date should be formatted.
+
+   The following keywords are accepted for FORMAT:
+
+   :DATE-ONLY -> DDMTHYYY
+   :BRIEF -> DD-MTH-YYYY HH:MM
+   any of (:DDMTH :DDMTHYY :DDMTHYYYY :DD-MTH-YYYY :DDMMYY 
+           :MMDDYY :DDMMYYYY :MMDDYYYY :YYYYMMDD :YYYYMM :YYMMDD :YYYY-MM-DD)
+    -> whatever WRITE-LOCAL-DATE produces for that format specification.
+   For all other values of FORMAT, YYYY-MM-DD HH:MM:SS is produced."
+  (check-types (integer-time date))
+  (with-output-to-string (stream)
+    (case format
+      (:date-only
+       (write-local-date date stream
+                        :format :ddmthyyyy
+                        :show-weekday nil
+                        :use-uppercase t))
+      (:brief
+       (write-local-date date stream
+                        :format :dd-mth-yyyy
+                        :show-weekday nil
+                        :use-uppercase t)
+       (write-string " " stream)
+       (write-local-tofd date stream :format :hh-mm))
+      ;;---*** DLD: Shouldn't this take all the formats WRITE-LOCAL-DATE takes?
+      ;;---*** msalib: why yes, yes it should
+      ((:ddmth :ddmthyy :ddmthyyyy :dd-mth-yyyy :ddmmyy 
+       :mmddyy :ddmmyyyy :mmddyyyy 
+       :yyyymmdd :yyyymm :yymmdd :yyyy-mm-dd)
+       (write-local-date date stream
+                        :format format
+                        :show-weekday nil
+                        :use-uppercase t))
+      (otherwise
+       (write-local-date date stream
+                        :format :yyyy-mm-dd
+                        :show-weekday nil
+                        :use-uppercase t)
+       (write-string " " stream)
+       (write-local-tofd date stream :format :hh-mm-ss)))))
+
+(defun local-tofd-to-string (tofd &optional (format :hh-mm))
+  "Return the given time of day TOFD as string.  FORMAT may be specified to
+   control the format produced by WRITE-LOCAL-TOFD, it defaults to :HH-MM."
+  (check-types (integer-time tofd))
+  (with-output-to-string (stream)
+    (write-local-tofd tofd stream :format format)))
+
+(defun compute-date-change (time1-zul time2-zul)
+  "If TIME1-ZUL and TIME2-ZUL are on the same date, return nil, otherwise return
+   the days between the two.  See LOCAL-DATE-OFFSET for an explanation of
+   the value returned."
+  (let ((offset (local-date-offset (local-time time1-zul) (local-time time2-zul))))
+    (cond ((izerop offset) nil)
+         (t offset))))
+  
+(defun date-change-indicator (time1-zul time2-zul)
+  "Return a ``date change'' indicator that describes how many days are between
+   TIME1-ZUL and TIME2-ZUL.  If they are on the same date, \" \" is returned,
+   otherwise it will be a string consisting of a #\+ or a #\- if TIME1-ZUL is
+   before or after TIME2-ZUL and the number of days between the two."
+  (let ((delta (compute-date-change time1-zul time2-zul)))
+    (cond ((null    delta) "  ")
+         ((iminusp delta) (format nil "-~D" (- delta)))
+         ((iplusp  delta) (format nil "+~D" delta)))))
+
+(defun write-iso8601-time (time utc/local time-format &optional (stream *standard-output*))
+  (check-types (integer-time time))
+  (multiple-value-bind (seconds minutes hours day month year weekday)
+      (decode-integer-time time)
+    (write-exploded-date day month year weekday stream
+                        :format :yyyy-mm-dd
+                        :show-weekday nil)
+    (write-string "T" stream)
+    (write-exploded-tofd seconds minutes hours stream
+                        :format time-format)
+    (ecase utc/local
+      (:utc (write-string "Z" stream))
+      (:local nil))
+    nil))
+
+(defun write-iso8601-utc (time-utc &optional (stream *standard-output*))
+  "Write an integer-time to a stream in ISO-8601 format.
+ decode as UTC, write with UTC/ZULU indicator."
+  (write-iso8601-time time-utc :utc :hh-mm-ss stream))
+
+(defun write-iso8601-local (time &optional (stream *standard-output*))
+  "Write an integer-time to a stream in ISO-8601 format.
+ decode as UTC, write as local (w/o ZULU indicator)."
+  (write-iso8601-time time :local :hh-mm-ss stream))
+
+(defun time-to-iso8601-string (time utc/local time-format)
+  "Create a string in ISO-8601 format from an integer-time."
+  (with-output-to-string (stream)
+    (write-iso8601-time time utc/local time-format stream)))
+
+(defun utc-to-iso8601-string (time-utc)
+  "Create a string in ISO-8601 format from a UTC integer-time."
+  (time-to-iso8601-string time-utc :utc :hh-mm-ss))
+
+#|
+write-date defaults:  :dd-mmm-yyyy
+                      no weekday
+                      with time
+                      no tzone
+
+:ddmmm        n
+:dd-mmm       n
+:ddmmm[yy]    n
+:ddmmmyy      n
+:dd-mmm[-yy]  n
+:ddmmmyyyy    n
+:dd-mmm-yyyy  maybe write-time, maybe write-tzone
+:ddmmyy       n
+:ddmmyyyy     n
+:dd-mm-yy    maybe write-time, maybe write-tzone  " HH:MM:SS" "[+/-]HH:MM"
+:dd-mm-yyyy  maybe write-time, maybe write-tzone
+:yyyy-mm-dd  maybe write-time, maybe write-tzone
+:mmyy      n
+:mmmyyyy   n
+:ccexp     n
+:ddmmm365  n
+|#
+
+(defun cl-user::utc%ISO8601 (stream utc colon-p at-sign-p)
+  "Format function for dates, prints UTC in ISO-8601 format to STREAM.  If
+   prefixed by a colon in the format specification (e.g. ~:/utc%ISO8601/),
+   uppercase letters are used.  UTC may be either an integer-time or a
+   zoned-time, but it is always printed as UTC time."
+  
+  (declare (ignore at-sign-p))
+  (check-types ((or integer-time zoned-time) utc))
+  (etypecase utc
+    (integer-time (write-iso8601-utc utc stream))
+    (zoned-time (write-zoned-time utc stream
+                                 :date-as :iso8601 :as-utc t :use-uppercase (not colon-p)))))
+
+(defun cl-user::utc%DDMTH (stream utc colon-p at-sign-p)
+  "Format function for dates, prints UTC in DDMTH format to STREAM.  If prefixed
+   by a colon in the format specification (e.g. ~:/utc%DDMTH/), uppercase
+   letters are used.  UTC may be either an integer-time or a zoned-time, but it
+   is always printed as UTC time."
+  (declare (ignore at-sign-p))
+  (check-types ((or integer-time zoned-time) utc))
+  (etypecase utc
+    (integer-time (multiple-value-bind (secs mins hrs day mth yr wkday)
+                     (decode-integer-time utc)
+                   (declare (ignore hrs secs mins))
+                   (write-exploded-date day mth yr wkday stream :format :ddmth :show-weekday nil)))
+    (zoned-time (write-zoned-time utc stream
+                                 :date-as :ddmth :as-utc t :time-as nil :use-uppercase (not colon-p)))))
+
+(defun cl-user::utc%DDMTHYYYY (stream utc colon-p at-sign-p)
+  "Format function for dates, prints UTC in DDMTHYYYY format to STREAM.  If
+   prefixed by a colon in the format specification (e.g. ~:/utc%DDMTHYYYY/),
+   uppercase letters are used.  UTC may be either an integer-time or a
+   zoned-time, but it is always printed as UTC time."
+  (declare (ignore at-sign-p))
+  (check-types ((or integer-time zoned-time) utc))
+  (etypecase utc
+    (integer-time (multiple-value-bind (secs mins hrs day mth yr wkday)
+                     (decode-integer-time utc)
+                   (declare (ignore hrs secs mins))
+                   (write-exploded-date day mth yr wkday stream :format :ddmthyyyy :show-weekday nil)))
+    (zoned-time (write-zoned-time utc stream
+                                 :date-as :ddmthyyyy :as-utc t :time-as nil :use-uppercase (not colon-p)))))
+
+(defun cl-user::zul%ISO8601 (stream zul colon-p at-sign-p)
+  (declare (ignore at-sign-p))
+  (check-types (zoned-time zul))
+  
+  (write-zoned-time zul stream :date-as :iso8601 :use-uppercase (not colon-p)))
+
+(defun cl-user::loc%DDMTH (stream date colon-p at-sign-p)
+  (declare (ignore at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :ddmth :use-uppercase (not colon-p)))
+
+(defun cl-user::loc%DDMTHYY (stream date colon-p at-sign-p)
+  (declare (ignore at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :ddmthyy :use-uppercase (not colon-p)))
+
+(defun cl-user::loc%DDMTHYYYY (stream date colon-p at-sign-p)
+  (declare (ignore at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :ddmthyyyy :use-uppercase (not colon-p)))
+
+(defun cl-user::loc%DDMTHYYWW (stream date colon-p at-sign-p)
+  (declare (ignore at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :ddmthyy :show-weekday :after :use-uppercase (not colon-p)))
+
+;; was DDMMMYYYY
+(defun cl-user::loc%DD-MTH-YYYY (stream date colon-p at-sign-p)
+  (declare (ignore at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :dd-mth-yyyy :use-uppercase (not colon-p)))
+
+(defun cl-user::loc%DDMMYY (stream date colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :ddmmyy))
+
+(defun cl-user::loc%YYMMDD (stream date colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :yymmdd))
+
+(defun cl-user::loc%WW-YYYY-MM-DD (stream date colon-p at-sign-p)
+  (declare (ignore at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream
+                   :format :yyyy-mm-dd :show-weekday :before :use-uppercase (not colon-p)))
+
+#||
+(defun cl-user::loc%WW-YYYY-MTH-DD (stream date colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :yyyy-mth-dd :show-weekday :before))
+||#
+
+(defun cl-user::loc%WW-DD-MTH-YYYY (stream date colon-p at-sign-p)
+  (declare (ignore at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream
+                   :format :dd-mth-yyyy :show-weekday :before :use-uppercase (not colon-p)))
+
+;; was YYYYMMDDHHMM
+(defun cl-user::loc%YYYY-MM-DD-HH-MM-SS (stream date colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :yyyy-mm-dd)
+  (write-string " " stream)
+  (write-local-tofd date stream :format :hh-mm-ss))
+
+(defun cl-user::loc%YYYY-MM-DDTHH-MM-SS (stream date colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :yyyy-mm-dd)
+  (write-string "T" stream)
+  (write-local-tofd date stream :format :hh-mm-ss))
+
+(defun cl-user::loc%YYYY-MM-DD (stream date colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :yyyy-mm-dd))
+
+(defun cl-user::loc%YYYY-MM-DDTHH-MM (stream date colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :yyyy-mm-dd)
+  (write-string "T" stream)
+  (write-local-tofd date stream :format :hh-mm))
+
+;; was MMYY
+(defun cl-user::loc%MM-YY (stream date colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :nu/mm/yy))
+
+;; was CCEXP
+(defun cl-user::loc%MMYY (STREAM date colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :nu/mmyy))
+
+(defun cl-user::loc%YYYY-MM (STREAM date colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time date))
+  (write-local-date date stream :format :yyyy-mm))
+
+
+;; moved 'ddmmm365' to locations.lisp as 'loc%airimp-date'
+
+(defun cl-user::loc%HH-MM (stream time colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (write-local-tofd (get-local-time time) stream :format :hh-mm))
+
+(defun cl-user::loc%_H-MM (stream time colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time time))
+  (write-local-tofd time stream :format :_h-mm))
+
+(defun cl-user::loc%HHMM (stream time colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time time))
+  (write-local-tofd time stream :format :hhmm))
+
+(defun cl-user::loc%HHMMAP (stream time colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time time))
+  (write-local-tofd time stream :format :hhmmap))
+
+(defun cl-user::loc%_HMMAP (stream time colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time time))
+  (write-local-tofd time stream :format :_hmmap))
+
+;; was HHMMAMPM
+(defun cl-user::loc%HH-MM-AP (stream time colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time time))
+  (write-local-tofd time stream :format :hh-mm-ap))
+
+
+(defun cl-user::loc%H-MM-AP-WORD (stream time colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer-time time))
+  (write-local-tofd time stream :format :h-mm-ap-word))
+
+
+(defun cl-user::dur%H_M (stream duration colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer duration))
+  (let ((sign (cond ((< duration 0)
+                     (setf duration (- duration))
+                     "(-)")
+                    (t ""))))
+    (multiple-value-bind (s minutes hours)
+        (decode-integer-tofd duration)
+      (declare (ignore s))
+      (if (zerop hours)
+       (format stream "~A~Dm" sign minutes)
+       (format stream "~A~Dh ~Dm" sign hours minutes)))))
+
+(defun cl-user::dur%HHMM (stream duration colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer duration))
+  (let ((sign (cond ((< duration 0)
+                     (setf duration (- duration))
+                     "-")
+                    (t ""))))
+    (multiple-value-bind (s minutes hours)
+        (decode-integer-tofd duration)
+      (declare (ignore s))
+      (if (zerop hours)
+       (format stream "~A0:~2,'0D" sign minutes)
+       (format stream "~A~D:~2,'0D" sign hours minutes)))))
+
+(defun cl-user::dur%+HHMM (stream duration colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer duration))
+  (let ((sign (cond ((< duration 0)
+                     (setf duration (- duration))
+                     "-")
+                    (t "+"))))
+    (multiple-value-bind (s minutes hours)
+        (decode-integer-tofd duration)
+      (declare (ignore s))
+      (if (zerop hours)
+       (format stream "~A0:~2,'0D" sign minutes)
+       (format stream "~A~D:~2,'0D" sign hours minutes)))))
+
+(defun cl-user::dur%_hmm (stream duration colon-p at-sign-p)
+  (declare (ignore colon-p at-sign-p))
+  (check-types (integer duration))
+  (let ((sign (cond ((< duration 0)
+                     (setf duration (- duration))
+                     "-")
+                    (t ""))))
+    (multiple-value-bind (s minutes hours)
+        (decode-integer-tofd duration)
+      (declare (ignore s))
+      (if (zerop hours)
+       (format stream "~A 0:~2,'0D" sign minutes)
+       (format stream "~A~2,' D:~2,'0D" sign hours minutes)))))
+
+;; "WKD1, MTH1 DD1"
+;; "WKD1-WKD2, MTH1 DD1-DD2"
+;; "WKD1-WKD2, MTH1 DD1-MTH2 DD2"
+;; "MTH1 DD1, YYYY1"
+;; "MTH1 DD1-MTH2 DD2, YYY1"
+;; "MTH1 DD1, YYY1-MTH2 DD2, YYY2"
+;;
+;; case must be :UPPER or :MIXED.
+;;
+;; show-weekday must be NIL, :UPFRONT, :INTERSPERSED, or :AUTOMATIC.  If :AUTOMATIC, then acts like
+;; :UPFRONT for ranges up to 7 days, and :INTERSPERSED for ranges of 8 days or more.
+;;
+(defun write-local-date-range (time1 time2 &optional (stream *standard-output*)
+                                    &key (case :upper) long-p (show-weekday :AUTOMATIC)
+                                         show-year-p)
+  (unless (>= time2 time1)
+    (rotatef time1 time2))
+
+  (flet ((write-weekday (dofw)
+          (write-string (svref (if long-p
+                                   (ecase case
+                                     (:upper $uppercase-days-long)
+                                        ;(:lower $lowercase-days-long)
+                                     (:mixed $mixedcase-days-long))
+                                   (ecase case
+                                     (:upper $uppercase-days)
+                                        ;(:lower $lowercase-days)
+                                     (:mixed $mixedcase-days)))
+                               dofw)
+                        stream))
+        (write-month (m)
+          (write-string (svref (if long-p
+                                   (ecase case
+                                     (:upper $uppercase-months-long)
+                                        ;(:lower $lowercase-months-long)
+                                     (:mixed $mixedcase-months-long))
+                                   (ecase case
+                                     (:upper $uppercase-months)
+                                        ;(:lower $lowercase-months)
+                                     (:mixed $mixedcase-months)))
+                               (i- m 1)) stream)))
+
+    (declare (dynamic-extent #'write-weekday #'write-month))
+    (multiple-value-bind (d1 m1 y1 dofw1)
+       (decode-integer-date time1)
+      (multiple-value-bind (d2 m2 y2 dofw2)
+         (decode-integer-date time2)
+       (let* ((same-day-p (local-date-equal time1 time2))
+              (same-year-p (i= y1 y2))
+              (same-month-p (or same-day-p (and same-year-p (i= m1 m2)))))
+         (when (eq show-weekday :AUTOMATIC)
+           (setf show-weekday (if (<= (local-date-offset time1 time2) 7) :UPFRONT :INTERSPERSED)))
+         (when show-weekday
+           (write-weekday dofw1)
+           (unless same-day-p
+             (when (eq show-weekday :UPFRONT)
+               (write-string "-" stream)
+               (write-weekday dofw2)))
+           (write-string ", " stream))
+         (write-month m1)
+         (write-string " " stream)
+         (write-integer d1 stream)
+         (when (and show-year-p (or same-day-p (not same-year-p)))
+           (write-string ", " stream)
+           (write-integer y1 stream))
+         (unless same-day-p
+           (if (eq show-weekday :INTERSPERSED)
+               (progn
+                 (write-string " - " stream)
+                 (write-weekday dofw2)
+                 (write-string ", " stream))
+               (write-string "-" stream))
+           (when (or (not same-month-p) (eq show-weekday :INTERSPERSED))
+             (write-month m2)
+             (write-string " " stream))
+           (write-integer d2 stream)
+           (when show-year-p
+             (write-string ", " stream)
+             (write-integer y2 stream))))))))
+
+
+;; "WKD1, MTH1 DD1"
+;; "WKD1-WKD2, MTH1 DD1-DD2"
+;; "WKD1-WKD2, MTH1 DD1-MTH2 DD2"
+;; 'time-pair' should be a cons of two integer-times
+(defun cl-user::loc%WW-MTH-DD-PAIR (stream time-pair colon-p at-sign-p)
+  (write-local-date-range (car time-pair) (cdr time-pair) stream
+                         :case (if colon-p :mixed :upper) :long-p at-sign-p))
+
+(defun cl-user::loc%WW-MTH-DD (stream date-local colon-p at-sign-p)
+  (check-types (integer-date date-local))
+  (let ((case (if colon-p :mixed :upper))
+        (long-p at-sign-p))
+    (flet ((write-weekday (dofw)
+             (write-string (svref (if long-p
+                                      (ecase case
+                                        (:upper $uppercase-days-long)
+                                        ;(:lower $lowercase-days-long)
+                                        (:mixed $mixedcase-days-long))
+                                      (ecase case
+                                        (:upper $uppercase-days)
+                                        ;(:lower $lowercase-days)
+                                        (:mixed $mixedcase-days)))
+                                  dofw)
+                           stream))
+           (write-month (m)
+             (write-string (svref (if long-p
+                                      (ecase case
+                                        (:upper $uppercase-months-long)
+                                        ;(:lower $lowercase-months-long)
+                                        (:mixed $mixedcase-months-long))
+                                      (ecase case
+                                        (:upper $uppercase-months)
+                                        ;(:lower $lowercase-months)
+                                        (:mixed $mixedcase-months)))
+                                  (i- m 1)) stream)))
+      (declare (dynamic-extent #'write-weekday #'write-month))
+      (multiple-value-bind (d m y dofw)
+          (decode-integer-date date-local)
+        (declare (ignore y))
+        (write-weekday dofw)
+        (write-string ", " stream)
+        (write-month m)
+        (write-string " " stream)
+        (write-integer d stream)))))
+
+
+(defun %show-all-new-time-formats (time)
+  (cl-user::loc%DDMTH *standard-output* time nil nil) (write-char #\Newline)
+  (cl-user::loc%DDMTHYY *standard-output* time nil nil) (write-char #\Newline)
+  (cl-user::loc%DD-MTH-YYYY *standard-output* time nil nil) (write-char #\Newline)
+  (cl-user::loc%YYYY-MM-DD-HH-MM-SS *standard-output* time nil nil) (write-char #\Newline)
+  (cl-user::loc%MM-YY *standard-output* time nil nil) (write-char #\Newline)
+  (cl-user::loc%MMYY *standard-output* time nil nil) (write-char #\Newline)
+  (cl-user::loc%HHMM *standard-output* time nil nil) (write-char #\Newline)
+  (cl-user::loc%HHMMAP *standard-output* time nil nil) (write-char #\Newline)
+  (cl-user::loc%HH-MM-AP *standard-output* time nil nil) (write-char #\Newline)
+  time
+  )
+
+(defun-inline itus-to-ms (itus)
+  (round itus #.(/ internal-time-units-per-second 1000)))
+
+(defun-inline itus-to-us (itus)
+  (round itus #.(/ internal-time-units-per-second (expt 10 6))))