(in-package :kira)
(defstruct local-time
(day 0 :type fixnum)
(sec 0 :type fixnum)
(msec 0 :type fixnum)
(zone nil))
(defconstant +cycle+ 146097 "The number of days in a 400-year cycle.")
(defconstant +century+ 36524 "The number of days in a single century.")
(defconstant +c-hours+ 43200 "The number of seconds in 12 hours.")
(defstatic +decoded-days+ (make-array +century+ :element-type '(unsigned-byte 16)))
(defstatic +decoded-secs+ (make-array +c-hours+ :element-type '(unsigned-byte 16)))
(declaim (type (simple-array (unsigned-byte 16) (*)) +decoded-days+))
(declaim (type (simple-array (unsigned-byte 16) (*)) +decoded-secs+))
(defun encode-date (year month day)
(declare (type (unsigned-byte 7) year))
(declare (type (unsigned-byte 4) month))
(declare (type (unsigned-byte 5) day))
(let ((year-byte (ash year 9)) (month-byte (ash month 5)))
(declare (type (unsigned-byte 16) year-byte month-byte))
(logior year-byte (the fixnum (logior month-byte day)))))
(defun days-in-month (month year)
(check-type year (integer 0 100))
(cond ((and (= month 2) (/= year 100) (= (rem year 4) 0)) 29)
((svref #(31 28 31 30 31 30 31 31 30 31 30 31) (1- month)))))
(let ((year 0) (month 3) (day 1) (days-in-month 31))
(declare (type fixnum year month day days-in-month))
(loop for index of-type fixnum from 0 below +century+
do (let ((entry (encode-date year month day)))
(declare (type (unsigned-byte 16) entry))
(setf (aref +decoded-days+ index) entry)
(cond ((< day days-in-month) (incf day))
(t (unless (<= (incf month) 12) (setq month 1) (incf year))
(setq days-in-month (days-in-month month year) day 1))))))
(loop with index fixnum = 0 and entries = +decoded-secs+
for hour below 12 as entry fixnum = (ash hour 12)
do (loop for minute of-type fixnum below 60
do (setf (ldb (byte 6 6) entry) minute)
(loop for sec of-type fixnum below 60
do (setf (ldb (byte 6 0) entry) sec)
(setf (aref entries index) entry)
(incf index))))
(defmacro normalize-time-slot
(var constant-limit carry)
(with-gensyms (old again)
`(prog ((,old ,var))
(declare (type fixnum ,old))
,again (when (minusp ,var)
(incf ,var ,constant-limit)
(decf ,carry) (go ,again))
(when (>= ,var ,constant-limit)
(decf ,var ,constant-limit)
(incf ,carry) (go ,again))
(return (/= ,old ,var)))))
(eval-always
(defconstant +day+ (* 2 +c-hours+))
(defconstant +1970-01-01+ -11017)
(defconstant +1900-01-01+ -36584))
(defun normalize-local-time (local-time)
(require-type local-time 'local-time)
(let ((day (local-time-day local-time))
(sec (local-time-sec local-time))
(msec (local-time-msec local-time)))
(declare (type fixnum day sec msec))
(declare (optimize (safety 0) (speed 3)))
(when (normalize-time-slot msec 1000 sec)
(setf (local-time-msec local-time) msec)
(setf (local-time-sec local-time) sec))
(when (normalize-time-slot sec +day+ day)
(setf (local-time-day local-time) day)
(setf (local-time-sec local-time) sec))
local-time))
(defun decode-local-time (local-time)
(normalize-local-time local-time)
(multiple-value-bind (year month day)
(day-to-date (local-time-day local-time))
(multiple-value-bind (hour minute sec)
(sec-to-time (local-time-sec local-time))
(values (local-time-msec local-time)
sec minute hour
day month year))))
(defun day-to-date (day)
(check-type day fixnum)
(locally (declare (fixnum day))
(let ((cycle 0) (century 0) (year 2000))
(declare (type fixnum cycle century year))
(when (or (minusp day) (>= day +century+))
(multiple-value-setq (cycle day)
(floor day +cycle+))
(let ((cycle-years (* 400 cycle)))
(declare (type fixnum cycle-years))
(incf year cycle-years))
(when (= day (1- +cycle+))
(return-from day-to-date (values (+ year 400) 2 29)))
(multiple-value-setq (century day) (floor day +century+))
(incf year (the fixnum (* 100 century))))
(let ((entry (aref +decoded-days+ day)))
(declare (type (unsigned-byte 16) entry))
(let ((year-offset (ldb (byte 7 9) entry)))
(declare (type fixnum year-offset))
(values (the fixnum (+ year year-offset))
(ldb (byte 4 5) entry)
(ldb (byte 5 0) entry)))))))
(defun sec-to-time (sec)
(check-type sec fixnum)
(let* ((shifted-hour-p
(when (>= sec +c-hours+)
(decf sec +c-hours+)))
(entry (aref +decoded-secs+ sec)))
(declare (type (unsigned-byte 16) entry))
(let ((hour-offset (ldb (byte 4 12) entry)))
(declare (type (unsigned-byte 4) hour-offset))
(let ((hour (if shifted-hour-p (+ 12 hour-offset) hour-offset)))
(values hour (ldb (byte 6 6) entry) (ldb (byte 6 0) entry))))))
(defmacro time-to-sec (hour minute sec)
`(+ ,sec (* 60 (+ ,minute (* 60 ,hour)))))
(defun encode-local-time (msec sec minute hour day month year &optional zone)
(let ((day (date-to-day year month day)) (sec (time-to-sec hour minute sec)))
(make-local-time :day day :sec sec :msec msec :zone zone)))
(defun date-to-day (year month day)
(check-type day (unsigned-byte 5))
(check-type year (integer -99999 99999))
(locally (declare (type (unsigned-byte 5) month))
(let* ((year-rotation (sbit #*110000000000 (1- month)))
(day-offset 0) (mar-based-year (- year 2000 year-rotation)))
(declare (type fixnum year-rotation day-offset mar-based-year))
(when (or (minusp mar-based-year) (>= mar-based-year 100))
(let ((cycle 0) (century 0))
(declare (type fixnum cycle century))
(multiple-value-setq (cycle mar-based-year)
(floor mar-based-year 400))
(let ((cycle-days (* +cycle+ cycle)))
(declare (type fixnum cycle-days))
(incf day-offset cycle-days))
(when (and (= mar-based-year 399) (= month 2) (= day 29))
(return-from date-to-day (the fixnum (+ day-offset (1- +cycle+)))))
(setf (values century mar-based-year) (floor mar-based-year 100))
(incf day-offset (the fixnum (* +century+ century)))))
(loop with y of-type fixnum = (+ mar-based-year year-rotation)
with start of-type fixnum = 0 with end of-type fixnum = +century+
with key of-type (unsigned-byte 16) = (encode-date y month day)
for index of-type fixnum = (ash (the fixnum (+ start end)) -1)
for entry of-type (unsigned-byte 16) = (aref +decoded-days+ index)
when (= entry key) do (return-from date-to-day (+ day-offset index))
if (> entry key) do (setq end index) else do (setq start (1+ index))
when (= start end) do (error "Invalid date.")))))
(defun local-time (&key universal unix (msec 0) (zone 0))
(when (and universal unix) (error "Specify either ~S or ~S time." :universal :unix))
(multiple-value-bind (day-relative-to-epoch sec) (floor (or universal unix) +day+)
(let ((day (+ day-relative-to-epoch (the fixnum (if unix +1970-01-01+ +1900-01-01+)))))
(make-local-time :day day :sec sec :msec msec :zone zone))))
(defun unix-time (local-time)
(let ((day (local-time-day (normalize-local-time local-time))))
(+ (* (- day +1970-01-01+) +day+) (local-time-sec local-time))))
(defun universal-time (local-time)
(let ((day (local-time-day (normalize-local-time local-time))))
(+ (* (- day +1900-01-01+) +day+) (local-time-sec local-time))))
(defmethod print-object ((local-time local-time) stream &aux (zone (local-time-zone local-time)))
(multiple-value-bind (msec sec minute hour day month year) (decode-local-time local-time)
(declare (type fixnum msec sec minute hour day month year))
(unless (typep year '(integer -99999 99999))
(return-from print-object (call-next-method)))
(when (or *print-escape* *print-readably*)
(write-char #\@ stream))
(multiple-value-bind (year-sign year-digits)
(if (typep year ' (integer 0 9999)) (values nil 4) (values (if (minusp year) #\- #\+) 5))
(format stream "~@[~C~]~V,'0D-~2,'0D-~2,'0DT~2,'0D:~2,'0D:~2,'0D.~3,'0D~:@/kira:print-zone/"
year-sign year-digits (abs year) month day hour minute sec msec zone))))
(defun print-zone (stream zone &optional colon-p at-sign-p)
(cond ((not zone) (unless at-sign-p (write-string "-0000" stream)))
((and (eql zone 0) colon-p) (write-string (if at-sign-p "Z" "GMT") stream))
(t (let ((zone-sign-marker (if (minusp zone) (prog1 #\- (absf zone)) #\+)))
(multiple-value-bind (hour-offset minute-offset) (sec-to-time zone)
(let ((control (if at-sign-p "~C~2,'0D:~2,'0D" "~C~2,'0D~2,'0D")))
(format stream control zone-sign-marker hour-offset minute-offset)))))))
(defmethod make-load-form ((local-time local-time) &optional environment)
(make-load-form-saving-slots local-time :environment environment))
(defun get-local-time ()
(rlet ((timeofday :timeval))
(gettimeofday timeofday)
(let* ((sec (pref timeofday :timeval.tv_sec))
(usec (pref timeofday :timeval.tv_usec))
(msec (floor usec 1000)))
(declare (type (unsigned-byte 40) sec))
(declare (type (unsigned-byte 20) usec))
(declare (type (unsigned-byte 10) msec))
(multiple-value-bind (day sec) (floor sec +day+)
(setq day (the fixnum (+ (the fixnum day) +1970-01-01+)))
(make-local-time :day day :sec sec :msec msec :zone 0)))))
(defun local-time= (local-time-1 local-time-2)
(normalize-local-time local-time-1) (normalize-local-time local-time-2)
(and (= (local-time-day local-time-1) (local-time-day local-time-2))
(= (local-time-sec local-time-1) (local-time-sec local-time-2))
(= (local-time-msec local-time-1) (local-time-msec local-time-2))
(eql (local-time-zone local-time-1) (local-time-zone local-time-2))))
(defun local-time/= (local-time-1 local-time-2)
(not (local-time= local-time-1 local-time-2)))
(defun local-time< (local-time-1 local-time-2)
(progn (normalize-local-time local-time-1) (normalize-local-time local-time-2))
(assert (eql (local-time-zone local-time-1) (local-time-zone local-time-2)))
(or (< (local-time-day local-time-1) (local-time-day local-time-2))
(when (= (local-time-day local-time-1) (local-time-day local-time-2))
(or (< (local-time-sec local-time-1) (local-time-sec local-time-2))
(when (= (local-time-sec local-time-1) (local-time-sec local-time-2))
(< (local-time-msec local-time-1) (local-time-msec local-time-2)))))))
(defun local-time<= (local-time-1 local-time-2)
(or (local-time= local-time-1 local-time-2)
(local-time< local-time-1 local-time-2)))
(defun local-time> (local-time-1 local-time-2)
(unless (local-time= local-time-1 local-time-2)
(local-time< local-time-2 local-time-1)))
(defun local-time>= (local-time-1 local-time-2)
(or (local-time= local-time-1 local-time-2)
(local-time> local-time-1 local-time-2)))
(defmacro get-digit (&aux (base (char-code #\0)))
(let ((place-value `(- (char-code digit-char) ,base)))
`(the (integer 0 9) (or (lexer-case (digit-char (lexer-error))
(digit (the (integer 0 9) ,place-value)))
(lexer-error)))))
(defmacro get-field (digits)
(with-gensyms (field-value)
(let ((digits-left (1- digits)))
(when (= digits 1) (return-from get-field '(get-digit)))
(let ((field-typespec `(integer 0 ,(1- (expt 10 digits)))))
`(the ,field-typespec
(let* ((,field-value (get-field ,digits-left))
(,field-value (* 10 ,field-value))
(,field-value (+ ,field-value (get-digit))))
(declare (type ,field-typespec ,field-value))
,field-value))))))
(defmacro %parse-zone ()
`(lexer-case (operator)
(#\Z (setq zone 0))
("-+" (let* ((hour-offset (get-field 2))
(minute-offset (progn (lexer-match #\:) (get-field 2)))
(computed-offset (time-to-sec hour-offset minute-offset 0)))
(declare (type (signed-byte 24) hour-offset minute-offset computed-offset))
(when (eql operator #\-) (setq computed-offset (- computed-offset)))
(setq zone computed-offset)))))
(defun parse-zone (string &key (start 0) end junk-allowed)
"Accept a zone offset (like -08:00) or the letter Z."
(with-lexer (string start end)
(with-lexer-error ("zone" junk-allowed)
(let (zone) (%parse-zone) zone))))
(defun parse-timestring (string &key (start 0) end junk-allowed)
(let ((month 1) (day 1) (hour 0) (minute 0) (sec 0) (msec 0))
(declare (type fixnum month day hour minute sec msec))
(with-lexer (string start end)
(let (operator year allow-zone-p zone)
(with-lexer-error ("timestring" junk-allowed)
(if (setq operator (lexer-match "-+"))
(setq year (get-field 5))
(setq year (get-field 4)))
(locally (declare (type (signed-byte 24) year))
(when (eql operator #\-) (setq year (- year))))
(block fixed-fields
(unless (lexer-match #\-)
(return-from fixed-fields))
(setq month (get-field 2))
(unless (lexer-match #\-)
(return-from fixed-fields))
(setq day (get-field 2))
(unless (lexer-match " T")
(return-from fixed-fields))
(setq hour (get-field 2))
(setq allow-zone-p t)
(unless (lexer-match #\:)
(return-from fixed-fields))
(setq minute (get-field 2))
(unless (lexer-match #\:)
(return-from fixed-fields))
(setq sec (get-field 2))
(unless (lexer-match #\.)
(return-from fixed-fields))
(setq msec (get-field 3)))
(when allow-zone-p (%parse-zone))
(unless (= lexer-position lexer-end)
(lexer-error)))
(values (and year (encode-local-time msec sec minute hour day month year zone))
(the array-index (+ (the array-index start) (lexer-relative-position))))))))
(set-macro-character
#\@ #'(lambda (stream character)
(declare (ignore character))
(loop with char of-type character = #\@
with string = (make-buffer 32 'character)
do (setq char (peek-char nil stream nil #\@ nil))
while (char-of-class-p char '(or "-:.+" alphanumeric))
do (vector-push-extend (read-char stream) string)
finally (return (parse-timestring string)))))
(defun local-time-day-of-week (local-time)
(let ((day (local-time-day (normalize-local-time local-time))))
(declare (fixnum day)) (mod (the fixnum (+ 3 day)) 7)))
(defun day-name (day-of-week)
(svref #("Sunday" "Monday"
"Tuesday" "Wednesday"
"Thursday" "Friday"
"Saturday") day-of-week))
(defun %print-date-part (stream name &optional colon-p at-sign-p)
(declare (ignore at-sign-p) (optimize (safety 0) (speed 3)))
(write-simple-string name stream 0 (if colon-p 3 nil)))
(defun print-day-name (stream day-of-week &optional colon-p at-sign-p)
(%print-date-part stream (day-name day-of-week) colon-p at-sign-p))
(defun month-name (month-number)
(svref #("Invalid-Month"
"January"
"February"
"March"
"April"
"May"
"June"
"July"
"August"
"September"
"October"
"November"
"December")
month-number))
(defun print-month-name (stream month-number &optional colon-p at-sign-p)
(%print-date-part stream (month-name month-number) colon-p at-sign-p))
(defun 822-time (stream local-time &optional colon-p at-sign-p)
(multiple-value-bind (msec sec minute hour day month year)
(decode-local-time local-time)
(declare (ignore msec))
(let ((date-separator (if colon-p #\- #\Space)))
(format stream "~:/kira:print-day-name/, ~
~2,'0D~C~:/kira:print-month-name/~
~C~D ~2,'0D:~2,'0D:~2,'0D "
(local-time-day-of-week local-time)
day date-separator month date-separator
year hour minute sec))
(let ((zone (local-time-zone local-time)))
(print-zone stream zone (not at-sign-p)))))
(defun get-localized-time (&optional (local-time (get-local-time)))
(setq local-time (copy-local-time (normalize-local-time local-time)))
(locally (declare (optimize (safety 0) (speed 3)) (type local-time local-time))
(let ((sec (local-time-sec local-time)) (zone (local-time-zone local-time)))
(cond ((not (require-type zone '(or null (signed-byte 24)))) local-time)
(t (let* ((minutes-west (get-timezone (unix-time local-time)))
(new-zone (- (the fixnum (* 60 minutes-west))))
(new-sec (+ (the fixnum (- sec zone)) new-zone)))
(declare (type fixnum minutes-west new-sec new-zone))
(setf (local-time-zone local-time) new-zone)
(setf (local-time-sec local-time) new-sec)
(normalize-local-time local-time)))))))
(defun print-local-time (local-time &optional stream)
(multiple-value-bind (msec sec minute hour day month year) (decode-local-time local-time)
(declare (ignore msec) (type (integer 0 #.most-positive-fixnum) sec minute hour day month year))
(format stream "~D-~:@(~:/kira:print-month-name/~)-~D ~2,'0D:~2,'0D:~2,'0D" day month year hour
minute sec)))