Initial upload version 0.7
This commit is contained in:
parent
d25c3d51ae
commit
c306e8a033
11 changed files with 2015 additions and 0 deletions
60
cl-dates.asd
Normal file
60
cl-dates.asd
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
;;; Copyright (c) 2017, Sudhir Shenoy. All rights reserved.
|
||||
|
||||
;;; Redistribution and use in source and binary forms, with or without
|
||||
;;; modification, are permitted provided that the following conditions
|
||||
;;; are met:
|
||||
|
||||
;;; * Redistributions of source code must retain the above copyright
|
||||
;;; notice, this list of conditions and the following disclaimer.
|
||||
|
||||
;;; * Redistributions in binary form must reproduce the above
|
||||
;;; copyright notice, this list of conditions and the following
|
||||
;;; disclaimer in the documentation and/or other materials
|
||||
;;; provided with the distribution.
|
||||
|
||||
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
|
||||
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
(in-package :cl-user)
|
||||
|
||||
(defpackage :cl-dates
|
||||
(:use :cl :asdf))
|
||||
|
||||
(in-package :cl-dates)
|
||||
|
||||
(defsystem :cl-dates
|
||||
:version "0.7"
|
||||
:description "Date-time library for Common Lisp"
|
||||
:author "Sudhir Shenoy"
|
||||
:license "BSD"
|
||||
:serial t
|
||||
:components ((:file "packages")
|
||||
(:file "util")
|
||||
(:file "timezones")
|
||||
(:file "dates")
|
||||
(:file "parse-date")
|
||||
(:file "print-date")
|
||||
(:file "date-arith")))
|
||||
|
||||
(defsystem :cl-dates-test
|
||||
:description "Date-time library tests"
|
||||
:author "Sudhir Shenoy"
|
||||
:license "BSD"
|
||||
:depends-on (:cl-dates)
|
||||
:serial t
|
||||
:components ((:file "test-main")
|
||||
(:file "test-dates")
|
||||
(:file "test-parse-date")))
|
||||
|
||||
(defmethod perform ((o test-op) (c (eql (find-system :cl-dates))))
|
||||
(operate 'load-op :cl-dates-test)
|
||||
(funcall (intern (symbol-name :run-all-tests) (find-package :cl-dates-test))))
|
||||
97
date-arith.lisp
Normal file
97
date-arith.lisp
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
;;; Copyright (c) 2017, Sudhir Shenoy. All rights reserved.
|
||||
|
||||
;;; Redistribution and use in source and binary forms, with or without
|
||||
;;; modification, are permitted provided that the following conditions
|
||||
;;; are met:
|
||||
|
||||
;;; * Redistributions of source code must retain the above copyright
|
||||
;;; notice, this list of conditions and the following disclaimer.
|
||||
|
||||
;;; * Redistributions in binary form must reproduce the above
|
||||
;;; copyright notice, this list of conditions and the following
|
||||
;;; disclaimer in the documentation and/or other materials
|
||||
;;; provided with the distribution.
|
||||
|
||||
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
|
||||
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
(in-package :cl-dates)
|
||||
|
||||
;; Date comparisons - strip off time component
|
||||
(defun date= (&rest dates)
|
||||
(every (lambda (a b) (= (jday-number a) (jday-number b))) dates (cdr dates)))
|
||||
|
||||
(defun date/= (&rest dates)
|
||||
(notevery (lambda (a b) (= (jday-number a) (jday-number b))) dates (cdr dates)))
|
||||
|
||||
(defun date> (&rest dates)
|
||||
(every (lambda (a b) (> (jday-number a) (jday-number b))) dates (cdr dates)))
|
||||
|
||||
(defun date< (&rest dates)
|
||||
(every (lambda (a b) (< (jday-number a) (jday-number b))) dates (cdr dates)))
|
||||
|
||||
(defun date>= (&rest dates)
|
||||
(every (lambda (a b) (>= (jday-number a) (jday-number b))) dates (cdr dates)))
|
||||
|
||||
(defun date<= (&rest dates)
|
||||
(every (lambda (a b) (<= (jday-number a) (jday-number b))) dates (cdr dates)))
|
||||
|
||||
;; Date arithmetic
|
||||
|
||||
(defun date+ (date days)
|
||||
"Advance date by given number of days"
|
||||
(+ date days))
|
||||
|
||||
(defun date- (date days)
|
||||
"Retreat date by given number of days"
|
||||
(- date days))
|
||||
|
||||
(defun date-diff (dt1 dt2)
|
||||
"Return (positive) number of days between two dates"
|
||||
(abs (- (jday-number dt1) (jday-number dt2))))
|
||||
|
||||
(defun nth-day-of-week (date dow n)
|
||||
"Returns the nth day of the week e.g., second Saturday of the month in which date falls.
|
||||
If n is large enough to make the date fall in a future month, the last valid day in
|
||||
the month is returned."
|
||||
(multiple-value-bind (yy mm dd h m s) (date->ymd date)
|
||||
(declare (ignore dd))
|
||||
(let ((dt (loop for dd = (ymd->date yy mm 1 h m s) then (1+ dd)
|
||||
until (eq dow (day-of-week dd))
|
||||
finally (return dd))))
|
||||
(if (< n 2)
|
||||
dt
|
||||
(dotimes (i (1- n) dt)
|
||||
(let ((next-dt (+ 7 dt)))
|
||||
(multiple-value-bind (yy1 mm1 dd1 h1 m1 s1) (date->ymd next-dt)
|
||||
(declare (ignore yy1 dd1 h1 m1 s1))
|
||||
(if (/= mm1 mm)
|
||||
(return dt)
|
||||
(setf dt next-dt)))))))))
|
||||
|
||||
(defun first-of-next-month (date)
|
||||
"Returns date for 1st of the following month"
|
||||
(multiple-value-bind (yy mm dd h m s) (date->ymd date)
|
||||
(declare (ignore dd))
|
||||
(if (= mm 12)
|
||||
(ymd->date (1+ yy) 1 1 h m s)
|
||||
(ymd->date yy (1+ m) 1 h m s))))
|
||||
|
||||
(defun last-day-of-month (date)
|
||||
"Returns last day in curent month"
|
||||
(1- (first-of-next-month date)))
|
||||
|
||||
(defun last-day-of-prev-month (date)
|
||||
"Returns last day of previous month"
|
||||
(multiple-value-bind (yy mm dd h m s) (date->ymd date)
|
||||
(declare (ignore dd))
|
||||
(1- (ymd->date yy mm 1 h m s))))
|
||||
283
dates.lisp
Normal file
283
dates.lisp
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
;;; Copyright (c) 2017, Sudhir Shenoy. All rights reserved.
|
||||
|
||||
;;; Redistribution and use in source and binary forms, with or without
|
||||
;;; modification, are permitted provided that the following conditions
|
||||
;;; are met:
|
||||
|
||||
;;; * Redistributions of source code must retain the above copyright
|
||||
;;; notice, this list of conditions and the following disclaimer.
|
||||
|
||||
;;; * Redistributions in binary form must reproduce the above
|
||||
;;; copyright notice, this list of conditions and the following
|
||||
;;; disclaimer in the documentation and/or other materials
|
||||
;;; provided with the distribution.
|
||||
|
||||
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
|
||||
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
(in-package :cl-dates)
|
||||
|
||||
(defun jday-number (date)
|
||||
"Returns the Julian day number for the given Julian date"
|
||||
(floor (+ date 1/2)))
|
||||
|
||||
(defparameter +days-of-week+ #(:monday :tuesday :wednesday :thursday :friday :saturday :sunday))
|
||||
(defun day-of-week (date)
|
||||
"Returns the day of week on which the given Julian date falls"
|
||||
(aref +days-of-week+ (mod (jday-number date) 7)))
|
||||
|
||||
(defun ymd->date (yy mm dd &optional (hour 0) (min 0) (sec 0) zone)
|
||||
"Return the Julian date corresponding to the given date and time. No
|
||||
compensation is made for the Gregorian Calendar introduction.
|
||||
Note that the Julian date is an integer when the time is noon so a
|
||||
date without time (midnight) will have a fractional part of 0.5.
|
||||
|
||||
If timezone is specified, it should be either an alphabetic code e.g., \"IST\"
|
||||
or a numeric offset in fractions of an hour e.g., +5.5"
|
||||
(let* ((a (floor (* 1/12 (- 14 mm))))
|
||||
(b (- yy a))
|
||||
(c (floor (/ b 100)))
|
||||
(day-frac (hms->day-fraction hour min sec))
|
||||
(jdate (+ (floor (* 30.6001d0 (+ (* a 12) mm 1)))
|
||||
(- (floor (* 365.25d0 (+ b 4716))) 1524)
|
||||
(floor (/ c 4))
|
||||
(- c)
|
||||
2
|
||||
dd
|
||||
(- day-frac 0.5d0)))
|
||||
(offset (zone-to-offset zone)))
|
||||
(- jdate (/ offset 24))))
|
||||
|
||||
(defun date->ymd (date)
|
||||
"Returns 6 values corresponding to the given datetime-
|
||||
year, month, day, hour, minute, second. Second may be a floating
|
||||
point value but the first five are aways integers"
|
||||
(let* ((jd (jday-number date))
|
||||
(e (floor (/ (- jd 1867216.25d0) 36524.25d0)))
|
||||
(f (+ 1 jd e (- (floor (/ e 4)))))
|
||||
(g (+ f 1524))
|
||||
(h (floor (/ (- g 122.1d0) 365.25d0)))
|
||||
(i (floor (* 365.25d0 h)))
|
||||
(j (floor (/ (- g i) 30.6001d0)))
|
||||
(dd (- g i (floor (* j 30.6001d0))))
|
||||
(mm (- j (* 12 (floor (/ j 14))) 1))
|
||||
(yy (+ h (floor (* 1/12 (- 14 mm))) -4716))
|
||||
(day-frac (multiple-value-bind (day frac) (truncate date)
|
||||
(declare (ignore day))
|
||||
(if (>= frac 0.5d0)
|
||||
;; midnight to noon
|
||||
(- frac 0.5d0)
|
||||
(+ frac 0.5d0)))))
|
||||
(multiple-value-bind (h m s) (day-fraction->hms day-frac)
|
||||
(values yy mm dd h m s))))
|
||||
|
||||
(defun valid-date-p (yy mm dd)
|
||||
"Check that year, month and day form a valid calendar date"
|
||||
(multiple-value-bind (y m d hr mn sc) (date->ymd (ymd->date yy mm dd))
|
||||
(declare (ignore hr mn sc))
|
||||
(and (= yy y) (= mm m) (= dd d))))
|
||||
|
||||
(defun valid-time-p (h m s)
|
||||
"Check that hour, minute and second are valid numbers"
|
||||
(and (<= 0 h 23) (<= 0 m 59) (>= s 0) (< s 60)))
|
||||
|
||||
(defun todays-date ()
|
||||
"Return current date-time (UTC)"
|
||||
(multiple-value-bind (s m h dd mm yy dw dst zone)
|
||||
(decode-universal-time (get-universal-time) 0)
|
||||
(declare (ignore h m s dw dst zone))
|
||||
(ymd->date yy mm dd)))
|
||||
|
||||
(defun todays-datetime (&optional zone)
|
||||
"Return current date-time (UTC)"
|
||||
(let ((offset (zone-to-offset zone)))
|
||||
(multiple-value-bind (s m h dd mm yy dw dst zone)
|
||||
(decode-universal-time (get-universal-time) 0)
|
||||
(declare (ignore dw dst zone))
|
||||
(- (ymd->date yy mm dd h m s) (/ offset 24)))))
|
||||
|
||||
(defun date->javascript-time (date)
|
||||
"Convert a datetime to the corresponding time value in Javascript"
|
||||
(let ((days (- date 2440587.5d0))) ; days since 1-Jan-1970
|
||||
(truncate (* days 86400 1000)))) ; number of milliseconds
|
||||
|
||||
(defun easter-day (yy &optional (want-gregorian-date nil))
|
||||
"Returns the date for Easter Sunday in the given year.
|
||||
Accurate until approx. 4000 CE
|
||||
Returns a Julian date if want-gregorian-date is NIL. Otherwise,
|
||||
it returns year, month, day, hour, minute and second as 6 values"
|
||||
(let* ((century (floor (/ yy 100)))
|
||||
(remain-19 (mod yy 19))
|
||||
(temp (+ (floor (* 1/2 (- century 15)))
|
||||
202
|
||||
(* -11 remain-19))))
|
||||
;; calculate date of Paschal moon
|
||||
(cond ((member century '(21 24 25 27 28 29 30 31 32 34 35 38))
|
||||
(decf temp))
|
||||
((member century '(33 36 37 39 40))
|
||||
(decf temp 2)))
|
||||
(setf temp (mod temp 30))
|
||||
(let ((temp-a (+ temp 21)))
|
||||
(when (or (= 29 temp)
|
||||
(and (= 28 temp) (> remain-19 10)))
|
||||
(decf temp-a))
|
||||
;; find next Sunday
|
||||
(let* ((temp-b (mod (- temp-a 19) 7))
|
||||
(temp-c (mod (- 40 century) 4)))
|
||||
(when (= temp-c 3)
|
||||
(incf temp-c))
|
||||
(when (> temp-c 1)
|
||||
(incf temp-c))
|
||||
(setf temp (mod yy 100))
|
||||
(let* ((temp-d (mod (+ temp (floor (/ temp 4))) 7))
|
||||
(temp-e (1+ (mod (- 20 temp-b temp-c temp-d) 7)))
|
||||
(dd (+ temp-a temp-e))
|
||||
(mm 3))
|
||||
(when (> dd 31)
|
||||
(setf dd (- dd 31)
|
||||
mm 4))
|
||||
(if want-gregorian-date
|
||||
(values yy mm dd)
|
||||
(ymd->date yy mm dd)))))))
|
||||
|
||||
(defun vernal-equinox (yy &optional (want-gregorian-date nil))
|
||||
"Return UTC date-time of the vernal (spring) equinox for the given year.
|
||||
Returns a Julian date if want-gregorian-date is NIL. Otherwise,
|
||||
it returns year, month, day, hour, minute and second as 6 values"
|
||||
(calc-equinox-or-solstice-date 1 yy want-gregorian-date))
|
||||
(defun summer-solstice (yy &optional (want-gregorian-date nil))
|
||||
"Return UTC date-time of the summer solstice for the given year.
|
||||
Returns a Julian date if want-gregorian-date is NIL. Otherwise,
|
||||
it returns year, month, day, hour, minute and second as 6 values"
|
||||
(calc-equinox-or-solstice-date 2 yy want-gregorian-date))
|
||||
(defun autumnal-equinox (yy &optional (want-gregorian-date nil))
|
||||
"Return UTC date-time of the autumnal equinox for the given year.
|
||||
Returns a Julian date if want-gregorian-date is NIL. Otherwise,
|
||||
it returns year, month, day, hour, minute and second as 6 values"
|
||||
(calc-equinox-or-solstice-date 3 yy want-gregorian-date))
|
||||
(defun winter-solstice (yy &optional (want-gregorian-date nil))
|
||||
"Return UTC date-time of the winter solstice for the given year.
|
||||
Returns a Julian date if want-gregorian-date is NIL. Otherwise,
|
||||
it returns year, month, day, hour, minute and second as 6 values"
|
||||
(calc-equinox-or-solstice-date 4 yy want-gregorian-date))
|
||||
|
||||
;; Formulae are from Chapter 27 of Jan Meeus' Astronomical Algorithms
|
||||
;; Valid for years between 1000-3000
|
||||
;; Accurate to within a minute from 1950-2050
|
||||
(defun calc-equinox-or-solstice-date (which yy want-gregorian-date)
|
||||
(let* ((mf (/ (- yy 2000) 1000))
|
||||
(date (cond ((= which 1) (+ 2451623.80984d0
|
||||
(* mf 365242.37404d0)
|
||||
(* mf mf 0.05169d0)
|
||||
(* mf mf mf -0.00411d0)
|
||||
(* mf mf mf mf -0.00057d0)))
|
||||
((= which 2) (+ 2451716.56767d0
|
||||
(* mf 365241.62603d0)
|
||||
(* mf mf 0.00325d0)
|
||||
(* mf mf mf 0.00888d0)
|
||||
(* mf mf mf mf -0.00030d0)))
|
||||
((= which 3) (+ 2451810.21715d0
|
||||
(* mf 365242.01767d0)
|
||||
(* mf mf -0.11575d0)
|
||||
(* mf mf mf -0.00337d0)
|
||||
(* mf mf mf mf 0.00078d0)))
|
||||
((= which 4) (+ 2451900.05952d0
|
||||
(* mf 365242.74049d0)
|
||||
(* mf mf -0.06223d0)
|
||||
(* mf mf mf -0.00823d0)
|
||||
(* mf mf mf mf 0.00032d0)))
|
||||
(t (error "Invalid param which: ~a" which)))))
|
||||
(setf date (apply-correction-factors yy date))
|
||||
(if want-gregorian-date
|
||||
(date->ymd date)
|
||||
date)))
|
||||
|
||||
(defun cos-degrees (deg)
|
||||
(cos (/ (* deg pi) 180d0)))
|
||||
|
||||
;; Jan Meeus' Astronomical Algorithms, Chapter 27
|
||||
(defun apply-correction-factors (yy jd)
|
||||
(let* ((t0 (/ (- jd 2451545d0) 36525d0))
|
||||
(w (- (* 35999.373d0 t0) 2.47d0))
|
||||
(dl (+ 1.0 (* 0.0334d0 (cos-degrees w))
|
||||
(* 0.0007d0 (cos-degrees (* 2.0d0 w)))))
|
||||
(s (periodic-24 t0)))
|
||||
;; get correct time in TDT (terrestrial dynamic time)
|
||||
(incf jd (/ (* 0.00001d0 s) dl))
|
||||
;; apply correction TDT -> UTC
|
||||
(tdt-to-utc yy jd)))
|
||||
|
||||
;; Jan Meeus' Astronomical Algorithms, Chapter 27
|
||||
(defparameter +periodic-24-a+ #(485d0 203d0 199d0 182d0 156d0 136d0 77d0 74d0 70d0
|
||||
58d0 52d0 50d0 45d0 44d0 29d0 18d0 17d0 16d0 14d0
|
||||
12d0 12d0 12d0 9d0 8d0))
|
||||
(defparameter +periodic-24-b+ #(324.96d0 337.23d0 342.08d0 27.85d0 73.14d0 171.52d0
|
||||
222.54d0 296.72d0 243.58d0 119.81d0 297.17d0 21.02d0
|
||||
247.54d0 325.15d0 60.93d0 155.12d0 288.79d0 198.04d0
|
||||
199.76d0 95.39d0 287.11d0 320.81d0 227.73d0 15.45d0))
|
||||
(defparameter +periodic-24-c+ #(1934.136d0 32964.467d0 20.186d0 445267.112d0 45036.886d0
|
||||
22518.443d0 65928.934d0 3034.906d0 9037.513d0 33718.147d0
|
||||
150.678d0 2281.226d0 29929.562d0 31555.956d0 4443.417d0
|
||||
67555.328d0 4562.452d0 62894.029d0 31436.921d0 14577.848d0
|
||||
31931.756d0 34777.259d0 1222.114d0 16859.074d0))
|
||||
(defun periodic-24 (t0)
|
||||
(loop for i from 0 below 24
|
||||
summing (* (aref +periodic-24-a+ i)
|
||||
(cos-degrees (+ (aref +periodic-24-b+ i)
|
||||
(* t0 (aref +periodic-24-c+ i)))))))
|
||||
|
||||
;; TDT -> UTC conversion: from Meeus' Astronomical Algorithms, Chapter 10
|
||||
;; Applies affsets in seconds to convert from Terrestrial Dynamic Time to UTC
|
||||
;;
|
||||
;; Offsets are directly available for even-numbered years between 1620-2002.
|
||||
;; Offsets for odd-numbered years are linearly interpolated.
|
||||
;; Offsets for years before 1620 and after 2002 (upto 2100) are obtained by
|
||||
;; applying interpolation formulae.
|
||||
;; 2000 and 2002 year data is from NASA and others from Meeus.
|
||||
(defparameter +tdt-offsets+
|
||||
#(121 112 103 95 88 82 77 72 68 63 60 56 53 51 48 46 44 42 40 38 ; 1620-1658
|
||||
35 33 31 29 26 24 22 20 18 16 14 12 11 10 9 8 7 7 7 7 ; 1660-1698
|
||||
7 7 8 8 9 9 9 9 9 10 10 10 10 10 10 10 10 11 11 11 ; 1700-1738
|
||||
11 11 12 12 12 12 13 13 13 14 14 14 14 15 15 15 15 15 16 16 ; 1740-1778
|
||||
16 16 16 16 16 16 15 15 14 13 13.1 12.5 12.2 12 12 12 12 12 12 11.9 ; 1780-1818
|
||||
11.6 11 10.2 9.2 8.2 7.1 6.2 5.6 5.4 5.3 5.4 5.6 5.9 6.2 6.5 ; 1820-1848
|
||||
6.8 7.1 7.3 7.5 7.6 7.7 7.3 6.2 5.2 2.7 1.4 -1.2 -2.8 -3.8 -4.8 ; 1850-1878
|
||||
-5.5 -5.3 -5.6 -5.7 -5.9 -6 -6.3 -6.5 -6.2 -4.7 -2.8 -0.1 2.6 5.3 7.7 ; 1880-1908
|
||||
10.4 13.3 16 18.2 20.2 21.1 22.4 23.5 23.8 24.3 24 23.9 23.9 23.7 24 ; 1910-1938
|
||||
24.3 25.3 26.2 27.3 28.2 29.1 30 30.7 31.4 32.2 33.1 34 35 36.5 38.3 ; 1940-1968
|
||||
40.2 42.2 44.5 46.5 48.5 50.5 52.5 53.8 54.9 55.8 56.9 58.3 60 61.6 63 ; 1970-1998
|
||||
63.8 64.3)) ; 2000-2002
|
||||
(defparameter +offsets-first-year+ 1620)
|
||||
(defparameter +offsets-last-year+ (+ +offsets-first-year+
|
||||
(* 2 (1- (length +tdt-offsets+)))))
|
||||
(defun tdt-to-utc (yy jd)
|
||||
(let* ((yt (/ (- yy 2000) 100d0)) ; centuries from epoch
|
||||
(delta-t ; calculated offset in seconds
|
||||
(cond ((<= +offsets-first-year+ yy +offsets-last-year+)
|
||||
;; correction directly available from table
|
||||
(if (evenp yy)
|
||||
;; lookup directly
|
||||
(aref +tdt-offsets+ (* 1/2 (- yy +offsets-first-year+)))
|
||||
;; interpolate
|
||||
(* 1/2 (+ (aref +tdt-offsets+ (* 1/2 (- (1+ yy) +offsets-first-year+)))
|
||||
(aref +tdt-offsets+ (* 1/2 (- (1- yy) +offsets-first-year+)))))))
|
||||
((< yy 948)
|
||||
(+ 2177d0 (* 497d0 yt) (* 44.1d0 yt yt)))
|
||||
((< yy 947 +offsets-first-year+)
|
||||
(+ 102d0 (* 102d0 yt) (* 25.3d0 yt yt)))
|
||||
((<= 2000 yy 2100)
|
||||
(+ 102d0 (* 102d0 yt) (* 25.3d0 yt yt)
|
||||
(* 0.37d0 (- yy 2100))))
|
||||
;; no adjustment available for years later than 2100
|
||||
(t 0))))
|
||||
;; decrement date by offset seconds (as a fraction of a day)
|
||||
(decf jd (hms->day-fraction 0 0 delta-t))))
|
||||
78
packages.lisp
Normal file
78
packages.lisp
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
;;; Copyright (c) 2017, Sudhir Shenoy. All rights reserved.
|
||||
|
||||
;;; Redistribution and use in source and binary forms, with or without
|
||||
;;; modification, are permitted provided that the following conditions
|
||||
;;; are met:
|
||||
|
||||
;;; * Redistributions of source code must retain the above copyright
|
||||
;;; notice, this list of conditions and the following disclaimer.
|
||||
|
||||
;;; * Redistributions in binary form must reproduce the above
|
||||
;;; copyright notice, this list of conditions and the following
|
||||
;;; disclaimer in the documentation and/or other materials
|
||||
;;; provided with the distribution.
|
||||
|
||||
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
|
||||
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
(in-package :cl-user)
|
||||
|
||||
(defpackage :cl-dates
|
||||
(:nicknames :dt)
|
||||
(:use :common-lisp)
|
||||
(:export
|
||||
;; Make a new date
|
||||
:ymd->date ; date from y/m/d/h/m/s components
|
||||
:string->date ; date from string
|
||||
:todays-date ; system date
|
||||
:todays-datetime ; system date-time
|
||||
;; Date converters
|
||||
:date->ymd ; to components
|
||||
:date->string ; to string
|
||||
:date->long-string ; to string (verbose)
|
||||
:date->javascript-time ; to JS datetime
|
||||
:date->local-time ; to local time zone
|
||||
:month->string ; full name of month
|
||||
:dow->string ; day of week as string
|
||||
;; Special dates for given year
|
||||
:easter-day ; easter day
|
||||
:vernal-equinox ; spring equinox date-time
|
||||
:summer-solstice ; summer solstice date-time
|
||||
:autumnal-equinox ; autumn equinox date-time
|
||||
:winter-solstice ; winter solstice date-time
|
||||
;; Miscellaneous functions
|
||||
:valid-date-p
|
||||
:valid-time-p
|
||||
:jday-number ; Julian day number
|
||||
:day-of-week ; Day of week for date
|
||||
;; Comparisons
|
||||
:date= :date/=
|
||||
:date< :date<=
|
||||
:date> :date>=
|
||||
;; Calendar date arithmetic
|
||||
:date+ :date- ; add/subtract days to date
|
||||
:date-diff ; days between two dates
|
||||
:nth-day-of-week ; 'n' weeks offset from start of month
|
||||
:first-of-next-month ; first day of next month
|
||||
:last-day-of-month ; last calendar day in month
|
||||
:last-day-of-prev-month ; last calendar day in previous month
|
||||
;; Business date arithmetic
|
||||
;; :previous-workday ; last working day before date (which can be a holiday)
|
||||
;; :next-workday ; next working day after date (which can be a holiday)
|
||||
;; :bdate+ :bdate- ; add/subtract working days to date
|
||||
;; :add-calendar-months ; add/subtract months obeying day count conventions
|
||||
;; :add-months ; add/subtract months obeying day count conventions
|
||||
;; :last-workday-of-month ; Last working day in month
|
||||
;; :generate-date-series ; Series of coupon dates following day conventions
|
||||
))
|
||||
|
||||
|
||||
592
parse-date.lisp
Normal file
592
parse-date.lisp
Normal file
|
|
@ -0,0 +1,592 @@
|
|||
;;; Copyright (c) 2017, Sudhir Shenoy. All rights reserved.
|
||||
|
||||
;;; Redistribution and use in source and binary forms, with or without
|
||||
;;; modification, are permitted provided that the following conditions
|
||||
;;; are met:
|
||||
|
||||
;;; * Redistributions of source code must retain the above copyright
|
||||
;;; notice, this list of conditions and the following disclaimer.
|
||||
|
||||
;;; * Redistributions in binary form must reproduce the above
|
||||
;;; copyright notice, this list of conditions and the following
|
||||
;;; disclaimer in the documentation and/or other materials
|
||||
;;; provided with the distribution.
|
||||
|
||||
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
|
||||
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
(in-package :cl-dates)
|
||||
|
||||
(defparameter +white-space+ '(#\space #\tab #\return #\linefeed #\newline))
|
||||
(defun white-space-p (char)
|
||||
(member char +white-space+ :test #'char=))
|
||||
|
||||
(defun tokenize (str)
|
||||
(let ((token-list nil)
|
||||
(state nil)
|
||||
(token nil)
|
||||
(seen-alpha nil))
|
||||
(labels ((split-list (test list &aux (start list) (end list))
|
||||
(loop while (and end (setq start (member-if-not test end)))
|
||||
collect (ldiff start (setq end (member-if test start)))))
|
||||
(emit-token ()
|
||||
(if (and (member state '(:in-alpha-num :in-num-alpha))
|
||||
(or seen-alpha
|
||||
(> (count #\. token) 1)
|
||||
(char= #\. (car token)))
|
||||
;; ugly special casing for a.m./p.m. strings
|
||||
(not (equal token '(#\. #\m #\. #\a)))
|
||||
(not (equal token '(#\m #\. #\a)))
|
||||
(not (equal token '(#\. #\m #\. #\p)))
|
||||
(not (equal token '(#\m #\. #\p))))
|
||||
(progn
|
||||
;; separate "23.jun.2017" => "23" "." "jun" "." "2017"
|
||||
;; any trailing period will be dropped
|
||||
(setf token (nreverse token))
|
||||
(loop for tk on (split-list #'(lambda(x) (char= x #\.)) token)
|
||||
do (progn
|
||||
(push (coerce (car tk) 'string) token-list)
|
||||
(unless (null (cdr tk))
|
||||
(push "." token-list)))))
|
||||
(push (coerce (nreverse token) 'string) token-list))
|
||||
;; reset state
|
||||
(setf state nil seen-alpha nil token nil)))
|
||||
(do ((i 0 (1+ i))
|
||||
(len-str (length str)))
|
||||
((>= i len-str))
|
||||
(let ((ch (schar str i)))
|
||||
(case state
|
||||
((nil)
|
||||
(push ch token)
|
||||
(setf state (cond ((alpha-char-p ch) :in-alpha)
|
||||
((digit-char-p ch) :in-num)
|
||||
((white-space-p ch) :in-space)
|
||||
(t :in-separators))))
|
||||
(:in-separators
|
||||
(if (or (alphanumericp ch) (white-space-p ch))
|
||||
(progn
|
||||
(decf i)
|
||||
(emit-token))
|
||||
(push ch token)))
|
||||
(:in-space
|
||||
(when (not (white-space-p ch))
|
||||
;; throw away white space and reset state
|
||||
(decf i)
|
||||
(setf token nil state nil)))
|
||||
(:in-alpha
|
||||
(setf seen-alpha t)
|
||||
(cond ((alpha-char-p ch) (push ch token))
|
||||
((char= ch #\.)
|
||||
(push ch token)
|
||||
(setf state :in-alpha-num))
|
||||
(t (decf i)
|
||||
(emit-token))))
|
||||
(:in-num
|
||||
(cond ((digit-char-p ch) (push ch token))
|
||||
((char= ch #\.)
|
||||
(push ch token)
|
||||
(setf state :in-num-alpha))
|
||||
(t (decf i)
|
||||
(emit-token))))
|
||||
(:in-alpha-num
|
||||
(setf seen-alpha t)
|
||||
(cond ((or (alpha-char-p ch) (char= ch #\.)) (push ch token))
|
||||
((and (digit-char-p ch) token (char= #\. (car token)))
|
||||
(push ch token)
|
||||
(setf state :in-num-alpha))
|
||||
(t (decf i)
|
||||
(emit-token))))
|
||||
(:in-num-alpha
|
||||
(cond ((or (digit-char-p ch) (char= ch #\.)) (push ch token))
|
||||
((and (alpha-char-p ch) token (char= #\. (car token)))
|
||||
(push ch token)
|
||||
(setf state :in-alpha-num))
|
||||
(t(decf i)
|
||||
(emit-token)))))))
|
||||
(when (and token (not (member state '(:in-space :in-separators))))
|
||||
(emit-token)))
|
||||
(nreverse token-list)))
|
||||
|
||||
(defun advance-date (dt dow dir)
|
||||
(if (eq dir :closest)
|
||||
(let ((next (advance-date dt dow :next))
|
||||
(prev (advance-date dt dow :prev)))
|
||||
(if (< (- dt prev) (- next dt))
|
||||
prev
|
||||
next))
|
||||
(let ((inc (if (eq dir :next) 1 -1)))
|
||||
(loop
|
||||
(if (eq (day-of-week dt) dow)
|
||||
(return dt)
|
||||
(incf dt inc))))))
|
||||
|
||||
(defun make-four-digit-year (year)
|
||||
(if (> year 99)
|
||||
year
|
||||
(multiple-value-bind (s m h dd mm yy dw dst zone)
|
||||
(decode-universal-time (get-universal-time) 0)
|
||||
(declare (ignore s m h dd mm dw dst zone))
|
||||
(let ((century (* 100 (truncate (/ yy 100)))))
|
||||
(incf year century)
|
||||
(if (>= (abs (- year yy)) 50)
|
||||
(if (< year yy)
|
||||
(+ year 100)
|
||||
(- year 100))
|
||||
year)))))
|
||||
|
||||
;; decide between yy/mm and mm/yy
|
||||
(defun assign-yy-mm (list precedence)
|
||||
(cond ((> (car list) 12) (values (car list) (cadr list)))
|
||||
((> (cadr list) 12) (values (cadr list) (car list)))
|
||||
((eq precedence :ymd) (values (car list) (cadr list)))
|
||||
(t (values (cadr list) (car list)))))
|
||||
|
||||
;; decide between yy/dd and dd/yy
|
||||
(defun assign-yy-dd (list precedence)
|
||||
(cond ((> (car list) 31) (values (car list) (cadr list)))
|
||||
((> (cadr list) 31) (values (cadr list) (car list)))
|
||||
((eq precedence :ymd) (values (car list) (cadr list)))
|
||||
(t (values (cadr list) (car list)))))
|
||||
|
||||
;; decide between dd/mm and mm/dd
|
||||
(defun assign-mm-dd (list precedence)
|
||||
(cond ((> (car list) 12) (values (cadr list) (car list)))
|
||||
((> (cadr list) 12) (values (car list) (cadr list)))
|
||||
((or (eq precedence :ymd) (eq precedence :mdy))
|
||||
(values (car list) (cadr list)))
|
||||
(t (values (cadr list) (car list)))))
|
||||
|
||||
;; decide order in which day, month and year appear given 2 or 3 numbers
|
||||
(defun assign-yy-mm-dd (list precedence)
|
||||
(if (= 2 (length list))
|
||||
(cond ((and (> (car list) 12) (> (cadr list) 12))
|
||||
(multiple-value-bind (yy dd) (assign-yy-dd list precedence)
|
||||
(values yy nil dd)))
|
||||
(t (multiple-value-bind (yy mm) (assign-yy-mm list precedence)
|
||||
(values yy mm nil))))
|
||||
(let (tmp)
|
||||
(cond ((every #'(lambda(x) (<= x 12)) list)
|
||||
;; 01/02/03
|
||||
(cond ((eq precedence :mdy) (values (third list) (first list) (second list)))
|
||||
((eq precedence :dmy) (values (third list) (second list) (first list)))
|
||||
(t (values (first list) (second list) (third list)))))
|
||||
((setf tmp (find-if #'(lambda(x) (> x 31)) list))
|
||||
;; 12/5/55
|
||||
(setf list (remove tmp list :count 1))
|
||||
(multiple-value-bind (mm dd) (assign-mm-dd list precedence)
|
||||
(values tmp mm dd)))
|
||||
((= 1 (count-if #'(lambda(x) (<= x 12)) list))
|
||||
(setf tmp (find-if #'(lambda(x) (<= x 12)) list)
|
||||
list (remove tmp list :count 1))
|
||||
(multiple-value-bind (yy dd) (assign-yy-dd list precedence)
|
||||
(values yy tmp dd)))
|
||||
(t ;; 5/6/27
|
||||
(cond ((eq precedence :ymd)
|
||||
(multiple-value-bind (mm dd) (assign-mm-dd (cdr list) precedence)
|
||||
(values (car list) mm dd)))
|
||||
((eq precedence :dmy)
|
||||
(multiple-value-bind (yy mm) (assign-yy-mm (cdr list) precedence)
|
||||
(values yy mm (car list))))
|
||||
(t (multiple-value-bind (yy dd) (assign-yy-dd (cdr list) precedence)
|
||||
(values yy (car list) dd)))))))))
|
||||
|
||||
;; struct to hold values during parse
|
||||
(defstruct (date-components (:conc-name dt-))
|
||||
dow yr mth day hr min sec tz)
|
||||
|
||||
(defparameter +date-separators+ '("." "/" "-"))
|
||||
|
||||
(defun string->date (string &key (reference-date (todays-date)) (precedence :ymd))
|
||||
(setf string (string-downcase (string-trim +white-space+ string)))
|
||||
(unless (member precedence '(:ymd :dmy :mdy))
|
||||
(error "invalid precedence ~a" precedence))
|
||||
(macrolet ((when-null-set (var field value)
|
||||
`(when (null (,field ,var)) (setf (,field ,var) ,value))))
|
||||
(let* ((res (make-date-components))
|
||||
(tokens (tokenize string))
|
||||
(num-tok (length tokens))
|
||||
(skipped-tokens nil) ; tokens that were ignored
|
||||
(relative-dow nil) ; when day of week is qualified by this/next/prev
|
||||
(date-comps nil) ; uncertain whether day/month/year
|
||||
(num-date-comps 0) ; length of date-comps + date components in res
|
||||
(time-parsed nil)) ; whether all of h/m/s have been parsed
|
||||
(do ((i 0 (1+ i)))
|
||||
((>= i num-tok))
|
||||
(let* ((token (elt tokens i))
|
||||
(len (length token))
|
||||
(next-tok (if (>= (1+ i) num-tok) nil (elt tokens (1+ i))))
|
||||
(prev-tok (if (> i 0) (elt tokens (1- i)) nil))
|
||||
(num (parse-number token))
|
||||
tmp)
|
||||
(when (numberp num)
|
||||
(cond
|
||||
;; e.g. yyyymmddThhmmss or yyyymmddThh:mm:ss
|
||||
((and (= num-date-comps 3)
|
||||
(or (= len 2) (= len 4) (= len 6))
|
||||
prev-tok (string= prev-tok "t")
|
||||
(null (find #\. token)))
|
||||
;; if colon separated time, only hour will be parsed
|
||||
;; cannot set time-parsed to T unless len > 2
|
||||
(setf (dt-hr res) (parse-integer (subseq token 0 2)))
|
||||
(when (>= len 4)
|
||||
(setf (dt-min res) (parse-integer (subseq token 2 4))
|
||||
time-parsed t))
|
||||
(when (= len 6)
|
||||
(setf (dt-sec res) (parse-integer (subseq token 4)))))
|
||||
;; YYMMDD or HHMMSS
|
||||
((or (= len 6)
|
||||
(and (> len 7) (setf tmp (position #\. token)) (= tmp 6)))
|
||||
(if (and (= 0 num-date-comps) (= len 6)
|
||||
(or (null prev-tok) (string/= prev-tok "t")))
|
||||
;; date if not yet parsed and not explicitly marked as time
|
||||
(let* ((yy (truncate (/ num 10000)))
|
||||
(mm (truncate (- (/ num 100) (* yy 100))))
|
||||
(dd (truncate (- num (+ (* yy 10000) (* mm 100))))))
|
||||
(setf date-comps (list dd mm yy)) ; reverse order of specification
|
||||
(incf num-date-comps 3))
|
||||
;; time
|
||||
(setf (dt-hr res) (parse-integer (subseq token 0 2))
|
||||
(dt-min res) (parse-integer (subseq token 2 4))
|
||||
(dt-sec res) (parse-number (subseq token 4))
|
||||
time-parsed t)))
|
||||
;; YYYYMMDD - only format for 8 digit date (ddmmyyyy must be e.g. dd/mm/yyyy)
|
||||
((= len 8)
|
||||
(when (find #\. token)
|
||||
;; nnnn.nnn
|
||||
(return-from string->date nil))
|
||||
(let* ((yy (truncate (/ num 10000)))
|
||||
(mm (truncate (- (/ num 100) (* yy 100))))
|
||||
(dd (truncate (- num (+ (* yy 10000) (* mm 100))))))
|
||||
(setf (dt-yr res) yy (dt-mth res) mm (dt-day res) dd)
|
||||
(incf num-date-comps 3)))
|
||||
;; YYYYMMDDHHMM[SS[.ss]]
|
||||
((or (= len 12) (= len 14)
|
||||
(and (> len 15) (setf tmp (position #\. token)) (= tmp 14)))
|
||||
(setf (dt-yr res) (parse-integer (subseq token 0 4))
|
||||
(dt-mth res) (parse-integer (subseq token 4 6))
|
||||
(dt-day res) (parse-integer (subseq token 6 8))
|
||||
(dt-hr res) (parse-integer (subseq token 8 10))
|
||||
(dt-min res) (parse-integer (subseq token 10 12)))
|
||||
(incf num-date-comps 3)
|
||||
(when (> len 12)
|
||||
(setf (dt-sec res) (parse-number (subseq token 12))))
|
||||
(setf time-parsed t))
|
||||
;; HH:MM:SS
|
||||
((or (and next-tok (string= next-tok ":"))
|
||||
(and (not (null (dt-hr res))) prev-tok (string= prev-tok ":")))
|
||||
;; distinguish case when coming here from yyyymmddThh:mm:ss
|
||||
(unless (and prev-tok (not time-parsed) (string= prev-tok ":"))
|
||||
(setf (dt-hr res) num)
|
||||
(incf i 2)) ; advance pointer to minute
|
||||
(setf num (parse-number (elt tokens i)))
|
||||
(when (null num)
|
||||
(return-from string->date nil))
|
||||
;; also handle weirdness like hh:mm.ss
|
||||
(multiple-value-bind (int frac) (truncate num)
|
||||
(setf (dt-min res) int)
|
||||
(when (> frac 0)
|
||||
(setf (dt-sec res) (* 60 frac))
|
||||
(when (and (< (1+ i) num-tok) (string= (elt tokens (1+ i)) ":"))
|
||||
;; can't handle hh:mm.ss:wtf
|
||||
(return-from string->date nil))))
|
||||
(when (and (< (1+ i) num-tok) (string= (elt tokens (1+ i)) ":"))
|
||||
(incf i 2) ; position on second
|
||||
;; both hh:mm:<eos> and hh:mm:ss are acceptable
|
||||
(when (< i num-tok)
|
||||
(setf num (parse-number (elt tokens i)))
|
||||
(when (null num)
|
||||
(return-from string->date nil))
|
||||
(setf (dt-sec res) num))))
|
||||
;; dd/mm/yy and variants
|
||||
((and next-tok (member next-tok +date-separators+ :test #'string=))
|
||||
(let ((sep next-tok)
|
||||
(is-ymd nil))
|
||||
(if (> num 31)
|
||||
;; year - default to yymmdd format
|
||||
(setf (dt-yr res) num
|
||||
is-ymd t)
|
||||
(push num date-comps))
|
||||
(incf num-date-comps)
|
||||
(incf i 2) ; position on second component
|
||||
(when (>= i num-tok)
|
||||
;; reject strings ending in trailing slash
|
||||
(return-from string->date nil))
|
||||
(setf num (parse-number (elt tokens i)))
|
||||
(if (numberp num)
|
||||
(cond ((and is-ymd (<= num 12)) (setf (dt-mth res) num))
|
||||
(is-ymd (setf (dt-day res) num))
|
||||
(t (push num date-comps)))
|
||||
(progn
|
||||
(setf tmp (str-to-month (elt tokens i)))
|
||||
(if tmp
|
||||
(setf (dt-mth res) tmp)
|
||||
;; not number or month name
|
||||
(return-from string->date nil))))
|
||||
(incf num-date-comps)
|
||||
(when (and (< (1+ i) num-tok) (string= sep (elt tokens (1+ i))))
|
||||
(incf i 2) ; position on last component
|
||||
(when (>= i num-tok)
|
||||
(return-from string->date nil)) ; trailing slash
|
||||
(setf num (parse-number (elt tokens i)))
|
||||
(when (null num)
|
||||
;; month name in last position is not accepted
|
||||
(return-from string->date nil))
|
||||
(cond ((and is-ymd (not (null (dt-day res)))) (setf (dt-mth res) num))
|
||||
(is-ymd (setf (dt-day res) num))
|
||||
(t (push num date-comps)))
|
||||
(incf num-date-comps))))
|
||||
;; 2nd, 3rd, 21st etc. - parse as day
|
||||
((and next-tok (str-is-day-suffix next-tok))
|
||||
(incf i) ; skip suffix
|
||||
(setf (dt-day res) num)
|
||||
(incf num-date-comps))
|
||||
;; 13h45m, 13h 45, etc - parse as time
|
||||
((and next-tok (setf tmp (str-to-hms next-tok)))
|
||||
(loop
|
||||
;; consume following tokens as far as possible
|
||||
(multiple-value-bind (int frac) (truncate num)
|
||||
(cond ((eq tmp :hour)
|
||||
(setf (dt-hr res) int)
|
||||
(when (> frac 0)
|
||||
(setf (dt-min res) (* 60 frac))))
|
||||
((eq tmp :minute)
|
||||
(setf (dt-min res) int)
|
||||
(when (> frac 0)
|
||||
(setf (dt-sec res) (* 60 frac))))
|
||||
((eq tmp :second)
|
||||
(setf (dt-sec res) num
|
||||
time-parsed t))))
|
||||
(incf i) ; pointer is on suffix
|
||||
(when (or (>= (1+ i) num-tok) (eq tmp :second))
|
||||
(return)) ; done
|
||||
(setf num (parse-number (elt tokens (1+ i))))
|
||||
(when (null num)
|
||||
(return))
|
||||
(incf i) ; pointer on number after time suffix
|
||||
(if (or (>= (1+ i) num-tok)
|
||||
(null (str-to-hms (elt tokens (1+ i)))))
|
||||
;; no time suffix - set to min/sec based on what prev token was
|
||||
(progn
|
||||
(if (eq tmp :hour)
|
||||
(multiple-value-bind (int frac) (truncate num)
|
||||
(setf (dt-min res) int)
|
||||
(when (> frac 0)
|
||||
(setf (dt-sec res) (* 60 frac))))
|
||||
(setf (dt-sec res) num))
|
||||
(setf time-parsed t)
|
||||
(return))
|
||||
;; set up var for next loop iteration
|
||||
(setf tmp (str-to-hms (elt tokens (1+ i)))))))
|
||||
;; time with am/pm indicator e.g. 10 am
|
||||
((and next-tok (setf tmp (str-to-ampm next-tok)))
|
||||
(when (> num 12)
|
||||
(return-from string->date nil))
|
||||
(multiple-value-bind (int frac) (truncate num)
|
||||
(if (and (< int 12) (eq tmp :pm))
|
||||
(incf int 12)
|
||||
(when (and (= int 12) (eq tmp :am))
|
||||
(setf int 0)))
|
||||
(setf (dt-hr res) int)
|
||||
(when (> frac 0)
|
||||
;; 10.37 pm - frac is minutes not fraction of hour
|
||||
(setf (dt-min res) frac
|
||||
time-parsed t))))
|
||||
;; all other numbers - assume it is a date component
|
||||
(t (if (and prev-tok (string= prev-tok "of") (not (null (dt-mth res))))
|
||||
;; june of 1976
|
||||
(setf (dt-yr res) num)
|
||||
(push num date-comps))
|
||||
(incf num-date-comps)))
|
||||
;; avoid giant if-then-else
|
||||
(go end-of-do-loop))
|
||||
;; token is not a number
|
||||
(cond
|
||||
;; weekday
|
||||
((setf tmp (str-to-weekday token)) (setf (dt-dow res) tmp))
|
||||
;; relative day of week
|
||||
((setf tmp (str-to-relative-dow token)) (setf relative-dow tmp))
|
||||
;; today / tomorrow etc
|
||||
((setf tmp (str-to-relative-date token))
|
||||
(when (/= 0 num-date-comps)
|
||||
(return-from string->date nil))
|
||||
(multiple-value-bind (yy mm dd h m s) (date->ymd (+ reference-date tmp))
|
||||
(setf (dt-yr res) yy
|
||||
(dt-mth res) mm
|
||||
(dt-day res) dd
|
||||
num-date-comps 3)
|
||||
(when-null-set res dt-hr h)
|
||||
(when-null-set res dt-min m)
|
||||
(when-null-set res dt-sec s)))
|
||||
;; am/pm
|
||||
((setf tmp (str-to-ampm token))
|
||||
(let ((hr (dt-hr res)))
|
||||
(when (null hr)
|
||||
;; am/pm not accepted before time string
|
||||
(return-from string->date nil))
|
||||
(if (and (= hr 12) (eq tmp :am))
|
||||
(setf hr 0)
|
||||
(when (and (< hr 12) (eq tmp :pm))
|
||||
(incf hr 12)))
|
||||
(setf (dt-hr res) hr)))
|
||||
;; time zone name
|
||||
((or (and (> (length token) 2) (setf tmp (str-to-tz-offset token)))
|
||||
;; military time zone spec valid only at end of string
|
||||
(and (<= (length token) 2) (null next-tok) (setf tmp (str-to-tz-offset token))))
|
||||
(when (null (dt-hr res))
|
||||
(return-from string->date nil))
|
||||
;; if a time zone is specified as e.g. GMT+9 or JST-1,
|
||||
;; the next cond-clause for numeric offset will process it.
|
||||
;; offset is stored as fractions of a day since we add it to the julian date
|
||||
(setf (dt-tz res) (/ tmp 24)))
|
||||
;; Numeric time zone offset: e.g., +0530 or +05:30 or -5
|
||||
;; also handles GMT+9 etc in conjunction with previous clause
|
||||
((and (or (string= token "+") (string= token "-"))
|
||||
next-tok (not (null (parse-number next-tok))))
|
||||
(when (null (dt-hr res))
|
||||
(return-from string->date nil))
|
||||
(let ((sign (if (string= token "+") 1 -1))
|
||||
offset-hrs)
|
||||
(incf i) ; position on number
|
||||
(cond
|
||||
;; e.g. 5:30
|
||||
((and (< (1+ i) num-tok) (string= ":" (elt tokens (1+ i))))
|
||||
(setf offset-hrs (/ (parse-integer next-tok) 24))
|
||||
(incf i 2) ; position on minutes
|
||||
(if (or (>= i num-tok) (null (setf tmp (parse-number (elt tokens i)))))
|
||||
(return-from string->date nil)
|
||||
(incf offset-hrs (/ tmp 1440))))
|
||||
;; e.g., 3.5 or 11
|
||||
((or (not (null (position #\. next-tok))) (< (length next-tok) 3))
|
||||
(setf offset-hrs (/ (parse-number next-tok) 24)))
|
||||
;; e.g., 0530
|
||||
((= 4 (length next-tok))
|
||||
(setf offset-hrs (+ (/ (parse-integer (subseq next-tok 0 2)) 24)
|
||||
(/ (parse-integer (subseq next-tok 2 4)) 1440))))
|
||||
;; e.g., 530
|
||||
((= 3 (length next-tok))
|
||||
(setf offset-hrs (+ (/ (parse-integer (subseq next-tok 0 1)) 24)
|
||||
(/ (parse-integer (subseq next-tok 1 3)) 1440))))
|
||||
;; all others are invalid
|
||||
(t (return-from string->date nil)))
|
||||
(setf offset-hrs (* sign offset-hrs))
|
||||
(if (null (dt-tz res))
|
||||
(setf (dt-tz res) offset-hrs)
|
||||
(incf (dt-tz res) offset-hrs))))
|
||||
;; Alphabetic month name
|
||||
((setf tmp (str-to-month token))
|
||||
(when (dt-mth res)
|
||||
(return-from string->date nil)) ; duplicate spec
|
||||
(setf (dt-mth res) tmp)
|
||||
(incf num-date-comps)
|
||||
(when (and next-tok (member next-tok +date-separators+ :test #'string=))
|
||||
;; Jul/23/1956
|
||||
(let ((sep next-tok))
|
||||
(incf i 2) ; point to second component
|
||||
(when (< i num-tok)
|
||||
(setf num (parse-number (elt tokens i)))
|
||||
(when (null num)
|
||||
(return-from string->date nil))
|
||||
(incf num-date-comps)
|
||||
(if (> num 31)
|
||||
(setf (dt-yr res) num) ; clearly a year
|
||||
(push num date-comps))
|
||||
(when (and (< (1+ i) num-tok) (string= sep (elt tokens (1+ i))))
|
||||
(incf i 2) ; point to third component
|
||||
(setf num (parse-number (elt tokens i)))
|
||||
(when (null num)
|
||||
(return-from string->date nil))
|
||||
(incf num-date-comps)
|
||||
(if (> num 31)
|
||||
(if (dt-yr res)
|
||||
;; Jul/99/1985
|
||||
(return-from string->date nil)
|
||||
;; unambiguous date
|
||||
(setf (dt-yr res) num
|
||||
(dt-day res) (pop date-comps)))
|
||||
(if (dt-yr res)
|
||||
(setf (dt-day res) num)
|
||||
(push num date-comps))))))))
|
||||
;; default - skip token
|
||||
(t (push token skipped-tokens))))
|
||||
end-of-do-loop)
|
||||
(when (> num-date-comps 3)
|
||||
(return-from string->date nil))
|
||||
(when (and (= num-date-comps 0) (dt-dow res)
|
||||
(null relative-dow))
|
||||
;; day of week without date is relative to reference date
|
||||
(setf relative-dow :closest))
|
||||
(when date-comps
|
||||
(setf date-comps (nreverse date-comps)) ; order of appearance
|
||||
(let ((len (length date-comps)))
|
||||
(cond ((or (= 3 num-date-comps len)
|
||||
(= 2 num-date-comps len)) ; all uncertain
|
||||
(multiple-value-bind (yy mm dd) (assign-yy-mm-dd date-comps precedence)
|
||||
(setf (dt-yr res) yy (dt-mth res) mm (dt-day res) dd)))
|
||||
((and (= 3 num-date-comps) (= 2 len)) ; 2 of 3 are not certain
|
||||
(cond ((dt-day res) (multiple-value-bind (yy mm) (assign-yy-mm date-comps precedence)
|
||||
(setf (dt-yr res) yy (dt-mth res) mm)))
|
||||
((dt-mth res) (multiple-value-bind (yy dd) (assign-yy-dd date-comps precedence)
|
||||
(setf (dt-yr res) yy (dt-day res) dd)))
|
||||
((dt-yr res) (multiple-value-bind (mm dd) (assign-mm-dd date-comps precedence)
|
||||
(setf (dt-mth res) mm (dt-day res) dd)))))
|
||||
((and (= 3 num-date-comps) (= 1 len)) ; assign to null memeber in res
|
||||
(when-null-set res dt-yr (car date-comps))
|
||||
(when-null-set res dt-mth (car date-comps))
|
||||
(when-null-set res dt-day (car date-comps)))
|
||||
((and (= 2 num-date-comps) (= 1 len))
|
||||
(cond ((dt-yr res) ;; dd or mm
|
||||
(cond ((> (car date-comps) 12) (setf (dt-day res) (car date-comps)))
|
||||
((eq precedence :dmy) (setf (dt-day res) (car date-comps)))
|
||||
(t (setf (dt-mth res) (car date-comps)))))
|
||||
((dt-mth res) ;; dd or yy
|
||||
(if (> (car date-comps) 31)
|
||||
(setf (dt-yr res) (car date-comps))
|
||||
(setf (dt-day res) (car date-comps))))
|
||||
((dt-day res) ;; mm or yy
|
||||
(if (> (car date-comps) 12)
|
||||
(setf (dt-yr res) (car date-comps))
|
||||
(setf (dt-mth res) (car date-comps))))))
|
||||
(t ;; i.e., num-date-comps = 1 so one of dd or mm or yy
|
||||
(cond ((> (car date-comps) 31) (setf (dt-yr res) (car date-comps)))
|
||||
((<= (car date-comps) 12) (setf (dt-mth res) (car date-comps)))
|
||||
(t ;; between 12 and 31
|
||||
(setf (dt-day res) (car date-comps))))))))
|
||||
;; check that we could assign all date components
|
||||
(when (/= num-date-comps (+ (if (dt-yr res) 1 0) (if (dt-mth res) 1 0) (if (dt-day res) 1 0)))
|
||||
;; duplicate day / mth / year
|
||||
(return-from string->date nil))
|
||||
;; handle two-digit years
|
||||
(when (dt-yr res)
|
||||
(setf (dt-yr res) (make-four-digit-year (dt-yr res))))
|
||||
;; copy all missing values from the reference date
|
||||
(multiple-value-bind (yy mm dd h m s) (date->ymd reference-date)
|
||||
(when-null-set res dt-yr yy)
|
||||
(when-null-set res dt-mth mm)
|
||||
(when-null-set res dt-day dd)
|
||||
(when-null-set res dt-hr h)
|
||||
(when-null-set res dt-min m)
|
||||
(when-null-set res dt-sec s))
|
||||
(unless (and (valid-date-p (dt-yr res) (dt-mth res) (dt-day res))
|
||||
(valid-time-p (dt-hr res) (dt-min res) (dt-sec res)))
|
||||
(return-from string->date nil))
|
||||
(let ((dt (ymd->date (dt-yr res) (dt-mth res) (dt-day res)
|
||||
(dt-hr res) (dt-min res) (dt-sec res))))
|
||||
(if (and relative-dow (dt-dow res))
|
||||
(setf dt (advance-date dt (dt-dow res) relative-dow))
|
||||
(when (dt-dow res)
|
||||
(when (and (= 3 num-date-comps) (not (eq (dt-dow res) (day-of-week dt))))
|
||||
;; date was misparsed or day of week is not consistent with date
|
||||
(return-from string->date nil))))
|
||||
(when (dt-tz res)
|
||||
;; +ve offset = subtract, -ve offset = add
|
||||
(decf dt (dt-tz res)))
|
||||
(values dt skipped-tokens)))))
|
||||
108
print-date.lisp
Normal file
108
print-date.lisp
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
;;; Copyright (c) 2017, Sudhir Shenoy. All rights reserved.
|
||||
|
||||
;;; Redistribution and use in source and binary forms, with or without
|
||||
;;; modification, are permitted provided that the following conditions
|
||||
;;; are met:
|
||||
|
||||
;;; * Redistributions of source code must retain the above copyright
|
||||
;;; notice, this list of conditions and the following disclaimer.
|
||||
|
||||
;;; * Redistributions in binary form must reproduce the above
|
||||
;;; copyright notice, this list of conditions and the following
|
||||
;;; disclaimer in the documentation and/or other materials
|
||||
;;; provided with the distribution.
|
||||
|
||||
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
|
||||
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
(in-package :cl-dates)
|
||||
|
||||
(defun date->string (date &key (format :human) zone)
|
||||
"Return a string representation of a datetime as per the desired format.
|
||||
Format must be specified as one of (:human :iso-8601 :asctime :rfc-822 :rfc-850).
|
||||
|
||||
The time is omitted if it is exactly midnight or noon
|
||||
|
||||
If timezone is not null, the datetime is first translated to the given timezone
|
||||
from UTC and then converted to a string. Timezone can be specified as an alphabetic
|
||||
code or a numeric offset in fractions of hours.
|
||||
|
||||
The :human format is the same as :iso-8601 except that the separators between date,
|
||||
time and timezone are spaces to make it more readable to the human eye."
|
||||
(let ((offset (zone-to-offset zone)))
|
||||
(incf date (/ offset 24))
|
||||
(multiple-value-bind (yy mm dd h m s) (date->ymd date)
|
||||
(let ((year (format nil "~d" yy))
|
||||
(month (case format
|
||||
((:asctime :rfc-822 :rfc-850) (three-letter-month mm))
|
||||
(otherwise (format nil "~2,'0d" mm))))
|
||||
(day (format nil "~2,'0d" dd))
|
||||
(dow (case format
|
||||
((:asctime :rfc-822 :rfc-850) (three-letter-dow (day-of-week date)))
|
||||
(otherwise ""))))
|
||||
(if (or (integerp date)
|
||||
(= 1/2 (mod date 1)))
|
||||
(date-only-string year month day dow format)
|
||||
(date-time-string year month day dow h m s offset format))))))
|
||||
|
||||
(defun date->long-string (date &key zone (date-only nil))
|
||||
"Converts a date to a long string with day of week and month fully spelled out
|
||||
e.g., \"Thursday, 6 July 2017, 09:38:43.567 +0900\".
|
||||
|
||||
If :date-only is true, the time and timezone are omitted."
|
||||
(let ((offset (zone-to-offset zone)))
|
||||
(incf date (/ offset 24))
|
||||
(multiple-value-bind (yy mm dd h m s) (date->ymd date)
|
||||
(if date-only
|
||||
(format nil "~a, ~d ~a ~d" (dow->string (day-of-week date))
|
||||
dd (month->string mm) yy)
|
||||
(let* ((tz-hh (truncate (abs offset)))
|
||||
(tz-mm (truncate (* 60 (- (abs offset) tz-hh))))
|
||||
(tz-str (cond ((= 0 offset) "UTC")
|
||||
((< offset 0) (format nil "-~2,'0d~2,'0d" tz-hh tz-mm))
|
||||
(t (format nil "+~2,'0d~2,'0d" tz-hh tz-mm))))
|
||||
(time-str (if (< (- s (truncate s)) 0.001)
|
||||
(format nil "~2,'0d:~2,'0d:~2,'0d" h m (truncate s))
|
||||
(format nil "~2,'0d:~2,'0d:~6,3,,,'0f" h m s))))
|
||||
(format nil "~a, ~d ~a ~d, ~a ~a" (dow->string (day-of-week date))
|
||||
dd (month->string mm) yy time-str tz-str))))))
|
||||
|
||||
(defun date-only-string (yy mm dd dow fmt)
|
||||
(case fmt
|
||||
(:rfc-822 (format nil "~a, ~a ~a ~a" dow dd mm yy))
|
||||
(:asctime (format nil "~a ~a ~a ~a" dow mm dd yy))
|
||||
(:rfc-850 (format nil "~a, ~a-~a-~a" dow dd mm yy))
|
||||
(t (format nil "~a-~a-~a" yy mm dd))))
|
||||
|
||||
(defun date-time-string (yy mm dd dow h m s offset fmt)
|
||||
(let* ((tz-hh (truncate (abs offset)))
|
||||
(tz-mm (truncate (* 60 (- (abs offset) tz-hh))))
|
||||
(tz-str (case fmt
|
||||
((:rfc-822 :rfc-850 :asctime)
|
||||
(cond ((= 0 offset) "GMT")
|
||||
((< offset 0) (format nil "-~2,'0d~2,'0d" tz-hh tz-mm))
|
||||
(t (format nil "+~2,'0d~2,'0d" tz-hh tz-mm))))
|
||||
(t (cond ((= 0 offset) (if (eq fmt :iso-8601) "Z" "UTC"))
|
||||
((< offset 0) (format nil "-~2,'0d:~2,'0d" tz-hh tz-mm))
|
||||
(t (format nil "+~2,'0d:~2,'0d" tz-hh tz-mm))))))
|
||||
(time-str (if (< (- s (truncate s)) 0.001)
|
||||
(format nil "~2,'0d:~2,'0d:~2,'0d" h m (truncate s))
|
||||
(format nil "~2,'0d:~2,'0d:~6,3,,,'0f" h m s))))
|
||||
(case fmt
|
||||
(:rfc-822 (format nil "~a, ~a ~a ~a ~a ~a" dow dd mm yy time-str tz-str))
|
||||
(:asctime (format nil "~a ~a ~a ~a ~a ~a" dow mm dd time-str tz-str yy))
|
||||
(:rfc-850 (format nil "~a, ~a-~a-~a ~a ~a" dow dd mm yy time-str tz-str))
|
||||
(:iso-8601 (format nil "~a-~a-~aT~a~a" yy mm dd time-str tz-str))
|
||||
(t (format nil "~a-~a-~a ~a ~a" yy mm dd time-str tz-str)))))
|
||||
|
||||
|
||||
|
||||
128
test-dates.lisp
Normal file
128
test-dates.lisp
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
;;; Copyright (c) 2017, Sudhir Shenoy. All rights reserved.
|
||||
|
||||
;;; Redistribution and use in source and binary forms, with or without
|
||||
;;; modification, are permitted provided that the following conditions
|
||||
;;; are met:
|
||||
|
||||
;;; * Redistributions of source code must retain the above copyright
|
||||
;;; notice, this list of conditions and the following disclaimer.
|
||||
|
||||
;;; * Redistributions in binary form must reproduce the above
|
||||
;;; copyright notice, this list of conditions and the following
|
||||
;;; disclaimer in the documentation and/or other materials
|
||||
;;; provided with the distribution.
|
||||
|
||||
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
|
||||
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
(in-package :cl-dates-test)
|
||||
|
||||
(deftest julian ()
|
||||
(format t "Julian date conversions ...~%")
|
||||
(check
|
||||
(= (ymd->date 2017 2 5) 2457789.5)
|
||||
(= (ymd->date 1959 12 29) 2436931.5)
|
||||
(= (ymd->date 1959 12 29 18 12 26.3) 2436932.2586376667d0)
|
||||
(equal '(2017 2 5 0 0 0.0d0)
|
||||
(multiple-value-call #'list (date->ymd 2457789.5)))
|
||||
(= (ymd->date 1600 2 29) 2305506.5)
|
||||
(= (ymd->date 1600 2 29 9 31 23.5) 2305506.896799773d0)
|
||||
(equal '(1600 2 29 9 31 23.500385284423828d0)
|
||||
(multiple-value-call #'list (date->ymd 2305506.896799773d0)))
|
||||
(= (date->javascript-time (ymd->date 2017 6 18)) 1497744000000)
|
||||
(= (jday-number (ymd->date 2017 6 18)) 2457923)))
|
||||
|
||||
(deftest misc-fns ()
|
||||
(format t "Miscellaneous date functions ...~%")
|
||||
(check
|
||||
(valid-date-p 2000 2 29)
|
||||
(null (valid-date-p 1900 2 29))
|
||||
(null (valid-date-p 1234 65 789))
|
||||
|
||||
(eq :monday (day-of-week (ymd->date 2017 2 6)))
|
||||
(eq :wednesday (day-of-week (ymd->date 1959 9 23)))
|
||||
|
||||
(= (nth-day-of-week (ymd->date 2017 2 5) :tuesday 3) (ymd->date 2017 2 21))
|
||||
(= (nth-day-of-week (ymd->date 2017 2 5) :thursday 3) (ymd->date 2017 2 16))
|
||||
;; overflow month - returns last tuesday
|
||||
(= (nth-day-of-week (ymd->date 2017 2 5) :tuesday 7) (ymd->date 2017 2 28))
|
||||
;; any number <= 1 should return 1st tuesday
|
||||
(= (nth-day-of-week (ymd->date 2017 2 5) :tuesday -7) (ymd->date 2017 2 7))))
|
||||
|
||||
(deftest print-fns ()
|
||||
(format t "Conversions to strings ...~%")
|
||||
(let ((dt1 (ymd->date 2017 2 16))
|
||||
(dt2 (ymd->date 2017 2 16 17 30 25 +9)))
|
||||
(check
|
||||
(string= (dow->string :monday) "Monday")
|
||||
(string= (dow->string :tuesday) "Tuesday")
|
||||
(string= (dow->string :wednesday) "Wednesday")
|
||||
(string= (dow->string :thursday) "Thursday")
|
||||
(string= (dow->string :friday) "Friday")
|
||||
(string= (dow->string :saturday) "Saturday")
|
||||
(string= (dow->string :sunday) "Sunday")
|
||||
|
||||
(string= (month->string 1) "January")
|
||||
(string= (month->string 2) "February")
|
||||
(string= (month->string 3) "March")
|
||||
(string= (month->string 4) "April")
|
||||
(string= (month->string 5) "May")
|
||||
(string= (month->string 6) "June")
|
||||
(string= (month->string 7) "July")
|
||||
(string= (month->string 8) "August")
|
||||
(string= (month->string 9) "September")
|
||||
(string= (month->string 10) "October")
|
||||
(string= (month->string 11) "November")
|
||||
(string= (month->string 12) "December")
|
||||
|
||||
(string= (date->string dt1) "2017-02-16")
|
||||
(string= (date->string dt1 :format :iso-8601) "2017-02-16")
|
||||
(string= (date->string dt1 :format :asctime) "Thu Feb 16 2017")
|
||||
(string= (date->string dt1 :format :rfc-822) "Thu, 16 Feb 2017")
|
||||
(string= (date->string dt1 :format :rfc-850) "Thu, 16-Feb-2017")
|
||||
|
||||
(string= (date->string dt2) "2017-02-16 08:30:25 UTC")
|
||||
(string= (date->string dt2 :format :iso-8601) "2017-02-16T08:30:25Z")
|
||||
(string= (date->string dt2 :format :asctime) "Thu Feb 16 08:30:25 GMT 2017")
|
||||
(string= (date->string dt2 :format :rfc-822) "Thu, 16 Feb 2017 08:30:25 GMT")
|
||||
(string= (date->string dt2 :format :rfc-850) "Thu, 16-Feb-2017 08:30:25 GMT")
|
||||
|
||||
(string= (date->string dt2 :zone "JST") "2017-02-16 17:30:25 +09:00")
|
||||
(string= (date->string dt2 :zone "JST" :format :iso-8601) "2017-02-16T17:30:25+09:00")
|
||||
(string= (date->string dt2 :zone "JST" :format :asctime) "Thu Feb 16 17:30:25 +0900 2017")
|
||||
(string= (date->string dt2 :zone "JST" :format :rfc-822) "Thu, 16 Feb 2017 17:30:25 +0900")
|
||||
(string= (date->string dt2 :zone "JST" :format :rfc-850) "Thu, 16-Feb-2017 17:30:25 +0900"))))
|
||||
|
||||
(deftest special-dates ()
|
||||
(format t "Computation of special dates ...~%")
|
||||
(check
|
||||
(= (easter-day 2001) (ymd->date 2001 4 15))
|
||||
(= (easter-day 2002) (ymd->date 2002 3 31))
|
||||
(= (easter-day 2005) (ymd->date 2005 3 27))
|
||||
(= (easter-day 2011) (ymd->date 2011 4 24))
|
||||
|
||||
(= (vernal-equinox 2017)
|
||||
(ymd->date 2017 3 20 10 28 32.05221712589264d0))
|
||||
(= (summer-solstice 2017)
|
||||
(ymd->date 2017 6 21 4 23 43.49940687417984d0))
|
||||
(= (autumnal-equinox 2017)
|
||||
(ymd->date 2017 9 22 20 1 8.430179357528687D0))
|
||||
(= (winter-solstice 2017)
|
||||
(ymd->date 2017 12 21 16 27 51.39586955308914d0))
|
||||
(= (vernal-equinox 1959)
|
||||
(ymd->date 1959 3 21 8 55 7.991203665733337d0))
|
||||
(= (summer-solstice 1959)
|
||||
(ymd->date 1959 6 22 3 49 50.55352360010147d0))
|
||||
(= (autumnal-equinox 1959)
|
||||
(ymd->date 1959 9 23 19 8 29.363870322704315D0))
|
||||
(= (winter-solstice 1959)
|
||||
(ymd->date 1959 12 22 14 34 33.68946969509125d0))))
|
||||
89
test-main.lisp
Normal file
89
test-main.lisp
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
;;; Copyright (c) 2017, Sudhir Shenoy. All rights reserved.
|
||||
|
||||
;;; Redistribution and use in source and binary forms, with or without
|
||||
;;; modification, are permitted provided that the following conditions
|
||||
;;; are met:
|
||||
|
||||
;;; * Redistributions of source code must retain the above copyright
|
||||
;;; notice, this list of conditions and the following disclaimer.
|
||||
|
||||
;;; * Redistributions in binary form must reproduce the above
|
||||
;;; copyright notice, this list of conditions and the following
|
||||
;;; disclaimer in the documentation and/or other materials
|
||||
;;; provided with the distribution.
|
||||
|
||||
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
|
||||
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
(in-package :cl-user)
|
||||
|
||||
(defpackage :cl-dates-test
|
||||
(:use :common-lisp :cl-dates)
|
||||
(:export :run-all-tests))
|
||||
|
||||
(in-package :cl-dates-test)
|
||||
|
||||
;;;
|
||||
;;; Peter Siebel's test framework from Practical Common Lisp with
|
||||
;;; slight modifications
|
||||
;;;
|
||||
(defvar *test-name* nil)
|
||||
(defvar *verbose-results* nil)
|
||||
(defvar *total-tests* 0)
|
||||
(defvar *failed-tests* 0)
|
||||
|
||||
(defmacro deftest (name parameters &body body)
|
||||
"Define a test function. Within a test function we can call
|
||||
other test functions or use 'check' to run individual test
|
||||
cases."
|
||||
`(defun ,name ,parameters
|
||||
(let ((*test-name* (append *test-name* (list ',name))))
|
||||
,@body)))
|
||||
|
||||
(defmacro check (&body forms)
|
||||
"Run each expression in 'forms' as a test case."
|
||||
`(combine-results
|
||||
,@(loop for f in forms collect `(report-result ,f ',f))))
|
||||
|
||||
(defmacro combine-results (&body forms)
|
||||
"Combine the results (as booleans) of evaluating 'forms' in order."
|
||||
(let ((result (gensym)))
|
||||
`(let ((,result t))
|
||||
,@(loop for f in forms collect `(unless ,f (setf ,result nil)))
|
||||
,result)))
|
||||
|
||||
(defun report-result (result form)
|
||||
"Report the results of a single test case. Called by 'check'."
|
||||
(incf *total-tests*)
|
||||
(when (not result)
|
||||
(incf *failed-tests*))
|
||||
(if *verbose-results*
|
||||
(format t "~:[FAIL~;pass~] ... ~a: ~a~%" result *test-name* form)
|
||||
(when (not result)
|
||||
(format t "FAIL ... ~a: ~a~%" *test-name* form)))
|
||||
result)
|
||||
|
||||
(defun run-all-tests (&key (verbose nil))
|
||||
(let ((*total-tests* 0)
|
||||
(*failed-tests* 0)
|
||||
(*verbose-results* verbose)
|
||||
(status (combine-results
|
||||
(julian)
|
||||
(misc-fns)
|
||||
(special-dates)
|
||||
(print-fns)
|
||||
(parse-dates))))
|
||||
(unless (zerop *total-tests*)
|
||||
(format t "~d tests executed - ~d passed (~,2f%)~%"
|
||||
*total-tests* (- *total-tests* *failed-tests*)
|
||||
(/ (* 100 (- *total-tests* *failed-tests*)) *total-tests*)))
|
||||
status))
|
||||
203
test-parse-date.lisp
Normal file
203
test-parse-date.lisp
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
;;; Copyright (c) 2017, Sudhir Shenoy. All rights reserved.
|
||||
|
||||
;;; Redistribution and use in source and binary forms, with or without
|
||||
;;; modification, are permitted provided that the following conditions
|
||||
;;; are met:
|
||||
|
||||
;;; * Redistributions of source code must retain the above copyright
|
||||
;;; notice, this list of conditions and the following disclaimer.
|
||||
|
||||
;;; * Redistributions in binary form must reproduce the above
|
||||
;;; copyright notice, this list of conditions and the following
|
||||
;;; disclaimer in the documentation and/or other materials
|
||||
;;; provided with the distribution.
|
||||
|
||||
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
|
||||
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
(in-package :cl-dates-test)
|
||||
|
||||
;; Rounding errors in fractional seconds when changing timezones require
|
||||
;; testing for approximate equality in some test cases
|
||||
(defparameter +max-error+ 1/1000000)
|
||||
(defun a= (a b)
|
||||
(< (abs (- a b)) +max-error+))
|
||||
|
||||
(deftest parse-dates ()
|
||||
(format t "Parse dates from strings ...~%")
|
||||
(let ((dt (ymd->date 2003 9 25 1 36 28)))
|
||||
(check
|
||||
;; Fully specified date with time and timezone
|
||||
(= (string->date "Thu Sep 25 10:36:28 JST 2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(null (string->date "Wed Sep 25 10:36:28 JST 2003" :reference-date dt)) ; day of week doesn't match date
|
||||
(= (string->date "2003 10:36:28 JST 25 Sep Thu" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "Thu, 25 Sep 2003 10:36:28 JST" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "Thu, 25 Sep 2003 10:36:28 +0900" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "2003-09-25T10:49:41-03:00" :reference-date dt) (ymd->date 2003 9 25 13 49 41))
|
||||
(a= (string->date "2003-09-25T10:49:41.5-03:00" :reference-date dt) (ymd->date 2003 9 25 13 49 41.5))
|
||||
(= (string->date "20030925T10:49:41-03:00" :reference-date dt) (ymd->date 2003 9 25 13 49 41))
|
||||
(a= (string->date "20030925T10:49:41.5-03:00" :reference-date dt) (ymd->date 2003 9 25 13 49 41.5))
|
||||
|
||||
;; Partially specified dates (alphanumeric)
|
||||
(= (string->date "Thu Sep 25 10:36:28 2003" :reference-date dt) (ymd->date 2003 9 25 10 36 28))
|
||||
(= (string->date "Thu Sep 25 10:36:28" :reference-date dt) (ymd->date 2003 9 25 10 36 28))
|
||||
(= (string->date "Thu Sep 10:36:28" :reference-date dt) (ymd->date 2003 9 25 10 36 28))
|
||||
(= (string->date "Thu 10:36:28" :reference-date dt) (ymd->date 2003 9 25 10 36 28))
|
||||
(= (string->date "Sep 10:36:28" :reference-date dt) (ymd->date 2003 9 25 10 36 28))
|
||||
(= (string->date "10:36:28" :reference-date dt) (ymd->date 2003 9 25 10 36 28))
|
||||
(= (string->date "10:36" :reference-date dt) (ymd->date 2003 9 25 10 36 28))
|
||||
(= (string->date "Thu Sep 25 2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "Sep 25 2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "Sep 2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "Sep" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "2003-09-25T10:49:41" :reference-date dt) (ymd->date 2003 9 25 10 49 41))
|
||||
(= (string->date "2003-09-25T10:49" :reference-date dt) (ymd->date 2003 9 25 10 49 28))
|
||||
(= (string->date "2003-09-25T10" :reference-date dt) (ymd->date 2003 9 25 10 36 28))
|
||||
(= (string->date "2003-09-25" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "20030925T104941" :reference-date dt) (ymd->date 2003 9 25 10 49 41))
|
||||
(= (string->date "20030925T1049" :reference-date dt) (ymd->date 2003 9 25 10 49 28))
|
||||
(= (string->date "20030925T10" :reference-date dt) (ymd->date 2003 9 25 10 36 28))
|
||||
(= (string->date "20030925T10:49:41" :reference-date dt) (ymd->date 2003 9 25 10 49 41))
|
||||
(= (string->date "20030925T10:49" :reference-date dt) (ymd->date 2003 9 25 10 49 28))
|
||||
(= (string->date "20030925T10" :reference-date dt) (ymd->date 2003 9 25 10 36 28))
|
||||
(= (string->date "20030925" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
|
||||
;; Partially specified (numeric with possible alphabetic month name
|
||||
(= (string->date "19970902090807" :reference-date dt) (ymd->date 1997 9 2 9 8 7))
|
||||
(= (string->date "199709020908" :reference-date dt) (ymd->date 1997 9 2 9 8 28))
|
||||
(= (string->date "2003-Sep-25" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "25-Sep-2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "Sep-25-2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "09-25-2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28)) ; no ambiguity because 25 is a day
|
||||
(= (string->date "25-09-2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28)) ; no ambiguity because 25 is a day
|
||||
(= (string->date "10-09-2003" :precedence :dmy :reference-date dt)
|
||||
(ymd->date 2003 9 10 1 36 28)) ; explicitly dd/mm/yy
|
||||
(= (string->date "10-09-2003" :precedence :mdy :reference-date dt)
|
||||
(ymd->date 2003 10 9 1 36 28)) ; explicitly mm/dd/yy
|
||||
(= (string->date "10-09-03" :precedence :mdy :reference-date dt)
|
||||
(ymd->date 2003 10 9 1 36 28)) ; explicitly mm/dd/yy
|
||||
(= (string->date "10-09-03" :precedence :ymd :reference-date dt)
|
||||
(ymd->date 2010 9 3 1 36 28)) ; explicitly yy/mm/dd
|
||||
(= (string->date "2003.09.25" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "2003.Sep.25" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "25.Sep.2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "09.25.2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "25.09.2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "2003/09/25" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "2003/Sep/25" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "25/Sep/2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "09/25/2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "25/09/2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "10.09.2003" :precedence :dmy :reference-date dt)
|
||||
(ymd->date 2003 9 10 1 36 28)) ; explicitly dd/mm/yy
|
||||
(= (string->date "10.09.2003" :precedence :mdy :reference-date dt)
|
||||
(ymd->date 2003 10 9 1 36 28)) ; explicitly mm/dd/yy
|
||||
(= (string->date "10.09.03" :precedence :dmy :reference-date dt)
|
||||
(ymd->date 2003 9 10 1 36 28)) ; explicitly dd/mm/yy
|
||||
(= (string->date "10.09.03" :precedence :mdy :reference-date dt)
|
||||
(ymd->date 2003 10 9 1 36 28)) ; explicitly mm/dd/yy
|
||||
(= (string->date "10/09/2003" :precedence :dmy :reference-date dt)
|
||||
(ymd->date 2003 9 10 1 36 28)) ; explicitly dd/mm/yy
|
||||
(= (string->date "10/09/2003" :precedence :mdy :reference-date dt)
|
||||
(ymd->date 2003 10 9 1 36 28)) ; explicitly mm/dd/yy
|
||||
(= (string->date "10/09/03" :precedence :dmy :reference-date dt)
|
||||
(ymd->date 2003 9 10 1 36 28)) ; explicitly dd/mm/yy
|
||||
(= (string->date "10/09/03" :precedence :mdy :reference-date dt)
|
||||
(ymd->date 2003 10 9 1 36 28)) ; explicitly mm/dd/yy
|
||||
(null (string->date "2003/25/Sep" :reference-date dt)) ; invalid - yy/dd/MMM
|
||||
(= (string->date "2003 09 25" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "2003 Sep 25" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "2003 25 Sep" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "25 Sep 2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "Sep 25 2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "09 25 2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "25 09 2003" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "10 09 2003" :precedence :dmy :reference-date dt)
|
||||
(ymd->date 2003 9 10 1 36 28)) ; explicitly dd/mm/yy
|
||||
(= (string->date "10 09 2003" :precedence :mdy :reference-date dt)
|
||||
(ymd->date 2003 10 9 1 36 28)) ; explicitly mm/dd/yy
|
||||
(= (string->date "10 09 03" :precedence :dmy :reference-date dt)
|
||||
(ymd->date 2003 9 10 1 36 28)) ; explicitly dd/mm/yy
|
||||
(= (string->date "10 09 03" :precedence :mdy :reference-date dt)
|
||||
(ymd->date 2003 10 9 1 36 28)) ; explicitly mm/dd/yy
|
||||
(= (string->date "03 25 Sep" :reference-date dt)
|
||||
(ymd->date 2003 9 25 1 36 28)) ; default is yy/mm/dd
|
||||
(= (string->date "25 03 Sep" :precedence :dmy :reference-date dt)
|
||||
(ymd->date 2003 9 25 1 36 28)) ; explicitly day before year
|
||||
|
||||
;; Assorted time formats
|
||||
(a= (string->date "10h16m38.5s" :reference-date dt) (ymd->date 2003 9 25 10 16 38.5))
|
||||
(= (string->date "10h16m38s" :reference-date dt) (ymd->date 2003 9 25 10 16 38))
|
||||
(a= (string->date "10h16m" :reference-date dt) (ymd->date 2003 9 25 10 16 28))
|
||||
(= (string->date "10h" :reference-date dt) (ymd->date 2003 9 25 10 36 28))
|
||||
(a= (string->date "10 h 16" :reference-date dt) (ymd->date 2003 9 25 10 16 28))
|
||||
(= (string->date "10h am" :reference-date dt) (ymd->date 2003 9 25 10 36 28))
|
||||
(= (string->date "10h pm" :reference-date dt) (ymd->date 2003 9 25 22 36 28))
|
||||
(= (string->date "10 am" :reference-date dt) (ymd->date 2003 9 25 10 36 28))
|
||||
(= (string->date "10 pm" :reference-date dt) (ymd->date 2003 9 25 22 36 28))
|
||||
(= (string->date "10:00 am" :reference-date dt) (ymd->date 2003 9 25 10 0 28))
|
||||
(= (string->date "10:00 pm" :reference-date dt) (ymd->date 2003 9 25 22 0 28))
|
||||
(= (string->date "10:00am" :reference-date dt) (ymd->date 2003 9 25 10 0 28))
|
||||
(= (string->date "10:00pm" :reference-date dt) (ymd->date 2003 9 25 22 0 28))
|
||||
(= (string->date "10:00a.m." :reference-date dt) (ymd->date 2003 9 25 10 0 28))
|
||||
(= (string->date "10:00p.m." :reference-date dt) (ymd->date 2003 9 25 22 0 28))
|
||||
(= (string->date "10:00a.m" :reference-date dt) (ymd->date 2003 9 25 10 0 28))
|
||||
(= (string->date "10:00p.m" :reference-date dt) (ymd->date 2003 9 25 22 0 28))
|
||||
|
||||
;; relative dates etc
|
||||
(= (string->date "Sep 03" :reference-date dt) (ymd->date 2003 9 3 1 36 28))
|
||||
(= (string->date "Sep of 03" :reference-date dt) (ymd->date 2003 9 25 1 36 28))
|
||||
(= (string->date "Wed" :reference-date dt) (ymd->date 2003 9 24 1 36 28))
|
||||
(= (string->date "Wednesday" :reference-date dt) (ymd->date 2003 9 24 1 36 28))
|
||||
(= (string->date "last Wednesday" :reference-date dt) (ymd->date 2003 9 24 1 36 28))
|
||||
(= (string->date "next Wednesday" :reference-date dt) (ymd->date 2003 10 1 1 36 28))
|
||||
(= (string->date "October" :reference-date dt) (ymd->date 2003 10 25 1 36 28))
|
||||
(= (string->date "31-Dec-00" :precedence :dmy :reference-date dt)
|
||||
(ymd->date 2000 12 31 1 36 28))
|
||||
|
||||
;; Random verbiage / unusual formats
|
||||
(= (string->date "The date is the 25th of September of 2003, exactly at 10:49:41 with time zone -03:00"
|
||||
:reference-date dt) (ymd->date 2003 9 25 13 49 41))
|
||||
(null (string->date "Today is the 25th of September of 2003, exactly at 10:49:41 with time zone -03:00"
|
||||
:reference-date dt)) ; 'today' is assumed to be a date spec so a duplicate date is detected
|
||||
(= (string->date " July 4 , 1976 12:01:02 am " :reference-date dt)
|
||||
(ymd->date 1976 7 4 0 1 2))
|
||||
(= (string->date "today 12:35" :reference-date dt) (ymd->date 2003 9 25 12 35 28))
|
||||
(= (string->date "today 12:35 JST" :reference-date dt) (ymd->date 2003 9 25 3 35 28))
|
||||
(= (string->date "tomorrow 12:35" :reference-date dt) (ymd->date 2003 9 26 12 35 28))
|
||||
(= (string->date "yesterday 12:35 am JST" :reference-date dt) (ymd->date 2003 9 23 15 35 28)) ; date is 2 days prior in UTC
|
||||
(= (string->date "Wed Jul 10, '96" :reference-date dt) (ymd->date 1996 7 10 1 36 28))
|
||||
(a= (string->date "1996.07.10 AD at 15:08:56 PDT" :reference-date dt) (ymd->date 1996 7 10 22 8 56))
|
||||
(= (string->date "1996.07.10 AD at 12:08 PM" :reference-date dt) (ymd->date 1996 7 10 12 8 28))
|
||||
(= (string->date "Saturday, April 12, 1952 AD 3:30:42pm PST" :reference-date dt) (ymd->date 1952 4 12 23 30 42))
|
||||
(= (string->date "November 5, 1994, 8:15:30 am EST" :reference-date dt) (ymd->date 1994 11 5 13 15 30))
|
||||
(= (string->date "1994-11-05T08:15:30-05:00" :reference-date dt) (ymd->date 1994 11 5 13 15 30))
|
||||
(= (string->date "1994-11-05T08:15:30Z" :reference-date dt) (ymd->date 1994 11 5 8 15 30))
|
||||
(= (string->date "0:01:02" :reference-date dt) (ymd->date 2003 9 25 0 1 2))
|
||||
(= (string->date "12h 01m02s am" :reference-date dt) (ymd->date 2003 9 25 0 1 2))
|
||||
(= (string->date "0:01:02 on July 4, 1976" :reference-date dt) (ymd->date 1976 7 4 0 1 2))
|
||||
(null (string->date "July 4, 1976 pm 12:01:02" :reference-date dt)) ; am/pm must come after time spec
|
||||
(= (string->date "July 4, 4pm 4:01:02" :reference-date dt)
|
||||
(ymd->date 2003 7 4 4 1 2)) ; 4pm overridden by 4:01:02
|
||||
(= (string->date "July 4, 4pm" :reference-date dt) (ymd->date 2003 7 4 16 36 28)) ; this is ok
|
||||
(a= (string->date "04.04.95 00:22" :reference-date dt) (ymd->date 1995 4 4 0 22 28))
|
||||
(= (string->date "950404 122212" :reference-date dt) (ymd->date 1995 4 4 12 22 12))
|
||||
(= (string->date "0:00 PM, PST" :reference-date dt) (ymd->date 2003 9 25 20 0 28))
|
||||
(= (string->date "12:08 PM" :reference-date dt) (ymd->date 2003 9 25 12 8 28))
|
||||
(= (string->date "5:50 A.M on June 13, 1990" :reference-date dt) (ymd->date 1990 6 13 5 50 28))
|
||||
(= (string->date "01h02m03" :reference-date dt) (ymd->date 2003 9 25 1 2 3))
|
||||
(= (string->date "01h02" :reference-date dt) (ymd->date 2003 9 25 1 2 28))
|
||||
(= (string->date "01h02s" :reference-date dt) (ymd->date 2003 9 25 1 36 2))
|
||||
(= (string->date "01m02" :reference-date dt) (ymd->date 2003 9 25 1 1 2))
|
||||
(a= (string->date "01m02h" :reference-date dt) (ymd->date 2003 9 25 2 1 28))
|
||||
(= (string->date "2004 10 April 11h30m" :reference-date dt) (ymd->date 2004 4 10 11 30 28)))))
|
||||
260
timezones.lisp
Normal file
260
timezones.lisp
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
;;; Copyright (c) 2017, Sudhir Shenoy. All rights reserved.
|
||||
|
||||
;;; Redistribution and use in source and binary forms, with or without
|
||||
;;; modification, are permitted provided that the following conditions
|
||||
;;; are met:
|
||||
|
||||
;;; * Redistributions of source code must retain the above copyright
|
||||
;;; notice, this list of conditions and the following disclaimer.
|
||||
|
||||
;;; * Redistributions in binary form must reproduce the above
|
||||
;;; copyright notice, this list of conditions and the following
|
||||
;;; disclaimer in the documentation and/or other materials
|
||||
;;; provided with the distribution.
|
||||
|
||||
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
|
||||
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
(in-package :cl-dates)
|
||||
|
||||
;; hash table containing mappings from time zone abbreviation to GMT offset
|
||||
(defparameter +tz-info+ (make-hash-table :test #'equalp))
|
||||
|
||||
(defun str-to-tz-offset (str)
|
||||
(gethash str +tz-info+))
|
||||
|
||||
(defun zone-to-offset (zone)
|
||||
(typecase zone
|
||||
(number zone)
|
||||
(string (or (str-to-tz-offset zone) 0))
|
||||
(t 0)))
|
||||
|
||||
(progn
|
||||
;; Time zone abbreviation to offset mapping table (source: Wikipedia)
|
||||
;; Wherever there are duplicate abbreviations, the more economically significant
|
||||
;; (in terms of trading centres) zone has been retained
|
||||
(let ((info '(("ACDT" 10.5 "Australian Central Daylight Savings Time")
|
||||
("ACST" 9.5 "Australian Central Standard Time")
|
||||
("ACT" -5 "Acre Time")
|
||||
("ADT" -3 "Atlantic Daylight Time")
|
||||
("AEDT" 11 "Australian Eastern Daylight Savings Time")
|
||||
("AEST" 10 "Australian Eastern Standard Time")
|
||||
("AFT" 4.5 "Afghanistan Time")
|
||||
("AKDT" -8 "Alaska Daylight Time")
|
||||
("AKST" -9 "Alaska Standard Time")
|
||||
("AMST" -3 "Amazon Summer Time (Brazil)")
|
||||
("AMT" -4 "Amazon Time (Brazil)")
|
||||
("AMT" 4 "Armenia Time")
|
||||
("ART" -3 "Argentina Time")
|
||||
("AST" 3 "Arabia Standard Time")
|
||||
;; ("AST" -4 "Atlantic Standard Time")
|
||||
("AWST" 8 "Australian Western Standard Time")
|
||||
("AZOST" 0 "Azores Summer Time")
|
||||
("AZOT" -1 "Azores Standard Time")
|
||||
("AZT" 4 "Azerbaijan Time")
|
||||
("BDT" 8 "Brunei Time")
|
||||
("BIOT" 6 "British Indian Ocean Time")
|
||||
("BIT" -12 "Baker Island Time")
|
||||
("BOT" -4 "Bolivia Time")
|
||||
("BRST" -2 "Brasilia Summer Time")
|
||||
("BRT" -3 "Brasilia Time")
|
||||
;; ("BST" 6 "Bangladesh Standard Time")
|
||||
;; ("BST" 11 "Bougainville Standard Time")
|
||||
("BST" 1 "British Summer Time")
|
||||
("BTT" 6 "Bhutan Time")
|
||||
("CAT" 2 "Central Africa Time")
|
||||
("CCT" 6.5 "Cocos Islands Time")
|
||||
("CDT" -5 "Central Daylight Time (North America)")
|
||||
;; ("CDT" -4 "Cuba Daylight Time")
|
||||
("CEST" 2 "Central European Summer Time (Cf. HAEC)")
|
||||
("CET" 1 "Central European Time")
|
||||
("CHADT" 13.75 "Chatham Daylight Time")
|
||||
("CHAST" 12.75 "Chatham Standard Time")
|
||||
("CHOT" 8 "Choibalsan Standard Time")
|
||||
("CHOST" 9 "Choibalsan Summer Time")
|
||||
("CHST" 10 "Chamorro Standard Time")
|
||||
("CHUT" 10 "Chuuk Time")
|
||||
("CIST" -8 "Clipperton Island Standard Time")
|
||||
("CIT" 8 "Central Indonesia Time")
|
||||
("CKT" -10 "Cook Island Time")
|
||||
("CLST" -3 "Chile Summer Time")
|
||||
("CLT" -4 "Chile Standard Time")
|
||||
("COST" -4 "Colombia Summer Time")
|
||||
("COT" -5 "Colombia Time")
|
||||
("CST" -6 "Central Standard Time (North America)")
|
||||
;; ("CST" 8 "China Standard Time")
|
||||
;; ("CST" -5 "Cuba Standard Time")
|
||||
("ACST" 9.5 "Central Standard Time (Australia)")
|
||||
("ACDT" 10.5 "Central Summer Time (Australia)")
|
||||
("CT" 8 "China time")
|
||||
("CVT" -1 "Cape Verde Time")
|
||||
("CXT" 7 "Christmas Island Time")
|
||||
("DAVT" 7 "Davis Time")
|
||||
("DDUT" 10 "Dumont d'Urville Time")
|
||||
("DFT" 1 "AIX specific equivalent of Central European Time")
|
||||
("EASST" -5 "Easter Island Summer Time")
|
||||
("EAST" -6 "Easter Island Standard Time")
|
||||
("EAT" 3 "East Africa Time")
|
||||
("ECT" -4 "Eastern Caribbean Time (does not recognise DST)")
|
||||
;; ("ECT" -5 "Ecuador Time")
|
||||
("EDT" -4 "Eastern Daylight Time (North America)")
|
||||
("AEDT" 11 "Eastern Summer Time (Australia)")
|
||||
("EEST" 3 "Eastern European Summer Time")
|
||||
("EET" 2 "Eastern European Time")
|
||||
("EGST" 0 "Eastern Greenland Summer Time")
|
||||
("EGT" -1 "Eastern Greenland Time")
|
||||
("EIT" 9 "Eastern Indonesian Time")
|
||||
("EST" -5 "Eastern Standard Time (North America)")
|
||||
("AEST" 10 "Eastern Standard Time (Australia)")
|
||||
("FET" 3 "Further-eastern European Time")
|
||||
("FJT" 12 "Fiji Time")
|
||||
("FKST" -3 "Falkland Islands Summer Time")
|
||||
("FKT" -4 "Falkland Islands Time")
|
||||
("FNT" -2 "Fernando de Noronha Time")
|
||||
("GALT" -6 "Galapagos Time")
|
||||
("GAMT" -9 "Gambier Islands")
|
||||
("GET" 4 "Georgia Standard Time")
|
||||
("GFT" -3 "French Guiana Time")
|
||||
("GILT" 12 "Gilbert Island Time")
|
||||
("GIT" -9 "Gambier Island Time")
|
||||
("GMT" 0 "Greenwich Mean Time")
|
||||
;; ("GST" -2 "South Georgia and the South Sandwich Islands")
|
||||
("GST" 4 "Gulf Standard Time")
|
||||
("GYT" -4 "Guyana Time")
|
||||
("HADT" -9 "Hawaii-Aleutian Daylight Time")
|
||||
("HAEC" 2 "Heure Avancée d'Europe Centrale francised name for CEST")
|
||||
("HAST" -10 "Hawaii-Aleutian Standard Time")
|
||||
("HKT" 8 "Hong Kong Time")
|
||||
("HMT" 5 "Heard and McDonald Islands Time")
|
||||
("HOVST" 8 "Khovd Summer Time")
|
||||
("HOVT" 7 "Khovd Standard Time")
|
||||
("ICT" 7 "Indochina Time")
|
||||
("IDT" 3 "Israel Daylight Time")
|
||||
("IOT" 3 "Indian Ocean Time")
|
||||
("IRDT" 4.5 "Iran Daylight Time")
|
||||
("IRKT" 8 "Irkutsk Time")
|
||||
("IRST" 3.5 "Iran Standard Time")
|
||||
("IST" 5.5 "Indian Standard Time")
|
||||
;; ("IST" 1 "Irish Standard Time[6]")
|
||||
;; ("IST" 2 "Israel Standard Time")
|
||||
("JST" 9 "Japan Standard Time")
|
||||
("KGT" 6 "Kyrgyzstan time")
|
||||
("KOST" 11 "Kosrae Time")
|
||||
("KRAT" 7 "Krasnoyarsk Time")
|
||||
("KST" 9 "Korea Standard Time")
|
||||
("LHST" 10.5 "Lord Howe Standard Time")
|
||||
;; ("LHST" 11 "Lord Howe Summer Time")
|
||||
("LINT" 14 "Line Islands Time")
|
||||
("MAGT" 12 "Magadan Time")
|
||||
("MART" -9.5 "Marquesas Islands Time")
|
||||
("MAWT" 5 "Mawson Station Time")
|
||||
("MDT" -6 "Mountain Daylight Time (North America)")
|
||||
("MET" 1 "Middle European Time Same zone as CET")
|
||||
("MEST" 2 "Middle European Summer Time Same zone as CEST")
|
||||
("MHT" 12 "Marshall Islands")
|
||||
("MIST" 11 "Macquarie Island Station Time")
|
||||
("MIT" -9.5 "Marquesas Islands Time")
|
||||
("MMT" 6.5 "Myanmar Standard Time")
|
||||
("MSK" 3 "Moscow Time")
|
||||
;; ("MST" 8 "Malaysia Standard Time")
|
||||
("MST" -7 "Mountain Standard Time (North America)")
|
||||
("MUT" 4 "Mauritius Time")
|
||||
("MVT" 5 "Maldives Time")
|
||||
("MYT" 8 "Malaysia Time")
|
||||
("NCT" 11 "New Caledonia Time")
|
||||
("NDT" -2.5 "Newfoundland Daylight Time")
|
||||
("NFT" 11 "Norfolk Time")
|
||||
("NPT" 5.75 "Nepal Time")
|
||||
("NST" -3.5 "Newfoundland Standard Time")
|
||||
("NT" -3.5 "Newfoundland Time")
|
||||
("NUT" -11 "Niue Time")
|
||||
("NZDT" 13 "New Zealand Daylight Time")
|
||||
("NZST" 12 "New Zealand Standard Time")
|
||||
("OMST" 6 "Omsk Time")
|
||||
("ORAT" 5 "Oral Time")
|
||||
("PDT" -7 "Pacific Daylight Time (North America)")
|
||||
("PET" -5 "Peru Time")
|
||||
("PETT" 12 "Kamchatka Time")
|
||||
("PGT" 10 "Papua New Guinea Time")
|
||||
("PHOT" 13 "Phoenix Island Time")
|
||||
("PHT" 8 "Philippine Time")
|
||||
("PKT" 5 "Pakistan Standard Time")
|
||||
("PMDT" -2 "Saint Pierre and Miquelon Daylight time")
|
||||
("PMST" -3 "Saint Pierre and Miquelon Standard Time")
|
||||
("PONT" 11 "Pohnpei Standard Time")
|
||||
("PST" -8 "Pacific Standard Time (North America)")
|
||||
;; ("PST" 8 "Philippine Standard Time")
|
||||
("PYST" -3 "Paraguay Summer Time (South America)[7]")
|
||||
("PYT" -4 "Paraguay Time (South America)[8]")
|
||||
("RET" 4 "Réunion Time")
|
||||
("ROTT" -3 "Rothera Research Station Time")
|
||||
("SAKT" 11 "Sakhalin Island time")
|
||||
("SAMT" 4 "Samara Time")
|
||||
("SAST" 2 "South African Standard Time")
|
||||
("SBT" 11 "Solomon Islands Time")
|
||||
("SCT" 4 "Seychelles Time")
|
||||
("SGT" 8 "Singapore Time")
|
||||
("SLST" 5.5 "Sri Lanka Standard Time")
|
||||
("SRET" 11 "Srednekolymsk Time")
|
||||
("SRT" -3 "Suriname Time")
|
||||
;; ("SST" -11 "Samoa Standard Time")
|
||||
("SST" 8 "Singapore Standard Time")
|
||||
("SYOT" 3 "Showa Station Time")
|
||||
("TAHT" -10 "Tahiti Time")
|
||||
("THA" 7 "Thailand Standard Time")
|
||||
("TFT" 5 "Indian/Kerguelen")
|
||||
("TJT" 5 "Tajikistan Time")
|
||||
("TKT" 13 "Tokelau Time")
|
||||
("TLT" 9 "Timor Leste Time")
|
||||
("TMT" 5 "Turkmenistan Time")
|
||||
("TRT" 3 "Turkey Time")
|
||||
("TOT" 13 "Tonga Time")
|
||||
("TVT" 12 "Tuvalu Time")
|
||||
("ULAST" 9 "Ulaanbaatar Summer Time")
|
||||
("ULAT" 8 "Ulaanbaatar Standard Time")
|
||||
("USZ1" 2 "Kaliningrad Time")
|
||||
("UTC" 0 "Coordinated Universal Time")
|
||||
("UYST" -2 "Uruguay Summer Time")
|
||||
("UYT" -3 "Uruguay Standard Time")
|
||||
("UZT" 5 "Uzbekistan Time")
|
||||
("VET" -4 "Venezuelan Standard Time")
|
||||
("VLAT" 10 "Vladivostok Time")
|
||||
("VOLT" 4 "Volgograd Time")
|
||||
("VOST" 6 "Vostok Station Time")
|
||||
("VUT" 11 "Vanuatu Time")
|
||||
("WAKT" 12 "Wake Island Time")
|
||||
("WAST" 2 "West Africa Summer Time")
|
||||
("WAT" 1 "West Africa Time")
|
||||
("WEST" 1 "Western European Summer Time")
|
||||
("WET" 0 "Western European Time")
|
||||
("WIT" 7 "Western Indonesian Time")
|
||||
("WST" 8 "Western Standard Time")
|
||||
("YAKT" 9 "Yakutsk Time")
|
||||
("YEKT" 5 "Yekaterinburg Time"))))
|
||||
(dolist (tz info)
|
||||
(setf (gethash (car tz) +tz-info+) (cadr tz))))
|
||||
|
||||
;; add Military time zones
|
||||
(loop for ch across "ABCDEFGHI"
|
||||
do (let ((offset (1+ (- (char-code ch) (char-code #\A)))))
|
||||
(setf (gethash (string ch) +tz-info+) offset)))
|
||||
(setf (gethash "K" +tz-info+) 10
|
||||
(gethash "L" +tz-info+) 11
|
||||
(gethash "M" +tz-info+) 12
|
||||
(gethash "C*" +tz-info+) 3.5
|
||||
(gethash "D*" +tz-info+) 4.5
|
||||
(gethash "E*" +tz-info+) 5.5
|
||||
(gethash "F*" +tz-info+) 6.5)
|
||||
(loop for ch across "NOPQRSTUVWXY"
|
||||
do (let ((offset (- (1+ (- (char-code ch) (char-code #\N))))))
|
||||
(setf (gethash (string ch) +tz-info+) offset)))
|
||||
(setf (gethash "Z" +tz-info+) 0))
|
||||
117
util.lisp
Normal file
117
util.lisp
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
;;; Copyright (c) 2017, Sudhir Shenoy. All rights reserved.
|
||||
|
||||
;;; Redistribution and use in source and binary forms, with or without
|
||||
;;; modification, are permitted provided that the following conditions
|
||||
;;; are met:
|
||||
|
||||
;;; * Redistributions of source code must retain the above copyright
|
||||
;;; notice, this list of conditions and the following disclaimer.
|
||||
|
||||
;;; * Redistributions in binary form must reproduce the above
|
||||
;;; copyright notice, this list of conditions and the following
|
||||
;;; disclaimer in the documentation and/or other materials
|
||||
;;; provided with the distribution.
|
||||
|
||||
;;; THIS SOFTWARE IS PROVIDED BY THE AUTHOR 'AS IS' AND ANY EXPRESSED
|
||||
;;; OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
;;; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
;;; ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||
;;; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
;;; DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
||||
;;; GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
;;; INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
;;; WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
(in-package :cl-dates)
|
||||
|
||||
(defun parse-number (string)
|
||||
"Returns a number if string is numeric (ignoring surrounding white-space) else NIL"
|
||||
(handler-case (values (parse-integer string))
|
||||
(parse-error ()
|
||||
(multiple-value-bind (integer num-chars) (parse-integer string :junk-allowed t)
|
||||
(let ((len (length string)))
|
||||
(cond ((or (>= num-chars len)
|
||||
(and (= num-chars (1- len))
|
||||
(char= (char string num-chars) #\.)))
|
||||
integer)
|
||||
((char/= (char string num-chars) #\.) nil)
|
||||
(t (handler-case
|
||||
(multiple-value-bind (fraction frac-chars)
|
||||
(parse-integer string :start (1+ num-chars))
|
||||
(if (zerop fraction)
|
||||
integer
|
||||
(coerce (+ integer
|
||||
(/ fraction (expt 10 (- frac-chars num-chars 1))))
|
||||
'double-float)))
|
||||
(parse-error () nil)))))))))
|
||||
|
||||
(defun hms->day-fraction (hh mm ss)
|
||||
(/ (+ (* hh 3600) (* mm 60) ss) 86400))
|
||||
|
||||
(defun day-fraction->hms (day-frac)
|
||||
(let* ((secs (* day-frac 86400))
|
||||
(hh (floor (/ secs 3600))))
|
||||
(setf secs (- secs (* hh 3600)))
|
||||
(let* ((mm (floor (/ secs 60)))
|
||||
(ss (- secs (* mm 60))))
|
||||
(values hh mm ss))))
|
||||
|
||||
(defparameter +weekdays+ '((:monday . ("Mon" "Monday")) (:tuesday . ("Tue" "Tuesday" "Tues"))
|
||||
(:wednesday . ("Wed" "Wednesday")) (:thursday . ("Thu" "Thursday" "Thurs"))
|
||||
(:friday . ("Fri" "Friday")) (:saturday . ("Sat" "Saturday"))
|
||||
(:sunday . ("Sun" "Sunday"))))
|
||||
(defun dow->string (dow)
|
||||
(third (assoc dow +weekdays+)))
|
||||
|
||||
(defun three-letter-dow (dow)
|
||||
(second (assoc dow +weekdays+)))
|
||||
|
||||
(defun str-to-weekday (str)
|
||||
(loop for i from 0 below 7
|
||||
do (let ((list (elt +weekdays+ i)))
|
||||
(when (member str (cdr list) :test #'string-equal)
|
||||
(return-from str-to-weekday (car list))))))
|
||||
|
||||
(defparameter +months+ '((nil) ("Jan" "January") ("Feb" "February") ("Mar" "March") ("Apr" "April")
|
||||
("May" "May") ("Jun" "June") ("Jul" "July") ("Aug" "August")
|
||||
("Sep" "September" "Sept") ("Oct" "October") ("Nov" "November")
|
||||
("Dec" "December")))
|
||||
(defun month->string (mm)
|
||||
(second (elt +months+ mm)))
|
||||
|
||||
(defun three-letter-month (mm)
|
||||
(car (elt +months+ mm)))
|
||||
|
||||
(defun str-to-month (str)
|
||||
(loop for i from 1 to 12
|
||||
do (when (member str (elt +months+ i) :test #'string-equal)
|
||||
(return-from str-to-month i))))
|
||||
|
||||
(defparameter +time-suffixes+ '((:hour . ("h" "hr" "hrs" "hour" "hours"))
|
||||
(:minute . ("m" "min" "mins" "minute" "minutes"))
|
||||
(:second . ("s" "sec" "secs" "second" "seconds"))))
|
||||
(defun str-to-hms (str)
|
||||
(loop for i from 0 below 3
|
||||
do (let ((list (elt +time-suffixes+ i)))
|
||||
(when (member str (cdr list) :test #'string-equal)
|
||||
(return-from str-to-hms (car list))))))
|
||||
|
||||
(defun str-to-ampm (str)
|
||||
(cond ((member str '("a.m." "a.m" "am" "morning") :test #'string-equal) :am)
|
||||
((member str '("p.m." "p.m" "pm" "evening" "afternoon") :test #'string-equal) :pm)
|
||||
(t nil)))
|
||||
|
||||
(defun str-is-day-suffix (str)
|
||||
(not (null (member str '("st" "nd" "rd" "th") :test #'string-equal))))
|
||||
|
||||
(defun str-to-relative-dow (str)
|
||||
(cond ((member str '("last" "prev" "previous") :test #'string-equal) :prev)
|
||||
((member str '("next" "coming") :test #'string-equal) :next)
|
||||
((string-equal str "this") :closest)
|
||||
(t nil)))
|
||||
|
||||
(defparameter +date-words+ '(("today" . 0) ("tomorrow" . 1) ("yesterday" . -1)))
|
||||
(defun str-to-relative-date (str)
|
||||
(cdr (find str +date-words+ :test #'string-equal :key #'car)))
|
||||
Loading…
Add table
Add a link
Reference in a new issue