| Allegro CL version 10.1 Unrevised from 10.0 to 10.1. 10.0 version |
This document contains the following sections:
1.0 date-time introductionThe datetime module is loaded by evaluating:
(require :datetime)
Symbols naming operators, variables, and classes in the datetime
module are in the util.date-time
package. The
examples in the documentation assume (require
:datetime)
has been evaluated and that (use-package
:util.date-time)
has been evaluated in the current package.
The datetime module provides tools to parse and generate time expressions using the ISO 8601 standard as well as to manipulate and convert the resulting date-time instances.
Specifically, the datetime module does the following:
The Allegro locale-print-time and locale-format-time functions can be used to
display the contents of the date-time
instances. The default
print-object method for
date-time objects displays the date-time instance in ISO 8601
format. The global variable util.date-time:*date-time-fmt*
can be
used to override the default date-time printer.
Several additional functions for converting strings in a variety of time formats to universal times, and vice versa are also available. They are described in Section 9.0 General date to universal time parsers below.
ISO 8601 is an international standard for representing dates in the Gregorian calendar and times and representations of periods of times. (There are descriptions in a number of places. See, for example, ISO 8601 in the Wikipedia Encyclopedia, where you will find additional links. This essay was adapted from that Wikipedia article -- see Section 10.0 Copyright notice.)
ISO 8601 specifies formats for specifying dates (see Section 2.1 ISO 8601 dates) and times (see Section 2.2 ISO 8601 times).
All dates use the Gregorian Calendar, the calendar currently in use almost everywhere on earth, even dates prior to the implementation of the Gregorian Calendar on 15 October 1582. This means that the ISO 8601 date for an event prior to 15 October 1582 will very likely be different from the common specification of that date, and certainly be different from contemporary specifications of that date. (Thus, 14 October 1582 is a valid ISO 8601 date, but there was no such date in areas that accepted the Gregorian Calendar because that was when the adjustment of 10 days was made.
Also, ISO 8601 specifies that there was a year 0 while most references to BCE (Before Common Era) years assume there was no year 0. Thus Caesar invaded Britain in 54 BCE by most accounts but 53 BCE in ISO 8601.
These details are important for understanding and using ISO 8601, particularly for pre-modern dates, but we will not discuss them further here. Follow the links to discussions of ISO 8601 above to find out more about its definitions and background.
In the rest of this essay, we discuss the implementation and interface to ISO 8601 dates in Allegro CL.
An ISO 8601 description of a date can be as a
A Calendar date is written as YYYY-MM-DD (extended) or YYYYMMDD (basic). YYYY indicates a year with century. MM indicates the month of the year (01 through 12). DD indicates the day of that month (01 through 31). Examples: "19850412" and "1985-04-12" denote the same date: the 12th of April in 1985.
A Week date is written as YYYY-Www-D (extended) or YYYYWwwD (basic). YYYY indicates a year, ww indicates a week number (01 through 53 -- the W is the letter W), and D is the weekday number (1 through 7). Monday is 1, Sunday is 7. Week 01 is the week with the year's first Thursday in it. Examples: "1985-W15-5" is the fifth day of the week containing the fifteenth Thursday of the year 1985. This happens to be equivalent to the 12th of April in 1985 (1985-04-12).
Note that the week year can be different than the normal (Gregorian) year for dates at the end or start of the ordinary year. For example, "2008-12-29" is written as "2009-W01-1".
An Ordinal date is written as YYYY-DDD (extended) or YYYYDDD (basic). YYYY indicates a year, and DDD is the ordinal day in the year. For example, the date representing "1985-04-12" can also be written "1985-102".
ISO 8601 describes possible reduced precision and truncated date representations. For example, YYYY-MM is used to specify a month. Also, YY-MM-DD is used to specify a date in an implied century.
It is also possible to represent negative years or years greater than 9999. However, the Lisp datetime module parser needs to know that extra year digits are being used parsed to avoid ambiguities.
An ISO 8601 description of a time is HH:MM:SS (extended) or HHMMSS (basic). A 24-hour clock is used. Fractions may also be used with the time elements. For example, "14 hours, 30 and one half minutes" can be represented as "14:30.5" or "14:30,5" (that is, either a period or a comma may be used as the fraction separator).
An ISO 8601 description of a time zone is <time>Z or <time> followed by a plus (+) or minus (-) followed by HH:MM (extended) or HHMM (basic). The number is the offset from UTC. (UTC is Coordinated Universal Time, also called Greenwich Mean Time.)
An ISO 8601 description of a date-time combination is <date>T<time>. For example, 1985-04-12T23:20:50+02:00 is 23:20:50 on 1985-04-12 in the time zone that is 2 hours ahead of UTC.
Note that the Lisp datetime module allows a single space to be used in place of T to separate <date> and <time>.
The date-time
function is used to parse ISO 8601 date-time expressions. A date-time
instance is
returned.
A date-time
instance holds not only the result of the parse, but, when applicable,
other equivalent representations for the parsed date-time.
> (require :datetime) t > (use-package :util.date-time) t > (setq d (date-time "1985-04-12")) #<date-time "1985-04-12" @ ...> ;; Directly parsed information > (date-time-year d) 1985 ;; Directly parsed information > (date-time-ymd-month d) 4 ;; Directly parsed information > (date-time-ymd-day d) 12 ;; Calculated information from the parse > (date-time-yd-day d) 102 ;; Here is DESCRIBE on the date-time instance D. ;; Notice that no time information is present as ;; none was provided: > (describe d) #<date-time "1985-04-12" @ ...> is a date-time. ymd-yd-before-year-0 nil ymd-yd-century 19 ymd-yd-year-in-century 85 ymd-month 4 ymd-day 12 yd-day 102 ywd-before-year-0 nil ywd-century 19 ywd-decade-in-century 8 ywd-year-in-decade 5 ywd-week 15 ywd-day 5 zone-hour nil zone-minute nil hour nil hourf nil minute nil minutef nil second nil secondf nil Displayed using various locale-print-time fmts: "%Y-%m-%d" 1985-04-12 Calendar Date: "%Y-%m-%d" 1985-04-12 Ordinal Date: "%Y-%j" 1985-102 Week Date: "%G-W%V-%u" 1985-W15-5 >
By default, the calculations that complete the date-time instance are
done at parse time. These calculations can be suppressed by setting
the complete keyword argument to date-time to nil
(thus instructing the system not to calculate
value for all slots). An incomplete date-time instance can
subsequently be completed using the complete-date-time function, which
updates the date-time instance.
> (setq d (date-time "1985-04-12" :complete nil)) #<date-time "1985-04-12" @ ...> > (date-time-ymd-day d) 12 ;; The date-time was not completed, so the following ;; reader returns nil. > (date-time-yd-day d) nil ;; Update the date-time instance. > (complete-date-time d) #<date-time "1985-04-12" @ ...> > (date-time-yd-day d) 102
No checking in done to determine whether a string passed to date-time is a valid date. So, for example, no checking is done in this case:
cl-user(48) (util.date-time:date-time "2011-02-30") #<util.date-time:date-time "2011-02-30" @ #x2101948a>
Even though there is no 30th of February (in any year) a date-time object is returned. We do not do validation since it would slow down every call. It is easy enough to write a validation function, like the following:
cl-user(49): (defun valid-date-p (s) (string= s (format nil "~,v/locale-format-time/" "%Y-%m-%d" (util.date-time:complete-date-time s)))) valid-date-p cl-user(50): (valid-date-p "2011-02-28") t cl-user(51): (valid-date-p "2011-02-29") nil cl-user(52): (valid-date-p "2012-02-29") t cl-user(53):
The datetime Lisp module provides functions, date-time-to-ut and ut-to-date-time, that convert date-time instances holding both a date and a time to/from the associated Common Lisp universal-time (a non-negative integer) or an extended CL universal time (which can be zero or a ratio, positive or negative). Since ISO 8601 allows for dates/times to be specified outside the range of those representable by universal-time (i.e., dates before 1900, or times containing fractional seconds), an extended universal-time is used. This extended universal-time allows for negative values to represent dates before 1900. In addition, extended universal-times can be non-integer rationals representing times with fractional seconds.
> (setq d (date-time "1985-04-12T23:20:50+02:00")) #<date-time "1985-04-12T23:20:50+02:00" @ ...> > (setq ut (date-time-to-ut d)) 2691177650 > (decode-universal-time ut (- (date-time-zone d))) ;; Note the sign of the time zone is reversed: the ISO 8601 convention ;; and the CL convention (as implemented in Allegro CL) are opposite, ;; so a negative time zone in one is a positive time zone in the other. 50 ; second 20 ; minute 23 ; hour 12 ; day 4 ; month 1985 ; year 4 ; day of week nil ; daylight savings time -2 ; timezone
> (setq d (date-time "1885-04-12T23:20:50+02:00")) #<date-time "1885-04-12T23:20:50+02:00" @ ...> ;; The following returns a negative number since the date is before 1900. ;; > (setq ut (date-time-to-ut d)) -464481550
There are three special date-time designators:
:now
means the current time
:today
means the start (00:00:00) of the
current day
:zero
means the start of Jan 1, 0000
;; The examples for :now and :today are correct when the ;; document was written. You will, of course, get different values (date-time :now) => #<date-time "2006-07-11T22:28:08" @ #x7185242a> (date-time :today) => #<date-time "2006-07-11T00:00:00" @ #x7185ec6a> (date-time :zero) => #<date-time "0000-01-01T00:00:00" @ #x717dcad2>
date-time-to-ut
takes a defaults keyword argument whose default
value is :zero
. Thus, the default behavior is to
merge the argument date-time
instance with
(date-time :zero)
to get a complete date-time
instance that can be converted to universal-time (see merge-date-times). You can
override the default by specifying :today
,
:now
, or any other date-time instance.
(ut-to-date-time (date-time-to-ut "1985-04-12")) => #<date-time "1985-04-12T00:00:00+08:00" @ #x718bad0a> (ut-to-date-time (date-time-to-ut "1985-04")) => #<date-time "1985-04-01T00:00:00+08:00" @ #x718c108a> (ut-to-date-time (date-time-to-ut "1985")) => #<date-time "1985-01-01T00:00:00+08:00" @ #x718c7092> ;; Note that the default may not be desirable in the following case: ;; (ut-to-date-time (date-time-to-ut "85-04-12")) => #<date-time "0085-04-12T00:00:00+08:00" @ #x718ebcba> ;; The following are ways to specify different defaults: (ut-to-date-time (date-time-to-ut "85-04-12" :defaults (merge-date-times "1900" :zero))) => #<date-time "1985-04-12T00:00:00+08:00" @ #x7191cc8a> (ut-to-date-time (date-time-to-ut "85-04-12" :defaults :today)) => #<date-time "2085-04-12T00:00:00+08:00" @ #x71924f5a> (ut-to-date-time (date-time-to-ut "85-04-12" :defaults :now)) => #<date-time "2085-04-12T22:36:32+08:00" @ #x7192cce2>
Once you have a date-time value, you can print it with princ or get its printed representation as a string with format. Here is an example. We start with a universal time and go from there:
cl-user(16): (setq ut (get-universal-time)) 3488049643 ;; print a universal-time as a date-time ;; cl-user(25): (princ (util.date-time:ut-to-date-time ut)) 2010-07-13T15:40:43-07:00 ;; or put it in a string ;; cl-user(26): (format nil "~a" (util.date-time:ut-to-date-time ut)) "2010-07-13T15:40:43-07:00" ;; or get all sorts of other things from it ;; cl-user(17): (locale-print-time ut :fmt "Day is %A, Month is %B") Day is Tuesday, Month is July cl-user(18): (locale-print-time ut :fmt "Day is %A, Month is %B" :locale "nl_NL") Day is dinsdag, Month is juli cl-user(19): (locale-print-time ut :show-date t :show-time t :locale "nl_NL") dinsdag 13 juli 2010 15:40:43 uur cl-user(20): (locale-print-time ut :show-date t :show-time t :locale "en_US") Tuesday, July 13, 2010 03:40:43 PM cl-user(21): (locale-print-time ut :fmt "%Y-%m-%dT%H:%M:%S") 2010-07-13T15:40:43 cl-user(22):
ISO 8601 specifies textual representations for time-intervals. They may be specified in four ways:
A repeating interval is formed by adding "Rn/" to the beginning of an interval expression. For example, to repeat the interval P1Y2M10DT2H30M five times starting at 2002-03-01T13:00:00Z, use R5/2002-03-01T13:00:00Z/P1Y2M10DT2H30M
Time-intervals are represented as objects in the Lisp datetime module. They can be parsed using the parse-iso8601 function.
> (describe (parse-iso8601 "R5/2002-03-01T13:00:00Z/P1Y2M10DT2H30M")) #<time-interval R5/2002-03-01T13:00:00Z/P1Y2M10DT2H30M0S @ ...> is an instance of #<standard-class time-interval>: The following slots have :instance allocation: start #<date-time "2002-03-01T13:00:00Z" @ ...> end nil duration #<duration 1Y2M10DT2H30M0S @ ...> recurrences 5
The Lisp datetime module provides a way to add and subtract durations to/from date-time instances. The following steps describe the addition/subtraction procedures:
Adding durations of days, hours, minutes, and seconds to a date-time is an unambiguous operations, but adding months (often) and years (sometimes) is more problematic.
Consider a Month/Day: adding a month should result in (+ Month 1)/Day. That definition is fine when there is a (+ Month 1)/Day but often there isn't. This Adding a month to April 4 gives May 4, and a month to June 30 gives July 30. But what about adding a month to May 31? There is no June 31. The result could be July 1, but June 30 seems to make more sense (it would be uninitutive for the addition of one month to result in the Month value increasing by 2). ISO 8601 does not address this point. Allegro CL follows the XML schema behavior, which follows this rule:
The addition of one month result in the month value being increased by 1 (mod 12) and the day value being unchanged, unless the day does not exist in the new month, in which case the day value is the largest possible for the new month (i.e. the last day of the month).
But following that rule results duration addition and subtraction not
being inverse operations. For example, adding 1 month to
May 31
yields June 30
, but
subtracting 1 month from June 30
yields
May 30
, and not May 31
.
;; Here we use January 31, in a leap year: > (setq one-month (time-interval-duration (time-interval "P1M"))) #<duration 0Y1M0DT0H0M0S @ ...> > (setq dt (add-duration (date-time "19840131") one-month)) #<date-time "1984-02-29" @ ...> > (subtract-duration dt one-month) #<date-time "1984-01-29" @ ...> ;; Note that adding one month to January 31 (in a leap year) ;; produced February 29, and subtracting one month from that ;; produced January 29, not January 31. ;; The same point is made with this example (using August 31): > (subtract-duration (add-duration (date-time "--0831") one-month) one-month) #<date-time "--08-30" @ ...>
The same problem occurs with years, but only with leap years. February 29, 1984, or 1984/02/29, plus one year is February 28, 1985, or 1985/02/28 (as February 29, 1985 does not exist). 1985/02/28 minus one year is 1984/02/28, not 1984/02/29.
Duration addition and subtraction restricted to weeks, days, hours, minutes, and second are inverse operations because there is never any ambiguity about what adding or subtracting those values gives.
(The problem arises becaus duration addition for months and years are not a one-to-one functions. One month plus 1985/01/31, 1985/01/30, 1985/01/29, and 1985/01/28 all give 1985/02/28, so 1985/02/28 minus one month is necessarily ambiguous. This ambiguity is inherent: there is no definition of "adding one month" other than "always adding a specific number (31?) of days regardless of the starting month" that will not be ambiguous. But addition of days, hours, minutes, and second is always one to one and so always invertable.)
One consequence of the fact that adding a month or year is defined to keep the same day in the new month if possible, otherwise use the last day of the month is that duration addition is not associative: a date-time plus one month plus one month need not be the same as the date-time plus two months: (August 31 plus 2 months in October 31. August 31 plus one month is September 30, which plus one month is October 30. Similarly, February 29, 1984 plus four years is February 29, 1988, while February 29, 1984 plus two years in February 28, 1986, which plus two years is February 28, 1988.
A duration instance can be created directly without using the time-interval parser by using the Lisp duration function.
> (add-duration (date-time "1985-04-10T10:30:40") (duration "1MT1H4S")) #<date-time "1985-05-10T11:30:44" @ ...>
Instances of the date-time
encode a specific date and
time. The class is complicated because it includes several slots used
to hold different formats of the specific date-time instance. Slots
named with the ymd- prefix are those used for Calendar
(year-month-date) dates. Slots named with the yd- prefix are
those used for Ordinal (year-date) dates. Slots named with the
ywd- prefix are those used for Week (year-week-date) dates.
Some slots use the ymd-yd- prefix to indicate that they can be
used for either Calendar or Ordinal dates.
We describe some of the core slots here:
date-time
instances are to be created by the date-time and merge-date-times functions. They can
be updated by the merge-date-times and complete-date-time
functions. Otherwise, date-time
instances are immutable.
Instances of the duration
class encode an unanchored
period of time. (An unanchored time period has no specified start or
end. The class time-interval
has a duration and a
start and end.)
The slots in the duration
class are as follows. The
slot value can be accessed by the indicated reader generic
function. (The slots have no defined writers and should not be
modified.)
Instances of the time-interval
class encode a
specific period of time with a duration, a start, and an end, and
recurrences. (Any or all slots may be unspecified.)
The slots in the time-interval
class are as
follows. The slot value can be accessed by the indicated reader
generic function. (The slots have no defined writers and should not be
modified.)
date-time
object. The reader is
time-interval-start.
date-time
object or nil
. The reader is time-interval-end.
duration
object or nil
. The reader is time-interval-duration.
nil
. The reader is time-interval-recurrences.
The following operators are supported in the datetime module. Each is described on its own documentation page.
The following variables are defined in the datetime module:
There are three functions which parse strings representing dates and times, returning universal times; take universal times and return strings in various time formats; and convert strings with special commands into formatter-like functions.
These functions are not part of the date-time module described above
in this document (they are instead part of
the :anydate
module, which is autoloaded when any
of the exported functions in the module is called). They are useful
utility functions which can be used in conjunction with the date-time
functionality.
The functions are:
See the individual function descriptions for more information. Here are some example of how the functions work:
(string-to-universal-time "Thu, 01 Jan 04 19:48:21 GMT" :format :rfc2822) => 3281975301 :rfc2822 0 (universal-time-to-string 3281975301 :format :rfc2822) => "Thu, 01 Jan 2004 11:48:21 -0800" (string-to-universal-time "Thu, 01 Jan 04 19:48:21 GMT") => 3281975301 :rfc2822 0 (universal-time-to-string 3281975301) => "2004-01-01T11:48:21" (string-to-universal-time "Thu, 01 Jan 2004 19:48:21 GMT" :format :rfc2822) => 3281975301 :rfc2822 0 (universal-time-to-string 3281975301 :format :rfc2822) => "Thu, 01 Jan 2004 11:48:21 -0800" (string-to-universal-time "Thu, 01 Jan 2004 19:48:21 GMT") => 3281975301 :rfc2822 0 (universal-time-to-string 3281975301) => "2004-01-01T11:48:21" (string-to-universal-time "2003-12-31T10:14:55-08:00" :format :w3cdtf) => 3281883295 :w3cdtf 28800 (universal-time-to-string 3281883295 :format :w3cdtf) => "2003-12-31T10:14:55" (string-to-universal-time "2003-12-31T10:14:55-08:00") => 3281883295 :w3cdtf 28800 (universal-time-to-string 3281883295) => "2003-12-31T10:14:55" (string-to-universal-time "2003-12-31T10:14:55Z" :format :w3cdtf) => 3281854495 :w3cdtf 0 (universal-time-to-string 3281854495 :format :w3cdtf) => "2003-12-31T02:14:55" (string-to-universal-time "2003-12-31T10:14:55Z") => 3281854495 :w3cdtf 0 (universal-time-to-string 3281854495) => "2003-12-31T02:14:55" (string-to-universal-time "2003" :format :w3cdtf) => 3250396800 :w3cdtf :time-zone-not-specified (universal-time-to-string 3250396800 :format :w3cdtf) => "2003-01-01T00:00:00" (string-to-universal-time "2003") => 3250396800 :w3cdtf :time-zone-not-specified (universal-time-to-string 3250396800) => "2003-01-01T00:00:00" (string-to-universal-time "2003-12" :format :w3cdtf) => 3279254400 :w3cdtf :time-zone-not-specified (universal-time-to-string 3279254400 :format :w3cdtf) => "2003-12-01T00:00:00" (string-to-universal-time "2003-12") => 3279254400 :w3cdtf :time-zone-not-specified (universal-time-to-string 3279254400) => "2003-12-01T00:00:00" (string-to-universal-time "2003-12-31" :format :w3cdtf) => 3281846400 :w3cdtf :time-zone-not-specified (universal-time-to-string 3281846400 :format :w3cdtf) => "2003-12-31T00:00:00" (string-to-universal-time "2003-12-31") => 3281846400 :w3cdtf :time-zone-not-specified (universal-time-to-string 3281846400) => "2003-12-31T00:00:00" (string-to-universal-time "20031231" :format :iso8601) => 3281846400 :iso8601 :time-zone-not-specified (universal-time-to-string 3281846400 :format :iso8601) => "2003-12-31T00:00:00" (string-to-universal-time "20031231") => 3281846400 :iso8601 :time-zone-not-specified (universal-time-to-string 3281846400) => "2003-12-31T00:00:00" (string-to-universal-time "20031231" :format :iso8601 :native t) => #<util.date-time:date-time "2003-12-31" @ #x1000117b1f2> nil nil (string-to-universal-time "Sun Jan 4 16:29:06 2004" :format :asctime) => 3282251346 :asctime :time-zone-not-specified (universal-time-to-string 3282251346 :format :asctime) => "Sun Jan 4 16:29:06 2004" (string-to-universal-time "Sun Jan 4 16:29:06 2004") => 3282251346 :asctime :time-zone-not-specified (universal-time-to-string 3282251346) => "2004-01-04T16:29:06" (string-to-universal-time "2004-07-08 23:56:58" :format :mssql) => 3298345018 :mssql :time-zone-not-specified (universal-time-to-string 3298345018 :format :mssql) => "2004-07-08 23:56:58" (string-to-universal-time "2004-07-08 23:56:58") => 3298345018 :iso8601 :time-zone-not-specified (universal-time-to-string 3298345018) => "2004-07-08T23:56:58" (string-to-universal-time "2004-07-08 23:56:58.1" :format :mssql) => 32983450181/10 :mssql :time-zone-not-specified (universal-time-to-string 32983450181/10 :format :mssql) => "2004-07-08 23:56:58.1" (string-to-universal-time "2004-07-08 23:56:58.1") => 32983450181/10 :iso8601 :time-zone-not-specified (universal-time-to-string 32983450181/10) => "2004-07-08T23:56:58.1" (excl::universal-time-to-string (get-universal-time) :format "%H:%M:%S") => "11:01:44" (setq ut 3603660634) (universal-time-to-string ut :relative (+ ut 1 (* 10 3600))) => "10:00:01" (setq f (compile nil (ut-to-string-formatter "%Dd%2Hh%2Mm%2Ss"))) (universal-time-to-string ut :relative (+ ut (* 40 3666)) :format f) => "1d16h44m00" (setq f (compile nil (ut-to-string-formatter "%D day%p, %H hour%p, %M minute%p and %S second%p"))) (universal-time-to-string 3603660634 :relative (+ 3603660634 (* 41 3666)) :format f) => "1 day, 17 hours, 45 minutes and 6 seconds"
Material in this document is adapted from the Wikipedia article on ISO 8601 from July, 2006 (http://en.wikipedia.org/wiki/ISO_8601. Wikipedia material is governed by the GNU Documentation Copyleft (see GNU Free Documentation License). Therefore, notwithstanding any other notices in the document, this essay is governed by that same agreement. (But documents linked to from this document are not governed by that agreement unless they explicitly say so.) The text of this document is available in HTML using the usual View Source command. This essay is adapted from the Wikipedia article. It is not a copy and any errors or omissions are the responsibility of Franz Inc. Anyone who uses material in this essay under the Copyleft, please (1) link to this article using the link http://franz.com/support/documentation/current/doc/date-time.htm and (2) say explicitly that you have modified it (if you have).
Copyright (c) 1998-2022, Franz Inc. Lafayette, CA., USA. All rights reserved.
This page was not revised from the 10.0 page.
Created 2019.8.20.
| Allegro CL version 10.1 Unrevised from 10.0 to 10.1. 10.0 version |