Times Are Hard for Racketeers, too

APIs dealing with dates and times must be hard to get right. I'm not aware of any programming language whose standard library for dealing with times is both intuitive and comprehensive. Racket has built-in libraries that are second to none, but even it presently leaves something to be desired when it comes to support for handling dates and times.

For example, Racket's racket/date module (in its current incarnation) includes a date->string function, but no string->date function. In other words, it doesn't include a function for parsing time strings.

I recently needed time parsing as I wanted to specify times to a Racket program as strings such as "Sun, 06 Jan 2013 02:21:31 +0100", while using objects (such as Racket's date structure) as in-memory storage format in order to make the individual time components (such as month and year) readily queryable.

For the parsing support I turned to SRFI 19: Time Data Types and Procedures, which Racket implements in the form of the srfi/19 module. Said module does include a string->date function, albeit for a different date structure (tm:date).

One possibility for parsing and formatting times then is to use both racket/date and srfi/19 modules, and to convert between their respective date structures. A restriction there is that Racket date objects do not appear to be readily constructible from time components, but converting via seconds since Unix epoch (i.e., Unix time) is workable.

The code below shows how to convert between RFC 2822 time strings, Racket's native date objects, and Unix times (integers). All of the functions return times in UTC, regardless of representation.

#lang racket

(require (prefix-in d. racket/date))
(require (prefix-in s. srfi/19))

(define (date->unix-time d) ;; struct date -> integer
  (d.date->seconds d #f))

(define (unix-time->date t) ;; integer -> struct date
  (seconds->date t #f))

(define (rfc2822->unix-time s) ;; string -> integer
  (let ((d (s.string->date s "~a, ~d ~b ~Y ~H:~M:~S ~z")))
    (s.time-second (s.date->time-utc d))))

(define (date->rfc2822 d) ;; struct date -> string
  (parameterize ((d.date-display-format 'rfc2822))
    (d.date->string d #t)))

(define (unix-time->rfc2822 t) ;; integer -> string
  (date->rfc2822 (unix-time->date t)))

(define (rfc2822->date s) ;; string -> struct date
  (unix-time->date (rfc2822->unix-time s)))

(provide rfc2822->date rfc2822->unix-time
         unix-time->rfc2822 date->rfc2822)

Note that 'rfc2822 is one of the possible date-display-format choices supported by racket/date, and my personal favorite due to its readability. It is also widely supported. For example, try date -R on the Linux command line, or Time.now.rfc2822 in Ruby. For Emacs one can define support easily enough.

(defun insert-current-time-in-rfc2822 ()
  (interactive)
  (insert (format-time-string "%a, %d %b %Y %T %z")))

Addendum (12 May 2013)

Thanks to Asumu Takikawa's recent work on Racket date and srfi/19 compatibility, starting from Racket 5.3.4 the srfi/19 string->date function returns a racket/base compatible date (apparently provided that the format string has day, month, and year components). Going forward then for code like the above there should be no need to use srfi/19 functions other than string->date. The above code should still work, though.

Note (updated 13 Oct 2015): In some Racket versions (starting with 5.3.4, but by now fixed) the rfc2822->unix-time function given above does not work, due to a minor issue with srfi/19, one that causes an error when string->date is used with the format directive "~a" (and possibly some others). If you're using an affected version, one fix is to change the file “srfi/19/time.rkt” (somewhere around line 1470) to say (do-nothing (lambda (val object) object)), to avoid the complaint about 0 values in a context where 1 is expected.