Skip to content
time.lisp 100 KiB
Newer Older
Mahmud's avatar
Mahmud committed
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;                                                                  ;;;
;;; Free Software under MIT-Style license. See file LICENSE.         ;;;
Mahmud's avatar
Mahmud committed
;;;                                                                  ;;;
;;; Copyright (c) 2005-2008 ITA Software, Inc.  All rights reserved. ;;;
;;;                                                                  ;;;
;;; Original author: Matt Marjanovic                                 ;;;
;;;                                                                  ;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

#+xcvb (module (:depends-on ("macros")))

(in-package :quux-time)
Mahmud's avatar
Mahmud committed


;;; 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+
Mahmud's avatar
Mahmud committed
  (- (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)
Mahmud's avatar
Mahmud committed
                            (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-unreadable-object (zul stream :type t :identity t)
Mahmud's avatar
Mahmud committed
    (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.
Mahmud's avatar
Mahmud committed
(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 (1+ (- $it-cache-last-year $it-cache-first-year))))))
Mahmud's avatar
Mahmud committed
511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000
    (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]."