[Small-cl-src] format-time.lisp

Zach Beane xach at xach.com
Mon May 24 18:29:26 UTC 2004


;;;
;;; Usage example:
;;;
;;;  (format-time nil "~{Dayname}, ~{Monthname} ~:@{Date}") =>
;;;     "Monday, May 24th"
;;;

(defpackage :format-time
  (:use :cl)
  (:export :format-time :time-formatter))

(in-package :format-time)


;;; Errors

(define-condition format-time-error (error)
  ((complaint :reader format-time-error-complaint :initarg :complaint)
   (control-string :reader format-time-error-control-string
                   :initarg :control-string)
   (offset :reader format-time-error-offset :initarg :offset))
  (:report report-format-time-error))


(define-condition format-time-dumb-error (error)
  ((complaint :initarg :complaint :reader format-time-dumb-error-complaint))
  (:report (lambda (condition stream)
             (write-string (format-time-dumb-error-complaint condition)
                           stream))))


(defun report-format-time-error (condition stream)
  (format stream "error in time-format: ~A~%  ~A~%  ~v at T^~%"
          (format-time-error-complaint condition)
          (format-time-error-control-string condition)
          (format-time-error-offset condition)))



;;;
(eval-when (:compile-toplevel :load-toplevel :execute)
  (defvar *day-of-week-names*
    #("Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday" "Sunday"))

  (defvar *month-names*
    #("January" "February" "March" "April" "May" "June"
      "July" "August" "September" "October" "November" "December"))

  (defvar *number-suffixes*
    #("th" "st" "nd" "rd" "th" "th" "th" "th" "th" "th"))

  (defvar *decoded-time-values*
    '(second minute hour date month year day daylight-p zone))

  (defvar *date-formatters* (make-hash-table :test 'eq)))


(defun number-suffix (number)
  (svref *number-suffixes* (mod number 10)))


(defmacro def-date-formatter (name (directive type) args &body body)
  "Create a format-time directive processor implemented with a
function called NAME that processes a directive of type TYPE (one of
:STRING OR :NUMERIC) named by DIRECTIVE. ARGS must only consist of
symbols in *DECODED-TIME-VALUES*."
  (assert (subsetp args *decoded-time-values*))
  (assert (member type '(:string :numeric)))
  (let ((ignore-list (set-difference *decoded-time-values* args)))
    (setf (gethash directive *date-formatters*) name)
    (setf (get directive 'strftime-type) type)
    `(defun ,name (,@*decoded-time-values*)
      (declare (ignore , at ignore-list))
      , at body)))


(def-date-formatter format-dayname (dayname :string) (day)
  (svref *day-of-week-names* day))

(def-date-formatter format-monthname (monthname :string) (month)
  (svref *month-names* (1- month)))

(def-date-formatter format-day (day :numeric) (day)
  day)

(def-date-formatter format-date (date :numeric) (date)
  date)

(def-date-formatter format-year (year :numeric) (year)
  year)

(def-date-formatter format-yy (yy :numeric) (year)
  (mod year 100))

(def-date-formatter format-hh (hh :numeric) (hour)
  hour)

(def-date-formatter format-hh12 (hh12 :numeric) (hour)
  (let ((12hour (mod hour 12)))
    (if (zerop 12hour)
        12
        12hour)))

(def-date-formatter format-mm (mm :numeric) (minute)
  minute)

(def-date-formatter format-ss (ss :numeric) (second)
  second)

(def-date-formatter format-am (am :string) (hour)
  (if (< hour 12) "am" "pm"))



;;; parsing the control string
(defun make-directive-printer (directive colonp atp)
  (ecase (get directive 'strftime-type :error)
    (:string
     (cond ((not (or colonp atp))
            (lambda (arg stream)
              (write-string arg stream)))
           ((not colonp)
            (lambda (arg stream)
              (write-string (string-upcase arg) stream)))
           ((not atp)
            (lambda (arg stream)
              (dotimes (i 3)
                (write-char (schar arg i) stream))))
           (t
            (lambda (arg stream)
              (dotimes (i 3)
                (write-char (char-upcase (schar arg i)) stream))))))
    (:numeric
     (cond ((not (or colonp atp))
            (lambda (arg stream)
              (princ arg stream)))
           ((not colonp)
            (lambda (arg stream)
              (format stream "~2,'0D" arg)))
           ((not atp)
            (lambda (arg stream)
              (format stream "~2,' D" arg)))
           (t
            (lambda (arg stream)
              (format stream "~D~A" arg (number-suffix arg))))))
    (:error
     (error 'format-time-dumb-error
            :complaint "unknown format-time directive"))))


(defun make-directive-function (name-string colonp atp)
  (let* ((name (intern (string-upcase name-string)
                       (find-package "FORMAT-TIME")))
         (result-func (gethash name *date-formatters*))
         (format-func (make-directive-printer name colonp atp)))
    (lambda (stream second minute hour date month year day daylight-p zone)
      (funcall format-func
               (funcall result-func second minute hour date month year day daylight-p zone)
               stream))))



(defun tokenize-control-string (string)
  "Convert the control string STRING to a list of constant strings and
printing functions. Signals FORMAT-TIME-ERROR if there is a problem
with the control string."
  (declare (string string))
  (let ((pos 0)
        (end (length string))
        (results nil))
    (loop
     (when (>= pos end)
       (return (nreverse results)))
     (let ((directive (position #\~ string :start pos)))
       (cond ((null directive)
              (push (if (zerop pos) string (subseq string pos)) results)
              (return (nreverse results)))
             (t
              (when (< pos directive)
                (push (subseq string pos directive) results))
              (multiple-value-bind (func new-pos)
                  (parse-directive string directive)
                (push func results)
                (setf pos new-pos))))))))


(defun parse-directive (string start)
  "Convert a single directive from STRING starting at START into a
processing function."
  (declare (string string) (fixnum start))
  (let ((colonp nil)
        (atp nil)
        (posn (1+ start))
        (end (length string)))
    (labels ((pos-error (message &optional (pos posn))
               (error 'format-time-error
                      :complaint message
                      :control-string string
                      :offset (1- pos)))
             (get-char ()
               (if (= posn end)
                   (pos-error "string ended before directive was found")
                 (prog1 
                     (schar string posn)
                   (incf posn)))))
      (loop
       (let ((char (get-char)))
         (case char
           ((#\:)
            (if colonp
                (pos-error "too many colons supplied")
                (setf colonp t)))
           ((#\@)
            (if atp
                (pos-error "too many at-signs supplied")
                (setf atp t)))
           ((#\{)
            (let ((name-end (position #\} string :start posn)))
              (if name-end
                  (return
                    (values (handler-case
                                (make-directive-function
                                 (subseq string posn name-end) colonp atp)
                              (format-time-dumb-error (condition)
                                (pos-error (format-time-dumb-error-complaint condition) (1+ posn))))

                            (1+ name-end)))
                  (pos-error "no corresponding close brace"))))))))))


;;; user interface

(defun %format-time (stream control-string tz time)
  (multiple-value-bind (second minute hour date month year day daylight-p zone)
      (decode-universal-time time tz)
    (dolist (item (tokenize-control-string control-string))
      (etypecase item
        (string (write-string item stream))
        (function (funcall item stream
                           second minute hour date month year day daylight-p zone))))))

                                       

(defun format-time (stream-designator control-string
                    &key (tz 0) (time (get-universal-time)))
  "Format a universal time into a human-readable time."
  (etypecase stream-designator
    (null
     (with-output-to-string (stream)
       (%format-time stream control-string tz time)))
    (string
     (with-output-to-string (stream stream-designator)
       (%format-time stream control-string tz time)))
    ((member t)
     (%format-time *standard-output* control-string tz time))
    (stream
     (%format-time stream-designator control-string tz time))))


(defun time-formatter-form (item)
  (if (stringp item)
      `(write-string ,item)
      `(funcall ,item *standard-output*
        second minute hour date month year day daylight-p zone)))


(defun %time-formatter-body (control-string)
  (let* ((items (tokenize-control-string control-string))
         (forms (mapcar #'time-formatter-form items)))
    `(lambda (*standard-output* &key (tz 0) (time (get-universal-time)))
      ,@(if (every #'stringp items)
            `((declare (ignore tz time))
              , at forms)
            `((multiple-value-bind
                    (second minute hour date month year day daylight-p zone)
                  (decode-universal-time time tz)
                , at forms))))))
        

(defmacro time-formatter (control-string)
  `#',(%time-formatter-body control-string))






More information about the Small-cl-src mailing list