diff options
Diffstat (limited to 'sql/time.lisp')
-rw-r--r-- | sql/time.lisp | 1359 |
1 files changed, 1359 insertions, 0 deletions
diff --git a/sql/time.lisp b/sql/time.lisp new file mode 100644 index 0000000..0bb18c3 --- /dev/null +++ b/sql/time.lisp @@ -0,0 +1,1359 @@ +;;;; -*- Mode: LISP; Syntax: ANSI-Common-Lisp; Base: 10 -*- +;;;; ************************************************************************* +;;;; +;;;; A variety of structures and function for creating and +;;;; manipulating dates, times, durations and intervals for +;;;; CLSQL. +;;;; +;;;; This file was originally part of ODCL and is Copyright (c) 2002 - +;;;; 2003 onShore Development, Inc. +;;;; +;;;; CLSQL users are granted the rights to distribute and use this software +;;;; as governed by the terms of the Lisp Lesser GNU Public License +;;;; (http://opensource.franz.com/preamble.html), also known as the LLGPL. +;;;; ************************************************************************* + +(in-package #:clsql-sys) + +;; ------------------------------------------------------------ +;; Months + +(defvar *month-keywords* + '(:january :february :march :april :may :june :july :august :september + :october :november :december)) + +(defvar *month-names* + '("" "January" "February" "March" "April" "May" "June" "July" "August" + "September" "October" "November" "December")) + +(defun month-name (month-index) + (nth month-index *month-names*)) + +(defun ordinal-month (month-keyword) + "Return the zero-based month number for the given MONTH keyword." + (position month-keyword *month-keywords*)) + + +;; ------------------------------------------------------------ +;; Days + +(defvar *day-keywords* + '(:sunday :monday :tuesday :wednesday :thursday :friday :saturday)) + +(defvar *day-names* + '("Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday")) + +(defun day-name (day-index) + (nth day-index *day-names*)) + +(defun ordinal-day (day-keyword) + "Return the zero-based day number for the given DAY keyword." + (position day-keyword *day-keywords*)) + + +;; ------------------------------------------------------------ +;; time classes: wall-time, duration + +(eval-when (:compile-toplevel :load-toplevel) + +(defstruct (wall-time (:conc-name time-) + (:constructor %make-wall-time) + (:print-function %print-wall-time)) + (mjd 0 :type fixnum) + (second 0 :type fixnum) + (usec 0 :type fixnum)) + +(defun %print-wall-time (time stream depth) + (declare (ignore depth)) + (if *print-escape* + (format stream "#<WALL-TIME: ~a>" (format-time nil time)) + (format-time stream time :format :pretty))) + +(defstruct (duration (:constructor %make-duration) + (:print-function %print-duration)) + (year 0 :type fixnum) + (month 0 :type fixnum) + (day 0 :type fixnum) + (hour 0 :type fixnum) + (second 0 :type fixnum) + (minute 0 :type fixnum) + (usec 0 :type fixnum)) + +(defun %print-duration (duration stream depth) + (declare (ignore depth)) + (if *print-escape* + (format stream "#<DURATION: ~a>" + (format-duration nil duration :precision :second)) + (format-duration stream duration :precision :second))) + +(defstruct (date (:constructor %make-date) + (:print-function %print-date)) + (mjd 0 :type fixnum)) + +(defun %print-date (date stream depth) + (declare (ignore depth)) + (if *print-escape* + (format stream "#<DATE: ~a>" (format-date nil date)) + (format-date stream date :format :pretty))) + +);eval-when + +(defun duration-timestring (duration) + (let ((second (duration-second duration)) + (minute (duration-minute duration)) + (hour (duration-hour duration)) + (day (duration-day duration)) + (month (duration-month duration)) + (year (duration-year duration))) + (format nil "P~dY~dM~dD~dH~dM~dS" year month day hour minute second))) + + +;; ------------------------------------------------------------ +;; Constructors + +(defun make-time (&key (year 0) (month 1) (day 1) (hour 0) (minute 0) + (second 0) (usec 0) (offset 0)) + (let ((mjd (gregorian-to-mjd month day year)) + (sec (+ (* hour 60 60) + (* minute 60) + second (- offset)))) + (multiple-value-bind (day-add raw-sec) + (floor sec (* 60 60 24)) + (%make-wall-time :mjd (+ mjd day-add) :second raw-sec :usec usec)))) + +(defun make-date (&key (year 0) (month 1) (day 1) (hour 0) (minute 0) + (second 0) (usec 0) (offset 0)) + (time->date (make-time :year year :month month :day day :hour hour + :minute minute :second second :usec usec :offset offset))) + +(defun copy-time (time) + (%make-wall-time :mjd (time-mjd time) + :second (time-second time))) + +(defun utime->time (utime) + "Return a pair: (GREGORIAN DAY . TIME-OF-DAY)" + (multiple-value-bind (second minute hour day mon year) + (decode-universal-time utime) + (make-time :year year :month mon :day day :hour hour :minute minute + :second second))) + +(defun date->time (date) + "Returns a walltime for the given date" + (%make-wall-time :mjd (date-mjd date))) + +(defun time->date (time) + "Returns a date for the given wall time (obvious loss in resolution)" + (%make-date :mjd (time-mjd time))) + +(defun get-time () + "Return a pair: (GREGORIAN DAY . TIME-OF-DAY)" + (utime->time (get-universal-time))) + +(defun get-date () + "Returns a date for today" + (time->date (get-time))) + +(defun make-duration (&key (year 0) (month 0) (day 0) (hour 0) (minute 0) + (second 0) (usec 0)) + (multiple-value-bind (second-add usec-1000000) + (floor usec 1000000) + (multiple-value-bind (minute-add second-60) + (floor (+ second second-add) 60) + (multiple-value-bind (hour-add minute-60) + (floor (+ minute minute-add) 60) + (multiple-value-bind (day-add hour-24) + (floor (+ hour hour-add) 24) + (%make-duration :year year :month month :day (+ day day-add) + :hour hour-24 + :minute minute-60 + :second second-60 + :usec usec-1000000)))))) + + +;; ------------------------------------------------------------ +;; Accessors + +(defun time-hms (time) + (multiple-value-bind (hourminute second) + (floor (time-second time) 60) + (multiple-value-bind (hour minute) + (floor hourminute 60) + (values hour minute second)))) + +(defun time-ymd (time) + (destructuring-bind (month day year) + (mjd-to-gregorian (time-mjd time)) + (values year month day))) + +(defun time-dow (time) + "Return the 0 indexed Day of the week starting with Sunday" + (mod (+ 3 (time-mjd time)) 7)) + +(defun decode-time (time) + "returns the decoded time as multiple values: usec, second, minute, hour, + day, month, year, integer day-of-week" + (multiple-value-bind (year month day) + (time-ymd time) + (multiple-value-bind (hour minute second) + (time-hms time) + (values (time-usec time) second minute hour day month year (mod (+ (time-mjd time) 3) 7))))) + +(defun date-ymd (date) + (time-ymd (date->time date))) + +(defun date-dow (date) + (time-dow (date->time date))) + +(defun decode-date (date) + "returns the decoded date as multiple values: day month year integer day-of-week" + (multiple-value-bind (year month day) + (time-ymd (date->time date)) + (values day month year (date-dow date)))) + +;; duration specific +(defun duration-reduce (duration precision &optional round) + (ecase precision + (:usec + (+ (duration-usec duration) + (* (duration-reduce duration :second) 1000000))) + (:second + (+ (if round + (floor (duration-usec duration) 500000) + 0) + (duration-second duration) + (* (duration-reduce duration :minute) 60))) + (:minute + (+ (if round + (floor (duration-second duration) 30) + 0) + (duration-minute duration) + (* (duration-reduce duration :hour) 60))) + (:hour + (+ (if round + (floor (duration-minute duration) 30) + 0) + (duration-hour duration) + (* (duration-reduce duration :day) 24))) + (:day + (+ (if round + (floor (duration-hour duration) 12) + 0) + (duration-day duration))))) + + +;; ------------------------------------------------------------ +;; Arithemetic and comparators + +(defun duration= (duration-a duration-b) + (= (duration-reduce duration-a :usec) + (duration-reduce duration-b :usec))) + +(defun duration< (duration-a duration-b) + (< (duration-reduce duration-a :usec) + (duration-reduce duration-b :usec))) + +(defun duration<= (duration-a duration-b) + (<= (duration-reduce duration-a :usec) + (duration-reduce duration-b :usec))) + +(defun duration>= (x y) + (duration<= y x)) + +(defun duration> (x y) + (duration< y x)) + +(defun %time< (x y) + (let ((mjd-x (time-mjd x)) + (mjd-y (time-mjd y))) + (if (/= mjd-x mjd-y) + (< mjd-x mjd-y) + (if (/= (time-second x) (time-second y)) + (< (time-second x) (time-second y)) + (< (time-usec x) (time-usec y)))))) + +(defun %time>= (x y) + (if (/= (time-mjd x) (time-mjd y)) + (>= (time-mjd x) (time-mjd y)) + (if (/= (time-second x) (time-second y)) + (>= (time-second x) (time-second y)) + (>= (time-usec x) (time-usec y))))) + +(defun %time<= (x y) + (if (/= (time-mjd x) (time-mjd y)) + (<= (time-mjd x) (time-mjd y)) + (if (/= (time-second x) (time-second y)) + (<= (time-second x) (time-second y)) + (<= (time-usec x) (time-usec y))))) + +(defun %time> (x y) + (if (/= (time-mjd x) (time-mjd y)) + (> (time-mjd x) (time-mjd y)) + (if (/= (time-second x) (time-second y)) + (> (time-second x) (time-second y)) + (> (time-usec x) (time-usec y))))) + +(defun %time= (x y) + (and (= (time-mjd x) (time-mjd y)) + (= (time-second x) (time-second y)) + (= (time-usec x) (time-usec y)))) + +(defun time= (number &rest more-numbers) + "Returns T if all of its arguments are numerically equal, NIL otherwise." + (do ((nlist more-numbers (cdr nlist))) + ((atom nlist) t) + (declare (list nlist)) + (if (not (%time= (car nlist) number)) (return nil)))) + +(defun time/= (number &rest more-numbers) + "Returns T if no two of its arguments are numerically equal, NIL otherwise." + (do* ((head number (car nlist)) + (nlist more-numbers (cdr nlist))) + ((atom nlist) t) + (declare (list nlist)) + (unless (do* ((nl nlist (cdr nl))) + ((atom nl) t) + (declare (list nl)) + (if (%time= head (car nl)) (return nil))) + (return nil)))) + +(defun time< (number &rest more-numbers) + "Returns T if its arguments are in strictly increasing order, NIL otherwise." + (do* ((n number (car nlist)) + (nlist more-numbers (cdr nlist))) + ((atom nlist) t) + (declare (list nlist)) + (if (not (%time< n (car nlist))) (return nil)))) + +(defun time> (number &rest more-numbers) + "Returns T if its arguments are in strictly decreasing order, NIL otherwise." + (do* ((n number (car nlist)) + (nlist more-numbers (cdr nlist))) + ((atom nlist) t) + (declare (list nlist)) + (if (not (%time> n (car nlist))) (return nil)))) + +(defun time<= (number &rest more-numbers) + "Returns T if arguments are in strictly non-decreasing order, NIL otherwise." + (do* ((n number (car nlist)) + (nlist more-numbers (cdr nlist))) + ((atom nlist) t) + (declare (list nlist)) + (if (not (%time<= n (car nlist))) (return nil)))) + +(defun time>= (number &rest more-numbers) + "Returns T if arguments are in strictly non-increasing order, NIL otherwise." + (do* ((n number (car nlist)) + (nlist more-numbers (cdr nlist))) + ((atom nlist) t) + (declare (list nlist)) + (if (not (%time>= n (car nlist))) (return nil)))) + +(defun time-max (number &rest more-numbers) + "Returns the greatest of its arguments." + (do ((nlist more-numbers (cdr nlist)) + (result number)) + ((null nlist) (return result)) + (declare (list nlist)) + (if (%time> (car nlist) result) (setf result (car nlist))))) + +(defun time-min (number &rest more-numbers) + "Returns the least of its arguments." + (do ((nlist more-numbers (cdr nlist)) + (result number)) + ((null nlist) (return result)) + (declare (list nlist)) + (if (%time< (car nlist) result) (setf result (car nlist))))) + +(defun time-compare (time-a time-b) + (let ((mjd-a (time-mjd time-a)) + (mjd-b (time-mjd time-b)) + (sec-a (time-second time-a)) + (sec-b (time-second time-b)) + (usec-a (time-usec time-a)) + (usec-b (time-usec time-b))) + (if (= mjd-a mjd-b) + (if (= sec-a sec-b) + (if (= usec-a usec-b) + :equal + (if (< usec-a usec-b) + :less-than + :greater-than)) + (if (< sec-a sec-b) + :less-than + :greater-than)) + (if (< mjd-a mjd-b) + :less-than + :greater-than)))) + +; now the same for dates +(eval-when (:compile-toplevel :load-toplevel) +(defun replace-string (string1 search-string replace-string &key (test #'string=)) + "Search within string1 for search-string, replace with replace-string, non-destructively." + (let ((replace-string-length (length replace-string)) + (search-string-length (length search-string))) + (labels ((sub-replace-string (current-string position) + (let ((found-position (search search-string current-string :test test :start2 position))) + (if (null found-position) + current-string + (sub-replace-string (concatenate 'string + (subseq current-string 0 found-position) + replace-string + (subseq current-string (+ found-position search-string-length))) + (+ position replace-string-length)))))) + (sub-replace-string string1 0)))) +);eval-when + +(defmacro wrap-time-for-date (time-func &key (result-func)) + (let ((date-func (intern (replace-string (symbol-name time-func) + (symbol-name-default-case "TIME") + (symbol-name-default-case "DATE"))))) + `(defun ,date-func (number &rest more-numbers) + (let ((result (apply #',time-func (mapcar #'date->time (cons number more-numbers))))) + ,(if result-func + `(funcall #',result-func result) + 'result))))) + +(wrap-time-for-date time=) +(wrap-time-for-date time/=) +(wrap-time-for-date time<) +(wrap-time-for-date time>) +(wrap-time-for-date time<=) +(wrap-time-for-date time>=) +(wrap-time-for-date time-max :result-func time->date) +(wrap-time-for-date time-min :result-func time->date) + +(defun date-compare (date-a date-b) + (time-compare (date->time date-a) (date->time date-b))) + +;; ------------------------------------------------------------ +;; Formatting and output +(defun db-timestring (time &key stream) + "return the string to store the given time in the database" + (if stream + (progn (write-char #\' stream) (iso-timestring time :stream stream) (write-char #\' stream)) + (concatenate 'string "'" (iso-timestring time) "'"))) + +(defun iso-timestring (time &key stream) + (multiple-value-bind (usec sec min hour day month year dow) + (decode-time time) + (declare (ignore dow)) + (flet ((fmt (stream) + (when (< year 1000) (princ #\0 stream)) + (when (< year 100) (princ #\0 stream)) + (when (< year 10) (princ #\0 stream)) + (princ year stream) + (princ #\- stream) + (when (< month 10) (princ #\0 stream)) + (princ month stream) + (princ #\- stream) + (when (< day 10) (princ #\0 stream)) + (princ day stream) + (princ #\T stream) ;strict ISO says T here isn't optional. + (when (< hour 10) (princ #\0 stream)) + (princ hour stream) + (princ #\: stream) + (when (< min 10) (princ #\0 stream)) + (princ min stream) + (princ #\: stream) + (when (< sec 10) (princ #\0 stream)) + (princ sec stream) + (when (and usec (plusp usec)) + ;; we dont do this because different dbs support differnt precision levels + (princ #\. stream) + (loop for i from 5 downto 0 + for x10 = (expt 10 i) + do (multiple-value-bind (quo rem) + (floor (/ usec x10)) + (setf usec (- usec (* quo x10))) + (princ quo stream) + (when (= rem 0) (return))))) + nil)) + (if stream + (fmt stream) + (with-output-to-string (stream) + (fmt stream)))))) + +(defun db-datestring (date) + (db-timestring (date->time date))) +(defun iso-datestring (date) + (iso-timestring (date->time date))) + + +;; ------------------------------------------------------------ +;; Intervals + +(defstruct interval + (start nil) + (end nil) + (name nil) + (contained nil) + (type nil) + (data nil)) + +;; fix : should also return :contains / :contained + +(defun interval-relation (x y) + "Compare the relationship of node x to node y. Returns either +:contained :contains :follows :overlaps or :precedes." + (let ((xst (interval-start x)) + (xend (interval-end x)) + (yst (interval-start y)) + (yend (interval-end y))) + (case (time-compare xst yst) + (:equal + (case (time-compare xend yend) + (:less-than + :contained) + ((:equal :greater-than) + :contains))) + (:greater-than + (case (time-compare xst yend) + ((:equal :greater-than) + :follows) + (:less-than + (case (time-compare xend yend) + ((:less-than :equal) + :contained) + ((:greater-than) + :overlaps))))) + (:less-than + (case (time-compare xend yst) + ((:equal :less-than) + :precedes) + (:greater-than + (case (time-compare xend yend) + (:less-than + :overlaps) + ((:equal :greater-than) + :contains)))))))) + +;; ------------------------------------------------------------ +;; interval lists + +(defun sort-interval-list (list) + (sort list (lambda (x y) + (case (interval-relation x y) + ((:precedes :contains) t) + ((:follows :overlaps :contained) nil))))) + +;; interval push will return its list of intervals in strict order. +(defun interval-push (interval-list interval &optional container-rule) + (declare (ignore container-rule)) + (let ((sorted-list (sort-interval-list interval-list))) + (dotimes (x (length sorted-list)) + (let ((elt (nth x sorted-list))) + (case (interval-relation elt interval) + (:follows + (return-from interval-push (insert-at-index x sorted-list interval))) + (:contains + (return-from interval-push + (replace-at-index x sorted-list + (make-interval :start (interval-start elt) + :end (interval-end elt) + :type (interval-type elt) + :contained (interval-push (interval-contained elt) interval) + :data (interval-data elt))))) + ((:overlaps :contained) + (error "Overlap"))))) + (append sorted-list (list interval)))) + +;; interval lists + +(defun interval-match (list time) + "Return the index of the first interval in list containing time" + ;; this depends on ordering of intervals! + (let ((list (sort-interval-list list))) + (dotimes (x (length list)) + (let ((elt (nth x list))) + (when (and (time<= (interval-start elt) time) + (time< time (interval-end elt))) + (return-from interval-match x)))))) + +(defun interval-clear (list time) + (dotimes (x (length list)) + (let ((elt (nth x list))) + (when (and (time<= (interval-start elt) time) + (time< time (interval-end elt))) + (if (interval-match (interval-contained elt) time) + (return-from interval-clear + (replace-at-index x list + (make-interval :start (interval-start elt) + :end (interval-end elt) + :type (interval-type elt) + :contained (interval-clear (interval-contained elt) time) + :data (interval-data elt)))) + (return-from interval-clear + (delete-at-index x list))))))) + +(defun interval-edit (list time start end &optional tag) + "Attempts to modify the most deeply nested interval in list which +begins at time. If no changes are made, returns nil." + ;; function required sorted interval list + (let ((list (sort-interval-list list))) + (if (null list) nil + (dotimes (x (length list)) + (let ((elt (nth x list))) + (when (and (time<= (interval-start elt) time) + (time< time (interval-end elt))) + (or (interval-edit (interval-contained elt) time start end tag) + (cond ((and (< 0 x) + (time< start (interval-end (nth (1- x) list)))) + (error "Overlap of previous interval")) + ((and (< x (1- (length list))) + (time< (interval-start (nth (1+ x) list)) end)) + (error "~S ~S ~S ~S Overlap of next interval" x (length list) (interval-start (nth (1+ x) list)) end )) + ((time= (interval-start elt) time) + (return-from interval-edit + (replace-at-index x list + (make-interval :start start + :end end + :type (interval-type elt) + :contained (restrict-intervals (interval-contained elt) start end) + :data (or tag (interval-data elt)))))))))))))) + +(defun restrict-intervals (list start end &aux newlist) + (let ((test-interval (make-interval :start start :end end))) + (dolist (elt list) + (when (equal :contained + (interval-relation elt test-interval)) + (push elt newlist))) + (nreverse newlist))) + +;;; utils from odcl/list.lisp + +(defun replace-at-index (idx list elt) + (cond ((= idx 0) + (cons elt (cdr list))) + ((= idx (1- (length list))) + (append (butlast list) (list elt))) + (t + (append (subseq list 0 idx) + (list elt) + (subseq list (1+ idx)))))) + +(defun insert-at-index (idx list elt) + (cond ((= idx 0) + (cons elt list)) + ((= idx (1- (length list))) + (append list (list elt))) + (t + (append (subseq list 0 idx) + (list elt) + (subseq list idx))))) + +(defun delete-at-index (idx list) + (cond ((= idx 0) + (cdr list)) + ((= idx (1- (length list))) + (butlast list)) + (t + (append (subseq list 0 idx) + (subseq list (1+ idx)))))) + + +;; ------------------------------------------------------------ +;; return MJD for Gregorian date + +(defun gregorian-to-mjd (month day year) + (let ((b 0) + (month-adj month) + (year-adj (if (< year 0) + (+ year 1) + year)) + d + c) + (when (< month 3) + (incf month-adj 12) + (decf year-adj)) + (unless (or (< year 1582) + (and (= year 1582) + (or (< month 10) + (and (= month 10) + (< day 15))))) + (let ((a (floor (/ year-adj 100)))) + (setf b (+ (- 2 a) (floor (/ a 4)))))) + (if (< year-adj 0) + (setf c (floor (- (* 365.25d0 year-adj) 679006.75d0))) + (setf c (floor (- (* 365.25d0 year-adj) 679006d0)))) + (setf d (floor (* 30.6001 (+ 1 month-adj)))) + ;; (cmsg "b ~s c ~s d ~s day ~s" b c d day) + (+ b c d day))) + +;; convert MJD to Gregorian date + +(defun mjd-to-gregorian (mjd) + (let (z r g a b c year month day) + (setf z (floor (+ mjd 678882))) + (setf r (- (+ mjd 678882) z)) + (setf g (- z .25)) + (setf a (floor (/ g 36524.25))) + (setf b (- a (floor (/ a 4)))) + (setf year (floor (/ (+ b g) 365.25))) + (setf c (- (+ b z) (floor (* 365.25 year)))) + (setf month (truncate (/ (+ (* 5 c) 456) 153))) + (setf day (+ (- c (truncate (/ (- (* 153 month) 457) 5))) r)) + (when (> month 12) + (incf year) + (decf month 12)) + (list month day year))) + +(defun duration+ (time &rest durations) + "Add each DURATION to TIME, returning a new wall-time value." + (let ((year (duration-year time)) + (month (duration-month time)) + (day (duration-day time)) + (hour (duration-hour time)) + (minute (duration-minute time)) + (second (duration-second time)) + (usec (duration-usec time))) + (dolist (duration durations) + (incf year (duration-year duration)) + (incf month (duration-month duration)) + (incf day (duration-day duration)) + (incf hour (duration-hour duration)) + (incf minute (duration-minute duration)) + (incf second (duration-second duration)) + (incf usec (duration-usec duration))) + (make-duration :year year :month month :day day :hour hour :minute minute + :second second :usec usec))) + +(defun duration- (duration &rest durations) + "Subtract each DURATION from TIME, returning a new duration value." + (let ((year (duration-year duration)) + (month (duration-month duration)) + (day (duration-day duration)) + (hour (duration-hour duration)) + (minute (duration-minute duration)) + (second (duration-second duration)) + (usec (duration-usec duration))) + (dolist (duration durations) + (decf year (duration-year duration)) + (decf month (duration-month duration)) + (decf day (duration-day duration)) + (decf hour (duration-hour duration)) + (decf minute (duration-minute duration)) + (decf second (duration-second duration)) + (decf usec (duration-usec duration))) + (make-duration :year year :month month :day day :hour hour :minute minute + :second second :usec usec))) + +;; Date + Duration + +(defun time+ (time &rest durations) + "Add each DURATION to TIME, returning a new wall-time value." + (let ((new-time (copy-time time))) + (dolist (duration durations) + (roll new-time + :year (duration-year duration) + :month (duration-month duration) + :day (duration-day duration) + :hour (duration-hour duration) + :minute (duration-minute duration) + :second (duration-second duration) + :usec (duration-usec duration) + :destructive t)) + new-time)) + +(defun date+ (date &rest durations) + "Add each DURATION to DATE, returning a new date value. +Note that (barring daylight saving time) 12h + 12h will result in a new day, but doing +it as separate calculations will not, as the time is chopped to a date before being returned." + (time->date (apply #'time+ (cons (date->time date) durations)))) + +(defun time- (time &rest durations) + "Subtract each DURATION from TIME, returning a new wall-time value." + (let ((new-time (copy-time time))) + (dolist (duration durations) + (roll new-time + :year (- (duration-year duration)) + :month (- (duration-month duration)) + :day (- (duration-day duration)) + :hour (- (duration-hour duration)) + :minute (- (duration-minute duration)) + :second (- (duration-second duration)) + :usec (- (duration-usec duration)) + :destructive t)) + new-time)) + +(defun date- (date &rest durations) + "Subtract each DURATION to DATE, returning a new date value. +Note that (barring daylight saving time) 12h + 12h will result in a new day, but doing +it as separate calculations will not, as the time is chopped to a date before being returned." + (time->date (apply #'time- (cons (date->time date) durations)))) + +(defun time-difference (time1 time2) + "Returns a DURATION representing the difference between TIME1 and +TIME2." + (flet ((do-diff (time1 time2) + + (let (day-diff sec-diff) + (setf day-diff (- (time-mjd time2) + (time-mjd time1))) + (if (> day-diff 0) + (progn (decf day-diff) + (setf sec-diff (+ (time-second time2) + (- (* 60 60 24) + (time-second time1))))) + (setf sec-diff (- (time-second time2) + (time-second time1)))) + (make-duration :day day-diff + :second sec-diff)))) + (if (time< time1 time2) + (do-diff time1 time2) + (do-diff time2 time1)))) + +(defun date-difference (date1 date2) + "Returns a DURATION representing the difference between TIME1 and +TIME2." + (time-difference (date->time date1) (date->time date2))) + +(defun format-date (stream date &key format + (date-separator "-") + (internal-separator " ")) + "produces on stream the datestring corresponding to the date +with the given options" + (format-time stream (date->time date) + :format format + :date-separator date-separator + :internal-separator internal-separator)) + +(defun format-time (stream time &key format + (date-separator "-") + (time-separator ":") + (internal-separator " ")) + "produces on stream the timestring corresponding to the wall-time +with the given options" + (let ((*print-circle* nil)) + (multiple-value-bind (usec second minute hour day month year dow) + (decode-time time) + (case format + (:pretty + (format stream "~A ~A, ~A ~D, ~D" + (pretty-time hour minute) + (day-name dow) + (month-name month) + day + year)) + (:short-pretty + (format stream "~A, ~D/~D/~D" + (pretty-time hour minute) + month day year)) + ((:iso :iso8601) (iso-timestring time :stream stream)) + (t (format stream "~2,'0D~A~2,'0D~A~2,'0D~A~2,'0D~A~2,'0D~A~2,'0D.~6,'0D" + year date-separator month date-separator day + internal-separator hour time-separator minute time-separator + second usec) + ))))) + +(defun pretty-time (hour minute) + (cond + ((eq hour 0) + (format nil "12:~2,'0D AM" minute)) + ((eq hour 12) + (format nil "12:~2,'0D PM" minute)) + ((< hour 12) + (format nil "~D:~2,'0D AM" hour minute)) + ((and (> hour 12) (< hour 24)) + (format nil "~D:~2,'0D PM" (- hour 12) minute)) + (t + (error "pretty-time got bad hour")))) + +(defun leap-days-in-days (days) + ;; return the number of leap days between Mar 1 2000 and + ;; (Mar 1 2000) + days, where days can be negative + (if (< days 0) + (ceiling (/ (- days) (* 365 4))) + (floor (/ days (* 365 4))))) + +(defun current-year () + (third (mjd-to-gregorian (time-mjd (get-time))))) + +(defun current-month () + (first (mjd-to-gregorian (time-mjd (get-time))))) + +(defun current-day () + (second (mjd-to-gregorian (time-mjd (get-time))))) + +(defun parse-date-time (string) + "parses date like 08/08/01, 8.8.2001, eg" + (when (> (length string) 1) + (let ((m (current-month)) + (d (current-day)) + (y (current-year))) + (let ((integers (mapcar #'parse-integer (hork-integers string)))) + (case (length integers) + (1 + (setf y (car integers))) + (2 + (setf m (car integers)) + (setf y (cadr integers))) + (3 + (setf m (car integers)) + (setf d (cadr integers)) + (setf y (caddr integers))) + (t + (return-from parse-date-time)))) + (when (< y 100) + (incf y 2000)) + (make-time :year y :month m :day d)))) + +(defun hork-integers (input) + (let ((output '()) + (start 0)) + (dotimes (x (length input)) + (unless (<= 48 (char-code (aref input x)) 57) + (push (subseq input start x) output) + (setf start (1+ x)))) + (nreverse (push (subseq input start) output)))) + +(defun merged-time (day time-of-day) + (%make-wall-time :mjd (time-mjd day) + :second (time-second time-of-day))) + +(defun time-meridian (hours) + (cond ((= hours 0) + (values 12 "AM")) + ((= hours 12) + (values 12 "PM")) + ((< 12 hours) + (values (- hours 12) "PM")) + (t + (values hours "AM")))) + +(defgeneric to-string (val &rest keys) + ) + +(defmethod to-string ((time wall-time) &rest keys) + (destructuring-bind (&key (style :daytime) &allow-other-keys) + keys + (print-date time style))) + +(defun print-date (time &optional (style :daytime)) + (multiple-value-bind (usec second minute hour day month year dow) + (decode-time time) + (declare (ignore usec second)) + (multiple-value-bind (hours meridian) + (time-meridian hour) + (ecase style + (:time-of-day + ;; 2:00 PM + (format nil "~d:~2,'0d ~a" hours minute meridian)) + (:long-day + ;; October 11th, 2000 + (format nil "~a ~d, ~d" (month-name month) day year)) + (:month + ;; October + (month-name month)) + (:month-year + ;; October 2000 + (format nil "~a ~d" (month-name month) year)) + (:full + ;; 11:08 AM, November 22, 2002 + (format nil "~d:~2,'0d ~a, ~a ~d, ~d" + hours minute meridian (month-name month) day year)) + (:full+weekday + ;; 11:09 AM Friday, November 22, 2002 + (format nil "~d:~2,'0d ~a ~a, ~a ~d, ~d" + hours minute meridian (nth dow *day-names*) + (month-name month) day year)) + (:daytime + ;; 11:09 AM, 11/22/2002 + (format-time nil time :format :short-pretty)) + (:day + ;; 11/22/2002 + (format nil "~d/~d/~d" month day year)))))) + +(defun time-element (time element) + (multiple-value-bind (usec second minute hour day month year dow) + (decode-time time) + (declare (ignore usec)) + (ecase element + (:seconds + second) + (:minutes + minute) + (:hours + hour) + (:day-of-month + day) + (:integer-day-of-week + dow) + (:day-of-week + (nth dow *day-keywords*)) + (:month + month) + (:year + year)))) + +(defun date-element (date element) + (time-element (date->time date) element)) + +(defun format-duration (stream duration &key (precision :minute)) + (let ((second (duration-second duration)) + (minute (duration-minute duration)) + (hour (duration-hour duration)) + (day (duration-day duration)) + (month (duration-month duration)) + (year (duration-year duration)) + (return (null stream)) + (stream (or stream (make-string-output-stream)))) + (ecase precision + (:day + (setf hour 0 second 0 minute 0)) + (:hour + (setf second 0 minute 0)) + (:minute + (setf second 0)) + (:second + t)) + (if (= 0 year month day hour minute) + (format stream "0 minutes") + (let ((sent? nil)) + (when (< 0 year) + (format stream "~d year~p" year year) + (setf sent? t)) + (when (< 0 month) + (when sent? + (write-char #\Space stream)) + (format stream "~d month~p" month month) + (setf sent? t)) + (when (< 0 day) + (when sent? + (write-char #\Space stream)) + (format stream "~d day~p" day day) + (setf sent? t)) + (when (< 0 hour) + (when sent? + (write-char #\Space stream)) + (format stream "~d hour~p" hour hour) + (setf sent? t)) + (when (< 0 minute) + (when sent? + (write-char #\Space stream)) + (format stream "~d min~p" minute minute) + (setf sent? t)) + (when (< 0 second) + (when sent? + (write-char #\Space stream)) + (format stream "~d sec~p" second second)))) + (when return + (get-output-stream-string stream)))) + +(defgeneric midnight (self)) +(defmethod midnight ((self wall-time)) + "truncate hours, minutes and seconds" + (%make-wall-time :mjd (time-mjd self))) + +(defun roll (date &key (year 0) (month 0) (day 0) (second 0) (hour 0) + (minute 0) (usec 0) (destructive nil)) + (unless (= 0 year month) + (multiple-value-bind (year-orig month-orig day-orig) + (time-ymd date) + (multiple-value-bind (new-year new-month) + (floor (+ month month-orig (* 12 (+ year year-orig))) 12) + (let ((new-date (make-time :year new-year + :month new-month + :day day-orig + :second (time-second date) + :usec usec))) + (if destructive + (setf (time-mjd date) (time-mjd new-date)) + (setq date new-date)))))) + (let ((mjd (time-mjd date)) + (sec (time-second date))) + (multiple-value-bind (sec-new usec-new) + (floor (+ usec (time-usec date) + (* 1000000 + (+ sec second + (* 60 minute) + (* 60 60 hour)))) + 1000000) + (multiple-value-bind (mjd-new sec-new) + (floor sec-new (* 60 60 24)) + (if destructive + (progn + (setf (time-mjd date) (+ mjd mjd-new day) + (time-second date) sec-new + (time-usec date) usec-new) + date) + (%make-wall-time :mjd (+ mjd mjd-new day) + :second sec-new + :usec usec-new)))))) + +(defun roll-to (date size position) + (ecase size + (:month + (ecase position + (:beginning + (roll date :day (+ 1 + (- (time-element date :day-of-month))))) + (:end + (roll date :day (+ (days-in-month (time-element date :month) + (time-element date :year)) + (- (time-element date :day-of-month))))))))) + +(defun week-containing (time) + (let* ((midn (midnight time)) + (dow (time-element midn :integer-day-of-week))) + (list (roll midn :day (- dow)) + (roll midn :day (- 7 dow))))) + +(defun leap-year? (year) + "t if YEAR is a leap yeap in the Gregorian calendar" + (and (= 0 (mod year 4)) + (or (not (= 0 (mod year 100))) + (= 0 (mod year 400))))) + +(defun valid-month-p (month) + "t if MONTH exists in the Gregorian calendar" + (<= 1 month 12)) + +(defun valid-gregorian-date-p (date) + "t if DATE (year month day) exists in the Gregorian calendar" + (let ((max-day (days-in-month (nth 1 date) (nth 0 date)))) + (<= 1 (nth 2 date) max-day))) + +(defun days-in-month (month year &key (careful t)) + "the number of days in MONTH of YEAR, observing Gregorian leap year +rules" + (declare (type fixnum month year)) + (when careful + (check-type month (satisfies valid-month-p) + "between 1 (January) and 12 (December)")) + (if (eql month 2) ; feb + (if (leap-year? year) + 29 28) + (let ((even (mod (1- month) 2))) + (if (< month 8) ; aug + (- 31 even) + (+ 30 even))))) + +(defun day-of-year (year month day &key (careful t)) + "the day number within the year of the date DATE. For example, +1987 1 1 returns 1" + (declare (type fixnum year month day)) + (when careful + (let ((date (list year month day))) + (check-type date (satisfies valid-gregorian-date-p) + "a valid Gregorian date"))) + (let ((doy (+ day (* 31 (1- month))))) + (declare (type fixnum doy)) + (when (< 2 month) + (setf doy (- doy (floor (+ 23 (* 4 month)) 10))) + (when (leap-year? year) + (incf doy))) + doy)) + +(defun parse-yearstring (string) + (let ((year (or (parse-integer-insensitively string) + (extract-roman string)))) + (when (and year (< 1500 year 2500)) + (make-time :year year)))) + +(defun parse-integer-insensitively (string) + (let ((start (position-if #'digit-char-p string)) + (end (position-if #'digit-char-p string :from-end t))) + (when (and start end) + (parse-integer (subseq string start (1+ end)) :junk-allowed t)))) + +(defvar *roman-digits* + '((#\M . 1000) + (#\D . 500) + (#\C . 100) + (#\L . 50) + (#\X . 10) + (#\V . 5) + (#\I . 1))) + +(defun extract-roman (string &aux parse) + (dotimes (x (length string)) + (let ((val (cdr (assoc (aref string x) *roman-digits*)))) + (when (and val parse (< (car parse) val)) + (push (- (pop parse)) parse)) + (push val parse))) + (apply #'+ parse)) + + +;; ------------------------------------------------------------ +;; Parsing iso-8601 timestrings + +(define-condition iso-8601-syntax-error (sql-user-error) + ((bad-component;; year, month whatever + :initarg :bad-component + :reader bad-component)) + (:report (lambda (c stream) + (format stream "Bad component: ~A " (bad-component c))))) + +(defun parse-timestring (timestring &key (start 0) end junk-allowed) + "parse a timestring and return the corresponding wall-time. If the +timestring starts with P, read a duration; otherwise read an ISO 8601 +formatted date string." + (declare (ignore junk-allowed)) + (etypecase timestring + (wall-time timestring) + (date (date->time timestring)) + (string + (let ((string (subseq timestring start end))) + (if (char= (aref string 0) #\P) + (parse-iso-8601-duration string) + (parse-iso-8601-time string)))))) + +(defun parse-datestring (datestring &key (start 0) end junk-allowed) + "parse a ISO 8601 timestring and return the corresponding date. +Will throw a hissy fit if the date string is a duration. Will ignore any precision beyond day (hour/min/sec/usec)." + (etypecase datestring + (date datestring) + (wall-time (time->date datestring)) + (string + (let ((parsed-value + (parse-timestring + datestring :start start :end end :junk-allowed junk-allowed))) + (etypecase parsed-value + (date parsed-value) + (wall-time (time->date parsed-value))))))) + + +(defvar *iso-8601-duration-delimiters* + '((#\Y . :years) + (#\D . :days) + (#\H . :hours) + (#\M . :months/minutes) + (#\S . :seconds))) + +(defun iso-8601-delimiter (elt) + (cdr (assoc elt *iso-8601-duration-delimiters*))) + +(defun iso-8601-duration-subseq (string end) + (let* ((pos (position-if #'iso-8601-delimiter string :end end :from-end t)) + (pos2 (when pos + (position-if-not #'digit-char-p string :end pos :from-end t))) + (number (when pos2 + (parse-integer + (subseq string (1+ pos2) pos) :junk-allowed t)))) + (when number + (values number + (1+ pos) + (1+ pos2) + (iso-8601-delimiter (aref string pos)))))) + +(defun parse-iso-8601-duration (string) + "return a wall-time from a duration string" + (block parse + (let ((years 0) + (months 0) + (days 0) + (secs 0) + (hours 0) + (minutes 0) + (index (length string)) + (months/minutes nil)) + (loop + (multiple-value-bind (duration end next-index duration-type) + (iso-8601-duration-subseq string index) + (declare (ignore end)) + (case duration-type + (:years + (incf years duration)) + (:months/minutes + (if months/minutes + (incf months duration) + (progn + (setq months/minutes t) + (incf minutes duration)))) + (:days + (setq months/minutes t) + (incf days duration)) + (:hours + (setq months/minutes t) + (incf hours duration)) + (:seconds + (incf secs duration)) + (t + (return-from parse + (make-duration + :year years :month months :day days :hour hours + :minute minutes :second secs)))) + (setf index next-index)))))) + +;; e.g. 2000-11-11 00:00:00-06 + +(defun parse-iso-8601-time (string) + "return the wall-time corresponding to the given ISO 8601 datestring" + (multiple-value-bind (year month day hour minute second usec offset) + (syntax-parse-iso-8601 string) + (make-time :year year + :month month + :day day + :hour hour + :minute minute + :second second + :usec usec + :offset offset))) + + +(defun syntax-parse-iso-8601 (string) + ;; use strlen to determine if fractional seconds are present in timestamp + (let ((strlen (length string)) + year month day hour minute second usec gmt-sec-offset) + (handler-case + (progn + (setf year (parse-integer string :start 0 :end 4) + month (parse-integer string :start 5 :end 7) + day (parse-integer string :start 8 :end 10) + hour (if (<= 13 strlen) + (parse-integer string :start 11 :end 13) + 0) + minute (if (<= 16 strlen) + (parse-integer string :start 14 :end 16) + 0) + second (if (<= 19 strlen) + (parse-integer string :start 17 :end 19) + 0)) + (cond + ((and (> strlen 19) + (or (char= #\, (char string 19)) + (char= #\. (char string 19)))) + (multiple-value-bind (parsed-usec usec-end) + (parse-integer string :start 20 :junk-allowed t) + (let ((parsed-usec (and parsed-usec + (floor (* parsed-usec (expt 10 (+ 6 (- usec-end) 20))))))) + (setf usec (or parsed-usec 0) + gmt-sec-offset (if (<= (+ 3 usec-end) strlen) + (let ((skip-to (or (position #\+ string :start 19) + (position #\- string :start 19)))) + (if skip-to + (* 60 60 + (parse-integer string :start skip-to + :end (+ skip-to 3))) + 0)) + 0))))) + (t + (setf usec 0 + gmt-sec-offset (if (<= 22 strlen) + (let ((skip-to (or (position #\+ string :start 19) + (position #\- string :start 19)))) + (if skip-to + (* 60 60 + (parse-integer string :start skip-to + :end (+ skip-to 3))) + 0)) + 0)))) + (unless (< 0 year) + (error 'iso-8601-syntax-error + :bad-component '(year . 0))) + (unless (< 0 month) + (error 'iso-8601-syntax-error + :bad-component '(month . 0))) + (unless (< 0 day) + (error 'iso-8601-syntax-error + :bad-component '(month . 0))) + (values year month day hour minute second usec gmt-sec-offset)) + (simple-error () + (error 'iso-8601-syntax-error + :bad-component + (car (find-if (lambda (pair) (null (cdr pair))) + `((year . ,year) (month . ,month) + (day . ,day) (hour . ,hour) + (minute . ,minute) (second . ,second) + (usec . ,usec) + (timezone . ,gmt-sec-offset))))))))) |