Mercurial > core / lisp/lib/obj/time/local.lisp
changeset 698: |
96958d3eb5b0 |
parent: |
9eb2c112aa16
|
author: |
Richard Westhaver <ellis@rwest.io> |
date: |
Fri, 04 Oct 2024 22:04:59 -0400 |
permissions: |
-rw-r--r-- |
description: |
fixes |
1 ;;; lib/obj/time/local.lisp --- Local Time 3 ;; from https://github.com/dlowe-net/local-time 7 ;; This file encodes 'human-readable' types into CLOS objects. Objects 8 ;; include timestamps, timezones and dates. 10 ;; This file doesn't explicitly encode durations (difference between 14 (in-package :obj/time) 18 (defclass timestamp () 19 ((day :accessor day-of :initarg :day :initform 0 :type integer) 20 (sec :accessor sec-of :initarg :sec :initform 0 :type integer) 21 (nsec :accessor nsec-of :initarg :nsec :initform 0 :type (integer 0 999999999)))) 29 (transitions #(0) :type simple-vector) 30 (indexes #(0) :type simple-vector) 31 (subzones #() :type simple-vector) 32 (leap-seconds nil :type list) 34 (name "anonymous" :type string) 35 (loaded nil :type boolean)) 37 (eval-when (:compile-toplevel :load-toplevel :execute) 38 (defconstant +timezone-offset-min+ -86400) 39 (defconstant +timezone-offset-max+ 86400)) 41 (deftype timezone-offset () 42 '(integer #.+timezone-offset-min+ #.+timezone-offset-max+)) 44 (defun %valid-time-of-day? (timestamp) 45 (zerop (day-of timestamp))) 47 (deftype time-of-day () 49 (satisfies %valid-time-of-day?))) 51 (defun %valid-date? (timestamp) 52 (and (zerop (sec-of timestamp)) 53 (zerop (nsec-of timestamp)))) 57 (satisfies %valid-date?))) 59 (defun zone-name (zone) 62 (define-condition invalid-timezone-file (error) 63 ((path :accessor path-of :initarg :path)) 64 (:report (lambda (condition stream) 65 (format stream "The file at ~a is not a timezone file." 66 (path-of condition))))) 68 (define-condition invalid-time-specification (error) 70 (:report "The time specification is invalid")) 72 (define-condition invalid-timestring (error) 73 ((timestring :accessor timestring-of :initarg :timestring) 74 (failure :accessor failure-of :initarg :failure)) 75 (:report (lambda (condition stream) 76 (format stream "Failed to parse ~S as an rfc3339 time: ~S" 77 (timestring-of condition) 78 (failure-of condition))))) 80 (defmethod make-load-form ((self timestamp) &optional environment) 81 (make-load-form-saving-slots self :environment environment)) 85 (declaim (inline now format-timestring %get-current-time 86 format-rfc3339-timestring to-rfc3339-timestring 87 format-rfc1123-timestring to-rfc1123-timestring) 88 (ftype (function (&rest t) string) format-rfc3339-timestring) 89 (ftype (function (&rest t) string) format-timestring) 90 (ftype (function (&rest t) fixnum) local-timezone) 91 (ftype (function (&rest t) (values 94 string)) timestamp-subzone) 95 (ftype (function (timestamp &key (:timezone timezone) (:offset (or null integer))) 96 (values (integer 0 999999999) 102 (integer -1000000 1000000) 111 (defvar *default-timezone*) 113 (defparameter *default-timezone-repository-path* 114 (flet ((try (project-home-directory) 115 (when project-home-directory 118 (merge-pathnames "zoneinfo/" 119 (make-pathname :directory (pathname-directory project-home-directory)))))))) 120 (or (when (find-package "ASDF") 121 (let ((path (eval (read-from-string 122 "(let ((system (asdf:find-system :obj/time nil))) 124 (asdf:component-pathname system)))")))) 126 (let ((path #.(or *compile-file-truename* 129 (try (merge-pathnames "../" path))))))) 131 ;;; Month information 132 (defparameter +month-names+ 133 #("" "January" "February" "March" "April" "May" "June" "July" "August" 134 "September" "October" "November" "December")) 135 (defparameter +short-month-names+ 136 #("" "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" 138 (defparameter +day-names+ 139 #("Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday")) 140 (defparameter +day-names-as-keywords+ 141 #(:sunday :monday :tuesday :wednesday :thursday :friday :saturday)) 142 (defparameter +short-day-names+ 143 #("Sun" "Mon" "Tue" "Wed" "Thu" "Fri" "Sat")) 144 (defparameter +minimal-day-names+ 145 #("Su" "Mo" "Tu" "We" "Th" "Fr" "Sa")) 147 (eval-when (:compile-toplevel :load-toplevel :execute) 148 (defconstant +months-per-year+ 12) 149 (defconstant +days-per-week+ 7) 150 (defconstant +hours-per-day+ 24) 151 (defconstant +minutes-per-day+ 1440) 152 (defconstant +minutes-per-hour+ 60) 153 (defconstant +seconds-per-day+ 86400) 154 (defconstant +seconds-per-hour+ 3600) 155 (defconstant +seconds-per-minute+ 60) 156 (defconstant +usecs-per-day+ 86400000000)) 158 (defparameter +iso-8601-date-format+ 159 '((:year 4) #\- (:month 2) #\- (:day 2))) 161 (defparameter +iso-8601-time-format+ 162 '((:hour 2) #\: (:min 2) #\: (:sec 2) #\. (:usec 6))) 164 (defparameter +iso-8601-format+ 165 ;; 2008-11-18T02:32:00.586931+01:00 166 (append +iso-8601-date-format+ (list #\T) +iso-8601-time-format+ (list :gmt-offset-or-z))) 168 (defparameter +rfc3339-format+ +iso-8601-format+) 170 (defparameter +rfc3339-format/date-only+ 171 '((:year 4) #\- (:month 2) #\- (:day 2))) 173 (defparameter +asctime-format+ 174 '(:short-weekday #\space :short-month #\space (:day 2 #\space) #\space 175 (:hour 2) #\: (:min 2) #\: (:sec 2) #\space 178 (defparameter +rfc-1123-format+ 179 ;; Sun, 06 Nov 1994 08:49:37 GMT 180 '(:short-weekday ", " (:day 2) #\space :short-month #\space (:year 4) #\space 181 (:hour 2) #\: (:min 2) #\: (:sec 2) #\space :gmt-offset-hhmm) 182 "See the RFC 1123 for the details about the possible values of the timezone field.") 184 (defparameter +iso-week-date-format+ 186 '((:iso-week-year 4) #\- #\W (:iso-week-number 2) #\- (:iso-week-day 1))) 188 (eval-when (:compile-toplevel :load-toplevel :execute) 189 (defparameter +rotated-month-days-without-leap-day+ 190 #.(coerce #(31 30 31 30 31 31 30 31 30 31 31 28) 191 '(simple-array fixnum (*)))) 193 (defparameter +rotated-month-offsets-without-leap-day+ 197 for days :across +rotated-month-days-without-leap-day+ 198 collect (incf sum days))) 199 '(simple-array fixnum (*))))) 201 ;; The astronomical julian date offset is the number of days between 202 ;; the current date and -4713-01-01T00:00:00+00:00 203 (defparameter +astronomical-julian-date-offset+ -2451605) 205 ;; The modified julian date is the number of days between the current 206 ;; date and 1858-11-17T12:00:00+00:00. TODO: For the sake of simplicity, 207 ;; we currently just do the date arithmetic and don't adjust for the 209 (defparameter +modified-julian-date-offset+ -51604) 211 (defun transition-position (needle haystack) 212 (declare (type integer needle) 213 (type (simple-array integer (*)) haystack) 214 (optimize (speed 3))) 216 with start fixnum = 0 217 with end fixnum = (length haystack) 218 ;; Invariant: haystack[start-1] <= needle < haystack[end] 219 for middle fixnum = (floor (+ end start) 2) 221 do (if (< needle (elt haystack middle)) 223 (setf start (1+ middle))) 225 (return (1- start)))) 227 (defvar *strict-first-subzone-validity* 229 "When true, raise an error if trying to get an offset before the first 232 (defun %subzone-as-of (timezone seconds days &optional guess-p) 233 "TIMEZONE is a realized timezone; SECONDS and DAYS are 'timestamp-values' 234 describing a local time, or null to ask for the subzone after the last 235 transition. Return the applicable subzone and the transition-index for that 237 When GUESS-P is true, the request is about SECONDS and DAYS in a timezone 238 which may not be UTC, and therefore the unix-time derived from SECONDS and 239 DAYS has an offset with respect to UTC: the offset of the subzone to be 241 (let* ((indexes (timezone-indexes timezone)) 242 (index-length (length indexes)) 243 (subzones (timezone-subzones timezone))) 244 (cond ((zerop index-length) 245 (values (elt subzones 0) nil)) 247 (let ((transition-idx (1- index-length))) 248 (values (elt subzones (elt indexes transition-idx)) 251 (let* ((transitions (timezone-transitions timezone)) 252 (unix-time (timestamp-values-to-unix seconds days)) 254 (transition-position (if guess-p 258 (subzone (elt subzones (elt indexes (max 0 transition-idx))))) 259 ;; Decide what to do when unix-time is before the first transition 260 (cond ((<= 0 transition-idx)) 261 ((and *strict-first-subzone-validity* 263 (- unix-time (subzone-offset subzone)) 265 (elt transitions 0))) 266 (error "Dates before ~A are not defined in ~A" 267 (multiple-value-list (decode-universal-time 268 (timestamp-to-universal 269 (unix-to-timestamp unix-time)) 272 (t (setf transition-idx 0))) 274 (< transition-idx (1- index-length))) ;there is a next 275 (let* ((next-idx (1+ transition-idx)) 276 (delta (- (elt transitions next-idx) unix-time))) 277 (when (<= delta 86400) ;check next offset 278 (let ((next-subzone (elt subzones (elt indexes next-idx)))) 279 (when (<= (+ delta (subzone-offset next-subzone)) 0) 280 ;; The next transition is valid 281 (setf transition-idx next-idx 282 subzone next-subzone)))))) 286 (defun %read-binary-integer (stream byte-count &optional (signed nil)) 287 "Read BYTE-COUNT bytes from the binary stream STREAM, and return an integer which is its representation in network byte order (MSB). If SIGNED is true, interprets the most significant bit as a sign indicator." 290 :for offset :from (* (1- byte-count) 8) :downto 0 :by 8 291 :do (setf (ldb (byte 8 offset) result) (read-byte stream)) 293 (let ((high-bit (* byte-count 8))) 294 (if (logbitp (1- high-bit) result) 295 (return (- result (ash 1 high-bit))) 299 (defun %string-from-unsigned-byte-vector (vector offset) 300 "Returns a string created from the vector of unsigned bytes VECTOR starting at OFFSET which is terminated by a 0." 301 (declare (type (vector (unsigned-byte 8)) vector)) 302 (let* ((null-pos (or (position 0 vector :start offset) (length vector))) 303 (result (make-string (- null-pos offset)))) 304 (loop for input-index :from offset :upto (1- null-pos) 305 for output-index :upfrom 0 306 do (setf (aref result output-index) (code-char (aref vector input-index)))) 309 (defun %find-first-std-offset (timezone-indexes timestamp-info) 310 (let ((subzone-idx (find-if 'subzone-daylight-p 312 :key (lambda (x) (aref timestamp-info x))))) 313 (subzone-offset (aref timestamp-info (or subzone-idx 0))))) 315 (defun %tz-verify-magic-number (inf zone) 316 ;; read and verify magic number 317 (let ((magic-buf (make-array 4 :element-type 'unsigned-byte))) 318 (read-sequence magic-buf inf :start 0 :end 4) 319 (when (string/= (map 'string #'code-char magic-buf) "TZif" :end1 4) 320 (error 'invalid-timezone-file :path (timezone-path zone)))) 321 ;; skip 16 bytes for "future use" 322 (let ((ignore-buf (make-array 16 :element-type 'unsigned-byte))) 323 (read-sequence ignore-buf inf :start 0 :end 16))) 325 (defun %tz-read-header (inf) 326 `(:utc-count ,(%read-binary-integer inf 4) 327 :wall-count ,(%read-binary-integer inf 4) 328 :leap-count ,(%read-binary-integer inf 4) 329 :transition-count ,(%read-binary-integer inf 4) 330 :type-count ,(%read-binary-integer inf 4) 331 :abbrev-length ,(%read-binary-integer inf 4))) 333 (defun %tz-read-transitions (inf count) 336 (loop for idx from 1 upto count 337 collect (%read-binary-integer inf 4 t)))) 339 (defun %tz-read-indexes (inf count) 342 (loop for idx from 1 upto count 343 collect (%read-binary-integer inf 1)))) 345 (defun %tz-read-subzone (inf count) 346 (loop for idx from 1 upto count 347 collect (list (%read-binary-integer inf 4 t) 348 (%read-binary-integer inf 1) 349 (%read-binary-integer inf 1)))) 351 (defun leap-seconds-sec (leap-seconds) 353 (defun leap-seconds-adjustment (leap-seconds) 356 (defun %tz-read-leap-seconds (inf count) 358 (loop for idx from 1 upto count 359 collect (%read-binary-integer inf 4) into sec 360 collect (%read-binary-integer inf 4) into adjustment 361 finally (return (cons (make-array count :initial-contents sec) 362 (make-array count :initial-contents adjustment)))))) 364 (defun %tz-read-abbrevs (inf length) 365 (let ((a (make-array length :element-type '(unsigned-byte 8)))) 371 (defun %tz-read-indicators (inf length) 372 ;; read standard/wall indicators 373 (let ((buf (make-array length :element-type '(unsigned-byte 8)))) 374 (read-sequence buf inf 379 :initial-contents buf))) 381 (defun %tz-make-subzones (raw-info abbrevs gmt-indicators std-indicators) 382 (declare (ignore gmt-indicators std-indicators)) 383 ;; TODO: handle TZ environment variables, which use the gmt and std 385 (make-array (length raw-info) 386 :element-type 'subzone 388 (loop for info in raw-info collect 391 :daylight-p (/= (second info) 0) 392 :abbrev (%string-from-unsigned-byte-vector abbrevs (third info)))))) 394 (defun %realize-timezone (zone &optional reload) 395 "If timezone has not already been loaded or RELOAD is non-NIL, loads the timezone information from its associated unix file. If the file is not a valid timezone file, the condition INVALID-TIMEZONE-FILE will be signaled." 396 (when (or reload (not (timezone-loaded zone))) 397 (with-open-file (inf (timezone-path zone) 399 :element-type 'unsigned-byte) 400 (%tz-verify-magic-number inf zone) 402 ;; read header values 403 (let* ((header (%tz-read-header inf)) 404 (timezone-transitions (%tz-read-transitions inf (getf header :transition-count))) 405 (subzone-indexes (%tz-read-indexes inf (getf header :transition-count))) 406 (subzone-raw-info (%tz-read-subzone inf (getf header :type-count))) 407 (abbreviation-buf (%tz-read-abbrevs inf (getf header :abbrev-length))) 408 (leap-second-info (%tz-read-leap-seconds inf (getf header :leap-count))) 409 (std-indicators (%tz-read-indicators inf (getf header :wall-count))) 410 (gmt-indicators (%tz-read-indicators inf (getf header :utc-count))) 411 (subzone-info (%tz-make-subzones subzone-raw-info 416 (setf (timezone-transitions zone) timezone-transitions) 417 (setf (timezone-indexes zone) subzone-indexes) 418 (setf (timezone-subzones zone) subzone-info) 419 (setf (timezone-leap-seconds zone) leap-second-info)) 420 (setf (timezone-loaded zone) t))) 423 (eval-when (:compile-toplevel :load-toplevel :execute) 424 (defun %make-simple-timezone (name abbrev offset) 425 (let ((subzone (obj/time::make-subzone :offset offset 428 (obj/time::make-timezone 429 :subzones (make-array 1 :initial-contents (list subzone)) 434 ;; to be used as #+#.(obj/time::package-with-symbol? "SB-EXT" "GET-TIME-OF-DAY") 435 (defun package-with-symbol? (package name) 436 (if (and (find-package package) 437 (find-symbol name package)) 441 (defparameter +utc-zone+ (%make-simple-timezone "Coordinated Universal Time" "UTC" 0)) 443 (defparameter +gmt-zone+ (%make-simple-timezone "Greenwich Mean Time" "GMT" 0)) 445 (defparameter +none-zone+ (%make-simple-timezone "Explicit Offset Given" "NONE" 0)) 447 (defparameter *location-name->timezone* 448 (make-hash-table :test 'equal 449 #+sbcl :synchronized #+sbcl t) 450 "A hashtable with entries like \"Europe/Budapest\" -> timezone-instance") 452 (defparameter *abbreviated-subzone-name->timezone-list* 453 (make-hash-table :test 'equal 454 #+sbcl :synchronized #+sbcl t) 455 "A hashtable of \"CEST\" -> list of timezones with \"CEST\" subzone") 457 (defmacro define-timezone (zone-name zone-file &key (load nil)) 458 "Define zone-name (a symbol or a string) as a new timezone, 459 lazy-loaded from zone-file (a pathname designator relative to the 460 zoneinfo directory on this system. If load is true, load immediately." 461 (declare (type (or string symbol) zone-name)) 462 (let ((zone-sym (if (symbolp zone-name) 464 (intern zone-name)))) 466 (defparameter ,zone-sym 467 (make-timezone :path ,zone-file 468 :name ,(if (symbolp zone-name) 469 (string-downcase (symbol-name zone-name)) 472 `((let ((timezone (%realize-timezone ,zone-sym))) 473 (setf (gethash (timezone-name timezone) 474 *location-name->timezone*) 476 (loop for subzone across (timezone-subzones timezone) 479 (gethash (subzone-abbrev subzone) 480 *abbreviated-subzone-name->timezone-list*)))))) 483 (eval-when (:load-toplevel :execute) 484 (let ((default-timezone-file #p"/etc/localtime")) 486 (define-timezone *default-timezone* default-timezone-file :load t) 488 (setf *default-timezone* +utc-zone+))))) 490 (defun find-timezone-by-location-name (name) 491 (when (zerop (hash-table-count *location-name->timezone*)) 492 (error "Seems like the timezone repository has not yet been loaded. Hint: see REREAD-TIMEZONE-REPOSITORY.")) 493 (gethash name *location-name->timezone*)) 496 (defun timezones-matching-subzone (abbreviated-name timestamp) 497 "Returns list of lists of active timezone, matched subzone and last transition time 498 for timezones that have subzone matching specified ABBREVIATED-NAME as of TIMESTAMP moment if provided. " 499 (loop for zone in (gethash abbreviated-name *abbreviated-subzone-name->timezone-list*) 500 ;; get the subzone and the latest transition index 501 for (subzone transition-idx) = (multiple-value-list (%subzone-as-of zone (sec-of timestamp) (day-of timestamp))) 502 if (equal abbreviated-name (subzone-abbrev subzone)) 503 collect (list zone subzone (when transition-idx (elt (timezone-transitions zone) transition-idx))))) 505 (defun all-timezones-matching-subzone (abbreviated-name) 506 "Returns list of lists of timezone, matched subzone and last transition time 507 for timezones that have subzone matching specified ABBREVIATED-NAME. Includes both active and historical timezones." 508 (loop for zone in (gethash abbreviated-name *abbreviated-subzone-name->timezone-list*) 509 for (subzone transition-idx) = (multiple-value-list (%subzone-as-of zone nil nil)) 510 if (equal abbreviated-name (subzone-abbrev subzone)) 511 collect (list zone subzone (when transition-idx (elt (timezone-transitions zone) transition-idx))) 514 nconc (loop for subzone-idx from 0 below (length (timezone-subzones zone)) 515 for sz = (elt (timezone-subzones zone) subzone-idx) 516 for tix = (position subzone-idx (timezone-indexes zone) :from-end t) 517 when (and tix (equal abbreviated-name (subzone-abbrev sz))) 518 collect (list zone sz (elt (timezone-transitions zone) tix))))) 520 (defun timezone= (timezone-1 timezone-2) 521 "Return two values indicating the relationship between timezone-1 and timezone-2. The first value is whether the two timezones are equal and the second value indicates whether it is sure or not. 524 \(values t t) means timezone-1 and timezone-2 are definitely equal. 525 \(values nil t) means timezone-1 and timezone-2 are definitely different. 526 \(values nil nil) means that it couldn't be determined." 527 (if (or (eq timezone-1 timezone-2) 528 (equalp timezone-1 timezone-2)) 532 (defun reread-timezone-repository (&key (timezone-repository *default-timezone-repository-path*)) 533 (check-type timezone-repository (or pathname string)) 534 (let ((root-directory (uiop:directory-exists-p timezone-repository))) 535 (unless root-directory 536 (error "REREAD-TIMEZONE-REPOSITORY was called with invalid PROJECT-DIRECTORY (~A)." 537 timezone-repository)) 538 (let ((cutoff-position (length (princ-to-string root-directory)))) 539 (flet ((visitor (file) 541 (let* ((full-name (subseq (princ-to-string file) cutoff-position)) 542 (timezone (%realize-timezone (make-timezone :path file :name full-name)))) 543 (setf (gethash full-name *location-name->timezone*) timezone) 544 (map nil (lambda (subzone) 545 (push timezone (gethash (subzone-abbrev subzone) 546 *abbreviated-subzone-name->timezone-list*))) 547 (timezone-subzones timezone))) 548 (invalid-timezone-file () nil)))) 549 (setf *location-name->timezone* 550 (make-hash-table :test 'equal 551 #+sbcl :synchronized #+sbcl t)) 552 (setf *abbreviated-subzone-name->timezone-list* 553 (make-hash-table :test 'equal 554 #+sbcl :synchronized #+sbcl t)) 555 (uiop:collect-sub*directories root-directory 559 (dolist (file (uiop:directory-files dir)) 560 (when (not (find "Etc" (pathname-directory file) 563 (uiop:collect-sub*directories (merge-pathnames "Etc/" root-directory) 567 (dolist (file (uiop:directory-files dir)) 568 (visitor file)))))))) 570 (defmacro make-timestamp (&rest args) 571 `(make-instance 'timestamp ,@args)) 573 (defun clone-timestamp (timestamp) 574 (make-instance 'timestamp 575 :nsec (nsec-of timestamp) 576 :sec (sec-of timestamp) 577 :day (day-of timestamp))) 579 (defun sec-day-subtimezone (sec day timezone) 580 (declare (type integer sec day) 581 (type timezone timezone)) 582 (let ((subzone (%subzone-as-of timezone sec day))) 583 (values (subzone-offset subzone) 584 (subzone-daylight-p subzone) 585 (subzone-abbrev subzone)))) 587 (defun timestamp-subtimezone (timestamp timezone) 588 "Return as multiple values the time zone as the number of seconds east of UTC, a boolean daylight-saving-p, and the customary abbreviation of the timezone." 589 (declare (type timestamp timestamp) 590 (type (or null timezone) timezone)) 591 (sec-day-subtimezone (sec-of timestamp) 593 (%realize-timezone (or timezone *default-timezone*)))) 595 (defun %adjust-to-offset (sec day offset) 596 "Returns two values, the values of new DAY and SEC slots of the timestamp adjusted to the given timezone." 597 (declare (type integer sec day offset)) 598 (multiple-value-bind (offset-day new-sec) 599 (floor (+ sec offset) +seconds-per-day+) 600 (values new-sec (+ day offset-day)))) 602 (defun %adjust-to-timezone (source timezone &optional offset) 603 (%adjust-to-offset (sec-of source) 606 (timestamp-subtimezone source timezone)))) 608 (defun timestamp-minimize-part (timestamp part &key 609 (timezone *default-timezone*) 611 (let* ((timestamp-parts '(:nsec :sec :min :hour :day :month)) 612 (part-count (position part timestamp-parts))) 613 (assert part-count nil 614 "timestamp-minimize-part called with invalid part ~a (expected one of ~a)" 617 (multiple-value-bind (nsec sec min hour day month year) 618 (decode-timestamp timestamp :timezone timezone) 619 (declare (ignore nsec)) 621 (if (> part-count 0) 0 sec) 622 (if (> part-count 1) 0 min) 623 (if (> part-count 2) 0 hour) 624 (if (> part-count 3) 1 day) 625 (if (> part-count 4) 1 month) 630 (defun timestamp-maximize-part (timestamp part &key 631 (timezone *default-timezone*) 633 (let* ((timestamp-parts '(:nsec :sec :min :hour :day :month)) 634 (part-count (position part timestamp-parts))) 635 (assert part-count nil 636 "timestamp-maximize-part called with invalid part ~a (expected one of ~a)" 639 (multiple-value-bind (nsec sec min hour day month year) 640 (decode-timestamp timestamp :timezone timezone) 641 (declare (ignore nsec)) 642 (let ((month (if (> part-count 4) 12 month))) 643 (encode-timestamp 999999999 644 (if (> part-count 0) 59 sec) 645 (if (> part-count 1) 59 min) 646 (if (> part-count 2) 23 hour) 647 (if (> part-count 3) (days-in-month month year) day) 653 (defmacro with-decoded-timestamp ((&key nsec sec minute hour day month year day-of-week daylight-p timezone offset) 654 timestamp &body forms) 655 "This macro binds variables to the decoded elements of TIMESTAMP. The TIMEZONE argument is used for decoding the timestamp, and is not bound by the macro. The value of DAY-OF-WEEK starts from 0 which means Sunday." 659 (macrolet ((initialize (&rest vars) 667 (push ,var variables))) 668 (setf ignores (nreverse ignores)) 669 (setf variables (nreverse variables)))) 670 (declare-fixnum-type (&rest vars) 675 (push `(type fixnum ,,var) types))) 676 (setf types (nreverse types))))) 678 (push `(type (integer 0 999999999) ,nsec) types)) 679 (declare-fixnum-type sec minute hour day month year) 680 (initialize nsec sec minute hour day month year day-of-week daylight-p)) 681 `(multiple-value-bind (,@variables) 682 (decode-timestamp ,timestamp :timezone ,(or timezone '*default-timezone*) :offset ,offset) 683 (declare (ignore ,@ignores) ,@types) 686 (defun %normalize-month-year-pair (month year) 687 "Normalizes the month/year pair: in case month is < 1 or > 12 the month and year are corrected to handle the overflow." 688 (multiple-value-bind (year-offset month-minus-one) 689 (floor (1- month) 12) 690 (values (1+ month-minus-one) 691 (+ year year-offset)))) 693 (defun days-in-month (month year) 694 "Returns the number of days in the given month of the specified year." 695 (let ((normal-days (aref +rotated-month-days-without-leap-day+ 696 (mod (+ month 9) 12)))) 698 (or (and (zerop (mod year 4)) 699 (plusp (mod year 100))) 700 (zerop (mod year 400)))) 701 (1+ normal-days) ; February on a leap year 704 ;; TODO scan all uses of FIX-OVERFLOW-IN-DAYS and decide where it's ok to silently fix and where should be and error reported 705 (defun %fix-overflow-in-days (day month year) 706 "In case the day number is higher than the maximal possible for the given month/year pair, returns the last day of the month." 707 (let ((max-day (days-in-month month year))) 712 (eval-when (:compile-toplevel :load-toplevel) 713 (defun %list-length= (num list) 714 "Tests for a list of length NUM without traversing the entire list to get the length." 715 (let ((c (nthcdr (1- num) list))) 716 (and c (endp (cdr c))))) 718 (defun %expand-adjust-timestamp-changes (timestamp changes visitor) 722 :for change in changes 726 (%list-length= 3 change) 727 (and (%list-length= 2 change) 728 (symbolp (first change)) 729 (or (string= (first change) :timezone) 730 (string= (first change) :utc-offset))) 731 (and (%list-length= 4 change) 732 (symbolp (third change)) 733 (or (string= (third change) :to) 734 (string= (third change) :by)))) 735 nil "Syntax error in expression ~S" change) 736 (let ((operation (first change)) 737 (part (second change)) 738 (value (if (%list-length= 3 change) 742 ((string= operation :set) 743 (push `(%set-timestamp-part ,part ,value) functions)) 744 ((string= operation :offset) 745 (push `(%offset-timestamp-part ,part ,value) functions)) 746 ((string= operation :utc-offset) 748 (push :utc-offset params)) 749 ((string= operation :timezone) 751 (push :timezone params)) 752 (t (error "Unexpected operation ~S" operation))))) 755 :for (function part value) in functions 757 (funcall visitor `(,function ,timestamp ,part ,value ,@params))))) 759 (defun %expand-adjust-timestamp (timestamp changes &key functional) 760 (let* ((old (gensym "OLD")) 765 (%expand-adjust-timestamp-changes old changes 769 (multiple-value-bind (nsec sec day) 771 (setf (nsec-of ,new) nsec) 772 (setf (sec-of ,new) sec) 773 (setf (day-of ,new) day)) 775 `((setf ,old ,new)))) 777 (setf forms (nreverse forms)) 778 `(let* ((,old ,timestamp) 780 `((,new (clone-timestamp ,old))))) 785 (defmacro adjust-timestamp (timestamp &body changes) 786 (%expand-adjust-timestamp timestamp changes :functional t)) 788 (defmacro adjust-timestamp! (timestamp &body changes) 789 (%expand-adjust-timestamp timestamp changes :functional nil)) 791 (defun %set-timestamp-part (time part new-value &key (timezone *default-timezone*) utc-offset) 792 ;; TODO think about error signalling. when, how to disable if it makes sense, ... 794 ((:nsec :sec-of-day :day) 795 (let ((nsec (nsec-of time)) 799 (:nsec (setf nsec (coerce new-value '(integer 0 999999999)))) 800 (:sec-of-day (setf sec (coerce new-value `(integer 0 ,+seconds-per-day+)))) 801 (:day (setf day new-value))) 802 (values nsec sec day))) 804 (with-decoded-timestamp (:nsec nsec :sec sec :minute minute :hour hour 805 :day day :month month :year year :timezone timezone :offset utc-offset) 808 (:sec (setf sec new-value)) 809 (:minute (setf minute new-value)) 810 (:hour (setf hour new-value)) 811 (:day-of-month (setf day new-value)) 812 (:month (setf month new-value) 813 (setf day (%fix-overflow-in-days day month year))) 814 (:year (setf year new-value) 815 (setf day (%fix-overflow-in-days day month year)))) 816 (encode-timestamp-into-values nsec sec minute hour day month year :timezone timezone :offset utc-offset))))) 818 (defun %offset-timestamp-part (time part offset &key (timezone *default-timezone*) utc-offset) 819 "Returns a time adjusted by the specified OFFSET. Takes care of 820 different kinds of overflows. The setting :day-of-week is possible 821 using a keyword symbol name of a week-day (see 822 +DAY-NAMES-AS-KEYWORDS+) as value. In that case point the result to 823 day given by OFFSET in the week that contains TIME." 824 (labels ((direct-adjust (part offset nsec sec day) 825 (cond ((eq part :day-of-week) 826 (with-decoded-timestamp (:day-of-week day-of-week 827 :nsec nsec :sec sec :minute minute :hour hour 828 :day day :month month :year year 829 :timezone timezone :offset utc-offset) 831 (let ((position (position offset +day-names-as-keywords+ :test #'eq))) 832 (assert position (position) "~S is not a valid day name" offset) 833 (let ((offset (+ (- (if (zerop day-of-week) 844 (setf day (+ (days-in-month month year) day))) 845 ((let ((days-in-month (days-in-month month year))) 846 (when (< days-in-month day) 851 (decf day days-in-month))))) 852 (encode-timestamp-into-values nsec sec minute hour day month year 853 :timezone timezone :offset utc-offset))))) 855 ;; The offset is zero, so just return the parts of the timestamp object 856 (values nsec sec day)) 858 (let ((old-utc-offset (or utc-offset 859 (timestamp-subtimezone time timezone))) 865 (multiple-value-bind (sec-offset new-nsec) 866 (floor (+ offset nsec) 1000000000) 867 ;; the time might need to be adjusted a bit more if q != 0 872 ((:sec :minute :hour) 873 (multiple-value-bind (days-offset new-sec) 874 (floor (+ sec (* offset (ecase part 876 (:minute +seconds-per-minute+) 877 (:hour +seconds-per-hour+)))) 879 (return-from direct-adjust (values nsec new-sec (+ day days-offset))))) 882 (setf new-utc-offset (or utc-offset 883 (timestamp-subtimezone (make-timestamp :nsec nsec :sec sec :day day) 885 (when (not (= old-utc-offset 887 ;; We hit the DST boundary. We need to restart again 888 ;; with :sec, but this time we know both old and new 889 ;; UTC offset will be the same, so it's safe to do 891 offset (- old-utc-offset 893 old-utc-offset new-utc-offset) 895 (return-from direct-adjust (values nsec sec day))))))))) 897 (safe-adjust (part offset time) 898 (with-decoded-timestamp (:nsec nsec :sec sec :minute minute :hour hour :day day 899 :month month :year year :timezone timezone :offset utc-offset) 901 (multiple-value-bind (month-new year-new) 902 (%normalize-month-year-pair 905 (:year (* 12 offset))) 908 ;; Almost there. However, it is necessary to check for 910 (encode-timestamp-into-values nsec sec minute hour 911 (%fix-overflow-in-days day month-new year-new) 913 :timezone timezone :offset utc-offset))))) 915 ((:nsec :sec :minute :hour :day :day-of-week) 916 (direct-adjust part offset 920 ((:month :year) (safe-adjust part offset time))))) 922 (defun timestamp-difference (time-a time-b) 923 "Returns the difference between TIME-A and TIME-B in seconds" 924 (let ((nsec (- (nsec-of time-a) (nsec-of time-b))) 925 (second (- (sec-of time-a) (sec-of time-b))) 926 (day (- (day-of time-a) (day-of time-b)))) 929 (incf nsec 1000000000)) 930 (when (minusp second) 932 (incf second +seconds-per-day+)) 933 (let ((result (+ (* day +seconds-per-day+) 936 ;; this incf turns the result into a float, so only do this when necessary 937 (incf result (/ nsec 1000000000d0))) 940 (defun timestamp+ (time amount unit &optional (timezone *default-timezone*) offset) 941 (multiple-value-bind (nsec sec day) 942 (%offset-timestamp-part time unit amount :timezone timezone :utc-offset offset) 943 (make-timestamp :nsec nsec 947 (defun timestamp- (time amount unit &optional (timezone *default-timezone*) offset) 948 (timestamp+ time (- amount) unit timezone offset)) 950 (defun %ts-day-of-week (ts-day) 951 (mod (+ 3 ts-day) 7)) 953 (defun timestamp-day-of-week (timestamp &key (timezone *default-timezone*) offset) 954 (%ts-day-of-week (nth-value 1 (%adjust-to-timezone timestamp timezone offset)))) 957 ;; http://java.sun.com/j2se/1.4.2/docs/api/java/util/GregorianCalendar.html 958 ;; (or something else, sorry :) this scheme only works back until 959 ;; 1582, the start of the gregorian calendar. see also 960 ;; DECODE-TIMESTAMP when fixing if fixing is desired at all. 961 (defun valid-timestamp-p (nsec sec minute hour day month year) 962 "Returns T if the time values refer to a valid time, otherwise returns NIL." 963 (and (<= 0 nsec 999999999) 968 (<= 1 day (days-in-month month year)) 971 (defun encode-sec-day (sec minute hour day month year) 972 (declare (type integer sec minute hour day month year)) 973 (values (+ (* hour +seconds-per-hour+) 974 (* minute +seconds-per-minute+) 976 (multiple-value-bind (ts-month ts-year) 978 (values (+ month 9) (- year 2001)) 979 (values (- month 3) (- year 2000))) 980 (+ (years-to-days ts-year) 981 (aref +rotated-month-offsets-without-leap-day+ ts-month) 984 (defun encode-offset (ts-sec ts-day timezone) 986 (%subzone-as-of (%realize-timezone (or timezone *default-timezone*)) 991 (defun encode-timestamp-into-values (nsec sec minute hour day month year 992 &key timezone offset) 993 "Returns (VALUES NSEC SEC DAY ZONE) ready to be used for 994 instantiating a new timestamp object. If the specified time is 995 invalid, the condition INVALID-TIME-SPECIFICATION is raised." 996 ;; If the user provided an explicit offset, we use that. Otherwise, 997 (declare (type integer nsec sec minute hour day month year) 998 (type (or integer null) offset)) 999 (unless (valid-timestamp-p nsec sec minute hour day month year) 1000 (error 'invalid-time-specification)) 1001 (multiple-value-bind (enc-sec enc-day) 1002 (encode-sec-day sec minute hour day month year) 1003 (multiple-value-bind (ts-sec ts-day) 1004 (%adjust-to-offset enc-sec 1007 (encode-offset enc-sec enc-day timezone)))) 1008 (values nsec ts-sec ts-day)))) 1010 (defun encode-timestamp (nsec sec minute hour day month year 1011 &key (timezone *default-timezone*) offset into) 1012 "Return a new TIMESTAMP instance corresponding to the specified time 1014 (declare (type integer nsec sec minute hour day month year)) 1015 (multiple-value-bind (nsec sec day) 1016 (encode-timestamp-into-values nsec sec minute hour day month year 1017 :timezone timezone :offset offset) 1020 (setf (nsec-of into) nsec) 1021 (setf (sec-of into) sec) 1022 (setf (day-of into) day) 1029 (defun universal-sec-day (universal) 1030 (let ((adjusted-universal (- universal #.(encode-universal-time 0 0 0 1 3 2000 0)))) 1031 (multiple-value-bind (ts-day ts-sec) 1032 (floor adjusted-universal +seconds-per-day+) 1033 (values ts-sec ts-day)))) 1035 (defun universal-to-timestamp (universal &key (nsec 0)) 1036 "Returns a timestamp corresponding to the given universal time." 1037 ;; universal time is seconds from 1900-01-01T00:00:00Z. 1038 (multiple-value-bind (ts-sec ts-day) 1039 (universal-sec-day universal) 1040 (make-timestamp :day ts-day :sec ts-sec :nsec nsec))) 1042 (defun ts-sec-day-to-universal (ts-sec ts-day) 1043 "Return the UNIVERSAL-TIME corresponding to the TIMESTAMP" 1044 ;; universal time is seconds from 1900-01-01T00:00:00Z 1045 (+ (* ts-day +seconds-per-day+) 1047 #.(encode-universal-time 0 0 0 1 3 2000 0))) 1049 (defun timestamp-to-universal (timestamp) 1050 "Return the UNIVERSAL-TIME corresponding to the TIMESTAMP" 1051 ;; universal time is seconds from 1900-01-01T00:00:00Z 1052 (ts-sec-day-to-universal (sec-of timestamp) (day-of timestamp))) 1054 (defun unix-to-timestamp (unix &key (nsec 0)) 1055 "Return a TIMESTAMP corresponding to UNIX, which is the number of seconds since the unix epoch, 1970-01-01T00:00:00Z." 1056 (multiple-value-bind (days secs) 1057 (floor unix +seconds-per-day+) 1058 (make-timestamp :day (- days 11017) :sec secs :nsec nsec))) 1060 (defun timestamp-values-to-unix (seconds day) 1061 "Return the Unix time corresponding to the values used to encode a TIMESTAMP" 1062 (+ (* (+ day 11017) +seconds-per-day+) seconds)) 1064 (defun timestamp-to-unix (timestamp) 1065 "Return the Unix time corresponding to the TIMESTAMP" 1066 (declare (type timestamp timestamp)) 1067 (timestamp-values-to-unix (sec-of timestamp) (day-of timestamp))) 1069 (defun %get-current-time () 1070 "Cross-implementation abstraction to get the current time measured from the unix epoch (1/1/1970). Should return (values sec nano-sec)." 1072 #+#.(obj/time::package-with-symbol? "SB-EXT" "GET-TIME-OF-DAY") ; available from sbcl 1.0.28.66 1073 (multiple-value-bind (sec nsec) (sb-ext:get-time-of-day) 1074 (values sec (* 1000 nsec))) 1075 #-#.(obj/time::package-with-symbol? "SB-EXT" "GET-TIME-OF-DAY") ; obsolete, scheduled to be deleted at the end of 2009 1076 (multiple-value-bind (success? sec nsec) (sb-unix:unix-gettimeofday) 1077 (assert success? () "sb-unix:unix-gettimeofday reported failure?!") 1078 (values sec (* 1000 nsec))))) 1081 "Use the `*clock*' special variable if you need to define your own idea of the current time. 1083 The value of this variable should have at least a method `obj/time::clock-now'; there may also be a `obj/time::clock-today' method, although the default based on `obj/time::clock-now' will probably do the job. 1084 The currently supported values in obj/time are: 1085 t - use the standard clock 1086 obj/time:leap-second-adjusted - use a clock which adjusts for leap seconds using the information in *default-timezone*.") 1089 "Returns a timestamp representing the present moment." 1090 (clock-now *clock*)) 1093 "Returns a timestamp representing the present day." 1094 (clock-today *clock*)) 1096 (defun format-date-simple (&optional dest timestamp) 1097 "Return a simple date string for today." 1098 (unless timestamp (setq timestamp (today))) 1099 (format-timestring dest timestamp 1100 :format '(:year #\- (:month 2) #\- (:day 2)))) 1102 (defgeneric clock-now (clock) 1103 (:documentation "Returns a timestamp for the current time given a clock.")) 1105 (defgeneric clock-today (clock) 1106 (:documentation "Returns a timestamp for the current date given a 1107 clock. The date is encoded by convention as a timestamp with the 1108 time set to 00:00:00UTC.")) 1110 (defun %leap-seconds-offset (leap-seconds sec) 1111 "Find the latest leap second adjustment effective at SEC system time." 1112 (elt (leap-seconds-adjustment leap-seconds) 1113 (transition-position sec (leap-seconds-sec leap-seconds)))) 1115 (defun %adjust-sec-for-leap-seconds (sec) 1116 "Ajdust SEC from system time to Unix time (on systems those clock does not jump back over leap seconds)." 1117 (let ((leap-seconds (timezone-leap-seconds (%realize-timezone *default-timezone*)))) 1119 (decf sec (%leap-seconds-offset leap-seconds sec)))) 1122 (defmethod clock-now ((clock (eql 'leap-second-adjusted))) 1123 (multiple-value-bind (sec nsec) (%get-current-time) 1124 (unix-to-timestamp (%adjust-sec-for-leap-seconds sec) 1127 (defmethod clock-now (clock) 1128 (declare (ignore clock)) 1129 (multiple-value-bind (sec nsec) (%get-current-time) 1130 (unix-to-timestamp sec :nsec nsec))) 1132 (defmethod clock-today (clock) 1133 ;; TODO should return a date value, anyhow we will decide to represent it eventually 1134 (let ((result (clock-now clock))) 1135 (setf (sec-of result) 0) 1136 (setf (nsec-of result) 0) 1139 (defmacro %defcomparator (name &body body) 1140 (let ((pair-comparator-name (intern (concatenate 'string "%" (string name))))) 1142 (declaim (inline ,pair-comparator-name)) 1143 (defun ,pair-comparator-name (time-a time-b) 1144 (assert (typep time-a 'timestamp) 1148 :expected-type 'timestamp) 1149 (assert (typep time-b 'timestamp) 1153 :expected-type 'timestamp) 1155 (defun ,name (&rest times) 1156 (declare (dynamic-extent times)) 1157 (loop for head on times 1159 always (,pair-comparator-name (first head) (second head)))) 1160 (define-compiler-macro ,name (&rest times) 1162 :for i :upfrom 0 :below (length times) 1163 :collect (gensym (concatenate 'string "TIME-" (princ-to-string i) "-"))))) 1167 :collect (list var time))) 1168 ;; we could evaluate comparisons of timestamp literals here 1170 :for (time-a time-b) :on vars 1172 :collect `(,',pair-comparator-name ,time-a ,time-b))))))))) 1174 (defun %timestamp-compare (time-a time-b) 1175 "Returns the symbols <, >, or =, describing the relationship between TIME-A and TIME-b." 1176 (declare (type timestamp time-a time-b)) 1178 ((< (day-of time-a) (day-of time-b)) '<) 1179 ((> (day-of time-a) (day-of time-b)) '>) 1180 ((< (sec-of time-a) (sec-of time-b)) '<) 1181 ((> (sec-of time-a) (sec-of time-b)) '>) 1182 ((< (nsec-of time-a) (nsec-of time-b)) '<) 1183 ((> (nsec-of time-a) (nsec-of time-b)) '>) 1186 (%defcomparator timestamp< 1187 (eql (%timestamp-compare time-a time-b) '<)) 1189 (%defcomparator timestamp<= 1190 (not (null (member (%timestamp-compare time-a time-b) '(< =))))) 1192 (%defcomparator timestamp> 1193 (eql (%timestamp-compare time-a time-b) '>)) 1195 (%defcomparator timestamp>= 1196 (not (null (member (%timestamp-compare time-a time-b) '(> =))))) 1198 (%defcomparator timestamp= 1199 (eql (%timestamp-compare time-a time-b) '=)) 1201 (defun timestamp/= (&rest timestamps) 1202 "Returns T if no pair of timestamps is equal. Otherwise return NIL." 1203 (declare (dynamic-extent timestamps)) 1204 (loop for ts-head on timestamps do 1205 (loop for ts in (rest ts-head) do 1206 (when (timestamp= (car ts-head) ts) 1207 (return-from timestamp/= nil)))) 1210 (defun contest (test list) 1211 "Applies TEST to pairs of elements in list, keeping the element which last tested T. Returns the winning element." 1212 (reduce (lambda (a b) (if (funcall test a b) a b)) list)) 1214 (defun timestamp-minimum (time &rest times) 1215 "Returns the earliest timestamp" 1216 (contest #'timestamp< (cons time times))) 1218 (defun timestamp-maximum (time &rest times) 1219 "Returns the latest timestamp" 1220 (contest #'timestamp> (cons time times))) 1222 (eval-when (:compile-toplevel :load-toplevel :execute) 1223 (defun years-to-days (years) 1224 "Given a number of years, returns the number of days in those years." 1225 (let* ((days (* years 365)) 1226 (l1 (floor years 4)) 1227 (l2 (floor years 100)) 1228 (l3 (floor years 400))) 1229 (+ days l1 (- l2) l3)))) 1231 (defun days-to-years (days) 1232 "Given a number of days, returns the number of years and the remaining days in that year." 1233 (let ((remaining-days days)) 1234 (multiple-value-bind (400-years remaining-days) 1235 (floor remaining-days #.(years-to-days 400)) 1236 (let* ((100-years (min (floor remaining-days #.(years-to-days 100)) 3)) 1237 (remaining-days (- remaining-days 1238 (* 100-years #.(years-to-days 100))))) 1239 (multiple-value-bind (4-years remaining-days) 1240 (floor remaining-days #.(years-to-days 4)) 1241 (let ((years (min 3 (floor remaining-days #.(years-to-days 1))))) 1242 (values (+ (* 400-years 400) 1246 (- remaining-days (* years 365)))))))) 1247 ;; the above is the macroexpansion of the following. uses metabang BIND, but kept for clarity because the expansion is unreadable. 1249 (bind ((remaining-days days) 1250 ((values 400-years remaining-days) (floor remaining-days #.(years-to-days 400))) 1251 (100-years (min (floor remaining-days #.(years-to-days 100)) 1253 (remaining-days (- remaining-days 1255 #.(years-to-days 100)))) 1256 ((values 4-years remaining-days) (floor remaining-days #.(years-to-days 4))) 1257 (years (min (floor remaining-days 365) 1259 (values (+ (* 400-years 400) 1263 (- remaining-days (* years 365))))) 1265 ;; TODO merge this functionality into timestamp-difference 1266 (defun timestamp-whole-year-difference (time-a time-b) 1267 "Returns the number of whole years elapsed between time-a and time-b (hint: anniversaries)." 1268 (declare (type timestamp time-b time-a)) 1269 (multiple-value-bind (nsec-b sec-b minute-b hour-b day-b month-b year-b day-of-week-b daylight-p-b offset-b) 1270 (decode-timestamp time-b) 1271 (declare (ignore day-of-week-b daylight-p-b)) 1272 (multiple-value-bind (nsec-a sec-a minute-a hour-a day-a month-a year-a) 1273 (decode-timestamp time-a) 1274 (declare (ignore nsec-a sec-a minute-a hour-a day-a month-a)) 1275 (let ((year-difference (- year-a year-b))) 1276 (if (timestamp<= (encode-timestamp nsec-b sec-b minute-b hour-b 1281 (+ year-difference year-b) 1285 (1- year-difference)))))) 1287 (defun %timestamp-decode-date (days) 1288 "Returns the year, month, and day, given the number of days from the epoch." 1289 (declare (type integer days)) 1290 (multiple-value-bind (years remaining-days) 1291 (days-to-years days) 1292 (let* ((leap-day-p (= remaining-days 365)) 1293 (rotated-1-based-month (if leap-day-p 1294 12 ; march is the first month and february is the last 1295 (position remaining-days +rotated-month-offsets-without-leap-day+ :test #'<))) 1296 (1-based-month (if (>= rotated-1-based-month 11) 1297 (- rotated-1-based-month 10) 1298 (+ rotated-1-based-month 2))) 1299 (1-based-day (if leap-day-p 1301 (1+ (- remaining-days (aref +rotated-month-offsets-without-leap-day+ 1302 (1- rotated-1-based-month))))))) 1305 (if (>= rotated-1-based-month 11) ; january is in the next year 1311 (defun %timestamp-decode-iso-week (timestamp) 1312 "Returns the year, week number, and day of week components of an ISO week date." 1313 ;; Algorithm from http://en.wikipedia.org/wiki/Talk:ISO_week_date#Algorithms 1314 (let* ((dn (timestamp-day-of-week timestamp)) 1315 (day-of-week (if (zerop dn) 7 dn)) ; ISO weekdays are Monday=1 and Sunday=7 1316 (nearest-thursday (timestamp+ timestamp (- 4 day-of-week) :day)) 1317 (year (timestamp-year nearest-thursday)) 1318 (month (timestamp-month nearest-thursday)) 1319 (day (timestamp-day nearest-thursday)) 1320 (ordinal-day (- (day-of (encode-timestamp 0 0 0 0 day month year :timezone +utc-zone+)) 1321 (day-of (encode-timestamp 0 0 0 0 1 1 year :timezone +utc-zone+))))) 1323 (1+ (floor ordinal-day 7)) 1326 (defun %timestamp-decode-time (seconds) 1327 "Returns the hours, minutes, and seconds, given the number of seconds since midnight." 1328 (declare (type integer seconds)) 1329 (multiple-value-bind (hours hour-remainder) 1330 (floor seconds +seconds-per-hour+) 1331 (multiple-value-bind (minutes seconds) 1332 (floor hour-remainder +seconds-per-minute+) 1338 (defun decode-sec-day (ts-sec ts-day) 1339 (multiple-value-bind (hour minute sec) 1340 (%timestamp-decode-time ts-sec) 1341 (multiple-value-bind (year month day) 1342 (%timestamp-decode-date ts-day) 1343 (values sec minute hour 1345 (%ts-day-of-week ts-day))))) 1347 (defun decode-timestamp (timestamp &key (timezone *default-timezone*) offset) 1348 "Returns the decoded time as multiple values: nsec, ss, mm, hh, day, month, year, day-of-week" 1349 (declare (type timestamp timestamp)) 1350 (let ((timezone (if offset (the timezone +none-zone+) timezone))) 1351 (multiple-value-bind (offset* daylight-p abbreviation) 1352 (timestamp-subtimezone timestamp timezone) 1353 (multiple-value-bind (sec minute hour day month year day-of-week) 1354 (multiple-value-call #'decode-sec-day 1355 (%adjust-to-offset (sec-of timestamp) (day-of timestamp) (or offset offset*))) 1356 (values (nsec-of timestamp) 1364 (defun timestamp-year (timestamp &key (timezone *default-timezone*)) 1365 "Returns the cardinal year upon which the timestamp falls." 1367 (%timestamp-decode-date 1368 (nth-value 1 (%adjust-to-timezone timestamp timezone))))) 1370 (defun timestamp-century (timestamp &key (timezone *default-timezone*)) 1371 "Returns the ordinal century upon which the timestamp falls." 1372 (let* ((year (timestamp-year timestamp :timezone timezone)) 1373 (sign (signum year))) 1376 (truncate (1- (abs year)) 100))))) 1378 (defun timestamp-millennium (timestamp &key (timezone *default-timezone*)) 1379 "Returns the ordinal millennium upon which the timestamp falls." 1380 (let* ((year (timestamp-year timestamp :timezone timezone)) 1381 (sign (signum year))) 1384 (truncate (1- (abs year)) 1000))))) 1386 (defun timestamp-decade (timestamp &key (timezone *default-timezone*)) 1387 "Returns the cardinal decade upon which the timestamp falls." 1388 (truncate (timestamp-year timestamp :timezone timezone) 10)) 1390 (defun timestamp-month (timestamp &key (timezone *default-timezone*)) 1391 "Returns the month upon which the timestamp falls." 1393 (%timestamp-decode-date 1394 (nth-value 1 (%adjust-to-timezone timestamp timezone))))) 1396 (defun timestamp-day (timestamp &key (timezone *default-timezone*)) 1397 "Returns the day of the month upon which the timestamp falls." 1399 (%timestamp-decode-date 1400 (nth-value 1 (%adjust-to-timezone timestamp timezone))))) 1402 (defun timestamp-hour (timestamp &key (timezone *default-timezone*)) 1404 (%timestamp-decode-time 1405 (nth-value 0 (%adjust-to-timezone timestamp timezone))))) 1407 (defun timestamp-minute (timestamp &key (timezone *default-timezone*)) 1409 (%timestamp-decode-time 1410 (nth-value 0 (%adjust-to-timezone timestamp timezone))))) 1412 (defun timestamp-second (timestamp &key (timezone *default-timezone*)) 1414 (%timestamp-decode-time 1415 (nth-value 0 (%adjust-to-timezone timestamp timezone))))) 1417 (defun timestamp-microsecond (timestamp) 1418 (floor (nsec-of timestamp) 1000)) 1420 (defun timestamp-millisecond (timestamp) 1421 (floor (nsec-of timestamp) 1000000)) 1423 (defun split-timestring (str &rest args) 1425 (apply #'%split-timestring (coerce str 'simple-string) args)) 1427 (defun %split-timestring (time-string &key 1429 (end (length time-string)) 1430 (fail-on-error t) (time-separator #\:) 1431 (date-separator #\-) 1432 (date-time-separator #\T) 1433 (fract-time-separators '(#\. #\,)) 1434 (allow-missing-elements t) 1435 (allow-missing-date-part allow-missing-elements) 1436 (allow-missing-time-part allow-missing-elements) 1437 (allow-missing-timezone-part allow-missing-time-part)) 1438 "Based on http://www.ietf.org/rfc/rfc3339.txt including the function names used. Returns (values year month day hour minute second nsec offset-hour offset-minute). On parsing failure, signals INVALID-TIMESTRING if FAIL-ON-ERROR is NIL, otherwise returns NIL." 1439 (declare (type character date-time-separator time-separator date-separator) 1440 (type simple-string time-string) 1441 (optimize (speed 3))) 1443 (let (year month day hour minute second nsec offset-hour offset-minute) 1444 (declare (type (or null fixnum) start end year month day hour minute second offset-hour offset-minute) 1445 (type (or null (signed-byte 32)) nsec)) 1446 (macrolet ((passert (expression) 1447 `(unless ,expression 1448 (parse-error ',expression))) 1449 (parse-integer-into (start-end place &optional low-limit high-limit) 1450 (let ((entry (gensym "ENTRY")) 1451 (value (gensym "VALUE")) 1452 (pos (gensym "POS")) 1453 (start (gensym "START")) 1454 (end (gensym "END"))) 1455 `(let ((,entry ,start-end)) 1457 (let ((,start (car ,entry)) 1458 (,end (cdr ,entry))) 1459 (multiple-value-bind (,value ,pos) (parse-integer time-string :start ,start :end ,end :junk-allowed t) 1460 (passert (= ,pos ,end)) 1461 (setf ,place ,value) 1462 ,(if (and low-limit high-limit) 1463 `(passert (<= ,low-limit ,place ,high-limit)) 1467 (passert allow-missing-elements) 1469 (with-parts-and-count ((start end split-chars) &body body) 1470 `(multiple-value-bind (parts count) (split ,start ,end ,split-chars) 1471 (declare (ignorable count) (type fixnum count) 1472 ;;(type #1=(cons (cons fixnum fixnum) (or null #1#)) parts) 1475 (labels ((split (start end chars) 1476 (declare (type fixnum start end)) 1477 (unless (consp chars) 1478 (setf chars (list chars))) 1479 (loop with last-match = start 1480 with match-count of-type (integer 0 #.most-positive-fixnum) = 0 1481 for index of-type fixnum upfrom start 1483 when (member (aref time-string index) chars :test #'char-equal) 1484 collect (prog1 (if (< last-match index) 1485 (cons last-match index) 1488 (setf last-match (1+ index))) 1490 finally (return (values (if (zerop (- index last-match)) 1493 (nconc result (list (cons last-match index))) 1494 (incf match-count))) 1497 (with-parts-and-count (start end date-time-separator) 1500 (full-date (first parts)) 1501 (passert allow-missing-date-part)) 1503 (full-time (second parts)) 1504 (passert allow-missing-time-part)) 1507 allow-missing-date-part 1508 (find time-separator time-string 1509 :start (car (first parts)) 1510 :end (cdr (first parts)))) 1511 (full-time (first parts)) 1514 allow-missing-time-part 1515 (find date-separator time-string 1516 :start (car (first parts)) 1517 :end (cdr (first parts)))) 1518 (full-date (first parts)) 1521 (full-date (start-end) 1522 (let ((parts (split (car start-end) (cdr start-end) date-separator))) 1523 (passert (%list-length= 3 parts)) 1524 (date-fullyear (first parts)) 1525 (date-month (second parts)) 1526 (date-mday (third parts)))) 1527 (date-fullyear (start-end) 1528 (parse-integer-into start-end year)) 1529 (date-month (start-end) 1530 (parse-integer-into start-end month 1 12)) 1531 (date-mday (start-end) 1532 (parse-integer-into start-end day 1 31)) 1533 (full-time (start-end) 1534 (let ((start (car start-end)) 1535 (end (cdr start-end))) 1536 (with-parts-and-count (start end (list #\Z #\- #\+)) 1537 (let* ((zulup (find #\Z time-string :test #'char-equal :start start :end end)) 1539 (if (find #\+ time-string :test #'char-equal :start start :end end) 1542 (passert (<= 1 count 2)) 1543 (unless (and (eq (first parts) nil) 1546 (partial-time (first parts))) 1551 (passert (or zulup allow-missing-timezone-part)) 1552 (let* ((entry (second parts)) 1555 (declare (type fixnum start end)) 1557 (not (zerop (- end start))))) 1559 (time-offset (second parts) sign)))))))) 1560 (partial-time (start-end) 1561 (with-parts-and-count ((car start-end) (cdr start-end) time-separator) 1562 (passert (eql count 3)) 1563 (time-hour (first parts)) 1564 (time-minute (second parts)) 1565 (time-second (third parts)))) 1566 (time-hour (start-end) 1567 (parse-integer-into start-end hour 0 23)) 1568 (time-minute (start-end) 1569 (parse-integer-into start-end minute 0 59)) 1570 (time-second (start-end) 1571 (with-parts-and-count ((car start-end) (cdr start-end) fract-time-separators) 1572 (passert (<= 1 count 2)) 1573 (let ((*read-eval* nil)) 1574 (parse-integer-into (first parts) second 0 59) 1576 (let* ((start (car (second parts))) 1577 (end (cdr (second parts)))) 1578 (declare (type (integer 0 #.array-dimension-limit) start end)) 1579 (passert (<= (- end start) 9)) 1580 (let ((new-end (position #\0 time-string 1586 (setf end (min (1+ new-end))))) 1587 (setf nsec (* (the (integer 0 999999999) (parse-integer time-string :start start :end end)) 1588 (aref #.(coerce #(1000000000 100000000 10000000 1589 1000000 100000 10000 1000 100 10 1) 1590 '(simple-array (signed-byte 32) (10))) 1593 (time-offset (start-end sign) 1594 (with-parts-and-count ((car start-end) (cdr start-end) time-separator) 1595 (passert (or (and allow-missing-timezone-part (zerop count)) 1602 (parse-integer-into (first parts) offset-hour 0 23) 1603 (parse-integer-into (second parts) offset-minute 0 59)) 1604 ((= (- (cdar parts) (caar parts)) 4) 1606 (parse-integer-into (cons (caar parts) 1609 (parse-integer-into (cons (+ (caar parts) 2) 1611 offset-minute 0 59)) 1612 ((= (- (cdar parts) (caar parts)) 2) 1614 (parse-integer-into (cons (caar parts) 1617 (setf offset-minute 0))) 1619 (setf offset-hour (* offset-hour sign) 1620 offset-minute (* offset-minute sign)))) 1621 (parse-error (failure) 1623 (error 'invalid-timestring :timestring time-string :failure failure) 1624 (return-from %split-timestring nil))) 1626 (return-from %split-timestring (list year month day hour minute second nsec offset-hour offset-minute)))) 1629 (defun parse-rfc3339-timestring (timestring &key (fail-on-error t) 1630 (allow-missing-time-part nil)) 1631 (parse-timestring timestring :fail-on-error fail-on-error 1632 :allow-missing-timezone-part nil 1633 :allow-missing-time-part allow-missing-time-part 1634 :allow-missing-date-part nil 1635 :fract-time-separators #\.)) 1637 (defun parse-timestring (timestring &key 1641 (time-separator #\:) 1642 (date-separator #\-) 1643 (date-time-separator #\T) 1644 (fract-time-separators '(#\. #\,)) 1645 (allow-missing-elements t) 1646 (allow-missing-date-part allow-missing-elements) 1647 (allow-missing-time-part allow-missing-elements) 1648 (allow-missing-timezone-part allow-missing-elements) 1650 "Parse a timestring and return the corresponding TIMESTAMP. 1651 See split-timestring for details. Unspecified fields in the 1652 timestring are initialized to their lowest possible value, 1653 and timezone offset is 0 (UTC) unless explicitly specified 1654 in the input string." 1655 (let ((parts (%split-timestring (coerce timestring 'simple-string) 1657 :end (or end (length timestring)) 1658 :fail-on-error fail-on-error 1659 :time-separator time-separator 1660 :date-separator date-separator 1661 :date-time-separator date-time-separator 1662 :fract-time-separators fract-time-separators 1663 :allow-missing-elements allow-missing-elements 1664 :allow-missing-date-part allow-missing-date-part 1665 :allow-missing-time-part allow-missing-time-part 1666 :allow-missing-timezone-part allow-missing-timezone-part))) 1668 (destructuring-bind (year month day hour minute second nsec offset-hour offset-minute) 1678 :offset (if offset-hour 1679 (+ (* offset-hour 3600) 1680 (* (or offset-minute 0) 60)) 1683 (defun ordinalize (day) 1684 "Return an ordinal string representing the position 1685 of DAY in a sequence (1st, 2nd, 3rd, 4th, etc)." 1686 (declare (type (integer 1 31) day)) 1687 (format nil "~d~a" day 1696 (defun %construct-timestring (timestamp format timezone) 1697 "Constructs a string representing TIMESTAMP given the FORMAT 1698 of the string and the TIMEZONE. 1699 See the documentation of FORMAT-TIMESTRING for the structure of FORMAT." 1700 (declare (type timestamp timestamp) 1701 (optimize (speed 3))) 1702 (multiple-value-bind (nsec sec minute hour day month year weekday daylight-p offset abbrev) 1703 (decode-timestamp timestamp :timezone timezone) 1704 (declare (ignore daylight-p)) 1705 (multiple-value-bind (iso-year iso-week iso-weekday) 1706 (%timestamp-decode-iso-week timestamp) 1707 (let ((*print-pretty* nil) 1708 (*print-circle* nil)) 1709 (with-output-to-string (result nil) 1710 (dolist (fmt format) 1712 ((member fmt '(:gmt-offset :gmt-offset-or-z :gmt-offset-hhmm)) 1713 (multiple-value-bind (offset-hours offset-secs) 1714 (truncate offset +seconds-per-hour+) 1715 (declare (fixnum offset-hours offset-secs)) 1716 (if (and (eql fmt :gmt-offset-or-z) (zerop offset)) 1718 (format result "~c~2,'0d~:[:~;~]~2,'0d" 1719 (if (minusp offset) #\- #\+) 1721 (eql fmt :gmt-offset-hhmm) 1722 (round (abs offset-secs) 1723 +seconds-per-minute+))))) 1724 ((eql fmt :short-year) 1725 (princ (mod year 100) result)) 1726 ((eql fmt :long-month) 1727 (princ (aref +month-names+ month) result)) 1728 ((eql fmt :short-month) 1729 (princ (aref +short-month-names+ month) result)) 1730 ((eql fmt :long-weekday) 1731 (princ (aref +day-names+ weekday) result)) 1732 ((eql fmt :short-weekday) 1733 (princ (aref +short-day-names+ weekday) result)) 1734 ((eql fmt :minimal-weekday) 1735 (princ (aref +minimal-day-names+ weekday) result)) 1736 ((eql fmt :timezone) 1737 (princ abbrev result)) 1739 (princ (if (< hour 12) "am" "pm") result)) 1740 ((eql fmt :ordinal-day) 1741 (princ (ordinalize day) result)) 1742 ((or (stringp fmt) (characterp fmt)) 1745 (let ((val (ecase (if (consp fmt) (car fmt) fmt) 1747 (:usec (floor nsec 1000)) 1748 (:msec (floor nsec 1000000)) 1752 (:hour12 (1+ (mod (1- hour) 12))) 1757 (:iso-week-year iso-year) 1758 (:iso-week-number iso-week) 1759 (:iso-week-day iso-weekday)))) 1764 (format result "-~v,vd" 1766 (or (third fmt) #\0) 1769 (format result "~v,vd" 1771 (or (third fmt) #\0) 1774 (defun format-timestring (destination timestamp &key 1775 (format +iso-8601-format+) 1776 (timezone *default-timezone*)) 1777 "Constructs a string representation of TIMESTAMP according 1778 to FORMAT and returns it. 1779 If destination is T, the string is written to *standard-output*. 1780 If destination is a stream, the string is written to the stream. 1782 FORMAT is a list containing one or more of strings, characters, 1783 and keywords. Strings and characters are output literally, 1784 while keywords are replaced by the values here: 1787 :MONTH *numeric month 1792 :WEEKDAY *numeric day of week starting from index 0, which means Sunday 1796 :ISO-WEEK-YEAR *year for ISO week date (can be different from regular calendar year) 1797 :ISO-WEEK-NUMBER *ISO week number (i.e. 1 through 53) 1798 :ISO-WEEK-DAY *ISO compatible weekday number (monday=1, sunday=7) 1799 :LONG-WEEKDAY long form of weekday (e.g. Sunday, Monday) 1800 :SHORT-WEEKDAY short form of weekday (e.g. Sun, Mon) 1801 :MINIMAL-WEEKDAY minimal form of weekday (e.g. Su, Mo) 1802 :SHORT-YEAR short form of year (last 2 digits, e.g. 41, 42 instead of 2041, 2042) 1803 :LONG-MONTH long form of month (e.g. January, February) 1804 :SHORT-MONTH short form of month (e.g. Jan, Feb) 1805 :HOUR12 *hour on a 12-hour clock 1806 :AMPM am/pm marker in lowercase 1807 :GMT-OFFSET the gmt-offset of the time, in +00:00 form 1808 :GMT-OFFSET-OR-Z like :GMT-OFFSET, but is Z when UTC 1809 :GMT-OFFSET-HHMM like :GMT-OFFSET, but in +0000 form 1810 :TIMEZONE timezone abbrevation for the time 1812 Elements marked by * can be placed in a list in the form 1813 \(:keyword padding &optional \(padchar #\\0)) 1815 The string representation of the value will be padded with the padchar. 1817 You can see examples in +ISO-8601-FORMAT+, +ASCTIME-FORMAT+, and +RFC-1123-FORMAT+." 1818 (declare (type (or boolean stream) destination)) 1819 (let ((result (%construct-timestring timestamp format timezone))) 1821 (write-string result (if (eq t destination) *standard-output* destination))) 1824 (defun format-rfc1123-timestring (destination timestamp &key 1825 (timezone *default-timezone*)) 1826 (format-timestring destination timestamp 1827 :format +rfc-1123-format+ 1828 :timezone timezone)) 1830 (defun to-rfc1123-timestring (timestamp) 1831 (format-rfc1123-timestring nil timestamp)) 1833 (defun format-rfc3339-timestring (destination timestamp &key 1836 (omit-timezone-part omit-time-part) 1838 (timezone *default-timezone*)) 1839 "Formats a timestring in the RFC 3339 format, a restricted form of the ISO-8601 timestring specification for Internet timestamps." 1840 (let ((rfc3339-format 1842 (not omit-date-part) 1843 (not omit-time-part) 1844 (not omit-timezone-part)) 1845 +rfc3339-format+ ; micro optimization 1847 (unless omit-date-part 1851 (unless (or omit-date-part 1854 (unless omit-time-part 1859 (unless omit-timezone-part 1862 '(:gmt-offset))))))) 1863 (format-timestring destination timestamp :format rfc3339-format :timezone timezone))) 1865 (defun to-rfc3339-timestring (timestamp) 1866 (format-rfc3339-timestring nil timestamp)) 1868 (defun %read-timestring (stream char) 1869 (declare (ignore char)) 1871 (with-output-to-string (str) 1872 (loop for c = (read-char stream nil) 1873 while (and c (or (digit-char-p c) (member c '(#\: #\T #\t #\: #\- #\+ #\Z #\.)))) 1875 finally (when c (unread-char c stream)))) 1876 :allow-missing-elements t)) 1878 (defun %read-universal-time (stream char arg) 1879 (declare (ignore char arg)) 1880 (universal-to-timestamp 1882 (with-output-to-string (str) 1883 (loop for c = (read-char stream nil) 1884 while (and c (digit-char-p c)) 1886 finally (when c (unread-char c stream))))))) 1888 (defun enable-read-macros () 1889 "Enables the local-time reader macros for literal timestamps and universal time." 1890 (set-macro-character #\@ '%read-timestring) 1891 (set-dispatch-macro-character #\# #\@ '%read-universal-time) 1894 (defvar *debug-timestamp* nil) 1896 (defmethod print-object ((object timestamp) stream) 1897 "Print the TIMESTAMP object using the standard reader notation" 1900 (print-unreadable-object (object stream :type t) 1901 (format stream "~d/~d/~d" 1906 (when *print-escape* 1907 (write-char #\@ stream)) 1908 (format-rfc3339-timestring stream object)))) 1910 (defmethod print-object ((object timezone) stream) 1911 "Print the TIMEZONE object in a reader-rejected manner." 1912 (print-unreadable-object (object stream :type t) 1913 (format stream "~:[UNLOADED~;~{~a~^ ~}~]" 1914 (timezone-loaded object) 1915 (map 'list #'subzone-abbrev (timezone-subzones object))))) 1917 (defun astronomical-julian-date (timestamp) 1918 "Returns the astronomical julian date referred to by the timestamp." 1919 (- (day-of timestamp) +astronomical-julian-date-offset+)) 1921 (defun modified-julian-date (timestamp) 1922 "Returns the modified julian date referred to by the timestamp." 1923 (- (day-of timestamp) +modified-julian-date-offset+)) 1925 (declaim (notinline format-timestring)) 1927 (defun encode-universal-time-with-tz (sec minute hour day month year &key timezone) 1928 "Like encode-universal-time, but with a timezone object instead of a timezone offset." 1929 ;; Use low level functions to prevent allocation of timestamp structures. 1930 (declare (type integer sec minute hour day month year)) 1931 (unless (valid-timestamp-p 0 sec minute hour day month year) 1932 (error 'invalid-time-specification)) 1933 (multiple-value-bind (ts-sec ts-day) 1934 (encode-sec-day sec minute hour day month year) 1935 (- (ts-sec-day-to-universal ts-sec ts-day) 1936 (encode-offset ts-sec 1938 (%realize-timezone (or timezone *default-timezone*)))))) 1940 (defun decode-universal-time-with-tz (universal &key timezone) 1941 "Like decode-universal-time, but with a timezone object instead of an timezone offset. 1942 Differences with regard to decode-universal-time: 1943 - the returned offset is the offset applicable in TIMEZONE at UNIVERSAL time, 1944 and thus corrected for DST; 1945 - returns one more value: the abbreviation of the active timezone." 1946 (multiple-value-bind (ts-sec ts-day) (universal-sec-day universal) 1947 (multiple-value-bind (offset daylight-p abbreviation) 1948 (sec-day-subtimezone ts-sec 1950 (%realize-timezone (or timezone *default-timezone*))) 1951 (multiple-value-bind (sec minute hour day month year day-of-week) 1952 (multiple-value-call #'decode-sec-day 1953 (%adjust-to-offset ts-sec ts-day offset)) 1954 (values sec minute hour 1956 (mod (1- day-of-week) 7) ;NB In CL: Monday = 0 1958 (/ offset -3600) ;NB In CL: hours west