;;;; -*- Mode: LISP; Syntax: ANSI-Common-Lisp; Base: 10 -*- ;;;; ************************************************************************* ;;;; ;;;; $Id$ ;;;; ;;;; 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)) (defun %print-wall-time (time stream depth) (declare (ignore depth)) (format stream "#" (format-time nil time))) (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)) (defun %print-duration (duration stream depth) (declare (ignore depth)) (format stream "#" (format-duration nil duration :precision :second))) );eval-when (defun duration-timestring (duration) (let ((second (duration-second duration)) (minute (duration-minute duration)) (hour (duration-hour duration)) (day (duration-day duration))) (format nil "P~dD~dH~dM~dS" day hour minute second))) ;; ------------------------------------------------------------ ;; Constructors (defun make-time (&key (year 0) (month 1) (day 1) (hour 0) (minute 0) (second 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)))) (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 get-time () "Return a pair: (GREGORIAN DAY . TIME-OF-DAY)" (utime->time (get-universal-time))) (defun make-duration (&key (year 0) (month 0) (day 0) (hour 0) (minute 0) (second 0)) (multiple-value-bind (minute-add second-60) (floor second 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))))) ;; ------------------------------------------------------------ ;; 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: 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 second minute hour day month year (mod (+ (time-mjd time) 3) 7))))) ;; duration specific (defun duration-reduce (duration precision &optional round) (ecase precision (:second (+ (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 :second) (duration-reduce duration-b :second))) (defun duration< (duration-a duration-b) (< (duration-reduce duration-a :second) (duration-reduce duration-b :second))) (defun duration<= (duration-a duration-b) (<= (duration-reduce duration-a :second) (duration-reduce duration-b :second))) (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) (< (time-second x) (time-second y))))) (defun %time>= (x y) (if (/= (time-mjd x) (time-mjd y)) (>= (time-mjd x) (time-mjd y)) (>= (time-second x) (time-second y)))) (defun %time<= (x y) (if (/= (time-mjd x) (time-mjd y)) (<= (time-mjd x) (time-mjd y)) (<= (time-second x) (time-second y)))) (defun %time> (x y) (if (/= (time-mjd x) (time-mjd y)) (> (time-mjd x) (time-mjd y)) (> (time-second x) (time-second y)))) (defun %time= (x y) (and (= (time-mjd x) (time-mjd y)) (= (time-second x) (time-second 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))) (if (= mjd-a mjd-b) (if (= sec-a sec-b) :equal (if (< sec-a sec-b) :less-than :greater-than)) (if (< mjd-a mjd-b) :less-than :greater-than)))) ;; ------------------------------------------------------------ ;; Formatting and output (defvar +decimal-printer+ #(#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9)) (defun db-timestring (time) "return the string to store the given time in the database" (declare (optimize (speed 3))) (let ((output (copy-seq "'XXXX-XX-XX XX:XX:XX'"))) (flet ((inscribe-base-10 (output offset size decimal) (declare (type fixnum offset size decimal) (type (simple-vector 10) +decimal-printer+)) (dotimes (x size) (declare (type fixnum x) (optimize (safety 0))) (multiple-value-bind (next this) (floor decimal 10) (setf (aref output (+ (- size x 1) offset)) (aref +decimal-printer+ this)) (setf decimal next))))) (multiple-value-bind (second minute hour day month year) (decode-time time) (inscribe-base-10 output 1 4 year) (inscribe-base-10 output 6 2 month) (inscribe-base-10 output 9 2 day) (inscribe-base-10 output 12 2 hour) (inscribe-base-10 output 15 2 minute) (inscribe-base-10 output 18 2 second) output)))) (defun iso-timestring (time) "return the string to store the given time in the database" (declare (optimize (speed 3))) (let ((output (copy-seq "XXXX-XX-XX XX:XX:XX"))) (flet ((inscribe-base-10 (output offset size decimal) (declare (type fixnum offset size decimal) (type (simple-vector 10) +decimal-printer+)) (dotimes (x size) (declare (type fixnum x) (optimize (safety 0))) (multiple-value-bind (next this) (floor decimal 10) (setf (aref output (+ (- size x 1) offset)) (aref +decimal-printer+ this)) (setf decimal next))))) (multiple-value-bind (second minute hour day month year) (decode-time time) (inscribe-base-10 output 0 4 year) (inscribe-base-10 output 5 2 month) (inscribe-base-10 output 8 2 day) (inscribe-base-10 output 11 2 hour) (inscribe-base-10 output 14 2 minute) (inscribe-base-10 output 17 2 second) output)))) ;; ------------------------------------------------------------ ;; 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))) (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))) (make-duration :year year :month month :day day :hour hour :minute minute :second second))) (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))) (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))) (make-duration :year year :month month :day day :hour hour :minute minute :second second))) ;; 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) :destructive t)) new-time)) (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)) :destructive t)) new-time)) (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 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 (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 (let ((string (iso-timestring time))) (if stream (write-string string stream) string))) (t (format stream "~2,'0D~A~2,'0D~A~2,'0D~A~2,'0D~A~2,'0D~A~2,'0D" year date-separator month date-separator day internal-separator hour time-separator minute time-separator second)))))) (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 () (second (mjd-to-gregorian (time-mjd (get-time))))) (defun current-day () (first (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 (second minute hour day month year dow) (decode-time time) (declare (ignore 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 (second minute hour day month year dow) (decode-time time) (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 format-duration (stream duration &key (precision :minute)) (let ((second (duration-second duration)) (minute (duration-minute duration)) (hour (duration-hour duration)) (day (duration-day 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 day hour minute) (format stream "0 minutes") (let ((sent? nil)) (when (< 0 day) (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) (destructive nil)) (unless (= 0 year month) (multiple-value-bind (year-orig month-orig day-orig) (time-ymd date) (setf date (make-time :year (+ year year-orig) :month (+ month month-orig) :day day-orig :second (time-second date))))) (let ((mjd (time-mjd date)) (sec (time-second date))) (multiple-value-bind (mjd-new sec-new) (floor (+ sec second (* 60 minute) (* 60 60 hour)) (* 60 60 24)) (if destructive (progn (setf (time-mjd date) (+ mjd mjd-new day) (time-second date) sec-new) date) (%make-wall-time :mjd (+ mjd mjd-new day) :second sec-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)) (let ((string (subseq timestring start end))) (if (char= (aref string 0) #\P) (parse-iso-8601-duration string) (parse-iso-8601-time string)))) (defvar *iso-8601-duration-delimiters* '((#\D . :days) (#\H . :hours) (#\M . :minutes) (#\S . :seconds))) (defun iso-8601-delimiter (elt) (cdr (assoc elt *iso-8601-duration-delimiters*))) (defun iso-8601-duration-subseq (string start) (let* ((pos (position-if #'iso-8601-delimiter string :start start)) (number (when pos (parse-integer (subseq string start pos) :junk-allowed t)))) (when number (values number (1+ pos) (iso-8601-delimiter (aref string pos)))))) (defun parse-iso-8601-duration (string) "return a wall-time from a duration string" (block parse (let ((days 0) (secs 0) (hours 0) (minutes 0) (index 1)) (loop (multiple-value-bind (duration next-index duration-type) (iso-8601-duration-subseq string index) (case duration-type (:hours (incf hours duration)) (:minutes (incf minutes duration)) (:seconds (incf secs duration)) (:days (incf days duration)) (t (return-from parse (make-duration :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 offset) (syntax-parse-iso-8601 string) (make-time :year year :month month :day day :hour hour :minute minute :second second :offset offset))) (defun syntax-parse-iso-8601 (string) (let (year month day hour minute second gmt-sec-offset) (handler-case (progn (setf year (parse-integer (subseq string 0 4)) month (parse-integer (subseq string 5 7)) day (parse-integer (subseq string 8 10)) hour (if (<= 13 (length string)) (parse-integer (subseq string 11 13)) 0) minute (if (<= 16 (length string)) (parse-integer (subseq string 14 16)) 0) second (if (<= 19 (length string)) (parse-integer (subseq string 17 19)) 0) gmt-sec-offset (if (<= 22 (length string)) (* 60 60 (parse-integer (subseq string 19 22))) 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 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) (timezone ,gmt-sec-offset)))))))))