diff options
-rw-r--r-- | doc/lispref/os.texi | 13 | ||||
-rw-r--r-- | etc/NEWS | 9 | ||||
-rw-r--r-- | lisp/calendar/iso8601.el | 370 | ||||
-rw-r--r-- | test/lisp/calendar/iso8601-tests.el | 291 |
4 files changed, 683 insertions, 0 deletions
diff --git a/doc/lispref/os.texi b/doc/lispref/os.texi index d397a125738..b3444838d3b 100644 --- a/doc/lispref/os.texi +++ b/doc/lispref/os.texi @@ -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 @@ -2056,6 +2056,15 @@ 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 floating-point operators do. diff --git a/lisp/calendar/iso8601.el b/lisp/calendar/iso8601.el new file mode 100644 index 00000000000..ab0077ac58d --- /dev/null +++ b/lisp/calendar/iso8601.el @@ -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 diff --git a/test/lisp/calendar/iso8601-tests.el b/test/lisp/calendar/iso8601-tests.el new file mode 100644 index 00000000000..2959f54b811 --- /dev/null +++ b/test/lisp/calendar/iso8601-tests.el @@ -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 |