f9199e12de0744e8b4504149115ed18bbb48b424
[sbcl.git] / src / code / format-time.lisp
1 ;;; time printing routines built upon the Common Lisp FORMAT function
2
3 ;;;; This software is part of the SBCL system. See the README file for
4 ;;;; more information.
5 ;;;;
6 ;;;; This software is derived from the CMU CL system, which was
7 ;;;; written at Carnegie Mellon University and released into the
8 ;;;; public domain. The software is in the public domain and is
9 ;;;; provided with absolutely no warranty. See the COPYING and CREDITS
10 ;;;; files for more information.
11
12 (in-package "SB!EXT")
13
14 (defconstant abbrev-weekday-table
15   '#("Mon" "Tue" "Wed" "Thu" "Fri" "Sat" "Sun"))
16
17 (defconstant long-weekday-table
18   '#("Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday"
19      "Sunday"))
20
21 (defconstant abbrev-month-table
22   '#("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov"
23      "Dec"))
24
25 (defconstant long-month-table
26   '#("January" "February" "March" "April" "May" "June" "July" "August"
27      "September" "October" "November" "December"))
28
29 ;;; The timezone-table is incomplete but workable.
30
31 (defconstant timezone-table
32   '#("GMT" "" "" "" "" "EST" "CST" "MST" "PST"))
33
34 (defconstant daylight-table
35   '#(nil nil nil nil nil "EDT" "CDT" "MDT" "PDT"))
36
37 ;;; Valid-Destination-P ensures the destination stream is okay
38 ;;; for the Format function.
39
40 (defun valid-destination-p (destination)
41   (or (not destination)
42       (eq destination 't)
43       (streamp destination)
44       (and (stringp destination)
45            (array-has-fill-pointer-p destination))))
46
47 ;;; Format-Universal-Time - External.
48
49 ;;; CMU CL made the default style :SHORT here. I've changed that to :LONG, on
50 ;;; the theory that since the 8/7/1999 style is hard to decode unambiguously,
51 ;;; you should have to ask for it explicitly. (I prefer YYYYMMDD myself, since
52 ;;; it sorts properly.:-) -- WHN 19990831
53 ;;;
54 ;;; FIXME: On the CMU CL mailing list 30 Jan 2000, Pierre Mai suggested
55 ;;;   OTOH it probably wouldn't be a major problem to change compile-file to 
56 ;;;   use for example :long, so that the output would be Month DD, YYYY, or
57 ;;;   even better to extend format-universal-time with a flag to output ISO
58 ;;;   8601 formats (like e.g. :iso-8601 and :iso-8601-short) and migrate
59 ;;;   slowly towards ISO dates in the user code...
60 ;;; The :ISO-8601 and :ISO-8601-SHORT options sound sensible to me. Maybe
61 ;;; someone will do them for CMU CL and we can steal them here.
62 (defun format-universal-time (destination universal-time
63                                           &key
64                                           (timezone nil)
65                                           (style :long)
66                                           (date-first t)
67                                           (print-seconds t)
68                                           (print-meridian t)
69                                           (print-timezone t)
70                                           (print-weekday t))
71   #!+sb-doc
72   "Format-Universal-Time formats a string containing the time and date
73    given by universal-time in a common manner. The destination is any
74    destination which can be accepted by the Format function. The
75    timezone keyword is an integer specifying hours west of Greenwich.
76    The style keyword can be :SHORT (numeric date), :LONG (months and
77    weekdays expressed as words), :ABBREVIATED (like :long but words are
78    abbreviated), or :GOVERNMENT (of the form \"XX Month XXXX XX:XX:XX\")
79    The keyword argument DATE-FIRST, if nil, will print the time first instead
80    of the date (the default). The PRINT- keywords, if nil, inhibit
81    the printing of the obvious part of the time/date."
82   (unless (valid-destination-p destination)
83     (error "~A: Not a valid format destination." destination))
84   (unless (integerp universal-time)
85     (error "~A: Universal-Time should be an integer." universal-time))
86   (when timezone
87     (unless (and (rationalp timezone) (<= -24 timezone 24))
88       (error "~A: Timezone should be a rational between -24 and 24." timezone))
89     (unless (zerop (rem timezone 1/3600))
90       (error "~A: Timezone is not a second (1/3600) multiple." timezone)))
91
92   (multiple-value-bind (secs mins hours day month year dow dst tz)
93       (if timezone
94           (decode-universal-time universal-time timezone)
95           (decode-universal-time universal-time))
96     (declare (fixnum secs mins hours day month year dow))
97     (let ((time-string "~2,'0D:~2,'0D")
98           (date-string
99            (case style
100              (:short "~D/~D/~D")             ;;  MM/DD/Y
101              ((:abbreviated :long) "~A ~D, ~D")  ;;  Month DD, Y
102              (:government "~2,'0D ~:@(~A~) ~D")      ;;  DD MON Y
103              (t
104               (error "~A: Unrecognized :style keyword value." style))))
105           (time-args
106            (list mins (max (mod hours 12) (1+ (mod (1- hours) 12)))))
107           (date-args (case style
108                        (:short
109                         (list month day year))
110                        (:abbreviated
111                         (list (svref abbrev-month-table (1- month)) day year))
112                        (:long
113                         (list (svref long-month-table (1- month)) day year))
114                        (:government
115                         (list day (svref abbrev-month-table (1- month))
116                               year)))))
117       (declare (simple-string time-string date-string))
118       (when print-weekday
119         (push (case style
120                 ((:short :long) (svref long-weekday-table dow))
121                 (:abbreviated (svref abbrev-weekday-table dow))
122                 (:government (svref abbrev-weekday-table dow)))
123               date-args)
124         (setq date-string
125               (concatenate 'simple-string "~A, " date-string)))
126       (when (or print-seconds (eq style :government))
127         (push secs time-args)
128         (setq time-string
129               (concatenate 'simple-string time-string ":~2,'0D")))
130       (when print-meridian
131         (push (signum (floor hours 12)) time-args)
132         (setq time-string
133               (concatenate 'simple-string time-string " ~[AM~;PM~]")))
134       (apply #'format destination
135              (if date-first
136                  (concatenate 'simple-string date-string " " time-string
137                               (if print-timezone " ~A"))
138                  (concatenate 'simple-string time-string " " date-string
139                               (if print-timezone " ~A")))
140              (if date-first
141                  (nconc date-args (nreverse time-args)
142                         (if print-timezone
143                             (list (timezone-name dst tz))))
144                  (nconc (nreverse time-args) date-args
145                         (if print-timezone
146                             (list (timezone-name dst tz)))))))))
147
148 (defun timezone-name (dst tz)
149   (if (and (integerp tz)
150            (or (and dst (= tz 0))
151                (<= 5 tz 8)))
152       (svref (if dst daylight-table timezone-table) tz)
153       (multiple-value-bind (rest seconds) (truncate (* tz 60 60) 60)
154         (multiple-value-bind (hours minutes) (truncate rest 60)
155           (format nil "[~C~D~@[~*:~2,'0D~@[~*:~2,'0D~]~]]"
156                   (if (minusp tz) #\- #\+)
157                   (abs hours)
158                   (not (and (zerop minutes) (zerop seconds)))
159                   (abs minutes)
160                   (not (zerop seconds))
161                   (abs seconds))))))
162
163 ;;; Format-Decoded-Time - External.
164 (defun format-decoded-time (destination seconds minutes hours
165                                           day month year
166                                           &key (timezone nil)
167                                           (style :short)
168                                           (date-first t)
169                                           (print-seconds t)
170                                           (print-meridian t)
171                                           (print-timezone t)
172                                           (print-weekday t))
173   #!+sb-doc
174   "Format-Decoded-Time formats a string containing decoded-time
175    expressed in a humanly-readable manner. The destination is any
176    destination which can be accepted by the Format function. The
177    timezone keyword is an integer specifying hours west of Greenwich.
178    The style keyword can be :short (numeric date), :long (months and
179    weekdays expressed as words), or :abbreviated (like :long but words are
180    abbreviated). The keyword date-first, if nil, will cause the time
181    to be printed first instead of the date (the default). The print-
182    keywords, if nil, inhibit the printing of certain semi-obvious
183    parts of the string."
184   (unless (valid-destination-p destination)
185     (error "~A: Not a valid format destination." destination))
186   (unless (and (integerp seconds) (<= 0 seconds 59))
187     (error "~A: Seconds should be an integer between 0 and 59." seconds))
188   (unless (and (integerp minutes) (<= 0 minutes 59))
189     (error "~A: Minutes should be an integer between 0 and 59." minutes))
190   (unless (and (integerp hours) (<= 0 hours 23))
191     (error "~A: Hours should be an integer between 0 and 23." hours))
192   (unless (and (integerp day) (<= 1 day 31))
193     (error "~A: Day should be an integer between 1 and 31." day))
194   (unless (and (integerp month) (<= 1 month 12))
195     (error "~A: Month should be an integer between 1 and 12." month))
196   (unless (and (integerp year) (plusp year))
197     (error "~A: Hours should be an non-negative integer." year))
198   (when timezone
199     (unless (and (integerp timezone) (<= 0 timezone 32))
200       (error "~A: Timezone should be an integer between 0 and 32."
201              timezone)))
202   (format-universal-time destination
203    (encode-universal-time seconds minutes hours day month year)
204    :timezone timezone :style style :date-first date-first
205    :print-seconds print-seconds :print-meridian print-meridian
206    :print-timezone print-timezone :print-weekday print-weekday))