Coverage report: /home/ellis/comp/core/lib/obj/time/local.lisp
Kind | Covered | All | % |
expression | 954 | 2950 | 32.3 |
branch | 53 | 286 | 18.5 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
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)
17
(defclass timestamp ()
18
((day :accessor day-of :initarg :day :initform 0 :type (unsigned-byte 16))
19
(sec :accessor sec-of :initarg :sec :initform 0 :type (unsigned-byte 16))
20
(nsec :accessor nsec-of :initarg :nsec :initform 0 :type (integer 0 999999999))))
28
(transitions #(0) :type simple-vector)
29
(indexes #(0) :type simple-vector)
30
(subzones #() :type simple-vector)
31
(leap-seconds nil :type list)
33
(name "anonymous" :type string)
34
(loaded nil :type boolean))
36
(eval-when (:compile-toplevel :load-toplevel :execute)
37
(defconstant +timezone-offset-min+ -86400)
38
(defconstant +timezone-offset-max+ 86400))
40
(deftype timezone-offset ()
41
'(integer #.+timezone-offset-min+ #.+timezone-offset-max+))
43
(defun %valid-time-of-day? (timestamp)
44
(zerop (day-of timestamp)))
46
(deftype time-of-day ()
48
(satisfies %valid-time-of-day?)))
50
(defun %valid-date? (timestamp)
51
(and (zerop (sec-of timestamp))
52
(zerop (nsec-of timestamp))))
56
(satisfies %valid-date?)))
58
(defun zone-name (zone)
61
(define-condition invalid-timezone-file (error)
62
((path :accessor path-of :initarg :path))
63
(:report (lambda (condition stream)
64
(format stream "The file at ~a is not a timezone file."
65
(path-of condition)))))
67
(define-condition invalid-time-specification (error)
69
(:report "The time specification is invalid"))
71
(define-condition invalid-timestring (error)
72
((timestring :accessor timestring-of :initarg :timestring)
73
(failure :accessor failure-of :initarg :failure))
74
(:report (lambda (condition stream)
75
(format stream "Failed to parse ~S as an rfc3339 time: ~S"
76
(timestring-of condition)
77
(failure-of condition)))))
79
(defmethod make-load-form ((self timestamp) &optional environment)
80
(make-load-form-saving-slots self :environment environment))
84
(declaim (inline now format-timestring %get-current-time
85
format-rfc3339-timestring to-rfc3339-timestring
86
format-rfc1123-timestring to-rfc1123-timestring)
87
(ftype (function (&rest t) string) format-rfc3339-timestring)
88
(ftype (function (&rest t) string) format-timestring)
89
(ftype (function (&rest t) fixnum) local-timezone)
90
(ftype (function (&rest t) (values
93
string)) timestamp-subzone)
94
(ftype (function (timestamp &key (:timezone timezone) (:offset (or null integer)))
95
(values (integer 0 999999999)
101
(integer -1000000 1000000)
110
(defvar *default-timezone*)
112
(defparameter *default-timezone-repository-path*
113
(flet ((try (project-home-directory)
114
(when project-home-directory
117
(merge-pathnames "zoneinfo/"
118
(make-pathname :directory (pathname-directory project-home-directory))))))))
119
(or (when (find-package "ASDF")
120
(let ((path (eval (read-from-string
121
"(let ((system (asdf:find-system :obj/time nil)))
123
(asdf:component-pathname system)))"))))
125
(let ((path #.(or *compile-file-truename*
128
(try (merge-pathnames "../" path)))))))
130
;;; Month information
131
(defparameter +month-names+
132
#("" "January" "February" "March" "April" "May" "June" "July" "August"
133
"September" "October" "November" "December"))
134
(defparameter +short-month-names+
135
#("" "Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov"
137
(defparameter +day-names+
138
#("Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday"))
139
(defparameter +day-names-as-keywords+
140
#(:sunday :monday :tuesday :wednesday :thursday :friday :saturday))
141
(defparameter +short-day-names+
142
#("Sun" "Mon" "Tue" "Wed" "Thu" "Fri" "Sat"))
143
(defparameter +minimal-day-names+
144
#("Su" "Mo" "Tu" "We" "Th" "Fr" "Sa"))
146
(eval-when (:compile-toplevel :load-toplevel :execute)
147
(defconstant +months-per-year+ 12)
148
(defconstant +days-per-week+ 7)
149
(defconstant +hours-per-day+ 24)
150
(defconstant +minutes-per-day+ 1440)
151
(defconstant +minutes-per-hour+ 60)
152
(defconstant +seconds-per-day+ 86400)
153
(defconstant +seconds-per-hour+ 3600)
154
(defconstant +seconds-per-minute+ 60)
155
(defconstant +usecs-per-day+ 86400000000))
157
(defparameter +iso-8601-date-format+
158
'((:year 4) #\- (:month 2) #\- (:day 2)))
160
(defparameter +iso-8601-time-format+
161
'((:hour 2) #\: (:min 2) #\: (:sec 2) #\. (:usec 6)))
163
(defparameter +iso-8601-format+
164
;; 2008-11-18T02:32:00.586931+01:00
165
(append +iso-8601-date-format+ (list #\T) +iso-8601-time-format+ (list :gmt-offset-or-z)))
167
(defparameter +rfc3339-format+ +iso-8601-format+)
169
(defparameter +rfc3339-format/date-only+
170
'((:year 4) #\- (:month 2) #\- (:day 2)))
172
(defparameter +asctime-format+
173
'(:short-weekday #\space :short-month #\space (:day 2 #\space) #\space
174
(:hour 2) #\: (:min 2) #\: (:sec 2) #\space
177
(defparameter +rfc-1123-format+
178
;; Sun, 06 Nov 1994 08:49:37 GMT
179
'(:short-weekday ", " (:day 2) #\space :short-month #\space (:year 4) #\space
180
(:hour 2) #\: (:min 2) #\: (:sec 2) #\space :gmt-offset-hhmm)
181
"See the RFC 1123 for the details about the possible values of the timezone field.")
183
(defparameter +iso-week-date-format+
185
'((:iso-week-year 4) #\- #\W (:iso-week-number 2) #\- (:iso-week-day 1)))
187
(eval-when (:compile-toplevel :load-toplevel :execute)
188
(defparameter +rotated-month-days-without-leap-day+
189
#.(coerce #(31 30 31 30 31 31 30 31 30 31 31 28)
190
'(simple-array fixnum (*))))
192
(defparameter +rotated-month-offsets-without-leap-day+
196
for days :across +rotated-month-days-without-leap-day+
197
collect (incf sum days)))
198
'(simple-array fixnum (*)))))
200
;; The astronomical julian date offset is the number of days between
201
;; the current date and -4713-01-01T00:00:00+00:00
202
(defparameter +astronomical-julian-date-offset+ -2451605)
204
;; The modified julian date is the number of days between the current
205
;; date and 1858-11-17T12:00:00+00:00. TODO: For the sake of simplicity,
206
;; we currently just do the date arithmetic and don't adjust for the
208
(defparameter +modified-julian-date-offset+ -51604)
210
(defun transition-position (needle haystack)
211
(declare (type integer needle)
212
(type (simple-array integer (*)) haystack)
213
(optimize (speed 3)))
215
with start fixnum = 0
216
with end fixnum = (length haystack)
217
;; Invariant: haystack[start-1] <= needle < haystack[end]
218
for middle fixnum = (floor (+ end start) 2)
220
do (if (< needle (elt haystack middle))
222
(setf start (1+ middle)))
224
(return (1- start))))
226
(defvar *strict-first-subzone-validity*
228
"When true, raise an error if trying to get an offset before the first
231
(defun %subzone-as-of (timezone seconds days &optional guess-p)
232
"TIMEZONE is a realized timezone; SECONDS and DAYS are 'timestamp-values'
233
describing a local time, or null to ask for the subzone after the last
234
transition. Return the applicable subzone and the transition-index for that
236
When GUESS-P is true, the request is about SECONDS and DAYS in a timezone
237
which may not be UTC, and therefore the unix-time derived from SECONDS and
238
DAYS has an offset with respect to UTC: the offset of the subzone to be
240
(let* ((indexes (timezone-indexes timezone))
241
(index-length (length indexes))
242
(subzones (timezone-subzones timezone)))
243
(cond ((zerop index-length)
244
(values (elt subzones 0) nil))
246
(let ((transition-idx (1- index-length)))
247
(values (elt subzones (elt indexes transition-idx))
250
(let* ((transitions (timezone-transitions timezone))
251
(unix-time (timestamp-values-to-unix seconds days))
253
(transition-position (if guess-p
257
(subzone (elt subzones (elt indexes (max 0 transition-idx)))))
258
;; Decide what to do when unix-time is before the first transition
259
(cond ((<= 0 transition-idx))
260
((and *strict-first-subzone-validity*
262
(- unix-time (subzone-offset subzone))
264
(elt transitions 0)))
265
(error "Dates before ~A are not defined in ~A"
266
(multiple-value-list (decode-universal-time
267
(timestamp-to-universal
268
(unix-to-timestamp unix-time))
271
(t (setf transition-idx 0)))
273
(< transition-idx (1- index-length))) ;there is a next
274
(let* ((next-idx (1+ transition-idx))
275
(delta (- (elt transitions next-idx) unix-time)))
276
(when (<= delta 86400) ;check next offset
277
(let ((next-subzone (elt subzones (elt indexes next-idx))))
278
(when (<= (+ delta (subzone-offset next-subzone)) 0)
279
;; The next transition is valid
280
(setf transition-idx next-idx
281
subzone next-subzone))))))
285
(defun %read-binary-integer (stream byte-count &optional (signed nil))
286
"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."
289
:for offset :from (* (1- byte-count) 8) :downto 0 :by 8
290
:do (setf (ldb (byte 8 offset) result) (read-byte stream))
292
(let ((high-bit (* byte-count 8)))
293
(if (logbitp (1- high-bit) result)
294
(return (- result (ash 1 high-bit)))
298
(defun %string-from-unsigned-byte-vector (vector offset)
299
"Returns a string created from the vector of unsigned bytes VECTOR starting at OFFSET which is terminated by a 0."
300
(declare (type (vector (unsigned-byte 8)) vector))
301
(let* ((null-pos (or (position 0 vector :start offset) (length vector)))
302
(result (make-string (- null-pos offset))))
303
(loop for input-index :from offset :upto (1- null-pos)
304
for output-index :upfrom 0
305
do (setf (aref result output-index) (code-char (aref vector input-index))))
308
(defun %find-first-std-offset (timezone-indexes timestamp-info)
309
(let ((subzone-idx (find-if 'subzone-daylight-p
311
:key (lambda (x) (aref timestamp-info x)))))
312
(subzone-offset (aref timestamp-info (or subzone-idx 0)))))
314
(defun %tz-verify-magic-number (inf zone)
315
;; read and verify magic number
316
(let ((magic-buf (make-array 4 :element-type 'unsigned-byte)))
317
(read-sequence magic-buf inf :start 0 :end 4)
318
(when (string/= (map 'string #'code-char magic-buf) "TZif" :end1 4)
319
(error 'invalid-timezone-file :path (timezone-path zone))))
320
;; skip 16 bytes for "future use"
321
(let ((ignore-buf (make-array 16 :element-type 'unsigned-byte)))
322
(read-sequence ignore-buf inf :start 0 :end 16)))
324
(defun %tz-read-header (inf)
325
`(:utc-count ,(%read-binary-integer inf 4)
326
:wall-count ,(%read-binary-integer inf 4)
327
:leap-count ,(%read-binary-integer inf 4)
328
:transition-count ,(%read-binary-integer inf 4)
329
:type-count ,(%read-binary-integer inf 4)
330
:abbrev-length ,(%read-binary-integer inf 4)))
332
(defun %tz-read-transitions (inf count)
335
(loop for idx from 1 upto count
336
collect (%read-binary-integer inf 4 t))))
338
(defun %tz-read-indexes (inf count)
341
(loop for idx from 1 upto count
342
collect (%read-binary-integer inf 1))))
344
(defun %tz-read-subzone (inf count)
345
(loop for idx from 1 upto count
346
collect (list (%read-binary-integer inf 4 t)
347
(%read-binary-integer inf 1)
348
(%read-binary-integer inf 1))))
350
(defun leap-seconds-sec (leap-seconds)
352
(defun leap-seconds-adjustment (leap-seconds)
355
(defun %tz-read-leap-seconds (inf count)
357
(loop for idx from 1 upto count
358
collect (%read-binary-integer inf 4) into sec
359
collect (%read-binary-integer inf 4) into adjustment
360
finally (return (cons (make-array count :initial-contents sec)
361
(make-array count :initial-contents adjustment))))))
363
(defun %tz-read-abbrevs (inf length)
364
(let ((a (make-array length :element-type '(unsigned-byte 8))))
370
(defun %tz-read-indicators (inf length)
371
;; read standard/wall indicators
372
(let ((buf (make-array length :element-type '(unsigned-byte 8))))
373
(read-sequence buf inf
378
:initial-contents buf)))
380
(defun %tz-make-subzones (raw-info abbrevs gmt-indicators std-indicators)
381
(declare (ignore gmt-indicators std-indicators))
382
;; TODO: handle TZ environment variables, which use the gmt and std
384
(make-array (length raw-info)
385
:element-type 'subzone
387
(loop for info in raw-info collect
390
:daylight-p (/= (second info) 0)
391
:abbrev (%string-from-unsigned-byte-vector abbrevs (third info))))))
393
(defun %realize-timezone (zone &optional reload)
394
"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."
395
(when (or reload (not (timezone-loaded zone)))
396
(with-open-file (inf (timezone-path zone)
398
:element-type 'unsigned-byte)
399
(%tz-verify-magic-number inf zone)
401
;; read header values
402
(let* ((header (%tz-read-header inf))
403
(timezone-transitions (%tz-read-transitions inf (getf header :transition-count)))
404
(subzone-indexes (%tz-read-indexes inf (getf header :transition-count)))
405
(subzone-raw-info (%tz-read-subzone inf (getf header :type-count)))
406
(abbreviation-buf (%tz-read-abbrevs inf (getf header :abbrev-length)))
407
(leap-second-info (%tz-read-leap-seconds inf (getf header :leap-count)))
408
(std-indicators (%tz-read-indicators inf (getf header :wall-count)))
409
(gmt-indicators (%tz-read-indicators inf (getf header :utc-count)))
410
(subzone-info (%tz-make-subzones subzone-raw-info
415
(setf (timezone-transitions zone) timezone-transitions)
416
(setf (timezone-indexes zone) subzone-indexes)
417
(setf (timezone-subzones zone) subzone-info)
418
(setf (timezone-leap-seconds zone) leap-second-info))
419
(setf (timezone-loaded zone) t)))
422
(eval-when (:compile-toplevel :load-toplevel :execute)
423
(defun %make-simple-timezone (name abbrev offset)
424
(let ((subzone (obj/time::make-subzone :offset offset
427
(obj/time::make-timezone
428
:subzones (make-array 1 :initial-contents (list subzone))
433
;; to be used as #+#.(obj/time::package-with-symbol? "SB-EXT" "GET-TIME-OF-DAY")
434
(defun package-with-symbol? (package name)
435
(if (and (find-package package)
436
(find-symbol name package))
440
(defparameter +utc-zone+ (%make-simple-timezone "Coordinated Universal Time" "UTC" 0))
442
(defparameter +gmt-zone+ (%make-simple-timezone "Greenwich Mean Time" "GMT" 0))
444
(defparameter +none-zone+ (%make-simple-timezone "Explicit Offset Given" "NONE" 0))
446
(defparameter *location-name->timezone*
447
(make-hash-table :test 'equal
448
#+sbcl :synchronized #+sbcl t)
449
"A hashtable with entries like \"Europe/Budapest\" -> timezone-instance")
451
(defparameter *abbreviated-subzone-name->timezone-list*
452
(make-hash-table :test 'equal
453
#+sbcl :synchronized #+sbcl t)
454
"A hashtable of \"CEST\" -> list of timezones with \"CEST\" subzone")
456
(defmacro define-timezone (zone-name zone-file &key (load nil))
457
"Define zone-name (a symbol or a string) as a new timezone,
458
lazy-loaded from zone-file (a pathname designator relative to the
459
zoneinfo directory on this system. If load is true, load immediately."
460
(declare (type (or string symbol) zone-name))
461
(let ((zone-sym (if (symbolp zone-name)
463
(intern zone-name))))
465
(defparameter ,zone-sym
466
(make-timezone :path ,zone-file
467
:name ,(if (symbolp zone-name)
468
(string-downcase (symbol-name zone-name))
471
`((let ((timezone (%realize-timezone ,zone-sym)))
472
(setf (gethash (timezone-name timezone)
473
*location-name->timezone*)
475
(loop for subzone across (timezone-subzones timezone)
478
(gethash (subzone-abbrev subzone)
479
*abbreviated-subzone-name->timezone-list*))))))
482
(eval-when (:load-toplevel :execute)
483
(let ((default-timezone-file #p"/etc/localtime"))
485
(define-timezone *default-timezone* default-timezone-file :load t)
487
(setf *default-timezone* +utc-zone+)))))
489
(defun find-timezone-by-location-name (name)
490
(when (zerop (hash-table-count *location-name->timezone*))
491
(error "Seems like the timezone repository has not yet been loaded. Hint: see REREAD-TIMEZONE-REPOSITORY."))
492
(gethash name *location-name->timezone*))
495
(defun timezones-matching-subzone (abbreviated-name timestamp)
496
"Returns list of lists of active timezone, matched subzone and last transition time
497
for timezones that have subzone matching specified ABBREVIATED-NAME as of TIMESTAMP moment if provided. "
498
(loop for zone in (gethash abbreviated-name *abbreviated-subzone-name->timezone-list*)
499
;; get the subzone and the latest transition index
500
for (subzone transition-idx) = (multiple-value-list (%subzone-as-of zone (sec-of timestamp) (day-of timestamp)))
501
if (equal abbreviated-name (subzone-abbrev subzone))
502
collect (list zone subzone (when transition-idx (elt (timezone-transitions zone) transition-idx)))))
504
(defun all-timezones-matching-subzone (abbreviated-name)
505
"Returns list of lists of timezone, matched subzone and last transition time
506
for timezones that have subzone matching specified ABBREVIATED-NAME. Includes both active and historical timezones."
507
(loop for zone in (gethash abbreviated-name *abbreviated-subzone-name->timezone-list*)
508
for (subzone transition-idx) = (multiple-value-list (%subzone-as-of zone nil nil))
509
if (equal abbreviated-name (subzone-abbrev subzone))
510
collect (list zone subzone (when transition-idx (elt (timezone-transitions zone) transition-idx)))
513
nconc (loop for subzone-idx from 0 below (length (timezone-subzones zone))
514
for sz = (elt (timezone-subzones zone) subzone-idx)
515
for tix = (position subzone-idx (timezone-indexes zone) :from-end t)
516
when (and tix (equal abbreviated-name (subzone-abbrev sz)))
517
collect (list zone sz (elt (timezone-transitions zone) tix)))))
519
(defun timezone= (timezone-1 timezone-2)
520
"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.
523
\(values t t) means timezone-1 and timezone-2 are definitely equal.
524
\(values nil t) means timezone-1 and timezone-2 are definitely different.
525
\(values nil nil) means that it couldn't be determined."
526
(if (or (eq timezone-1 timezone-2)
527
(equalp timezone-1 timezone-2))
531
(defun reread-timezone-repository (&key (timezone-repository *default-timezone-repository-path*))
532
(check-type timezone-repository (or pathname string))
533
(let ((root-directory (uiop:directory-exists-p timezone-repository)))
534
(unless root-directory
535
(error "REREAD-TIMEZONE-REPOSITORY was called with invalid PROJECT-DIRECTORY (~A)."
536
timezone-repository))
537
(let ((cutoff-position (length (princ-to-string root-directory))))
538
(flet ((visitor (file)
540
(let* ((full-name (subseq (princ-to-string file) cutoff-position))
541
(timezone (%realize-timezone (make-timezone :path file :name full-name))))
542
(setf (gethash full-name *location-name->timezone*) timezone)
543
(map nil (lambda (subzone)
544
(push timezone (gethash (subzone-abbrev subzone)
545
*abbreviated-subzone-name->timezone-list*)))
546
(timezone-subzones timezone)))
547
(invalid-timezone-file () nil))))
548
(setf *location-name->timezone*
549
(make-hash-table :test 'equal
550
#+sbcl :synchronized #+sbcl t))
551
(setf *abbreviated-subzone-name->timezone-list*
552
(make-hash-table :test 'equal
553
#+sbcl :synchronized #+sbcl t))
554
(uiop:collect-sub*directories root-directory
558
(dolist (file (uiop:directory-files dir))
559
(when (not (find "Etc" (pathname-directory file)
562
(uiop:collect-sub*directories (merge-pathnames "Etc/" root-directory)
566
(dolist (file (uiop:directory-files dir))
567
(visitor file))))))))
569
(defmacro make-timestamp (&rest args)
570
`(make-instance 'timestamp ,@args))
572
(defun clone-timestamp (timestamp)
573
(make-instance 'timestamp
574
:nsec (nsec-of timestamp)
575
:sec (sec-of timestamp)
576
:day (day-of timestamp)))
578
(defun sec-day-subtimezone (sec day timezone)
579
(declare (type integer sec day)
580
(type timezone timezone))
581
(let ((subzone (%subzone-as-of timezone sec day)))
582
(values (subzone-offset subzone)
583
(subzone-daylight-p subzone)
584
(subzone-abbrev subzone))))
586
(defun timestamp-subtimezone (timestamp timezone)
587
"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."
588
(declare (type timestamp timestamp)
589
(type (or null timezone) timezone))
590
(sec-day-subtimezone (sec-of timestamp)
592
(%realize-timezone (or timezone *default-timezone*))))
594
(defun %adjust-to-offset (sec day offset)
595
"Returns two values, the values of new DAY and SEC slots of the timestamp adjusted to the given timezone."
596
(declare (type integer sec day offset))
597
(multiple-value-bind (offset-day new-sec)
598
(floor (+ sec offset) +seconds-per-day+)
599
(values new-sec (+ day offset-day))))
601
(defun %adjust-to-timezone (source timezone &optional offset)
602
(%adjust-to-offset (sec-of source)
605
(timestamp-subtimezone source timezone))))
607
(defun timestamp-minimize-part (timestamp part &key
608
(timezone *default-timezone*)
610
(let* ((timestamp-parts '(:nsec :sec :min :hour :day :month))
611
(part-count (position part timestamp-parts)))
612
(assert part-count nil
613
"timestamp-minimize-part called with invalid part ~a (expected one of ~a)"
616
(multiple-value-bind (nsec sec min hour day month year)
617
(decode-timestamp timestamp :timezone timezone)
618
(declare (ignore nsec))
620
(if (> part-count 0) 0 sec)
621
(if (> part-count 1) 0 min)
622
(if (> part-count 2) 0 hour)
623
(if (> part-count 3) 1 day)
624
(if (> part-count 4) 1 month)
629
(defun timestamp-maximize-part (timestamp part &key
630
(timezone *default-timezone*)
632
(let* ((timestamp-parts '(:nsec :sec :min :hour :day :month))
633
(part-count (position part timestamp-parts)))
634
(assert part-count nil
635
"timestamp-maximize-part called with invalid part ~a (expected one of ~a)"
638
(multiple-value-bind (nsec sec min hour day month year)
639
(decode-timestamp timestamp :timezone timezone)
640
(declare (ignore nsec))
641
(let ((month (if (> part-count 4) 12 month)))
642
(encode-timestamp 999999999
643
(if (> part-count 0) 59 sec)
644
(if (> part-count 1) 59 min)
645
(if (> part-count 2) 23 hour)
646
(if (> part-count 3) (days-in-month month year) day)
652
(defmacro with-decoded-timestamp ((&key nsec sec minute hour day month year day-of-week daylight-p timezone offset)
653
timestamp &body forms)
654
"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."
658
(macrolet ((initialize (&rest vars)
666
(push ,var variables)))
667
(setf ignores (nreverse ignores))
668
(setf variables (nreverse variables))))
669
(declare-fixnum-type (&rest vars)
674
(push `(type fixnum ,,var) types)))
675
(setf types (nreverse types)))))
677
(push `(type (integer 0 999999999) ,nsec) types))
678
(declare-fixnum-type sec minute hour day month year)
679
(initialize nsec sec minute hour day month year day-of-week daylight-p))
680
`(multiple-value-bind (,@variables)
681
(decode-timestamp ,timestamp :timezone ,(or timezone '*default-timezone*) :offset ,offset)
682
(declare (ignore ,@ignores) ,@types)
685
(defun %normalize-month-year-pair (month year)
686
"Normalizes the month/year pair: in case month is < 1 or > 12 the month and year are corrected to handle the overflow."
687
(multiple-value-bind (year-offset month-minus-one)
688
(floor (1- month) 12)
689
(values (1+ month-minus-one)
690
(+ year year-offset))))
692
(defun days-in-month (month year)
693
"Returns the number of days in the given month of the specified year."
694
(let ((normal-days (aref +rotated-month-days-without-leap-day+
695
(mod (+ month 9) 12))))
697
(or (and (zerop (mod year 4))
698
(plusp (mod year 100)))
699
(zerop (mod year 400))))
700
(1+ normal-days) ; February on a leap year
703
;; 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
704
(defun %fix-overflow-in-days (day month year)
705
"In case the day number is higher than the maximal possible for the given month/year pair, returns the last day of the month."
706
(let ((max-day (days-in-month month year)))
711
(eval-when (:compile-toplevel :load-toplevel)
712
(defun %list-length= (num list)
713
"Tests for a list of length NUM without traversing the entire list to get the length."
714
(let ((c (nthcdr (1- num) list)))
715
(and c (endp (cdr c)))))
717
(defun %expand-adjust-timestamp-changes (timestamp changes visitor)
721
:for change in changes
725
(%list-length= 3 change)
726
(and (%list-length= 2 change)
727
(symbolp (first change))
728
(or (string= (first change) :timezone)
729
(string= (first change) :utc-offset)))
730
(and (%list-length= 4 change)
731
(symbolp (third change))
732
(or (string= (third change) :to)
733
(string= (third change) :by))))
734
nil "Syntax error in expression ~S" change)
735
(let ((operation (first change))
736
(part (second change))
737
(value (if (%list-length= 3 change)
741
((string= operation :set)
742
(push `(%set-timestamp-part ,part ,value) functions))
743
((string= operation :offset)
744
(push `(%offset-timestamp-part ,part ,value) functions))
745
((string= operation :utc-offset)
747
(push :utc-offset params))
748
((string= operation :timezone)
750
(push :timezone params))
751
(t (error "Unexpected operation ~S" operation)))))
754
:for (function part value) in functions
756
(funcall visitor `(,function ,timestamp ,part ,value ,@params)))))
758
(defun %expand-adjust-timestamp (timestamp changes &key functional)
759
(let* ((old (gensym "OLD"))
764
(%expand-adjust-timestamp-changes old changes
768
(multiple-value-bind (nsec sec day)
770
(setf (nsec-of ,new) nsec)
771
(setf (sec-of ,new) sec)
772
(setf (day-of ,new) day))
774
`((setf ,old ,new))))
776
(setf forms (nreverse forms))
777
`(let* ((,old ,timestamp)
779
`((,new (clone-timestamp ,old)))))
784
(defmacro adjust-timestamp (timestamp &body changes)
785
(%expand-adjust-timestamp timestamp changes :functional t))
787
(defmacro adjust-timestamp! (timestamp &body changes)
788
(%expand-adjust-timestamp timestamp changes :functional nil))
790
(defun %set-timestamp-part (time part new-value &key (timezone *default-timezone*) utc-offset)
791
;; TODO think about error signalling. when, how to disable if it makes sense, ...
793
((:nsec :sec-of-day :day)
794
(let ((nsec (nsec-of time))
798
(:nsec (setf nsec (coerce new-value '(integer 0 999999999))))
799
(:sec-of-day (setf sec (coerce new-value `(integer 0 ,+seconds-per-day+))))
800
(:day (setf day new-value)))
801
(values nsec sec day)))
803
(with-decoded-timestamp (:nsec nsec :sec sec :minute minute :hour hour
804
:day day :month month :year year :timezone timezone :offset utc-offset)
807
(:sec (setf sec new-value))
808
(:minute (setf minute new-value))
809
(:hour (setf hour new-value))
810
(:day-of-month (setf day new-value))
811
(:month (setf month new-value)
812
(setf day (%fix-overflow-in-days day month year)))
813
(:year (setf year new-value)
814
(setf day (%fix-overflow-in-days day month year))))
815
(encode-timestamp-into-values nsec sec minute hour day month year :timezone timezone :offset utc-offset)))))
817
(defun %offset-timestamp-part (time part offset &key (timezone *default-timezone*) utc-offset)
818
"Returns a time adjusted by the specified OFFSET. Takes care of
819
different kinds of overflows. The setting :day-of-week is possible
820
using a keyword symbol name of a week-day (see
821
+DAY-NAMES-AS-KEYWORDS+) as value. In that case point the result to
822
day given by OFFSET in the week that contains TIME."
823
(labels ((direct-adjust (part offset nsec sec day)
824
(cond ((eq part :day-of-week)
825
(with-decoded-timestamp (:day-of-week day-of-week
826
:nsec nsec :sec sec :minute minute :hour hour
827
:day day :month month :year year
828
:timezone timezone :offset utc-offset)
830
(let ((position (position offset +day-names-as-keywords+ :test #'eq)))
831
(assert position (position) "~S is not a valid day name" offset)
832
(let ((offset (+ (- (if (zerop day-of-week)
843
(setf day (+ (days-in-month month year) day)))
844
((let ((days-in-month (days-in-month month year)))
845
(when (< days-in-month day)
850
(decf day days-in-month)))))
851
(encode-timestamp-into-values nsec sec minute hour day month year
852
:timezone timezone :offset utc-offset)))))
854
;; The offset is zero, so just return the parts of the timestamp object
855
(values nsec sec day))
857
(let ((old-utc-offset (or utc-offset
858
(timestamp-subtimezone time timezone)))
864
(multiple-value-bind (sec-offset new-nsec)
865
(floor (+ offset nsec) 1000000000)
866
;; the time might need to be adjusted a bit more if q != 0
871
((:sec :minute :hour)
872
(multiple-value-bind (days-offset new-sec)
873
(floor (+ sec (* offset (ecase part
875
(:minute +seconds-per-minute+)
876
(:hour +seconds-per-hour+))))
878
(return-from direct-adjust (values nsec new-sec (+ day days-offset)))))
881
(setf new-utc-offset (or utc-offset
882
(timestamp-subtimezone (make-timestamp :nsec nsec :sec sec :day day)
884
(when (not (= old-utc-offset
886
;; We hit the DST boundary. We need to restart again
887
;; with :sec, but this time we know both old and new
888
;; UTC offset will be the same, so it's safe to do
890
offset (- old-utc-offset
892
old-utc-offset new-utc-offset)
894
(return-from direct-adjust (values nsec sec day)))))))))
896
(safe-adjust (part offset time)
897
(with-decoded-timestamp (:nsec nsec :sec sec :minute minute :hour hour :day day
898
:month month :year year :timezone timezone :offset utc-offset)
900
(multiple-value-bind (month-new year-new)
901
(%normalize-month-year-pair
904
(:year (* 12 offset)))
907
;; Almost there. However, it is necessary to check for
909
(encode-timestamp-into-values nsec sec minute hour
910
(%fix-overflow-in-days day month-new year-new)
912
:timezone timezone :offset utc-offset)))))
914
((:nsec :sec :minute :hour :day :day-of-week)
915
(direct-adjust part offset
919
((:month :year) (safe-adjust part offset time)))))
921
(defun timestamp-difference (time-a time-b)
922
"Returns the difference between TIME-A and TIME-B in seconds"
923
(let ((nsec (- (nsec-of time-a) (nsec-of time-b)))
924
(second (- (sec-of time-a) (sec-of time-b)))
925
(day (- (day-of time-a) (day-of time-b))))
928
(incf nsec 1000000000))
929
(when (minusp second)
931
(incf second +seconds-per-day+))
932
(let ((result (+ (* day +seconds-per-day+)
935
;; this incf turns the result into a float, so only do this when necessary
936
(incf result (/ nsec 1000000000d0)))
939
(defun timestamp+ (time amount unit &optional (timezone *default-timezone*) offset)
940
(multiple-value-bind (nsec sec day)
941
(%offset-timestamp-part time unit amount :timezone timezone :utc-offset offset)
942
(make-timestamp :nsec nsec
946
(defun timestamp- (time amount unit &optional (timezone *default-timezone*) offset)
947
(timestamp+ time (- amount) unit timezone offset))
949
(defun %ts-day-of-week (ts-day)
950
(mod (+ 3 ts-day) 7))
952
(defun timestamp-day-of-week (timestamp &key (timezone *default-timezone*) offset)
953
(%ts-day-of-week (nth-value 1 (%adjust-to-timezone timestamp timezone offset))))
956
;; http://java.sun.com/j2se/1.4.2/docs/api/java/util/GregorianCalendar.html
957
;; (or something else, sorry :) this scheme only works back until
958
;; 1582, the start of the gregorian calendar. see also
959
;; DECODE-TIMESTAMP when fixing if fixing is desired at all.
960
(defun valid-timestamp-p (nsec sec minute hour day month year)
961
"Returns T if the time values refer to a valid time, otherwise returns NIL."
962
(and (<= 0 nsec 999999999)
967
(<= 1 day (days-in-month month year))
970
(defun encode-sec-day (sec minute hour day month year)
971
(declare (type integer sec minute hour day month year))
972
(values (+ (* hour +seconds-per-hour+)
973
(* minute +seconds-per-minute+)
975
(multiple-value-bind (ts-month ts-year)
977
(values (+ month 9) (- year 2001))
978
(values (- month 3) (- year 2000)))
979
(+ (years-to-days ts-year)
980
(aref +rotated-month-offsets-without-leap-day+ ts-month)
983
(defun encode-offset (ts-sec ts-day timezone)
985
(%subzone-as-of (%realize-timezone (or timezone *default-timezone*))
990
(defun encode-timestamp-into-values (nsec sec minute hour day month year
991
&key timezone offset)
992
"Returns (VALUES NSEC SEC DAY ZONE) ready to be used for
993
instantiating a new timestamp object. If the specified time is
994
invalid, the condition INVALID-TIME-SPECIFICATION is raised."
995
;; If the user provided an explicit offset, we use that. Otherwise,
996
(declare (type integer nsec sec minute hour day month year)
997
(type (or integer null) offset))
998
(unless (valid-timestamp-p nsec sec minute hour day month year)
999
(error 'invalid-time-specification))
1000
(multiple-value-bind (enc-sec enc-day)
1001
(encode-sec-day sec minute hour day month year)
1002
(multiple-value-bind (ts-sec ts-day)
1003
(%adjust-to-offset enc-sec
1006
(encode-offset enc-sec enc-day timezone))))
1007
(values nsec ts-sec ts-day))))
1009
(defun encode-timestamp (nsec sec minute hour day month year
1010
&key (timezone *default-timezone*) offset into)
1011
"Return a new TIMESTAMP instance corresponding to the specified time
1013
(declare (type integer nsec sec minute hour day month year))
1014
(multiple-value-bind (nsec sec day)
1015
(encode-timestamp-into-values nsec sec minute hour day month year
1016
:timezone timezone :offset offset)
1019
(setf (nsec-of into) nsec)
1020
(setf (sec-of into) sec)
1021
(setf (day-of into) day)
1028
(defun universal-sec-day (universal)
1029
(let ((adjusted-universal (- universal #.(encode-universal-time 0 0 0 1 3 2000 0))))
1030
(multiple-value-bind (ts-day ts-sec)
1031
(floor adjusted-universal +seconds-per-day+)
1032
(values ts-sec ts-day))))
1034
(defun universal-to-timestamp (universal &key (nsec 0))
1035
"Returns a timestamp corresponding to the given universal time."
1036
;; universal time is seconds from 1900-01-01T00:00:00Z.
1037
(multiple-value-bind (ts-sec ts-day)
1038
(universal-sec-day universal)
1039
(make-timestamp :day ts-day :sec ts-sec :nsec nsec)))
1041
(defun ts-sec-day-to-universal (ts-sec ts-day)
1042
"Return the UNIVERSAL-TIME corresponding to the TIMESTAMP"
1043
;; universal time is seconds from 1900-01-01T00:00:00Z
1044
(+ (* ts-day +seconds-per-day+)
1046
#.(encode-universal-time 0 0 0 1 3 2000 0)))
1048
(defun timestamp-to-universal (timestamp)
1049
"Return the UNIVERSAL-TIME corresponding to the TIMESTAMP"
1050
;; universal time is seconds from 1900-01-01T00:00:00Z
1051
(ts-sec-day-to-universal (sec-of timestamp) (day-of timestamp)))
1053
(defun unix-to-timestamp (unix &key (nsec 0))
1054
"Return a TIMESTAMP corresponding to UNIX, which is the number of seconds since
1055
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 timestamp-to-octets (timestamp)
1070
"Return an octet-vector consisting of 2-byte day, 2-byte sec, and 4-byte nsec."
1071
(concatenate 'octet-vector
1072
(integer-to-octets (day-of timestamp) 16)
1073
(integer-to-octets (sec-of timestamp) 16)
1074
(integer-to-octets (nsec-of timestamp) 32)))
1076
(defun octets-to-timestamp (buf)
1077
"Return a timestamp from an 8-byte octet-vector."
1079
:day (octets-to-integer (subseq buf 0 2))
1080
:sec (octets-to-integer (subseq buf 2 4))
1081
:nsec (octets-to-integer (subseq buf 4 8))))
1083
(defun %get-current-time ()
1084
"Cross-implementation abstraction to get the current time measured from the unix epoch (1/1/1970). Should return (values sec nano-sec)."
1086
#+#.(obj/time::package-with-symbol? "SB-EXT" "GET-TIME-OF-DAY") ; available from sbcl 1.0.28.66
1087
(multiple-value-bind (sec nsec) (sb-ext:get-time-of-day)
1088
(values sec (* 1000 nsec)))
1089
#-#.(obj/time::package-with-symbol? "SB-EXT" "GET-TIME-OF-DAY") ; obsolete, scheduled to be deleted at the end of 2009
1090
(multiple-value-bind (success? sec nsec) (sb-unix:unix-gettimeofday)
1091
(assert success? () "sb-unix:unix-gettimeofday reported failure?!")
1092
(values sec (* 1000 nsec)))))
1095
"Use the `*clock*' special variable if you need to define your own idea of the current time.
1097
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.
1098
The currently supported values in obj/time are:
1099
t - use the standard clock
1100
obj/time:leap-second-adjusted - use a clock which adjusts for leap seconds using the information in *default-timezone*.")
1103
"Returns a timestamp representing the present moment."
1104
(clock-now *clock*))
1107
"Returns a timestamp representing the present day (assuming UTC)."
1108
(clock-today *clock*))
1110
(defun format-date-simple (&optional dest timestamp)
1111
"Return a simple date string for today."
1112
(unless timestamp (setq timestamp (today)))
1113
(format-timestring dest timestamp
1114
:format '(:year #\- (:month 2) #\- (:day 2))))
1116
(defgeneric clock-now (clock)
1117
(:documentation "Returns a timestamp for the current time given a clock."))
1119
(defgeneric clock-today (clock)
1120
(:documentation "Returns a timestamp for the current date given a
1121
clock. The date is encoded by convention as a timestamp with the time set to
1124
(defgeneric date (self)
1125
(:documentation "Return the date of object SELF."))
1127
(defgeneric duration (self)
1128
(:documentation "Return the duration of object SELF."))1
1130
(defun %leap-seconds-offset (leap-seconds sec)
1131
"Find the latest leap second adjustment effective at SEC system time."
1132
(elt (leap-seconds-adjustment leap-seconds)
1133
(transition-position sec (leap-seconds-sec leap-seconds))))
1135
(defun %adjust-sec-for-leap-seconds (sec)
1136
"Ajdust SEC from system time to Unix time (on systems those clock does not jump back over leap seconds)."
1137
(let ((leap-seconds (timezone-leap-seconds (%realize-timezone *default-timezone*))))
1139
(decf sec (%leap-seconds-offset leap-seconds sec))))
1142
(defmethod clock-now ((clock (eql 'leap-second-adjusted)))
1143
(multiple-value-bind (sec nsec) (%get-current-time)
1144
(unix-to-timestamp (%adjust-sec-for-leap-seconds sec)
1147
(defmethod clock-now (clock)
1148
(declare (ignore clock))
1149
(multiple-value-bind (sec nsec) (%get-current-time)
1150
(unix-to-timestamp sec :nsec nsec)))
1152
(defmethod clock-today (clock)
1153
;; TODO should return a date value, anyhow we will decide to represent it eventually
1154
(let ((result (clock-now clock)))
1155
(setf (sec-of result) 0)
1156
(setf (nsec-of result) 0)
1159
(defmacro %defcomparator (name &body body)
1160
(let ((pair-comparator-name (intern (concatenate 'string "%" (string name)))))
1162
(declaim (inline ,pair-comparator-name))
1163
(defun ,pair-comparator-name (time-a time-b)
1164
(assert (typep time-a 'timestamp)
1168
:expected-type 'timestamp)
1169
(assert (typep time-b 'timestamp)
1173
:expected-type 'timestamp)
1175
(defun ,name (&rest times)
1176
(declare (dynamic-extent times))
1177
(loop for head on times
1179
always (,pair-comparator-name (first head) (second head))))
1180
(define-compiler-macro ,name (&rest times)
1182
:for i :upfrom 0 :below (length times)
1183
:collect (gensym (concatenate 'string "TIME-" (princ-to-string i) "-")))))
1187
:collect (list var time)))
1188
;; we could evaluate comparisons of timestamp literals here
1190
:for (time-a time-b) :on vars
1192
:collect `(,',pair-comparator-name ,time-a ,time-b)))))))))
1194
(defun %timestamp-compare (time-a time-b)
1195
"Returns the symbols <, >, or =, describing the relationship between TIME-A and TIME-b."
1196
(declare (type timestamp time-a time-b))
1198
((< (day-of time-a) (day-of time-b)) '<)
1199
((> (day-of time-a) (day-of time-b)) '>)
1200
((< (sec-of time-a) (sec-of time-b)) '<)
1201
((> (sec-of time-a) (sec-of time-b)) '>)
1202
((< (nsec-of time-a) (nsec-of time-b)) '<)
1203
((> (nsec-of time-a) (nsec-of time-b)) '>)
1206
(%defcomparator timestamp<
1207
(eql (%timestamp-compare time-a time-b) '<))
1209
(%defcomparator timestamp<=
1210
(not (null (member (%timestamp-compare time-a time-b) '(< =)))))
1212
(%defcomparator timestamp>
1213
(eql (%timestamp-compare time-a time-b) '>))
1215
(%defcomparator timestamp>=
1216
(not (null (member (%timestamp-compare time-a time-b) '(> =)))))
1218
(%defcomparator timestamp=
1219
(eql (%timestamp-compare time-a time-b) '=))
1221
(defun timestamp/= (&rest timestamps)
1222
"Returns T if no pair of timestamps is equal. Otherwise return NIL."
1223
(declare (dynamic-extent timestamps))
1224
(loop for ts-head on timestamps do
1225
(loop for ts in (rest ts-head) do
1226
(when (timestamp= (car ts-head) ts)
1227
(return-from timestamp/= nil))))
1230
(defun contest (test list)
1231
"Applies TEST to pairs of elements in list, keeping the element which last
1232
tested T. Returns the winning element."
1233
(reduce (lambda (a b) (if (funcall test a b) a b)) list))
1235
(defun timestamp-minimum (time &rest times)
1236
"Returns the earliest timestamp"
1237
(contest #'timestamp< (cons time times)))
1239
(defun timestamp-maximum (time &rest times)
1240
"Returns the latest timestamp"
1241
(contest #'timestamp> (cons time times)))
1243
(eval-when (:compile-toplevel :load-toplevel :execute)
1244
(defun years-to-days (years)
1245
"Given a number of years, returns the number of days in those years."
1246
(let* ((days (* years 365))
1247
(l1 (floor years 4))
1248
(l2 (floor years 100))
1249
(l3 (floor years 400)))
1250
(+ days l1 (- l2) l3))))
1252
(defun days-to-years (days)
1253
"Given a number of days, returns the number of years and the remaining days in that year."
1254
(let ((remaining-days days))
1255
(multiple-value-bind (400-years remaining-days)
1256
(floor remaining-days #.(years-to-days 400))
1257
(let* ((100-years (min (floor remaining-days #.(years-to-days 100)) 3))
1258
(remaining-days (- remaining-days
1259
(* 100-years #.(years-to-days 100)))))
1260
(multiple-value-bind (4-years remaining-days)
1261
(floor remaining-days #.(years-to-days 4))
1262
(let ((years (min 3 (floor remaining-days #.(years-to-days 1)))))
1263
(values (+ (* 400-years 400)
1267
(- remaining-days (* years 365))))))))
1268
;; the above is the macroexpansion of the following. uses metabang BIND, but kept for clarity because the expansion is unreadable.
1270
(bind ((remaining-days days)
1271
((values 400-years remaining-days) (floor remaining-days #.(years-to-days 400)))
1272
(100-years (min (floor remaining-days #.(years-to-days 100))
1274
(remaining-days (- remaining-days
1276
#.(years-to-days 100))))
1277
((values 4-years remaining-days) (floor remaining-days #.(years-to-days 4)))
1278
(years (min (floor remaining-days 365)
1280
(values (+ (* 400-years 400)
1284
(- remaining-days (* years 365)))))
1286
;; TODO merge this functionality into timestamp-difference
1287
(defun timestamp-whole-year-difference (time-a time-b)
1288
"Returns the number of whole years elapsed between time-a and time-b (hint: anniversaries)."
1289
(declare (type timestamp time-b time-a))
1290
(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)
1291
(decode-timestamp time-b)
1292
(declare (ignore day-of-week-b daylight-p-b))
1293
(multiple-value-bind (nsec-a sec-a minute-a hour-a day-a month-a year-a)
1294
(decode-timestamp time-a)
1295
(declare (ignore nsec-a sec-a minute-a hour-a day-a month-a))
1296
(let ((year-difference (- year-a year-b)))
1297
(if (timestamp<= (encode-timestamp nsec-b sec-b minute-b hour-b
1302
(+ year-difference year-b)
1306
(1- year-difference))))))
1308
(defun %timestamp-decode-date (days)
1309
"Returns the year, month, and day, given the number of days from the epoch."
1310
(declare (type integer days))
1311
(multiple-value-bind (years remaining-days)
1312
(days-to-years days)
1313
(let* ((leap-day-p (= remaining-days 365))
1314
(rotated-1-based-month (if leap-day-p
1315
12 ; march is the first month and february is the last
1316
(position remaining-days +rotated-month-offsets-without-leap-day+ :test #'<)))
1317
(1-based-month (if (>= rotated-1-based-month 11)
1318
(- rotated-1-based-month 10)
1319
(+ rotated-1-based-month 2)))
1320
(1-based-day (if leap-day-p
1322
(1+ (- remaining-days (aref +rotated-month-offsets-without-leap-day+
1323
(1- rotated-1-based-month)))))))
1326
(if (>= rotated-1-based-month 11) ; january is in the next year
1332
(defun %timestamp-decode-iso-week (timestamp)
1333
"Returns the year, week number, and day of week components of an ISO week date."
1334
;; Algorithm from http://en.wikipedia.org/wiki/Talk:ISO_week_date#Algorithms
1335
(let* ((dn (timestamp-day-of-week timestamp))
1336
(day-of-week (if (zerop dn) 7 dn)) ; ISO weekdays are Monday=1 and Sunday=7
1337
(nearest-thursday (timestamp+ timestamp (- 4 day-of-week) :day))
1338
(year (timestamp-year nearest-thursday))
1339
(month (timestamp-month nearest-thursday))
1340
(day (timestamp-day nearest-thursday))
1341
(ordinal-day (- (day-of (encode-timestamp 0 0 0 0 day month year :timezone +utc-zone+))
1342
(day-of (encode-timestamp 0 0 0 0 1 1 year :timezone +utc-zone+)))))
1344
(1+ (floor ordinal-day 7))
1347
(defun %timestamp-decode-time (seconds)
1348
"Returns the hours, minutes, and seconds, given the number of seconds since midnight."
1349
(declare (type integer seconds))
1350
(multiple-value-bind (hours hour-remainder)
1351
(floor seconds +seconds-per-hour+)
1352
(multiple-value-bind (minutes seconds)
1353
(floor hour-remainder +seconds-per-minute+)
1359
(defun decode-sec-day (ts-sec ts-day)
1360
(multiple-value-bind (hour minute sec)
1361
(%timestamp-decode-time ts-sec)
1362
(multiple-value-bind (year month day)
1363
(%timestamp-decode-date ts-day)
1364
(values sec minute hour
1366
(%ts-day-of-week ts-day)))))
1368
(defun decode-timestamp (timestamp &key (timezone *default-timezone*) offset)
1369
"Returns the decoded time as multiple values: nsec, ss, mm, hh, day, month, year, day-of-week"
1370
(declare (type timestamp timestamp))
1371
(let ((timezone (if offset (the timezone +none-zone+) timezone)))
1372
(multiple-value-bind (offset* daylight-p abbreviation)
1373
(timestamp-subtimezone timestamp timezone)
1374
(multiple-value-bind (sec minute hour day month year day-of-week)
1375
(multiple-value-call #'decode-sec-day
1376
(%adjust-to-offset (sec-of timestamp) (day-of timestamp) (or offset offset*)))
1377
(values (nsec-of timestamp)
1385
(defun timestamp-year (timestamp &key (timezone *default-timezone*))
1386
"Returns the cardinal year upon which the timestamp falls."
1388
(%timestamp-decode-date
1389
(nth-value 1 (%adjust-to-timezone timestamp timezone)))))
1391
(defun timestamp-century (timestamp &key (timezone *default-timezone*))
1392
"Returns the ordinal century upon which the timestamp falls."
1393
(let* ((year (timestamp-year timestamp :timezone timezone))
1394
(sign (signum year)))
1397
(truncate (1- (abs year)) 100)))))
1399
(defun timestamp-millennium (timestamp &key (timezone *default-timezone*))
1400
"Returns the ordinal millennium upon which the timestamp falls."
1401
(let* ((year (timestamp-year timestamp :timezone timezone))
1402
(sign (signum year)))
1405
(truncate (1- (abs year)) 1000)))))
1407
(defun timestamp-decade (timestamp &key (timezone *default-timezone*))
1408
"Returns the cardinal decade upon which the timestamp falls."
1409
(truncate (timestamp-year timestamp :timezone timezone) 10))
1411
(defun timestamp-month (timestamp &key (timezone *default-timezone*))
1412
"Returns the month upon which the timestamp falls."
1414
(%timestamp-decode-date
1415
(nth-value 1 (%adjust-to-timezone timestamp timezone)))))
1417
(defun timestamp-day (timestamp &key (timezone *default-timezone*))
1418
"Returns the day of the month upon which the timestamp falls."
1420
(%timestamp-decode-date
1421
(nth-value 1 (%adjust-to-timezone timestamp timezone)))))
1423
(defun timestamp-hour (timestamp &key (timezone *default-timezone*))
1425
(%timestamp-decode-time
1426
(nth-value 0 (%adjust-to-timezone timestamp timezone)))))
1428
(defun timestamp-minute (timestamp &key (timezone *default-timezone*))
1430
(%timestamp-decode-time
1431
(nth-value 0 (%adjust-to-timezone timestamp timezone)))))
1433
(defun timestamp-second (timestamp &key (timezone *default-timezone*))
1435
(%timestamp-decode-time
1436
(nth-value 0 (%adjust-to-timezone timestamp timezone)))))
1438
(defun timestamp-microsecond (timestamp)
1439
(floor (nsec-of timestamp) 1000))
1441
(defun timestamp-millisecond (timestamp)
1442
(floor (nsec-of timestamp) 1000000))
1444
(defun split-timestring (str &rest args)
1446
(apply #'%split-timestring (coerce str 'simple-string) args))
1448
(defun %split-timestring (time-string &key
1450
(end (length time-string))
1451
(fail-on-error t) (time-separator #\:)
1452
(date-separator #\-)
1453
(date-time-separator #\T)
1454
(fract-time-separators '(#\. #\,))
1455
(allow-missing-elements t)
1456
(allow-missing-date-part allow-missing-elements)
1457
(allow-missing-time-part allow-missing-elements)
1458
(allow-missing-timezone-part allow-missing-time-part))
1459
"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."
1460
(declare (type character date-time-separator time-separator date-separator)
1461
(type simple-string time-string)
1462
(optimize (speed 3)))
1464
(let (year month day hour minute second nsec offset-hour offset-minute)
1465
(declare (type (or null fixnum) start end year month day hour minute second offset-hour offset-minute)
1466
(type (or null (signed-byte 32)) nsec))
1467
(macrolet ((passert (expression)
1468
`(unless ,expression
1469
(parse-error ',expression)))
1470
(parse-integer-into (start-end place &optional low-limit high-limit)
1471
(let ((entry (gensym "ENTRY"))
1472
(value (gensym "VALUE"))
1473
(pos (gensym "POS"))
1474
(start (gensym "START"))
1475
(end (gensym "END")))
1476
`(let ((,entry ,start-end))
1478
(let ((,start (car ,entry))
1479
(,end (cdr ,entry)))
1480
(multiple-value-bind (,value ,pos) (parse-integer time-string :start ,start :end ,end :junk-allowed t)
1481
(passert (= ,pos ,end))
1482
(setf ,place ,value)
1483
,(if (and low-limit high-limit)
1484
`(passert (<= ,low-limit ,place ,high-limit))
1488
(passert allow-missing-elements)
1490
(with-parts-and-count ((start end split-chars) &body body)
1491
`(multiple-value-bind (parts count) (split ,start ,end ,split-chars)
1492
(declare (ignorable count) (type fixnum count)
1493
;;(type #1=(cons (cons fixnum fixnum) (or null #1#)) parts)
1496
(labels ((split (start end chars)
1497
(declare (type fixnum start end))
1498
(unless (consp chars)
1499
(setf chars (list chars)))
1500
(loop with last-match = start
1501
with match-count of-type (integer 0 #.most-positive-fixnum) = 0
1502
for index of-type fixnum upfrom start
1504
when (member (aref time-string index) chars :test #'char-equal)
1505
collect (prog1 (if (< last-match index)
1506
(cons last-match index)
1509
(setf last-match (1+ index)))
1511
finally (return (values (if (zerop (- index last-match))
1514
(nconc result (list (cons last-match index)))
1515
(incf match-count)))
1518
(with-parts-and-count (start end date-time-separator)
1521
(full-date (first parts))
1522
(passert allow-missing-date-part))
1524
(full-time (second parts))
1525
(passert allow-missing-time-part))
1528
allow-missing-date-part
1529
(find time-separator time-string
1530
:start (car (first parts))
1531
:end (cdr (first parts))))
1532
(full-time (first parts))
1535
allow-missing-time-part
1536
(find date-separator time-string
1537
:start (car (first parts))
1538
:end (cdr (first parts))))
1539
(full-date (first parts))
1542
(full-date (start-end)
1543
(let ((parts (split (car start-end) (cdr start-end) date-separator)))
1544
(passert (%list-length= 3 parts))
1545
(date-fullyear (first parts))
1546
(date-month (second parts))
1547
(date-mday (third parts))))
1548
(date-fullyear (start-end)
1549
(parse-integer-into start-end year))
1550
(date-month (start-end)
1551
(parse-integer-into start-end month 1 12))
1552
(date-mday (start-end)
1553
(parse-integer-into start-end day 1 31))
1554
(full-time (start-end)
1555
(let ((start (car start-end))
1556
(end (cdr start-end)))
1557
(with-parts-and-count (start end (list #\Z #\- #\+))
1558
(let* ((zulup (find #\Z time-string :test #'char-equal :start start :end end))
1560
(if (find #\+ time-string :test #'char-equal :start start :end end)
1563
(passert (<= 1 count 2))
1564
(unless (and (eq (first parts) nil)
1567
(partial-time (first parts)))
1572
(passert (or zulup allow-missing-timezone-part))
1573
(let* ((entry (second parts))
1576
(declare (type fixnum start end))
1578
(not (zerop (- end start)))))
1580
(time-offset (second parts) sign))))))))
1581
(partial-time (start-end)
1582
(with-parts-and-count ((car start-end) (cdr start-end) time-separator)
1583
(passert (eql count 3))
1584
(time-hour (first parts))
1585
(time-minute (second parts))
1586
(time-second (third parts))))
1587
(time-hour (start-end)
1588
(parse-integer-into start-end hour 0 23))
1589
(time-minute (start-end)
1590
(parse-integer-into start-end minute 0 59))
1591
(time-second (start-end)
1592
(with-parts-and-count ((car start-end) (cdr start-end) fract-time-separators)
1593
(passert (<= 1 count 2))
1594
(let ((*read-eval* nil))
1595
(parse-integer-into (first parts) second 0 59)
1597
(let* ((start (car (second parts)))
1598
(end (cdr (second parts))))
1599
(declare (type (integer 0 #.array-dimension-limit) start end))
1600
(passert (<= (- end start) 9))
1601
(let ((new-end (position #\0 time-string
1607
(setf end (min (1+ new-end)))))
1608
(setf nsec (* (the (integer 0 999999999) (parse-integer time-string :start start :end end))
1609
(aref #.(coerce #(1000000000 100000000 10000000
1610
1000000 100000 10000 1000 100 10 1)
1611
'(simple-array (signed-byte 32) (10)))
1614
(time-offset (start-end sign)
1615
(with-parts-and-count ((car start-end) (cdr start-end) time-separator)
1616
(passert (or (and allow-missing-timezone-part (zerop count))
1623
(parse-integer-into (first parts) offset-hour 0 23)
1624
(parse-integer-into (second parts) offset-minute 0 59))
1625
((= (- (cdar parts) (caar parts)) 4)
1627
(parse-integer-into (cons (caar parts)
1630
(parse-integer-into (cons (+ (caar parts) 2)
1632
offset-minute 0 59))
1633
((= (- (cdar parts) (caar parts)) 2)
1635
(parse-integer-into (cons (caar parts)
1638
(setf offset-minute 0)))
1640
(setf offset-hour (* offset-hour sign)
1641
offset-minute (* offset-minute sign))))
1642
(parse-error (failure)
1644
(error 'invalid-timestring :timestring time-string :failure failure)
1645
(return-from %split-timestring nil)))
1647
(return-from %split-timestring (list year month day hour minute second nsec offset-hour offset-minute))))
1650
(defun parse-rfc3339-timestring (timestring &key (fail-on-error t)
1651
(allow-missing-time-part nil))
1652
(parse-timestring timestring :fail-on-error fail-on-error
1653
:allow-missing-timezone-part nil
1654
:allow-missing-time-part allow-missing-time-part
1655
:allow-missing-date-part nil
1656
:fract-time-separators #\.))
1658
(defun parse-timestring (timestring &key
1662
(time-separator #\:)
1663
(date-separator #\-)
1664
(date-time-separator #\T)
1665
(fract-time-separators '(#\. #\,))
1666
(allow-missing-elements t)
1667
(allow-missing-date-part allow-missing-elements)
1668
(allow-missing-time-part allow-missing-elements)
1669
(allow-missing-timezone-part allow-missing-elements)
1671
"Parse a timestring and return the corresponding TIMESTAMP.
1672
See split-timestring for details. Unspecified fields in the
1673
timestring are initialized to their lowest possible value,
1674
and timezone offset is 0 (UTC) unless explicitly specified
1675
in the input string."
1676
(let ((parts (%split-timestring (coerce timestring 'simple-string)
1678
:end (or end (length timestring))
1679
:fail-on-error fail-on-error
1680
:time-separator time-separator
1681
:date-separator date-separator
1682
:date-time-separator date-time-separator
1683
:fract-time-separators fract-time-separators
1684
:allow-missing-elements allow-missing-elements
1685
:allow-missing-date-part allow-missing-date-part
1686
:allow-missing-time-part allow-missing-time-part
1687
:allow-missing-timezone-part allow-missing-timezone-part)))
1689
(destructuring-bind (year month day hour minute second nsec offset-hour offset-minute)
1699
:offset (if offset-hour
1700
(+ (* offset-hour 3600)
1701
(* (or offset-minute 0) 60))
1704
(defun ordinalize (day)
1705
"Return an ordinal string representing the position
1706
of DAY in a sequence (1st, 2nd, 3rd, 4th, etc)."
1707
(declare (type (integer 1 31) day))
1708
(format nil "~d~a" day
1717
(defun %construct-timestring (timestamp format timezone)
1718
"Constructs a string representing TIMESTAMP given the FORMAT
1719
of the string and the TIMEZONE.
1720
See the documentation of FORMAT-TIMESTRING for the structure of FORMAT."
1721
(declare (type timestamp timestamp)
1722
(optimize (speed 3)))
1723
(multiple-value-bind (nsec sec minute hour day month year weekday daylight-p offset abbrev)
1724
(decode-timestamp timestamp :timezone timezone)
1725
(declare (ignore daylight-p))
1726
(multiple-value-bind (iso-year iso-week iso-weekday)
1727
(%timestamp-decode-iso-week timestamp)
1728
(let ((*print-pretty* nil)
1729
(*print-circle* nil))
1730
(with-output-to-string (result nil)
1731
(dolist (fmt format)
1733
((member fmt '(:gmt-offset :gmt-offset-or-z :gmt-offset-hhmm))
1734
(multiple-value-bind (offset-hours offset-secs)
1735
(truncate offset +seconds-per-hour+)
1736
(declare (fixnum offset-hours offset-secs))
1737
(if (and (eql fmt :gmt-offset-or-z) (zerop offset))
1739
(format result "~c~2,'0d~:[:~;~]~2,'0d"
1740
(if (minusp offset) #\- #\+)
1742
(eql fmt :gmt-offset-hhmm)
1743
(round (abs offset-secs)
1744
+seconds-per-minute+)))))
1745
((eql fmt :short-year)
1746
(princ (mod year 100) result))
1747
((eql fmt :long-month)
1748
(princ (aref +month-names+ month) result))
1749
((eql fmt :short-month)
1750
(princ (aref +short-month-names+ month) result))
1751
((eql fmt :long-weekday)
1752
(princ (aref +day-names+ weekday) result))
1753
((eql fmt :short-weekday)
1754
(princ (aref +short-day-names+ weekday) result))
1755
((eql fmt :minimal-weekday)
1756
(princ (aref +minimal-day-names+ weekday) result))
1757
((eql fmt :timezone)
1758
(princ abbrev result))
1760
(princ (if (< hour 12) "am" "pm") result))
1761
((eql fmt :ordinal-day)
1762
(princ (ordinalize day) result))
1763
((or (stringp fmt) (characterp fmt))
1766
(let ((val (ecase (if (consp fmt) (car fmt) fmt)
1768
(:usec (floor nsec 1000))
1769
(:msec (floor nsec 1000000))
1773
(:hour12 (1+ (mod (1- hour) 12)))
1778
(:iso-week-year iso-year)
1779
(:iso-week-number iso-week)
1780
(:iso-week-day iso-weekday))))
1785
(format result "-~v,vd"
1787
(or (third fmt) #\0)
1790
(format result "~v,vd"
1792
(or (third fmt) #\0)
1795
(defun format-timestring (destination timestamp &key
1796
(format +iso-8601-format+)
1797
(timezone *default-timezone*))
1798
"Constructs a string representation of TIMESTAMP according
1799
to FORMAT and returns it.
1800
If destination is T, the string is written to *standard-output*.
1801
If destination is a stream, the string is written to the stream.
1803
FORMAT is a list containing one or more of strings, characters,
1804
and keywords. Strings and characters are output literally,
1805
while keywords are replaced by the values here:
1808
:MONTH *numeric month
1813
:WEEKDAY *numeric day of week starting from index 0, which means Sunday
1817
:ISO-WEEK-YEAR *year for ISO week date (can be different from regular calendar year)
1818
:ISO-WEEK-NUMBER *ISO week number (i.e. 1 through 53)
1819
:ISO-WEEK-DAY *ISO compatible weekday number (monday=1, sunday=7)
1820
:LONG-WEEKDAY long form of weekday (e.g. Sunday, Monday)
1821
:SHORT-WEEKDAY short form of weekday (e.g. Sun, Mon)
1822
:MINIMAL-WEEKDAY minimal form of weekday (e.g. Su, Mo)
1823
:SHORT-YEAR short form of year (last 2 digits, e.g. 41, 42 instead of 2041, 2042)
1824
:LONG-MONTH long form of month (e.g. January, February)
1825
:SHORT-MONTH short form of month (e.g. Jan, Feb)
1826
:HOUR12 *hour on a 12-hour clock
1827
:AMPM am/pm marker in lowercase
1828
:GMT-OFFSET the gmt-offset of the time, in +00:00 form
1829
:GMT-OFFSET-OR-Z like :GMT-OFFSET, but is Z when UTC
1830
:GMT-OFFSET-HHMM like :GMT-OFFSET, but in +0000 form
1831
:TIMEZONE timezone abbrevation for the time
1833
Elements marked by * can be placed in a list in the form
1834
\(:keyword padding &optional \(padchar #\\0))
1836
The string representation of the value will be padded with the padchar.
1838
You can see examples in +ISO-8601-FORMAT+, +ASCTIME-FORMAT+, and +RFC-1123-FORMAT+."
1839
(declare (type (or boolean stream) destination))
1840
(let ((result (%construct-timestring timestamp format timezone)))
1842
(write-string result (if (eq t destination) *standard-output* destination)))
1845
(defun format-rfc1123-timestring (destination timestamp &key
1846
(timezone *default-timezone*))
1847
(format-timestring destination timestamp
1848
:format +rfc-1123-format+
1849
:timezone timezone))
1851
(defun to-rfc1123-timestring (timestamp)
1852
(format-rfc1123-timestring nil timestamp))
1854
(defun format-rfc3339-timestring (destination timestamp &key
1857
(omit-timezone-part omit-time-part)
1859
(timezone *default-timezone*))
1860
"Formats a timestring in the RFC 3339 format, a restricted form of the ISO-8601 timestring specification for Internet timestamps."
1861
(let ((rfc3339-format
1863
(not omit-date-part)
1864
(not omit-time-part)
1865
(not omit-timezone-part))
1866
+rfc3339-format+ ; micro optimization
1868
(unless omit-date-part
1872
(unless (or omit-date-part
1875
(unless omit-time-part
1880
(unless omit-timezone-part
1883
'(:gmt-offset)))))))
1884
(format-timestring destination timestamp :format rfc3339-format :timezone timezone)))
1886
(defun to-rfc3339-timestring (timestamp)
1887
(format-rfc3339-timestring nil timestamp))
1889
(defun %read-timestring (stream char)
1890
(declare (ignore char))
1892
(with-output-to-string (str)
1893
(loop for c = (read-char stream nil)
1894
while (and c (or (digit-char-p c) (member c '(#\: #\T #\t #\: #\- #\+ #\Z #\.))))
1896
finally (when c (unread-char c stream))))
1897
:allow-missing-elements t))
1899
(defun %read-universal-time (stream char arg)
1900
(declare (ignore char arg))
1901
(universal-to-timestamp
1903
(with-output-to-string (str)
1904
(loop for c = (read-char stream nil)
1905
while (and c (digit-char-p c))
1907
finally (when c (unread-char c stream)))))))
1909
(defun enable-read-macros ()
1910
"Enables the local-time reader macros for literal timestamps and universal time."
1911
(set-macro-character #\@ '%read-timestring)
1912
(set-dispatch-macro-character #\# #\@ '%read-universal-time)
1915
(defvar *debug-timestamp* nil)
1917
(defmethod print-object ((object timestamp) stream)
1918
"Print the TIMESTAMP object using the standard reader notation"
1921
(print-unreadable-object (object stream :type t)
1922
(format stream "~d/~d/~d"
1927
(when *print-escape*
1928
(write-char #\@ stream))
1929
(format-rfc3339-timestring stream object))))
1931
(defmethod print-object ((object timezone) stream)
1932
"Print the TIMEZONE object in a reader-rejected manner."
1933
(print-unreadable-object (object stream :type t)
1934
(format stream "~:[UNLOADED~;~{~a~^ ~}~]"
1935
(timezone-loaded object)
1936
(map 'list #'subzone-abbrev (timezone-subzones object)))))
1938
(defun astronomical-julian-date (timestamp)
1939
"Returns the astronomical julian date referred to by the timestamp."
1940
(- (day-of timestamp) +astronomical-julian-date-offset+))
1942
(defun modified-julian-date (timestamp)
1943
"Returns the modified julian date referred to by the timestamp."
1944
(- (day-of timestamp) +modified-julian-date-offset+))
1946
(declaim (notinline format-timestring))
1948
(defun encode-universal-time-with-tz (sec minute hour day month year &key timezone)
1949
"Like encode-universal-time, but with a timezone object instead of a timezone offset."
1950
;; Use low level functions to prevent allocation of timestamp structures.
1951
(declare (type integer sec minute hour day month year))
1952
(unless (valid-timestamp-p 0 sec minute hour day month year)
1953
(error 'invalid-time-specification))
1954
(multiple-value-bind (ts-sec ts-day)
1955
(encode-sec-day sec minute hour day month year)
1956
(- (ts-sec-day-to-universal ts-sec ts-day)
1957
(encode-offset ts-sec
1959
(%realize-timezone (or timezone *default-timezone*))))))
1961
(defun decode-universal-time-with-tz (universal &key timezone)
1962
"Like decode-universal-time, but with a timezone object instead of an timezone offset.
1963
Differences with regard to decode-universal-time:
1964
- the returned offset is the offset applicable in TIMEZONE at UNIVERSAL time,
1965
and thus corrected for DST;
1966
- returns one more value: the abbreviation of the active timezone."
1967
(multiple-value-bind (ts-sec ts-day) (universal-sec-day universal)
1968
(multiple-value-bind (offset daylight-p abbreviation)
1969
(sec-day-subtimezone ts-sec
1971
(%realize-timezone (or timezone *default-timezone*)))
1972
(multiple-value-bind (sec minute hour day month year day-of-week)
1973
(multiple-value-call #'decode-sec-day
1974
(%adjust-to-offset ts-sec ts-day offset))
1975
(values sec minute hour
1977
(mod (1- day-of-week) 7) ;NB In CL: Monday = 0
1979
(/ offset -3600) ;NB In CL: hours west