ToCDocOverviewCGDocRelNotesFAQIndexPermutedIndex
Allegro CL version 8.2
Unrevised from 8.1 to 8.2. Moderate update since 8.2 release.
8.1 version

Date-time (ISO 8601) support in Allegro CL

This document contains the following sections:

1.0 date-time introduction
2.0 ISO 8601
   2.1 ISO 8601 dates
   2.2 ISO 8601 times
   2.3 Parsing ISO 8601 date-time representations
   2.4 Validating ISO 8601 date-time representations
3.0 Conversions between date-time and CL universal-time
   3.1 Getting a date-time value as a string
4.0 Time-intervals
5.0 Adding/Subtracting Durations
6.0 date-time classes
   6.1 The date-time class
   6.2 The duration class
   6.3 The time-interval class
7.0 date-time operators
8.0 date-time variables
9.0 Copyright notice

Module and package names

The 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.



1.0 date-time introduction

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.



2.0 ISO 8601

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 9.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.


2.1 ISO 8601 dates

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.


2.2 ISO 8601 times

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>.


2.3 Parsing ISO 8601 date-time representations

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.

Example

 > (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.

Example [assumes the current package uses the util.date-time package]

 > (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

2.4 Validating ISO 8601 date-time representations

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):


3.0 Conversions between date-time and CL universal-time

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 number). 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.

Example 1 [assumes the current package uses the util.date-time package]

 > (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

Example 2 [assumes the current package uses the util.date-time package]

 > (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:

Examples [assumes the current package uses the util.date-time package]

;; 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.

Examples [assumes the current package uses the util.date-time package]

 (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>

3.1 Getting a date-time value as a string

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):


4.0 Time-intervals

ISO 8601 specifies textual representations for time-intervals. They may be specified in four ways:

  1. Start and end, such as 2002-03-01T13:00:00Z/2003-05-11T15:30:00Z
  2. Start and duration, such as 2002-03-01T13:00:00Z/P1Y2M10DT2H30M
  3. Duration and end, such as P1Y2M10DT2H30M/2003-05-11T15:30:00Z
  4. Duration only, such as P1Y2M10DT2H30M

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.

Example [assumes the current package uses the util.date-time package]

 > (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


5.0 Adding/Subtracting Durations

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:

  1. The <year, month> of the duration is added to or subtracted from the date. If the day is out of range, it is pinned to be within range. For example, April 31 is pinned to April 30.
  2. The <day, hour, minute, second> is added/subtracted component-wise. This operation can cause the year and month to change.

Duration addition and subtraction are not inverse operations

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.

Example [assumes the current package uses the util.date-time package]

;; 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.)

Duration addition is not associative

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.

Duration instances

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" @ ...>


6.0 date-time classes


6.1 The date-time class

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.


6.2 The duration class

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.)


6.3 The time-interval class

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.)



7.0 date-time operators

The following operators are supported in the datetime module. Each is described on its own documentation page.



8.0 date-time variables

The following variables are defined in the datetime module:



9.0 Copyright notice

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-2012, Franz Inc. Oakland, CA., USA. All rights reserved.
Documentation for Allegro CL version 8.2. This page was not revised from the 8.1 page.
Created 2010.1.21.

ToCDocOverviewCGDocRelNotesFAQIndexPermutedIndex
Allegro CL version 8.2
Unrevised from 8.1 to 8.2. Moderate update since 8.2 release.
8.1 version