changelog shortlog graph tags branches changeset files revisions annotate raw help

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
2 
3 ;; from https://github.com/dlowe-net/local-time
4 
5 ;;; Commentary:
6 
7 ;; This file encodes 'human-readable' types into CLOS objects. Objects
8 ;; include timestamps, timezones and dates.
9 
10 ;; This file doesn't explicitly encode durations (difference between
11 ;; time objects).
12 
13 ;;; Code:
14 (in-package :obj/time)
15 
16 ;;; Types
17 
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))))
22 
23 (defstruct subzone
24  (abbrev nil)
25  (offset nil)
26  (daylight-p nil))
27 
28 (defstruct timezone
29  (transitions #(0) :type simple-vector)
30  (indexes #(0) :type simple-vector)
31  (subzones #() :type simple-vector)
32  (leap-seconds nil :type list)
33  (path nil)
34  (name "anonymous" :type string)
35  (loaded nil :type boolean))
36 
37 (eval-when (:compile-toplevel :load-toplevel :execute)
38  (defconstant +timezone-offset-min+ -86400)
39  (defconstant +timezone-offset-max+ 86400))
40 
41 (deftype timezone-offset ()
42  '(integer #.+timezone-offset-min+ #.+timezone-offset-max+))
43 
44 (defun %valid-time-of-day? (timestamp)
45  (zerop (day-of timestamp)))
46 
47 (deftype time-of-day ()
48  '(and timestamp
49  (satisfies %valid-time-of-day?)))
50 
51 (defun %valid-date? (timestamp)
52  (and (zerop (sec-of timestamp))
53  (zerop (nsec-of timestamp))))
54 
55 (deftype date ()
56  '(and timestamp
57  (satisfies %valid-date?)))
58 
59 (defun zone-name (zone)
60  (timezone-name zone))
61 
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)))))
67 
68 (define-condition invalid-time-specification (error)
69  ()
70  (:report "The time specification is invalid"))
71 
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)))))
79 
80 (defmethod make-load-form ((self timestamp) &optional environment)
81  (make-load-form-saving-slots self :environment environment))
82 
83 ;;; Declaims
84 
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
92  timezone-offset
93  boolean
94  string)) timestamp-subzone)
95  (ftype (function (timestamp &key (:timezone timezone) (:offset (or null integer)))
96  (values (integer 0 999999999)
97  (integer 0 59)
98  (integer 0 59)
99  (integer 0 23)
100  (integer 1 31)
101  (integer 1 12)
102  (integer -1000000 1000000)
103  (integer 0 6)
104  t
105  timezone-offset
106  simple-string))
107  decode-timestamp))
108 
109 ;;; Variables
110 
111 (defvar *default-timezone*)
112 
113 (defparameter *default-timezone-repository-path*
114  (flet ((try (project-home-directory)
115  (when project-home-directory
116  (ignore-errors
117  (truename
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)))
123  (when system
124  (asdf:component-pathname system)))"))))
125  (try path)))
126  (let ((path #.(or *compile-file-truename*
127  '*load-truename*)))
128  (when path
129  (try (merge-pathnames "../" path)))))))
130 
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"
137  "Dec"))
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"))
146 
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))
157 
158 (defparameter +iso-8601-date-format+
159  '((:year 4) #\- (:month 2) #\- (:day 2)))
160 
161 (defparameter +iso-8601-time-format+
162  '((:hour 2) #\: (:min 2) #\: (:sec 2) #\. (:usec 6)))
163 
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)))
167 
168 (defparameter +rfc3339-format+ +iso-8601-format+)
169 
170 (defparameter +rfc3339-format/date-only+
171  '((:year 4) #\- (:month 2) #\- (:day 2)))
172 
173 (defparameter +asctime-format+
174  '(:short-weekday #\space :short-month #\space (:day 2 #\space) #\space
175  (:hour 2) #\: (:min 2) #\: (:sec 2) #\space
176  (:year 4)))
177 
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.")
183 
184 (defparameter +iso-week-date-format+
185  ;; 2009-W53-5
186  '((:iso-week-year 4) #\- #\W (:iso-week-number 2) #\- (:iso-week-day 1)))
187 
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 (*))))
192 
193  (defparameter +rotated-month-offsets-without-leap-day+
194  (coerce
195  (cons 0
196  (loop with sum = 0
197  for days :across +rotated-month-days-without-leap-day+
198  collect (incf sum days)))
199  '(simple-array fixnum (*)))))
200 
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)
204 
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
208 ;; time of day.
209 (defparameter +modified-julian-date-offset+ -51604)
210 
211 (defun transition-position (needle haystack)
212  (declare (type integer needle)
213  (type (simple-array integer (*)) haystack)
214  (optimize (speed 3)))
215  (loop
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)
220  while (< start end)
221  do (if (< needle (elt haystack middle))
222  (setf end middle)
223  (setf start (1+ middle)))
224  finally
225  (return (1- start))))
226 
227 (defvar *strict-first-subzone-validity*
228  nil
229  "When true, raise an error if trying to get an offset before the first
230 known transition.")
231 
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
236 subzone.
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
240 found."
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))
246  ((not seconds)
247  (let ((transition-idx (1- index-length)))
248  (values (elt subzones (elt indexes transition-idx))
249  transition-idx)))
250  (t
251  (let* ((transitions (timezone-transitions timezone))
252  (unix-time (timestamp-values-to-unix seconds days))
253  (transition-idx
254  (transition-position (if guess-p
255  (- unix-time 86400)
256  unix-time)
257  transitions))
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*
262  (< (if guess-p
263  (- unix-time (subzone-offset subzone))
264  unix-time)
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))
270  0))
271  timezone))
272  (t (setf transition-idx 0)))
273  (when (and guess-p
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))))))
283  (values subzone
284  transition-idx))))))
285 
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."
288  (loop
289  :with result = 0
290  :for offset :from (* (1- byte-count) 8) :downto 0 :by 8
291  :do (setf (ldb (byte 8 offset) result) (read-byte stream))
292  :finally (if signed
293  (let ((high-bit (* byte-count 8)))
294  (if (logbitp (1- high-bit) result)
295  (return (- result (ash 1 high-bit)))
296  (return result)))
297  (return result))))
298 
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))))
307  result))
308 
309 (defun %find-first-std-offset (timezone-indexes timestamp-info)
310  (let ((subzone-idx (find-if 'subzone-daylight-p
311  timezone-indexes
312  :key (lambda (x) (aref timestamp-info x)))))
313  (subzone-offset (aref timestamp-info (or subzone-idx 0)))))
314 
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)))
324 
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)))
332 
333 (defun %tz-read-transitions (inf count)
334  (make-array count
335  :initial-contents
336  (loop for idx from 1 upto count
337  collect (%read-binary-integer inf 4 t))))
338 
339 (defun %tz-read-indexes (inf count)
340  (make-array count
341  :initial-contents
342  (loop for idx from 1 upto count
343  collect (%read-binary-integer inf 1))))
344 
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))))
350 
351 (defun leap-seconds-sec (leap-seconds)
352  (car leap-seconds))
353 (defun leap-seconds-adjustment (leap-seconds)
354  (cdr leap-seconds))
355 
356 (defun %tz-read-leap-seconds (inf count)
357  (when (plusp 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))))))
363 
364 (defun %tz-read-abbrevs (inf length)
365  (let ((a (make-array length :element-type '(unsigned-byte 8))))
366  (read-sequence a inf
367  :start 0
368  :end length)
369  a))
370 
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
375  :start 0
376  :end length)
377  (make-array length
378  :element-type 'bit
379  :initial-contents buf)))
380 
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
384  ;; indicators
385  (make-array (length raw-info)
386  :element-type 'subzone
387  :initial-contents
388  (loop for info in raw-info collect
389  (make-subzone
390  :offset (first info)
391  :daylight-p (/= (second info) 0)
392  :abbrev (%string-from-unsigned-byte-vector abbrevs (third info))))))
393 
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)
398  :direction :input
399  :element-type 'unsigned-byte)
400  (%tz-verify-magic-number inf zone)
401 
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
412  abbreviation-buf
413  gmt-indicators
414  std-indicators)))
415 
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)))
421  zone)
422 
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
426  :daylight-p nil
427  :abbrev abbrev)))
428  (obj/time::make-timezone
429  :subzones (make-array 1 :initial-contents (list subzone))
430  :path nil
431  :name name
432  :loaded t)))
433 
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))
438  '(:and)
439  '(:or))))
440 
441 (defparameter +utc-zone+ (%make-simple-timezone "Coordinated Universal Time" "UTC" 0))
442 
443 (defparameter +gmt-zone+ (%make-simple-timezone "Greenwich Mean Time" "GMT" 0))
444 
445 (defparameter +none-zone+ (%make-simple-timezone "Explicit Offset Given" "NONE" 0))
446 
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")
451 
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")
456 
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)
463  zone-name
464  (intern zone-name))))
465  `(progn
466  (defparameter ,zone-sym
467  (make-timezone :path ,zone-file
468  :name ,(if (symbolp zone-name)
469  (string-downcase (symbol-name zone-name))
470  zone-name)))
471  ,@(when load
472  `((let ((timezone (%realize-timezone ,zone-sym)))
473  (setf (gethash (timezone-name timezone)
474  *location-name->timezone*)
475  timezone)
476  (loop for subzone across (timezone-subzones timezone)
477  do
478  (push timezone
479  (gethash (subzone-abbrev subzone)
480  *abbreviated-subzone-name->timezone-list*))))))
481  ,zone-sym)))
482 
483 (eval-when (:load-toplevel :execute)
484  (let ((default-timezone-file #p"/etc/localtime"))
485  (handler-case
486  (define-timezone *default-timezone* default-timezone-file :load t)
487  (t ()
488  (setf *default-timezone* +utc-zone+)))))
489 
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*))
494 
495 
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)))))
504 
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)))
512  else
513  when 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)))))
519 
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.
522 
523 In other words:
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))
529  (values t t)
530  (values nil nil)))
531 
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)
540  (handler-case
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
556  (constantly t)
557  (constantly t)
558  (lambda (dir)
559  (dolist (file (uiop:directory-files dir))
560  (when (not (find "Etc" (pathname-directory file)
561  :test #'string=))
562  (visitor file)))))
563  (uiop:collect-sub*directories (merge-pathnames "Etc/" root-directory)
564  (constantly t)
565  (constantly t)
566  (lambda (dir)
567  (dolist (file (uiop:directory-files dir))
568  (visitor file))))))))
569 
570 (defmacro make-timestamp (&rest args)
571  `(make-instance 'timestamp ,@args))
572 
573 (defun clone-timestamp (timestamp)
574  (make-instance 'timestamp
575  :nsec (nsec-of timestamp)
576  :sec (sec-of timestamp)
577  :day (day-of timestamp)))
578 
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))))
586 
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)
592  (day-of timestamp)
593  (%realize-timezone (or timezone *default-timezone*))))
594 
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))))
601 
602 (defun %adjust-to-timezone (source timezone &optional offset)
603  (%adjust-to-offset (sec-of source)
604  (day-of source)
605  (or offset
606  (timestamp-subtimezone source timezone))))
607 
608 (defun timestamp-minimize-part (timestamp part &key
609  (timezone *default-timezone*)
610  into)
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)"
615  part
616  timestamp-parts)
617  (multiple-value-bind (nsec sec min hour day month year)
618  (decode-timestamp timestamp :timezone timezone)
619  (declare (ignore nsec))
620  (encode-timestamp 0
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)
626  year
627  :timezone timezone
628  :into into))))
629 
630 (defun timestamp-maximize-part (timestamp part &key
631  (timezone *default-timezone*)
632  into)
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)"
637  part
638  timestamp-parts)
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)
648  month
649  year
650  :timezone timezone
651  :into into)))))
652 
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."
656  (let ((ignores)
657  (types)
658  (variables))
659  (macrolet ((initialize (&rest vars)
660  `(progn
661  ,@(loop
662  :for var :in vars
663  :collect `(progn
664  (unless ,var
665  (setf ,var (gensym))
666  (push ,var ignores))
667  (push ,var variables)))
668  (setf ignores (nreverse ignores))
669  (setf variables (nreverse variables))))
670  (declare-fixnum-type (&rest vars)
671  `(progn
672  ,@(loop
673  :for var :in vars
674  :collect `(when ,var
675  (push `(type fixnum ,,var) types)))
676  (setf types (nreverse types)))))
677  (when nsec
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)
684  ,@forms)))
685 
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))))
692 
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))))
697  (if (and (= month 2)
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
702  normal-days)))
703 
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)))
708  (if (> day max-day)
709  max-day
710  day)))
711 
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)))))
717 
718  (defun %expand-adjust-timestamp-changes (timestamp changes visitor)
719  (loop
720  :with params = ()
721  :with functions = ()
722  :for change in changes
723  :do
724  (progn
725  (assert (or
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)
739  (third change)
740  (fourth change))))
741  (cond
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)
747  (push part params)
748  (push :utc-offset params))
749  ((string= operation :timezone)
750  (push part params)
751  (push :timezone params))
752  (t (error "Unexpected operation ~S" operation)))))
753  :finally
754  (loop
755  :for (function part value) in functions
756  :do
757  (funcall visitor `(,function ,timestamp ,part ,value ,@params)))))
758 
759  (defun %expand-adjust-timestamp (timestamp changes &key functional)
760  (let* ((old (gensym "OLD"))
761  (new (if functional
762  (gensym "NEW")
763  old))
764  (forms (list)))
765  (%expand-adjust-timestamp-changes old changes
766  (lambda (change)
767  (push
768  `(progn
769  (multiple-value-bind (nsec sec day)
770  ,change
771  (setf (nsec-of ,new) nsec)
772  (setf (sec-of ,new) sec)
773  (setf (day-of ,new) day))
774  ,@(when functional
775  `((setf ,old ,new))))
776  forms)))
777  (setf forms (nreverse forms))
778  `(let* ((,old ,timestamp)
779  ,@(when functional
780  `((,new (clone-timestamp ,old)))))
781  ,@forms
782  ,old)))
783  ) ; eval-when
784 
785 (defmacro adjust-timestamp (timestamp &body changes)
786  (%expand-adjust-timestamp timestamp changes :functional t))
787 
788 (defmacro adjust-timestamp! (timestamp &body changes)
789  (%expand-adjust-timestamp timestamp changes :functional nil))
790 
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, ...
793  (case part
794  ((:nsec :sec-of-day :day)
795  (let ((nsec (nsec-of time))
796  (sec (sec-of time))
797  (day (day-of time)))
798  (case part
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)))
803  (otherwise
804  (with-decoded-timestamp (:nsec nsec :sec sec :minute minute :hour hour
805  :day day :month month :year year :timezone timezone :offset utc-offset)
806  time
807  (ecase part
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)))))
817 
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)
830  time
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)
834  7
835  day-of-week))
836  position)))
837  (incf day offset)
838  (cond
839  ((< day 1)
840  (decf month)
841  (when (< month 1)
842  (setf month 12)
843  (decf year))
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)
847  (incf month)
848  (when (= month 13)
849  (setf month 1)
850  (incf year))
851  (decf day days-in-month)))))
852  (encode-timestamp-into-values nsec sec minute hour day month year
853  :timezone timezone :offset utc-offset)))))
854  ((zerop offset)
855  ;; The offset is zero, so just return the parts of the timestamp object
856  (values nsec sec day))
857  (t
858  (let ((old-utc-offset (or utc-offset
859  (timestamp-subtimezone time timezone)))
860  new-utc-offset)
861  (tagbody
862  top
863  (ecase part
864  (:nsec
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
868  (setf part :sec
869  offset sec-offset
870  nsec new-nsec)
871  (go top)))
872  ((:sec :minute :hour)
873  (multiple-value-bind (days-offset new-sec)
874  (floor (+ sec (* offset (ecase part
875  (:sec 1)
876  (:minute +seconds-per-minute+)
877  (:hour +seconds-per-hour+))))
878  +seconds-per-day+)
879  (return-from direct-adjust (values nsec new-sec (+ day days-offset)))))
880  (:day
881  (incf day offset)
882  (setf new-utc-offset (or utc-offset
883  (timestamp-subtimezone (make-timestamp :nsec nsec :sec sec :day day)
884  timezone)))
885  (when (not (= old-utc-offset
886  new-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
890  (setf part :sec
891  offset (- old-utc-offset
892  new-utc-offset)
893  old-utc-offset new-utc-offset)
894  (go top))
895  (return-from direct-adjust (values nsec sec day)))))))))
896 
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)
900  time
901  (multiple-value-bind (month-new year-new)
902  (%normalize-month-year-pair
903  (+ (ecase part
904  (:month offset)
905  (:year (* 12 offset)))
906  month)
907  year)
908  ;; Almost there. However, it is necessary to check for
909  ;; overflows first
910  (encode-timestamp-into-values nsec sec minute hour
911  (%fix-overflow-in-days day month-new year-new)
912  month-new year-new
913  :timezone timezone :offset utc-offset)))))
914  (ecase part
915  ((:nsec :sec :minute :hour :day :day-of-week)
916  (direct-adjust part offset
917  (nsec-of time)
918  (sec-of time)
919  (day-of time)))
920  ((:month :year) (safe-adjust part offset time)))))
921 
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))))
927  (when (minusp nsec)
928  (decf second)
929  (incf nsec 1000000000))
930  (when (minusp second)
931  (decf day)
932  (incf second +seconds-per-day+))
933  (let ((result (+ (* day +seconds-per-day+)
934  second)))
935  (unless (zerop nsec)
936  ;; this incf turns the result into a float, so only do this when necessary
937  (incf result (/ nsec 1000000000d0)))
938  result)))
939 
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
944  :sec sec
945  :day day)))
946 
947 (defun timestamp- (time amount unit &optional (timezone *default-timezone*) offset)
948  (timestamp+ time (- amount) unit timezone offset))
949 
950 (defun %ts-day-of-week (ts-day)
951  (mod (+ 3 ts-day) 7))
952 
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))))
955 
956 ;; TODO read
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)
964  (<= 0 sec 59)
965  (<= 0 minute 59)
966  (<= 0 hour 23)
967  (<= 1 month 12)
968  (<= 1 day (days-in-month month year))
969  (/= year 0)))
970 
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+)
975  sec)
976  (multiple-value-bind (ts-month ts-year)
977  (if (< month 3)
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)
982  (1- day)))))
983 
984 (defun encode-offset (ts-sec ts-day timezone)
985  (subzone-offset
986  (%subzone-as-of (%realize-timezone (or timezone *default-timezone*))
987  ts-sec
988  ts-day
989  t)))
990 
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
1005  enc-day
1006  (- (or offset
1007  (encode-offset enc-sec enc-day timezone))))
1008  (values nsec ts-sec ts-day))))
1009 
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
1013 elements."
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)
1018  (if into
1019  (progn
1020  (setf (nsec-of into) nsec)
1021  (setf (sec-of into) sec)
1022  (setf (day-of into) day)
1023  into)
1024  (make-timestamp
1025  :nsec nsec
1026  :sec sec
1027  :day day))))
1028 
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))))
1034 
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)))
1041 
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+)
1046  ts-sec
1047  #.(encode-universal-time 0 0 0 1 3 2000 0)))
1048 
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)))
1053 
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)))
1059 
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))
1063 
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)))
1068 
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)."
1071  (progn
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)))))
1079 
1080 (defvar *clock* t
1081  "Use the `*clock*' special variable if you need to define your own idea of the current time.
1082 
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*.")
1087 
1088 (defun now ()
1089  "Returns a timestamp representing the present moment."
1090  (clock-now *clock*))
1091 
1092 (defun today ()
1093  "Returns a timestamp representing the present day."
1094  (clock-today *clock*))
1095 
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))))
1101 
1102 (defgeneric clock-now (clock)
1103  (:documentation "Returns a timestamp for the current time given a clock."))
1104 
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."))
1109 
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))))
1114 
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*))))
1118  (when leap-seconds
1119  (decf sec (%leap-seconds-offset leap-seconds sec))))
1120  sec)
1121 
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)
1125  :nsec nsec)))
1126 
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)))
1131 
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)
1137  result))
1138 
1139 (defmacro %defcomparator (name &body body)
1140  (let ((pair-comparator-name (intern (concatenate 'string "%" (string name)))))
1141  `(progn
1142  (declaim (inline ,pair-comparator-name))
1143  (defun ,pair-comparator-name (time-a time-b)
1144  (assert (typep time-a 'timestamp)
1145  nil
1146  'type-error
1147  :datum time-a
1148  :expected-type 'timestamp)
1149  (assert (typep time-b 'timestamp)
1150  nil
1151  'type-error
1152  :datum time-b
1153  :expected-type 'timestamp)
1154  ,@body)
1155  (defun ,name (&rest times)
1156  (declare (dynamic-extent times))
1157  (loop for head on times
1158  while (cdr head)
1159  always (,pair-comparator-name (first head) (second head))))
1160  (define-compiler-macro ,name (&rest times)
1161  (let ((vars (loop
1162  :for i :upfrom 0 :below (length times)
1163  :collect (gensym (concatenate 'string "TIME-" (princ-to-string i) "-")))))
1164  `(let (,@(loop
1165  :for var :in vars
1166  :for time :in times
1167  :collect (list var time)))
1168  ;; we could evaluate comparisons of timestamp literals here
1169  (and ,@(loop
1170  :for (time-a time-b) :on vars
1171  :while time-b
1172  :collect `(,',pair-comparator-name ,time-a ,time-b)))))))))
1173 
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))
1177  (cond
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)) '>)
1184  (t '=)))
1185 
1186 (%defcomparator timestamp<
1187  (eql (%timestamp-compare time-a time-b) '<))
1188 
1189 (%defcomparator timestamp<=
1190  (not (null (member (%timestamp-compare time-a time-b) '(< =)))))
1191 
1192 (%defcomparator timestamp>
1193  (eql (%timestamp-compare time-a time-b) '>))
1194 
1195 (%defcomparator timestamp>=
1196  (not (null (member (%timestamp-compare time-a time-b) '(> =)))))
1197 
1198 (%defcomparator timestamp=
1199  (eql (%timestamp-compare time-a time-b) '=))
1200 
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))))
1208  t)
1209 
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))
1213 
1214 (defun timestamp-minimum (time &rest times)
1215  "Returns the earliest timestamp"
1216  (contest #'timestamp< (cons time times)))
1217 
1218 (defun timestamp-maximum (time &rest times)
1219  "Returns the latest timestamp"
1220  (contest #'timestamp> (cons time times)))
1221 
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))))
1230 
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)
1243  (* 100-years 100)
1244  (* 4-years 4)
1245  years)
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.
1248  #+nil
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))
1252  3))
1253  (remaining-days (- remaining-days
1254  (* 100-years
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)
1258  3)))
1259  (values (+ (* 400-years 400)
1260  (* 100-years 100)
1261  (* 4-years 4)
1262  years)
1263  (- remaining-days (* years 365)))))
1264 
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
1277  (if (= month-b 2)
1278  (min 28 day-b)
1279  day-b)
1280  month-b
1281  (+ year-difference year-b)
1282  :offset offset-b)
1283  time-a)
1284  year-difference
1285  (1- year-difference))))))
1286 
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
1300  29
1301  (1+ (- remaining-days (aref +rotated-month-offsets-without-leap-day+
1302  (1- rotated-1-based-month)))))))
1303  (values
1304  (+ years
1305  (if (>= rotated-1-based-month 11) ; january is in the next year
1306  2001
1307  2000))
1308  1-based-month
1309  1-based-day))))
1310 
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+)))))
1322  (values year
1323  (1+ (floor ordinal-day 7))
1324  day-of-week)))
1325 
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+)
1333  (values
1334  hours
1335  minutes
1336  seconds))))
1337 
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
1344  day month year
1345  (%ts-day-of-week ts-day)))))
1346 
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)
1357  sec minute hour
1358  day month year
1359  day-of-week
1360  daylight-p
1361  (or offset offset*)
1362  abbreviation)))))
1363 
1364 (defun timestamp-year (timestamp &key (timezone *default-timezone*))
1365  "Returns the cardinal year upon which the timestamp falls."
1366  (nth-value 0
1367  (%timestamp-decode-date
1368  (nth-value 1 (%adjust-to-timezone timestamp timezone)))))
1369 
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)))
1374  (+ sign
1375  (* sign
1376  (truncate (1- (abs year)) 100)))))
1377 
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)))
1382  (+ sign
1383  (* sign
1384  (truncate (1- (abs year)) 1000)))))
1385 
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))
1389 
1390 (defun timestamp-month (timestamp &key (timezone *default-timezone*))
1391  "Returns the month upon which the timestamp falls."
1392  (nth-value 1
1393  (%timestamp-decode-date
1394  (nth-value 1 (%adjust-to-timezone timestamp timezone)))))
1395 
1396 (defun timestamp-day (timestamp &key (timezone *default-timezone*))
1397  "Returns the day of the month upon which the timestamp falls."
1398  (nth-value 2
1399  (%timestamp-decode-date
1400  (nth-value 1 (%adjust-to-timezone timestamp timezone)))))
1401 
1402 (defun timestamp-hour (timestamp &key (timezone *default-timezone*))
1403  (nth-value 0
1404  (%timestamp-decode-time
1405  (nth-value 0 (%adjust-to-timezone timestamp timezone)))))
1406 
1407 (defun timestamp-minute (timestamp &key (timezone *default-timezone*))
1408  (nth-value 1
1409  (%timestamp-decode-time
1410  (nth-value 0 (%adjust-to-timezone timestamp timezone)))))
1411 
1412 (defun timestamp-second (timestamp &key (timezone *default-timezone*))
1413  (nth-value 2
1414  (%timestamp-decode-time
1415  (nth-value 0 (%adjust-to-timezone timestamp timezone)))))
1416 
1417 (defun timestamp-microsecond (timestamp)
1418  (floor (nsec-of timestamp) 1000))
1419 
1420 (defun timestamp-millisecond (timestamp)
1421  (floor (nsec-of timestamp) 1000000))
1422 
1423 (defun split-timestring (str &rest args)
1424  (declare (inline))
1425  (apply #'%split-timestring (coerce str 'simple-string) args))
1426 
1427 (defun %split-timestring (time-string &key
1428  (start 0)
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)))
1442  (the list
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))
1456  (if ,entry
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))
1464  (values))
1465  (values)))
1466  (progn
1467  (passert allow-missing-elements)
1468  (values))))))
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)
1473  (type list parts))
1474  ,@body)))
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
1482  while (< index end)
1483  when (member (aref time-string index) chars :test #'char-equal)
1484  collect (prog1 (if (< last-match index)
1485  (cons last-match index)
1486  nil)
1487  (incf match-count)
1488  (setf last-match (1+ index)))
1489  into result
1490  finally (return (values (if (zerop (- index last-match))
1491  result
1492  (prog1
1493  (nconc result (list (cons last-match index)))
1494  (incf match-count)))
1495  match-count))))
1496  (parse ()
1497  (with-parts-and-count (start end date-time-separator)
1498  (cond ((= count 2)
1499  (if (first parts)
1500  (full-date (first parts))
1501  (passert allow-missing-date-part))
1502  (if (second parts)
1503  (full-time (second parts))
1504  (passert allow-missing-time-part))
1505  (done))
1506  ((and (= count 1)
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))
1512  (done))
1513  ((and (= count 1)
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))
1519  (done)))
1520  (parse-error nil)))
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))
1538  (sign (unless zulup
1539  (if (find #\+ time-string :test #'char-equal :start start :end end)
1540  1
1541  -1))))
1542  (passert (<= 1 count 2))
1543  (unless (and (eq (first parts) nil)
1544  (not (rest parts)))
1545  ;; not a single #\Z
1546  (partial-time (first parts)))
1547  (when zulup
1548  (setf offset-hour 0
1549  offset-minute 0))
1550  (if (= count 1)
1551  (passert (or zulup allow-missing-timezone-part))
1552  (let* ((entry (second parts))
1553  (start (car entry))
1554  (end (cdr entry)))
1555  (declare (type fixnum start end))
1556  (passert (or zulup
1557  (not (zerop (- end start)))))
1558  (unless zulup
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)
1575  (if (> count 1)
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
1581  :test-not #'eql
1582  :start start
1583  :end end
1584  :from-end t)))
1585  (when new-end
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)))
1591  (- end start)))))
1592  (setf nsec 0)))))
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))
1596  (= count 1)
1597  (= count 2)))
1598 
1599  (cond
1600  ((= count 2)
1601  ;; hh:mm offset
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)
1605  ;; hhmm offset
1606  (parse-integer-into (cons (caar parts)
1607  (+ (caar parts) 2))
1608  offset-hour 0 23)
1609  (parse-integer-into (cons (+ (caar parts) 2)
1610  (+ (caar parts) 4))
1611  offset-minute 0 59))
1612  ((= (- (cdar parts) (caar parts)) 2)
1613  ;; hh offset
1614  (parse-integer-into (cons (caar parts)
1615  (+ (caar parts) 2))
1616  offset-hour 0 23)
1617  (setf offset-minute 0)))
1618 
1619  (setf offset-hour (* offset-hour sign)
1620  offset-minute (* offset-minute sign))))
1621  (parse-error (failure)
1622  (if fail-on-error
1623  (error 'invalid-timestring :timestring time-string :failure failure)
1624  (return-from %split-timestring nil)))
1625  (done ()
1626  (return-from %split-timestring (list year month day hour minute second nsec offset-hour offset-minute))))
1627  (parse))))))
1628 
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 #\.))
1636 
1637 (defun parse-timestring (timestring &key
1638  start
1639  end
1640  (fail-on-error t)
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)
1649  (offset 0))
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)
1656  :start (or start 0)
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)))
1667  (when parts
1668  (destructuring-bind (year month day hour minute second nsec offset-hour offset-minute)
1669  parts
1670  (encode-timestamp
1671  (or nsec 0)
1672  (or second 0)
1673  (or minute 0)
1674  (or hour 0)
1675  (or day 1)
1676  (or month 3)
1677  (or year 2000)
1678  :offset (if offset-hour
1679  (+ (* offset-hour 3600)
1680  (* (or offset-minute 0) 60))
1681  offset))))))
1682 
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
1688  (if (<= 11 day 13)
1689  "th"
1690  (case (mod day 10)
1691  (1 "st")
1692  (2 "nd")
1693  (3 "rd")
1694  (t "th")))))
1695 
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)
1711  (cond
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))
1717  (princ #\Z result)
1718  (format result "~c~2,'0d~:[:~;~]~2,'0d"
1719  (if (minusp offset) #\- #\+)
1720  (abs offset-hours)
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))
1738  ((eql fmt :ampm)
1739  (princ (if (< hour 12) "am" "pm") result))
1740  ((eql fmt :ordinal-day)
1741  (princ (ordinalize day) result))
1742  ((or (stringp fmt) (characterp fmt))
1743  (princ fmt result))
1744  (t
1745  (let ((val (ecase (if (consp fmt) (car fmt) fmt)
1746  (:nsec nsec)
1747  (:usec (floor nsec 1000))
1748  (:msec (floor nsec 1000000))
1749  (:sec sec)
1750  (:min minute)
1751  (:hour hour)
1752  (:hour12 (1+ (mod (1- hour) 12)))
1753  (:day day)
1754  (:weekday weekday)
1755  (:month month)
1756  (:year year)
1757  (:iso-week-year iso-year)
1758  (:iso-week-number iso-week)
1759  (:iso-week-day iso-weekday))))
1760  (cond
1761  ((atom fmt)
1762  (princ val result))
1763  ((minusp val)
1764  (format result "-~v,vd"
1765  (second fmt)
1766  (or (third fmt) #\0)
1767  (abs val)))
1768  (t
1769  (format result "~v,vd"
1770  (second fmt)
1771  (or (third fmt) #\0)
1772  val))))))))))))
1773 
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.
1781 
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:
1785 
1786  :YEAR *year
1787  :MONTH *numeric month
1788  :DAY *day of month
1789  :HOUR *hour
1790  :MIN *minutes
1791  :SEC *seconds
1792  :WEEKDAY *numeric day of week starting from index 0, which means Sunday
1793  :MSEC *milliseconds
1794  :USEC *microseconds
1795  :NSEC *nanoseconds
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
1811 
1812 Elements marked by * can be placed in a list in the form
1813  \(:keyword padding &optional \(padchar #\\0))
1814 
1815 The string representation of the value will be padded with the padchar.
1816 
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)))
1820  (when destination
1821  (write-string result (if (eq t destination) *standard-output* destination)))
1822  result))
1823 
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))
1829 
1830 (defun to-rfc1123-timestring (timestamp)
1831  (format-rfc1123-timestring nil timestamp))
1832 
1833 (defun format-rfc3339-timestring (destination timestamp &key
1834  omit-date-part
1835  omit-time-part
1836  (omit-timezone-part omit-time-part)
1837  (use-zulu t)
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
1841  (if (and use-zulu
1842  (not omit-date-part)
1843  (not omit-time-part)
1844  (not omit-timezone-part))
1845  +rfc3339-format+ ; micro optimization
1846  (append
1847  (unless omit-date-part
1848  '((:year 4) #\-
1849  (:month 2) #\-
1850  (:day 2)))
1851  (unless (or omit-date-part
1852  omit-time-part)
1853  '(#\T))
1854  (unless omit-time-part
1855  '((:hour 2) #\:
1856  (:min 2) #\:
1857  (:sec 2) #\.
1858  (:usec 6)))
1859  (unless omit-timezone-part
1860  (if use-zulu
1861  '(:gmt-offset-or-z)
1862  '(:gmt-offset)))))))
1863  (format-timestring destination timestamp :format rfc3339-format :timezone timezone)))
1864 
1865 (defun to-rfc3339-timestring (timestamp)
1866  (format-rfc3339-timestring nil timestamp))
1867 
1868 (defun %read-timestring (stream char)
1869  (declare (ignore char))
1870  (parse-timestring
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 #\.))))
1874  do (princ c str)
1875  finally (when c (unread-char c stream))))
1876  :allow-missing-elements t))
1877 
1878 (defun %read-universal-time (stream char arg)
1879  (declare (ignore char arg))
1880  (universal-to-timestamp
1881  (parse-integer
1882  (with-output-to-string (str)
1883  (loop for c = (read-char stream nil)
1884  while (and c (digit-char-p c))
1885  do (princ c str)
1886  finally (when c (unread-char c stream)))))))
1887 
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)
1892  (values))
1893 
1894 (defvar *debug-timestamp* nil)
1895 
1896 (defmethod print-object ((object timestamp) stream)
1897  "Print the TIMESTAMP object using the standard reader notation"
1898  (cond
1899  (*debug-timestamp*
1900  (print-unreadable-object (object stream :type t)
1901  (format stream "~d/~d/~d"
1902  (day-of object)
1903  (sec-of object)
1904  (nsec-of object))))
1905  (t
1906  (when *print-escape*
1907  (write-char #\@ stream))
1908  (format-rfc3339-timestring stream object))))
1909 
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)))))
1916 
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+))
1920 
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+))
1924 
1925 (declaim (notinline format-timestring))
1926 
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
1937  ts-day
1938  (%realize-timezone (or timezone *default-timezone*))))))
1939 
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
1949  ts-day
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
1955  day month year
1956  (mod (1- day-of-week) 7) ;NB In CL: Monday = 0
1957  daylight-p
1958  (/ offset -3600) ;NB In CL: hours west
1959  abbreviation)))))