rfc3339.ss
;; THIS FILE IS GENERATED

(module rfc3339 mzscheme
(require (lib "9.ss" "srfi"))

;;; @Package     rfc3339.scm
;;; @Subtitle    RFC3339 Date and Time Format in Scheme
;;; @HomePage    http://www.neilvandyke.org/rfc3339-scm/
;;; @Author      Neil W. Van Dyke
;;; @AuthorEmail neil@@neilvandyke.org
;;; @Version     0.2
;;; @Date        2005-12-05

;; $Id: rfc3339.scm,v 1.72 2005/12/05 11:28:37 neil Exp $

;;; @legal
;;; Copyright @copyright{} 2005 Neil W. Van Dyke.  This program is Free
;;; Software; you can redistribute it and/or modify it under the terms of the
;;; GNU Lesser General Public License as published by the Free Software
;;; Foundation; either version 2.1 of the License, or (at your option) any
;;; later version.  This program is distributed in the hope that it will be
;;; useful, but without any warranty; without even the implied warranty of
;;; merchantability or fitness for a particular purpose.  See
;;; @indicateurl{http://www.gnu.org/copyleft/lesser.html} for details.  For
;;; other license options and consulting, contact the author.
;;; @end legal

;; (require (lib "9.ss" "srfi"))

(define-syntax %rfc3339:testeez
  (syntax-rules () ((_ x ...)
                    ;; (testeez x ...)
                    (error "Tests disabled.")
                    )))

(define %rfc3339:regexp          regexp)
(define %rfc3339:regexp-match    regexp-match)
(define %rfc3339:regexp-cc-minus "-")

;; TODO: Wait for Pregexp workaround or fix for alternations of clusters, which
;; have a known bug in Pregexp 1e9:
;;
;; (pregexp-match "(b)"     "b")  ==>  ("b" "b")
;; (pregexp-match "(a)|(b)" "b")  ==>  ("b" "b")
;;
;; (require (lib "pregexp.ss"))
;; (define %rfc3339:regexp          pregexp)
;; (define %rfc3339:regexp-match    pregexp-match)
;; (define %rfc3339:regexp-cc-minus "\\-")

;;; @section Introduction

;;; The @code{rfc3339.scm} package implements parsing, formatting, and simple
;;; validation of @uref{ftp://ftp.rfc-editor.org/in-notes/rfc3339.txt, RFC3339}
;;; date and time format, which is a subset of
;;; @uref{http://www.iso.ch/iso/en/prods-services/popstds/datesandtime.html,
;;; ISO 8601}, intended for use in Internet protocols.
;;;
;;; Note that full Scheme support of ISO 8601 is a very different project of
;;; the author, and not at all the intention of @code{rfc3339.scm}.
;;;
;;; @code{rfc3339.scm} requires R5RS, SRFI-6, SRFI-9, and two particular
;;; regular expression functions.  Note that the regular expression functions
;;; in @uref{http://www.ccs.neu.edu/home/dorai/pregexp/pregexp.html, Pregexp
;;; 1e9} will not work, but are expected to work in subsequent versions of
;;; Pregexp.  Thus far, @code{rfc3339.scm} has only been tested under PLT
;;; MzScheme.

;;; @section Record Type

;;; @code{rfc3339-record} is an abstract data type for the information in an
;;; RFC3339 format time and date.  (``@code{rfc3339-string}'' is used in
;;; identifiers to denote the RFC3339 format as a Scheme string.)

(define-record-type rfc3339-record
  (make-rfc3339-record year month mday hour minute second secfrac offsetmin)
  rfc3339-record?
  (year      rfc3339-record:year      rfc3339-record:set-year!)
  (month     rfc3339-record:month     rfc3339-record:set-month!)
  (mday      rfc3339-record:mday      rfc3339-record:set-mday!)
  (hour      rfc3339-record:hour      rfc3339-record:set-hour!)
  (minute    rfc3339-record:minute    rfc3339-record:set-minute!)
  (second    rfc3339-record:second    rfc3339-record:set-second!)
  (secfrac   rfc3339-record:secfrac   rfc3339-record:set-secfrac!)
  (offsetmin rfc3339-record:offsetmin rfc3339-record:set-offsetmin!))

;;; @defproc make-rfc3339-record year month mday hour minute second secfrac offsetmin
;;;
;;; Construct an @code{rfc3339-record} with the given field values.  Each of
;;; @var{year}, @var{month}, @var{mday}, @var{hour}, @var{minute}, and
;;; @var{second} is @code{#f} or a nonnegative integer.  @var{secfrac} is
;;; @code{#f} or a real number that is greater than or equal to 0.0 and less
;;; than 1.0.  @var{offsetmin} is @code{#f} or a nonnegative integer.  Note
;;; that @var{offsetmin} represents both the hour and minute components of an
;;; RFC3339 string.

;;; @defproc rfc3339-record? x
;;;
;;; Predicate for @code{rfc3339-record}.

;;; @defproc  rfc3339-record:year      rec
;;; @defprocx rfc3339-record:month     rec
;;; @defprocx rfc3339-record:mday      rec
;;; @defprocx rfc3339-record:hour      rec
;;; @defprocx rfc3339-record:minute    rec
;;; @defprocx rfc3339-record:second    rec
;;; @defprocx rfc3339-record:secfrac   rec
;;; @defprocx rfc3339-record:offsetmin rec
;;;
;;; Get the respective field value of @code{rfc3339-record} @var{rec}.

;;; @defproc  rfc3339-record:set-year!      rec val
;;; @defprocx rfc3339-record:set-month!     rec val
;;; @defprocx rfc3339-record:set-mday!      rec val
;;; @defprocx rfc3339-record:set-hour!      rec val
;;; @defprocx rfc3339-record:set-minute!    rec val
;;; @defprocx rfc3339-record:set-second!    rec val
;;; @defprocx rfc3339-record:set-secfrac!   rec val
;;; @defprocx rfc3339-record:set-offsetmin! rec val
;;;
;;; Set the respective field value of @code{rfc3339-record} @var{rec} to
;;; @var{val}.

;;; @defproc rfc3339-record->list rec
;;;
;;; Yields a list of the @code{rfc3339-record} @var{rec} fields, corresponding
;;; to the arguments of the @code{make-rfc3339-record} procedure.
;;;
;;; @lisp
;;; (rfc3339-record->list
;;;  (make-rfc3339-record 1985 4 12 23 20 50 0.52 0))
;;; @result{} (1985 4 12 23 20 50 0.52 0)
;;; @end lisp

(define (rfc3339-record->list rec)
  (list (rfc3339-record:year      rec)
        (rfc3339-record:month     rec)
        (rfc3339-record:mday      rec)
        (rfc3339-record:hour      rec)
        (rfc3339-record:minute    rec)
        (rfc3339-record:second    rec)
        (rfc3339-record:secfrac   rec)
        (rfc3339-record:offsetmin rec)))

;;; @section Parsing

;;; The parsing procedures are for constructing a @code{rfc3339-record}s,
;;; lists, and vectors from RFC3339 strings.  The underlying parser can also
;;; apply a user-supplied closure directly.

;;; @defproc parse-rfc3339-string str constructor
;;;
;;; Parses RFC3339 string @var{str} and applies procedure @var{constructor}
;;; with the parsed values.  The arguments of @var{constructor} are the same
;;; as those of @code{make-rfc3339-record}.

(define parse-rfc3339-string
  (let ((rx (%rfc3339:regexp
             (string-append
              "^ *"
              "(?:([0-9]+)(?:-([0-9]+)(?:-([0-9]+))?)?)?"
              "([Tt]| +)?"
              "(?:([0-9]+)(?::([0-9]+)(?::([0-9]+)(?:(.[0-9]+))?)?)?)?"
              ;;" *"
              "(?:([Zz])|(?:(["
              %rfc3339:regexp-cc-minus ; for pregexp 1e9
              "+])(?:([0-9]+)?(?::([0-9]+)?))))?"
              " *$"))))
    (lambda (str constructor)
      (cond ((%rfc3339:regexp-match rx str)
             =>
             (lambda (result)
               (apply (lambda (whole dy dm dd t th tm ts tf z os oh om)
                        ;; Note: Very gross, but we want to use the limited
                        ;; (and hopefully fast native) regexp.
                        (if (and dy (not dm) (not t) th)
                            (begin (set! th (string-append dy th))
                                   (set! dy #f)))
                        (constructor
                         (if dy (string->number dy) #f)
                         (if dm (string->number dm) #f)
                         (if dd (string->number dd) #f)
                         (if th (string->number th) #f)
                         (if tm (string->number tm) #f)
                         (if ts (string->number ts) #f)
                         (if tf (string->number tf) #f)
                         (cond (z 0)
                               ((or oh om)
                                (* (if (equal? os "-") -1 1)
                                   (+ (* (if oh (string->number oh) 0) 60)
                                      (if om (string->number om) 0))))
                               (else #f))))
                      result)))
            (else #f)))))

;;; @defproc  string->rfc3339-record str
;;;
;;; Yields an @code{rfc3339-record} from RFC3339 string @var{str}.

(define (string->rfc3339-record str)
  (parse-rfc3339-string str make-rfc3339-record))

;;; @defproc  rfc3339-string->list   str
;;; @defprocx rfc3339-string->vector str
;;;
;;; Yields a list or vector (respectively) from the parsed values of RFC3339
;;; string @var{str}.  The list and vector elements correspond to the arguments
;;; of @code{make-rfc3339-record}.
;;;
;;; @lisp
;;; (rfc3339-string->list   "1985-04-12T23:20:69.52+5:0")
;;; @result{}  (1985 4 12 23 20 69 0.52 300)
;;; (rfc3339-string->vector "1985-04-12T23:20:69.52+5:0")
;;; @result{} #(1985 4 12 23 20 69 0.52 300)
;;; @end lisp

(define (rfc3339-string->list   str) (parse-rfc3339-string str list))
(define (rfc3339-string->vector str) (parse-rfc3339-string str vector))

;;; @section Formatting

;;; An RFC3339 string format can be obtained from an @code{rfc3339-record}.

(define %rfc3339:write-zpad
  (let ((zeroes '#("" "0" "00" "000" "0000")))
    (lambda (num width port)
      (if num
          (let ((pad (cond ((< num 10)   (- width 1))
                           ((< num 100)  (- width 2))
                           ((< num 1000) (- width 3))
                           (else         0))))
            (if (> pad 0)
                (display (vector-ref zeroes pad) port))
            (display num port))
          (display (vector-ref zeroes width) port)))))

(define (%rfc3339:write-date port dy dm dd)
  (%rfc3339:write-zpad dy  4 port)
  (write-char                  #\-   port)
  (%rfc3339:write-zpad dm  2 port)
  (write-char                  #\-   port)
  (%rfc3339:write-zpad dd  2 port))

(define (%rfc3339:write-time port th tm ts tf)
  (%rfc3339:write-zpad th  2 port)
  (write-char                  #\:   port)
  (%rfc3339:write-zpad tm  2 port)
  (write-char                  #\:   port)
  (%rfc3339:write-zpad ts  2 port)
  (if tf
      (if (zero? tf)
          (display ".0" port)
          (let ((str (number->string tf)))
            (if (equal? str "0.0")
                (display ".0" port)
                (display (substring str 1 (string-length str)) port))))))

(define (%rfc3339:write-offset port om)
  (case om
    ((0)  (write-char #\Z port))
    ((#f) (if #f #f))
    (else (let ((om (if (< om 0)
                        (begin (write-char #\- port)
                               (- om))
                        (begin (write-char #\+ port)
                               om))))
            (display (quotient om 60) port)
            (write-char #\: port)
            (%rfc3339:write-zpad (remainder om 60) 2 port)))))

(define (%rfc3339:write-full port dy dm dd th tm ts tf om)
  (let ((d? (or dy dm dd))
        (t? (or th tm ts tf)))
    (if d?
        (%rfc3339:write-date port dy dm dd))
    (if (and d? t?)
        (write-char #\T port))
    (if t?
        (%rfc3339:write-time port th tm ts tf))
    (if om
        (%rfc3339:write-offset port om))))

;;; @defproc write-rfc3339 rec port
;;;
;;; Write an RFC3339 string format of @code{rfc3339-record} @var{rec} to
;;; output port @var{port}.

(define (write-rfc3339-record rec port)
  ;; TODO: Maybe lose this "apply".
  (apply %rfc3339:write-full port (rfc3339-record->list rec)))

;;; @defproc rfc3339-record->string rec
;;;
;;; Yield an RFC3339 string format of @code{rfc3339-record} @var{rec} as a
;;; Scheme string.

(define (rfc3339-record->string rec)
  (let ((os (open-output-string)))
    (write-rfc3339-record rec os)
    (let ((str (get-output-string os)))
      (close-output-port os)
      str)))

;;; @section Validation

;;; A few procedures are provided for validating @code{rfc3339-record}s.

(define-syntax %rfc3339:field-check
  (syntax-rules ()
    ((_ ?explain?-var ?var ?valid-expr)
     (cond ((not ?var)  (if ?explain?-var
                            (list 'missing (quote ?var))
                            #t))
           (?valid-expr #f)
           (else        (if ?explain?-var
                            (list 'invalid
                                  (quote ?var)
                                  ?var
                                  (quote ?valid-expr))
                            #t))))))

(define (%rfc3339:leap-year? year)
  (and (zero? (modulo year 4))
       (if (zero? (modulo year 100))
           (zero? (modulo year 400))
           #t)))

(define (%rfc3339:month-days year month)
  (case month
    ((1 3 5 7 8 10 12) 31)
    ((4 6 9 11)        30)
    ((2)               (if (%rfc3339:leap-year? year) 29 28))
    (else              #f)))

;;; @defproc  check-rfc3339-record-date   rec explain?
;;; @defprocx check-rfc3339-record-time   rec explain?
;;; @defprocx check-rfc3339-record-offset rec explain?
;;;
;;; Check the respective component of @code{rfc3339-record} @var{rec} for
;;; completeness and correctness, yielding @code{#f} iff no problems were
;;; detected.  If @var{explain?} is true, then true values of these procedures
;;; are lists that ``explain'' the error detected.  For example:
;;;
;;; @lisp
;;; (check-rfc3339-record-date
;;;  (string->rfc3339-record "1999-02")    #t)
;;; @result{} (missing mday)
;;;
;;; (check-rfc3339-record-date
;;;  (string->rfc3339-record "1999-02-29") #t)
;;; @result{}
;;; (invalid mday 29 (and (integer? mday)
;;;                       (<= 1 mday (month-days year month))))
;;;
;;; (check-rfc3339-record-date
;;;  (string->rfc3339-record "2000-02-29") #t)
;;; @result{} #f
;;; @end lisp
;;;
;;; Leap years are calculated correctly.  Leap seconds (61st seconds in
;;; minutes) are tolerated in any date and time.

(define check-rfc3339-record-date
  (let ((month-days %rfc3339:month-days))
    (lambda (rec explain?)
      (let ((year  (rfc3339-record:year  rec))
            (month (rfc3339-record:month rec))
            (mday  (rfc3339-record:mday  rec)))
        (or (%rfc3339:field-check
             explain? year  (and (integer? year) (>= year 1)))
            (%rfc3339:field-check
             explain? month (and (integer? month) (<= 1 month 12)))
            (%rfc3339:field-check
             explain? mday  (and (integer? mday)
                                 (<= 1 mday (month-days year month)))))))))

(define (check-rfc3339-record-time rec explain?)
  (let ((hour    (rfc3339-record:hour    rec))
        (minute  (rfc3339-record:minute  rec))
        (second  (rfc3339-record:second  rec))
        (secfrac (rfc3339-record:secfrac rec)))
    (or (%rfc3339:field-check
         explain? hour    (and (integer? hour)   (<= 0 hour   23)))
        (%rfc3339:field-check
         explain? minute  (and (integer? minute) (<= 0 minute 59)))
        (%rfc3339:field-check
         explain? second  (and (integer? second) (<= 0 second 60)))
        (%rfc3339:field-check
         explain? secfrac (and (real? secfrac)
                               (<= 0.0 secfrac)
                               (< secfrac 1.0))))))

(define (check-rfc3339-record-offset rec explain?)
  (let ((offsetmin (rfc3339-record:offsetmin rec)))
    (%rfc3339:field-check explain?
                          offsetmin
                          (integer? offsetmin))))

;;; @defproc check-rfc3339-record-full rec explain?
;;;
;;; Checks all three components.  See @code{check-rfc3339-record-date} et al.

(define (check-rfc3339-record-full rec explain?)
  (or (check-rfc3339-record-date   rec explain?)
      (check-rfc3339-record-time   rec explain?)
      (check-rfc3339-record-offset rec explain?)))

;;; @defproc valid-full-rfc3339-record? rec
;;;
;;; Yields a true value iff @code{check-rfc3339-record-full} yields a false
;;; value.

(define (valid-full-rfc3339-record? rec)
  (not (check-rfc3339-record-full rec #f)))

;;; @section SRFI-19 Interoperability

;;; @code{rfc3339.scm} has no dependency on SRFI-19, but a procedure is
;;; provided for constructing a SRFI-19 @code{date}.

;;; @defproc rfc3339-string->srfi19-date/constructor str make-date
;;;
;;; Contruct a SRFI-19 @code{date} from an RFC3339 string, where @var{str} is
;;; the string, and @var{make-date} is the SRFI-19 @code{date} constructor.
;;; Applications using SRFI-19 may wish to define an
;;; @code{rfc3339-string->date} procedure:
;;;
;;; @lisp
;;; (define (rfc3339-string->date str)
;;;   (rfc3339-string->srfi19-date/constructor str make-date))
;;; @end lisp

(define (rfc3339-string->srfi19-date/constructor str make-date)
  (parse-rfc3339-string
   str
   (lambda (dy dm dd th tm ts tf om)
     (make-date (if tf (inexact->exact (truncate (* 100 tf))) 0) ; nanosecond
                (or ts 0)               ; second
                (or tm 0)               ; minute
                (or th 0)               ; hour
                (or dd 0)               ; day
                (or dm 0)               ; month
                (or dy 0)               ; year
                (if om (* 60 om) 0)     ; zone-offset
                ))))

;; TODO: rfc3339-record->srfi19-date/constructor

;;; @section Tests

;;; The @code{rfc3339.scm} test suite can be enabled by editing the source code
;;; file and loading @uref{http://www.neilvandyke.org/testeez/, Testeez}.

(define (%rfc3339:test)
  (%rfc3339:testeez
   "rfc3339.scm"

   ;;

   (test/equal "leap years"
               (map %rfc3339:leap-year? '(1600 1984 1996 2000 2400))
               '(#t #t #t #t #t))

   (test/equal "non-leap years"
               (map %rfc3339:leap-year? '(1700 1800 1899 1900 2100))
               '(#f #f #f #f #f))

   ;;

   (test/equal ""
               (rfc3339-string->list "1985-04-12T23:20:50.52Z")
               '(1985 4 12 23 20 50 0.52 0))

   (test/equal ""
               (rfc3339-string->list "1985-04-12T23:20:50.52")
               '(1985 4 12 23 20 50 0.52 #f))

   (test/equal ""
               (rfc3339-string->list "1985-04-12T23:20:50")
               '(1985 4 12 23 20 50 #f #f))

   (test/equal ""
               (rfc3339-string->list "1985-04-12T23:20")
               '(1985 4 12 23 20 #f #f #f))

   (test/equal ""
               (rfc3339-string->list "1985-04-12T23")
               '(1985 4 12 23 #f #f #f #f))

   (test/equal ""
               (rfc3339-string->list "1985-04-12T")
               '(1985 4 12 #f #f #f #f #f))

   (test/equal ""
               (rfc3339-string->list "1985-04-12")
               '(1985 4 12 #f #f #f #f #f))

   (test/equal ""
               (rfc3339-string->list "1985-04")
               '(1985 4 #f #f #f #f #f #f))

   (test/equal ""
               (rfc3339-string->list "1985")
               '(1985 #f #f #f #f #f #f #f))

   ;;

   (test/equal
    ""
    (rfc3339-record->string (string->rfc3339-record "1111-11-11T11:11:11.123"))
    "1111-11-11T11:11:11.123")

   (test/equal
    ""
    (rfc3339-record->string (string->rfc3339-record "1111-11-11T11:11:11"))
    "1111-11-11T11:11:11")

   (test/equal
    ""
    (rfc3339-record->string (string->rfc3339-record "1111-11-11T11:11:11.0"))
    "1111-11-11T11:11:11.0")

   ;;

   (test/equal ""
               (rfc3339-record->string (string->rfc3339-record "1-1-1"))
               "0001-01-01")

   (test/equal ""
               (rfc3339-record->string (string->rfc3339-record "1:1:1"))
               "01:01:01")

   ;;

   (test/equal
    "" (rfc3339-record->string (string->rfc3339-record "T+0:0")) "Z")
   (test/equal
    "" (rfc3339-record->string (string->rfc3339-record "+0:0"))  "Z")
   (test/equal
    "" (rfc3339-record->string (string->rfc3339-record "T-0:0")) "Z")
   (test/equal
    "" (rfc3339-record->string (string->rfc3339-record "-0:0"))  "Z")
   (test/equal
    "" (rfc3339-record->string (string->rfc3339-record "TZ"))    "Z")
   (test/equal
    "" (rfc3339-record->string (string->rfc3339-record "Z"))     "Z")

   (test/equal
    "" (rfc3339-record->string (string->rfc3339-record "+0:01"))   "+0:01")
   (test/equal
    "" (rfc3339-record->string (string->rfc3339-record "+000:01")) "+0:01")

   ;;

   (test/equal
    "" (rfc3339-record->string (string->rfc3339-record "T12:34")) "12:34:00")
   (test/equal
    "" (rfc3339-record->string (string->rfc3339-record "12:34"))  "12:34:00")

   ))

;;; @unnumberedsec History

;;; @table @asis
;;;
;;; @item Version 0.2 --- 2005-12-05
;;; Release for PLT 299/3xx.  Changed portability note in light of Pregexp
;;; post-1e9 bug fix.  Minor documentation changes.
;;;
;;; @item Version 0.1 --- 2005-01-30
;;; Initial release.
;;;
;;; @end table

(provide (all-defined)))