Add an ISO 8601 parsing library

* doc/lispref/os.texi (Time Parsing): Document it.

* lisp/calendar/iso8601.el: New file.

* test/lisp/calendar/iso8601-tests.el: Test ISO8601 parsing functions.
This commit is contained in:
Lars Ingebrigtsen 2019-07-29 14:22:31 +02:00
parent 6cfda69d72
commit fa04c8b87e
4 changed files with 683 additions and 0 deletions

View file

@ -1622,6 +1622,19 @@ ISO 8601 string, like ``Fri, 25 Mar 2016 16:24:56 +0100'' or
less well-formed time strings as well.
@end defun
@vindex ISO 8601 date/time strings
@defun iso8601-parse string
For a more strict function (that will error out upon invalid input),
this function can be used instead. It's able to parse all variants of
the ISO 8601 standard, so in addition to the formats mentioned above,
it also parses things like ``1998W45-3'' (week number) and
``1998-245'' (ordinal day number). To parse durations, there's
@code{iso8601-parse-duration}, and to parse intervals, there's
@code{iso8601-parse-interval}. All these functions return decoded
time structures, except the final one, which returns three of them
(the start, the end, and the duration).
@end defun
@defun format-time-string format-string &optional time zone
This function converts @var{time} (or the current time, if

View file

@ -2055,6 +2055,15 @@ of various forms, including a new timestamp form '(TICKS . HZ)', where
TICKS is an integer and HZ is a positive integer denoting a clock
frequency. The old 'encode-time' API is still supported.
+++
*** A new package to parse ISO 8601 time, date, durations and
intervals has been added. The main function to use is
'iso8601-parse', but there's also 'iso8601-parse-date',
'iso8601-parse-time', 'iso8601-parse-duration' and
'iso8601-parse-interval'. All these functions return decoded time
structures, except the final one, which returns three of them (start,
end and duration).
+++
*** 'time-add', 'time-subtract', and 'time-less-p' now accept
infinities and NaNs too, and propagate them or return nil like

370
lisp/calendar/iso8601.el Normal file
View file

@ -0,0 +1,370 @@
;;; iso8601.el --- parse ISO 8601 date/time strings -*- lexical-binding:t -*-
;; Copyright (C) 2019 Free Software Foundation, Inc.
;; Keywords: dates
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs 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 the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; ISO8601 times basically look like 1985-04-01T15:23:49... Or so
;; you'd think. This is what everybody means when they say "ISO8601",
;; but it's in reality a quite large collection of syntaxes, including
;; week numbers, ordinal dates, durations and intervals. This package
;; has functions for parsing them all.
;;
;; The interface functions are `iso8601-parse', `iso8601-parse-date',
;; `iso8601-parse-time', `iso8601-parse-zone',
;; `iso8601-parse-duration' and `iso8601-parse-interval'. They all
;; return decoded time objects, except the last one, which returns a
;; list of three of them.
;;
;; (iso8601-parse-interval "P1Y2M10DT2H30M/2008W32T153000-01")
;; '((0 0 13 24 5 2007 nil nil -3600)
;; (0 30 15 3 8 2008 nil nil -3600)
;; (0 30 2 10 2 1 nil nil nil))
;;
;;
;; The standard can be found at:
;;
;; http://www.loc.gov/standards/datetime/iso-tc154-wg5_n0038_iso_wd_8601-1_2016-02-16.pdf
;;
;; The Wikipedia page on the standard is also informative:
;;
;; https://en.wikipedia.org/wiki/ISO_8601
;;
;; RFC3339 defines the subset that everybody thinks of as "ISO8601".
;;; Code:
(require 'time-date)
(require 'cl-lib)
(defun iso8601--concat-regexps (regexps)
(mapconcat (lambda (regexp)
(concat "\\(?:"
(replace-regexp-in-string "(" "(?:" regexp)
"\\)"))
regexps "\\|"))
(defconst iso8601--year-match
"\\([-+]\\)?\\([0-9][0-9][0-9][0-9]\\)")
(defconst iso8601--full-date-match
"\\([-+]\\)?\\([0-9][0-9][0-9][0-9]\\)-?\\([0-9][0-9]\\)-?\\([0-9][0-9]\\)")
(defconst iso8601--without-day-match
"\\([-+]\\)?\\([0-9][0-9][0-9][0-9]\\)-\\([0-9][0-9]\\)")
(defconst iso8601--outdated-date-match
"--\\([0-9][0-9]\\)-?\\([0-9][0-9]\\)")
(defconst iso8601--week-date-match
"\\([-+]\\)?\\([0-9][0-9][0-9][0-9]\\)-?W\\([0-9][0-9]\\)-?\\([0-9]\\)?")
(defconst iso8601--ordinal-date-match
"\\([-+]\\)?\\([0-9][0-9][0-9][0-9]\\)-?\\([0-9][0-9][0-9]\\)")
(defconst iso8601--date-match
(iso8601--concat-regexps
(list iso8601--year-match
iso8601--full-date-match
iso8601--without-day-match
iso8601--outdated-date-match
iso8601--week-date-match
iso8601--ordinal-date-match)))
(defconst iso8601--time-match
"\\([0-9][0-9]\\):?\\([0-9][0-9]\\)?:?\\([0-9][0-9]\\)?\\.?\\([0-9][0-9][0-9]\\)?")
(defconst iso8601--zone-match
"\\(Z\\|\\([-+]\\)\\([0-9][0-9]\\):?\\([0-9][0-9]\\)?\\)")
(defconst iso8601--full-time-match
(concat "\\(" (replace-regexp-in-string "(" "(?:" iso8601--time-match) "\\)"
"\\(" iso8601--zone-match "\\)?"))
(defconst iso8601--combined-match
(concat "\\(" iso8601--date-match "\\)"
"\\(?:T\\("
(replace-regexp-in-string "(" "(?:" iso8601--time-match)
"\\)"
"\\(" iso8601--zone-match "\\)?\\)?"))
(defconst iso8601--duration-full-match
"P\\([0-9]+Y\\)?\\([0-9]+M\\)?\\([0-9]+D\\)?\\(T\\([0-9]+H\\)?\\([0-9]+M\\)?\\([0-9]+S\\)?\\)?")
(defconst iso8601--duration-week-match
"P\\([0-9]+\\)W")
(defconst iso8601--duration-combined-match
(concat "P" iso8601--combined-match))
(defconst iso8601--duration-match
(iso8601--concat-regexps
(list iso8601--duration-full-match
iso8601--duration-week-match
iso8601--duration-combined-match)))
(defun iso8601-parse (string)
"Parse an ISO 8601 date/time string and return a `decoded-time' structure.
The ISO 8601 date/time strings look like \"2008-03-02T13:47:30\",
but shorter, incomplete strings like \"2008-03-02\" are valid, as
well as variants like \"2008W32\" (week number) and
\"2008-234\" (ordinal day number)."
(if (not (iso8601-valid-p string))
(signal 'wrong-type-argument string)
(let* ((date-string (match-string 1 string))
(time-string (match-string 2 string))
(zone-string (match-string 3 string))
(date (iso8601-parse-date date-string)))
;; The time portion is optional.
(when time-string
(let ((time (iso8601-parse-time time-string)))
(setf (decoded-time-hour date) (decoded-time-hour time))
(setf (decoded-time-minute date) (decoded-time-minute time))
(setf (decoded-time-second date) (decoded-time-second time))))
;; The time zone is optional.
(when zone-string
(setf (decoded-time-zone date)
;; The time zone in decoded times are in seconds.
(* (iso8601-parse-zone zone-string) 60)))
date)))
(defun iso8601-parse-date (string)
"Parse STRING (which should be on ISO 8601 format) and return a time value."
(cond
;; Just a year: [-+]YYYY.
((iso8601--match iso8601--year-match string)
(iso8601--decoded-time
:year (iso8601--adjust-year (match-string 1 string)
(match-string 2 string))))
;; Calendar dates: YYYY-MM-DD and variants.
((iso8601--match iso8601--full-date-match string)
(iso8601--decoded-time
:year (iso8601--adjust-year (match-string 1 string)
(match-string 2 string))
:month (match-string 3 string)
:day (match-string 4 string)))
;; Calendar date without day: YYYY-MM.
((iso8601--match iso8601--without-day-match string)
(iso8601--decoded-time
:year (iso8601--adjust-year (match-string 1 string)
(match-string 2 string))
:month (match-string 3 string)))
;; Outdated date without year: --MM-DD
((iso8601--match iso8601--outdated-date-match string)
(iso8601--decoded-time
:month (match-string 1 string)
:day (match-string 2 string)))
;; Week dates: YYYY-Www-D
((iso8601--match iso8601--week-date-match string)
(let* ((year (iso8601--adjust-year (match-string 1 string)
(match-string 2 string)))
(week (string-to-number (match-string 3 string)))
(day-of-week (and (match-string 4 string)
(string-to-number (match-string 4 string))))
(jan-start (decoded-time-weekday
(decode-time
(iso8601--encode-time
(iso8601--decoded-time :year year
:month 1
:day 4)))))
(correction (+ (if (zerop jan-start) 7 jan-start)
3))
(ordinal (+ (* week 7) (or day-of-week 0) (- correction))))
(cond
;; Monday 29 December 2008 is written "2009-W01-1".
((< ordinal 1)
(setq year (1- year)
ordinal (+ ordinal (if (date-leap-year-p year)
366 365))))
;; Sunday 3 January 2010 is written "2009-W53-7".
((> ordinal (if (date-leap-year-p year)
366 365))
(setq ordinal (- ordinal (if (date-leap-year-p year)
366 365))
year (1+ year))))
(let ((month-day (date-ordinal-to-time year ordinal)))
(iso8601--decoded-time :year year
:month (decoded-time-month month-day)
:day (decoded-time-day month-day)))))
;; Ordinal dates: YYYY-DDD
((iso8601--match iso8601--ordinal-date-match string)
(let* ((year (iso8601--adjust-year (match-string 1 string)
(match-string 2 string)))
(ordinal (string-to-number (match-string 3 string)))
(month-day (date-ordinal-to-time year ordinal)))
(iso8601--decoded-time :year year
:month (decoded-time-month month-day)
:day (decoded-time-day month-day))))
(t
(signal 'wrong-type-argument string))))
(defun iso8601--adjust-year (sign year)
(save-match-data
(let ((year (if (stringp year)
(string-to-number year)
year)))
(if (string= sign "-")
;; -0001 is 2 BCE.
(1- (- year))
year))))
(defun iso8601-parse-time (string)
"Parse STRING, which should be an ISO 8601 time string, and return a time value."
(if (not (iso8601--match iso8601--full-time-match string))
(signal 'wrong-type-argument string)
(let ((time (match-string 1 string))
(zone (match-string 2 string)))
(if (not (iso8601--match iso8601--time-match time))
(signal 'wrong-type-argument string)
(let ((hour (string-to-number (match-string 1 time)))
(minute (and (match-string 2 time)
(string-to-number (match-string 2 time))))
(second (and (match-string 3 time)
(string-to-number (match-string 3 time))))
;; Hm...
(_millisecond (and (match-string 4 time)
(string-to-number (match-string 4 time)))))
(iso8601--decoded-time :hour hour
:minute (or minute 0)
:second (or second 0)
:zone (and zone
(* 60 (iso8601-parse-zone
zone)))))))))
(defun iso8601-parse-zone (string)
"Parse STRING, which should be an ISO 8601 time zone.
Return the number of minutes."
(if (not (iso8601--match iso8601--zone-match string))
(signal 'wrong-type-argument string)
(if (match-string 2 string)
;; HH:MM-ish.
(let ((hour (string-to-number (match-string 3 string)))
(minute (and (match-string 4 string)
(string-to-number (match-string 4 string)))))
(* (if (equal (match-string 2 string) "-")
-1
1)
(+ (* hour 60)
(or minute 0))))
;; "Z".
0)))
(defun iso8601-valid-p (string)
"Say whether STRING is a valid ISO 8601 representation."
(iso8601--match iso8601--combined-match string))
(defun iso8601-parse-duration (string)
"Parse ISO 8601 durations on the form P3Y6M4DT12H30M5S."
(cond
((and (iso8601--match iso8601--duration-full-match string)
;; Just a "P" isn't valid; there has to be at least one
;; element, like P1M.
(> (length (match-string 0 string)) 2))
(iso8601--decoded-time :year (or (match-string 1 string) 0)
:month (or (match-string 2 string) 0)
:day (or (match-string 3 string) 0)
:hour (or (match-string 5 string) 0)
:minute (or (match-string 6 string) 0)
:second (or (match-string 7 string) 0)))
;; PnW: Weeks.
((iso8601--match iso8601--duration-week-match string)
(let ((weeks (string-to-number (match-string 1 string))))
;; Does this make sense? Hm...
(iso8601--decoded-time :day (* weeks 7))))
;; P<date>T<time>
((iso8601--match iso8601--duration-combined-match string)
(iso8601-parse (substring string 1)))
(t
(signal 'wrong-type-argument string))))
(defun iso8601-parse-interval (string)
"Parse ISO 8601 intervals."
(let ((bits (split-string string "/"))
start end duration)
(if (not (= (length bits) 2))
(signal 'wrong-type-argument string)
;; The intervals may be an explicit start/end times, or either a
;; start or an end, and an accompanying duration.
(cond
((and (string-match "\\`P" (car bits))
(iso8601-valid-p (cadr bits)))
(setq duration (iso8601-parse-duration (car bits))
end (iso8601-parse (cadr bits))))
((and (string-match "\\`P" (cadr bits))
(iso8601-valid-p (car bits)))
(setq duration (iso8601-parse-duration (cadr bits))
start (iso8601-parse (car bits))))
((and (iso8601-valid-p (car bits))
(iso8601-valid-p (cadr bits)))
(setq start (iso8601-parse (car bits))
end (iso8601-parse (cadr bits))))
(t
(signal 'wrong-type-argument string))))
(unless end
(setq end (decoded-time-add start duration)))
(unless start
(setq start (decoded-time-add end
;; We negate the duration so that
;; we get a subtraction.
(mapcar (lambda (elem)
(if (numberp elem)
(- elem)
elem))
duration))))
(list start end
(or duration
(decode-time (time-subtract (iso8601--encode-time end)
(iso8601--encode-time start))
(or (decoded-time-zone end) 0))))))
(defun iso8601--match (regexp string)
(string-match (concat "\\`" regexp "\\'") string))
(defun iso8601--value (elem &optional default)
(if (stringp elem)
(string-to-number elem)
(or elem default)))
(cl-defun iso8601--decoded-time (&key second minute hour
day month year
dst zone)
(list (iso8601--value second)
(iso8601--value minute)
(iso8601--value hour)
(iso8601--value day)
(iso8601--value month)
(iso8601--value year)
nil
dst
zone))
(defun iso8601--encode-time (time)
"Like `encode-time', but fill in nil values in TIME."
(setq time (copy-sequence time))
(unless (decoded-time-second time)
(setf (decoded-time-second time) 0))
(unless (decoded-time-minute time)
(setf (decoded-time-minute time) 0))
(unless (decoded-time-hour time)
(setf (decoded-time-hour time) 0))
(unless (decoded-time-day time)
(setf (decoded-time-day time) 1))
(unless (decoded-time-month time)
(setf (decoded-time-month time) 1))
(unless (decoded-time-year time)
(setf (decoded-time-year time) 0))
(encode-time time))
(provide 'iso8601)
;;; iso8601.el ends here

View file

@ -0,0 +1,291 @@
;;; iso8601-tests.el --- tests for calendar/iso8601.el -*- lexical-binding:t -*-
;; Copyright (C) 2019 Free Software Foundation, Inc.
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs 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 the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Code:
(require 'ert)
(require 'iso8601)
(ert-deftest test-iso8601-date-years ()
(should (equal (iso8601-parse-date "1985")
'(nil nil nil nil nil 1985 nil nil nil)))
(should (equal (iso8601-parse-date "-0003")
'(nil nil nil nil nil -4 nil nil nil)))
(should (equal (iso8601-parse-date "+1985")
'(nil nil nil nil nil 1985 nil nil nil))))
(ert-deftest test-iso8601-date-dates ()
(should (equal (iso8601-parse-date "1985-03-14")
'(nil nil nil 14 3 1985 nil nil nil)))
(should (equal (iso8601-parse-date "19850314")
'(nil nil nil 14 3 1985 nil nil nil)))
(should (equal (iso8601-parse-date "1985-02")
'(nil nil nil nil 2 1985 nil nil nil))))
(ert-deftest test-iso8601-date-obsolete ()
(should (equal (iso8601-parse-date "--02-01")
'(nil nil nil 1 2 nil nil nil nil)))
(should (equal (iso8601-parse-date "--0201")
'(nil nil nil 1 2 nil nil nil nil))))
(ert-deftest test-iso8601-date-weeks ()
(should (equal (iso8601-parse-date "2008W39-6")
'(nil nil nil 27 9 2008 nil nil nil)))
(should (equal (iso8601-parse-date "2009W01-1")
'(nil nil nil 29 12 2008 nil nil nil)))
(should (equal (iso8601-parse-date "2009W53-7")
'(nil nil nil 3 1 2010 nil nil nil))))
(ert-deftest test-iso8601-date-ordinals ()
(should (equal (iso8601-parse-date "1981-095")
'(nil nil nil 5 4 1981 nil nil nil))))
(ert-deftest test-iso8601-time ()
(should (equal (iso8601-parse-time "13:47:30")
'(30 47 13 nil nil nil nil nil nil)))
(should (equal (iso8601-parse-time "134730")
'(30 47 13 nil nil nil nil nil nil)))
(should (equal (iso8601-parse-time "1347")
'(0 47 13 nil nil nil nil nil nil))))
(ert-deftest test-iso8601-combined ()
(should (equal (iso8601-parse "2008-03-02T13:47:30")
'(30 47 13 2 3 2008 nil nil nil)))
(should (equal (iso8601-parse "2008-03-02T13:47:30Z")
'(30 47 13 2 3 2008 nil nil 0)))
(should (equal (iso8601-parse "2008-03-02T13:47:30+01:00")
'(30 47 13 2 3 2008 nil nil 3600)))
(should (equal (iso8601-parse "2008-03-02T13:47:30-01")
'(30 47 13 2 3 2008 nil nil -3600))))
(ert-deftest test-iso8601-duration ()
(should (equal (iso8601-parse-duration "P3Y6M4DT12H30M5S")
'(5 30 12 4 6 3 nil nil nil)))
(should (equal (iso8601-parse-duration "P1M")
'(0 0 0 0 1 0 nil nil nil)))
(should (equal (iso8601-parse-duration "PT1M")
'(0 1 0 0 0 0 nil nil nil)))
(should (equal (iso8601-parse-duration "P0003-06-04T12:30:05")
'(5 30 12 4 6 3 nil nil nil))))
(ert-deftest test-iso8601-invalid ()
(should-not (iso8601-valid-p " 2008-03-02T13:47:30-01"))
(should-not (iso8601-valid-p "2008-03-02T13:47:30-01:200"))
(should-not (iso8601-valid-p "2008-03-02T13:47:30-01 "))
(should-not (iso8601-valid-p "2008-03-02 T 13:47:30-01 "))
(should-not (iso8601-valid-p "20008-03-02T13:47:30-01")))
(ert-deftest test-iso8601-intervals ()
(should (equal
(iso8601-parse-interval "2007-03-01T13:00:00Z/2008-05-11T15:30:00Z")
'((0 0 13 1 3 2007 nil nil 0)
(0 30 15 11 5 2008 nil nil 0)
;; Hm... can't really use decode-time for time differences...
(0 30 2 14 3 1971 0 nil 0))))
(should (equal (iso8601-parse-interval "2007-03-01T13:00:00Z/P1Y2M10DT2H30M")
'((0 0 13 1 3 2007 nil nil 0)
(0 30 15 11 5 2008 nil nil 0)
(0 30 2 10 2 1 nil nil nil))))
(should (equal (iso8601-parse-interval "P1Y2M10DT2H30M/2008-05-11T15:30:00Z")
'((0 0 13 1 3 2007 nil nil 0)
(0 30 15 11 5 2008 nil nil 0)
(0 30 2 10 2 1 nil nil nil)))))
(ert-deftest standard-test-dates ()
(should (equal (iso8601-parse-date "19850412")
'(nil nil nil 12 4 1985 nil nil nil)))
(should (equal (iso8601-parse-date "1985-04-12")
'(nil nil nil 12 4 1985 nil nil nil)))
(should (equal (iso8601-parse-date "1985102")
'(nil nil nil 12 4 1985 nil nil nil)))
(should (equal (iso8601-parse-date "1985-102")
'(nil nil nil 12 4 1985 nil nil nil)))
(should (equal (iso8601-parse-date "1985W155")
'(nil nil nil 12 4 1985 nil nil nil)))
(should (equal (iso8601-parse-date "1985-W15-5")
'(nil nil nil 12 4 1985 nil nil nil)))
(should (equal (iso8601-parse-date "1985W15")
'(nil nil nil 7 4 1985 nil nil nil)))
(should (equal (iso8601-parse-date "1985-W15")
'(nil nil nil 7 4 1985 nil nil nil)))
(should (equal (iso8601-parse-date "1985-04")
'(nil nil nil nil 4 1985 nil nil nil)))
(should (equal (iso8601-parse-date "1985")
'(nil nil nil nil nil 1985 nil nil nil)))
(should (equal (iso8601-parse-date "+1985-04-12")
'(nil nil nil 12 4 1985 nil nil nil)))
(should (equal (iso8601-parse-date "+19850412")
'(nil nil nil 12 4 1985 nil nil nil))))
(ert-deftest standard-test-time-of-day-local-time ()
(should (equal (iso8601-parse-time "152746")
'(46 27 15 nil nil nil nil nil nil)))
(should (equal (iso8601-parse-time "15:27:46")
'(46 27 15 nil nil nil nil nil nil)))
(should (equal (iso8601-parse-time "1528")
'(0 28 15 nil nil nil nil nil nil)))
(should (equal (iso8601-parse-time "15:28")
'(0 28 15 nil nil nil nil nil nil)))
(should (equal (iso8601-parse-time "15")
'(0 0 15 nil nil nil nil nil nil))))
(ert-deftest standard-test-time-of-day-fractions ()
;; decoded-time doesn't support sub-second times.
;; (should (equal (iso8601-parse-time "152735,5")
;; '(46 27 15 nil nil nil nil nil nil)))
;; (should (equal (iso8601-parse-time "15:27:35,5")
;; '(46 27 15 nil nil nil nil nil nil)))
)
(ert-deftest standard-test-time-of-day-beginning-of-day ()
(should (equal (iso8601-parse-time "000000")
'(0 0 0 nil nil nil nil nil nil)))
(should (equal (iso8601-parse-time "00:00:00")
'(0 0 0 nil nil nil nil nil nil)))
(should (equal (iso8601-parse-time "0000")
'(0 0 0 nil nil nil nil nil nil)))
(should (equal (iso8601-parse-time "00:00")
'(0 0 0 nil nil nil nil nil nil))))
(ert-deftest standard-test-time-of-day-utc ()
(should (equal (iso8601-parse-time "232030Z")
'(30 20 23 nil nil nil nil nil 0)))
(should (equal (iso8601-parse-time "23:20:30Z")
'(30 20 23 nil nil nil nil nil 0)))
(should (equal (iso8601-parse-time "2320Z")
'(0 20 23 nil nil nil nil nil 0)))
(should (equal (iso8601-parse-time "23:20Z")
'(0 20 23 nil nil nil nil nil 0)))
(should (equal (iso8601-parse-time "23Z")
'(0 0 23 nil nil nil nil nil 0))))
(ert-deftest standard-test-time-of-day-zone ()
(should (equal (iso8601-parse-time "152746+0100")
'(46 27 15 nil nil nil nil nil 3600)))
(should (equal (iso8601-parse-time "15:27:46+0100")
'(46 27 15 nil nil nil nil nil 3600)))
(should (equal (iso8601-parse-time "152746+01")
'(46 27 15 nil nil nil nil nil 3600)))
(should (equal (iso8601-parse-time "15:27:46+01")
'(46 27 15 nil nil nil nil nil 3600)))
(should (equal (iso8601-parse-time "152746-0500")
'(46 27 15 nil nil nil nil nil -18000)))
(should (equal (iso8601-parse-time "15:27:46-0500")
'(46 27 15 nil nil nil nil nil -18000)))
(should (equal (iso8601-parse-time "152746-05")
'(46 27 15 nil nil nil nil nil -18000)))
(should (equal (iso8601-parse-time "15:27:46-05")
'(46 27 15 nil nil nil nil nil -18000))))
(ert-deftest standard-test-date-and-time-of-day ()
(should (equal (iso8601-parse "19850412T101530")
'(30 15 10 12 4 1985 nil nil nil)))
(should (equal (iso8601-parse "1985-04-12T10:15:30")
'(30 15 10 12 4 1985 nil nil nil)))
(should (equal (iso8601-parse "1985102T235030Z")
'(30 50 23 12 4 1985 nil nil 0)))
(should (equal (iso8601-parse "1985-102T23:50:30Z")
'(30 50 23 12 4 1985 nil nil 0)))
(should (equal (iso8601-parse "1985W155T235030")
'(30 50 23 12 4 1985 nil nil nil)))
(should (equal (iso8601-parse "1985-W155T23:50:30")
'(30 50 23 12 4 1985 nil nil nil))))
(ert-deftest standard-test-interval ()
;; A time interval starting at 20 minutes and 50 seconds past 23
;; hours on 12 April 1985 and ending at 30 minutes past 10 hours on
;; 25 June 1985.
(should (equal (iso8601-parse-interval "19850412T232050/19850625T103000")
'((50 20 23 12 4 1985 nil nil nil)
(0 30 10 25 6 1985 nil nil nil)
(10 9 11 15 3 1970 0 nil 0))))
(should (equal (iso8601-parse-interval
"1985-04-12T23:20:50/1985-06-25T10:30:00")
'((50 20 23 12 4 1985 nil nil nil)
(0 30 10 25 6 1985 nil nil nil)
(10 9 11 15 3 1970 0 nil 0))))
;; A time interval starting at 12 April 1985 and ending on 25 June
;; 1985.
;; This example doesn't seem valid according to the standard.
;; "0625" is unambiguous, and means "the year 625". Weird.
;; (should (equal (iso8601-parse-interval "19850412/0625")
;; '((nil nil nil 12 4 1985 nil nil nil)
;; (nil nil nil nil nil 625 nil nil nil)
;; (0 17 0 22 9 609 5 nil 0))))
;; A time interval of 2 years, 10 months, 15 days, 10 hours, 20
;; minutes and 30 seconds.
(should (equal (iso8601-parse-duration "P2Y10M15DT10H20M30S")
'(30 20 10 15 10 2 nil nil nil)))
(should (equal (iso8601-parse-duration "P00021015T102030")
'(30 20 10 15 10 2 nil nil nil)))
(should (equal (iso8601-parse-duration "P0002-10-15T10:20:30")
'(30 20 10 15 10 2 nil nil nil)))
;; A time interval of 1 year and 6 months.
(should (equal (iso8601-parse-duration "P1Y6M")
'(0 0 0 0 6 1 nil nil nil)))
(should (equal (iso8601-parse-duration "P0001-06")
'(nil nil nil nil 6 1 nil nil nil)))
;; A time interval of seventy-two hours.
(should (equal (iso8601-parse-duration "PT72H")
'(0 0 72 0 0 0 nil nil nil)))
;; Defined by start and duration
;; A time interval of 1 year, 2 months, 15 days and 12 hours,
;; beginning on 12 April 1985 at 20 minutes past 23 hours.
(should (equal (iso8601-parse-interval "19850412T232000/P1Y2M15DT12H")
'((0 20 23 12 4 1985 nil nil nil)
(0 20 11 28 6 1986 nil nil nil)
(0 0 12 15 2 1 nil nil nil))))
(should (equal (iso8601-parse-interval "1985-04-12T23:20:00/P1Y2M15DT12H")
'((0 20 23 12 4 1985 nil nil nil)
(0 20 11 28 6 1986 nil nil nil)
(0 0 12 15 2 1 nil nil nil))))
;; Defined by duration and end
;; A time interval of 1 year, 2 months, 15 days and 12 hours, ending
;; on 12 April 1985 at 20 minutes past 23 hour.
(should (equal (iso8601-parse-interval "P1Y2M15DT12H/19850412T232000")
'((0 20 11 28 1 1984 nil nil nil)
(0 20 23 12 4 1985 nil nil nil)
(0 0 12 15 2 1 nil nil nil)))))
;;; iso8601-tests.el ends here