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 | |||
| 1622 | less well-formed time strings as well. | 1622 | less well-formed time strings as well. |
| 1623 | @end defun | 1623 | @end defun |
| 1624 | 1624 | ||
| 1625 | @vindex ISO 8601 date/time strings | ||
| 1626 | @defun iso8601-parse string | ||
| 1627 | For a more strict function (that will error out upon invalid input), | ||
| 1628 | this function can be used instead. It's able to parse all variants of | ||
| 1629 | the ISO 8601 standard, so in addition to the formats mentioned above, | ||
| 1630 | it also parses things like ``1998W45-3'' (week number) and | ||
| 1631 | ``1998-245'' (ordinal day number). To parse durations, there's | ||
| 1632 | @code{iso8601-parse-duration}, and to parse intervals, there's | ||
| 1633 | @code{iso8601-parse-interval}. All these functions return decoded | ||
| 1634 | time structures, except the final one, which returns three of them | ||
| 1635 | (the start, the end, and the duration). | ||
| 1636 | @end defun | ||
| 1637 | |||
| 1625 | @defun format-time-string format-string &optional time zone | 1638 | @defun format-time-string format-string &optional time zone |
| 1626 | 1639 | ||
| 1627 | This function converts @var{time} (or the current time, if | 1640 | 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 | |||
| 2056 | frequency. The old 'encode-time' API is still supported. | 2056 | frequency. The old 'encode-time' API is still supported. |
| 2057 | 2057 | ||
| 2058 | +++ | 2058 | +++ |
| 2059 | *** A new package to parse ISO 8601 time, date, durations and | ||
| 2060 | intervals has been added. The main function to use is | ||
| 2061 | 'iso8601-parse', but there's also 'iso8601-parse-date', | ||
| 2062 | 'iso8601-parse-time', 'iso8601-parse-duration' and | ||
| 2063 | 'iso8601-parse-interval'. All these functions return decoded time | ||
| 2064 | structures, except the final one, which returns three of them (start, | ||
| 2065 | end and duration). | ||
| 2066 | |||
| 2067 | +++ | ||
| 2059 | *** 'time-add', 'time-subtract', and 'time-less-p' now accept | 2068 | *** 'time-add', 'time-subtract', and 'time-less-p' now accept |
| 2060 | infinities and NaNs too, and propagate them or return nil like | 2069 | infinities and NaNs too, and propagate them or return nil like |
| 2061 | floating-point operators do. | 2070 | 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 @@ | |||
| 1 | ;;; iso8601.el --- parse ISO 8601 date/time strings -*- lexical-binding:t -*- | ||
| 2 | |||
| 3 | ;; Copyright (C) 2019 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | ;; Keywords: dates | ||
| 6 | |||
| 7 | ;; This file is part of GNU Emacs. | ||
| 8 | |||
| 9 | ;; GNU Emacs is free software: you can redistribute it and/or modify | ||
| 10 | ;; it under the terms of the GNU General Public License as published by | ||
| 11 | ;; the Free Software Foundation, either version 3 of the License, or | ||
| 12 | ;; (at your option) any later version. | ||
| 13 | |||
| 14 | ;; GNU Emacs is distributed in the hope that it will be useful, | ||
| 15 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 16 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 17 | ;; GNU General Public License for more details. | ||
| 18 | |||
| 19 | ;; You should have received a copy of the GNU General Public License | ||
| 20 | ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. | ||
| 21 | |||
| 22 | ;;; Commentary: | ||
| 23 | |||
| 24 | ;; ISO8601 times basically look like 1985-04-01T15:23:49... Or so | ||
| 25 | ;; you'd think. This is what everybody means when they say "ISO8601", | ||
| 26 | ;; but it's in reality a quite large collection of syntaxes, including | ||
| 27 | ;; week numbers, ordinal dates, durations and intervals. This package | ||
| 28 | ;; has functions for parsing them all. | ||
| 29 | ;; | ||
| 30 | ;; The interface functions are `iso8601-parse', `iso8601-parse-date', | ||
| 31 | ;; `iso8601-parse-time', `iso8601-parse-zone', | ||
| 32 | ;; `iso8601-parse-duration' and `iso8601-parse-interval'. They all | ||
| 33 | ;; return decoded time objects, except the last one, which returns a | ||
| 34 | ;; list of three of them. | ||
| 35 | ;; | ||
| 36 | ;; (iso8601-parse-interval "P1Y2M10DT2H30M/2008W32T153000-01") | ||
| 37 | ;; '((0 0 13 24 5 2007 nil nil -3600) | ||
| 38 | ;; (0 30 15 3 8 2008 nil nil -3600) | ||
| 39 | ;; (0 30 2 10 2 1 nil nil nil)) | ||
| 40 | ;; | ||
| 41 | ;; | ||
| 42 | ;; The standard can be found at: | ||
| 43 | ;; | ||
| 44 | ;; http://www.loc.gov/standards/datetime/iso-tc154-wg5_n0038_iso_wd_8601-1_2016-02-16.pdf | ||
| 45 | ;; | ||
| 46 | ;; The Wikipedia page on the standard is also informative: | ||
| 47 | ;; | ||
| 48 | ;; https://en.wikipedia.org/wiki/ISO_8601 | ||
| 49 | ;; | ||
| 50 | ;; RFC3339 defines the subset that everybody thinks of as "ISO8601". | ||
| 51 | |||
| 52 | ;;; Code: | ||
| 53 | |||
| 54 | (require 'time-date) | ||
| 55 | (require 'cl-lib) | ||
| 56 | |||
| 57 | (defun iso8601--concat-regexps (regexps) | ||
| 58 | (mapconcat (lambda (regexp) | ||
| 59 | (concat "\\(?:" | ||
| 60 | (replace-regexp-in-string "(" "(?:" regexp) | ||
| 61 | "\\)")) | ||
| 62 | regexps "\\|")) | ||
| 63 | |||
| 64 | (defconst iso8601--year-match | ||
| 65 | "\\([-+]\\)?\\([0-9][0-9][0-9][0-9]\\)") | ||
| 66 | (defconst iso8601--full-date-match | ||
| 67 | "\\([-+]\\)?\\([0-9][0-9][0-9][0-9]\\)-?\\([0-9][0-9]\\)-?\\([0-9][0-9]\\)") | ||
| 68 | (defconst iso8601--without-day-match | ||
| 69 | "\\([-+]\\)?\\([0-9][0-9][0-9][0-9]\\)-\\([0-9][0-9]\\)") | ||
| 70 | (defconst iso8601--outdated-date-match | ||
| 71 | "--\\([0-9][0-9]\\)-?\\([0-9][0-9]\\)") | ||
| 72 | (defconst iso8601--week-date-match | ||
| 73 | "\\([-+]\\)?\\([0-9][0-9][0-9][0-9]\\)-?W\\([0-9][0-9]\\)-?\\([0-9]\\)?") | ||
| 74 | (defconst iso8601--ordinal-date-match | ||
| 75 | "\\([-+]\\)?\\([0-9][0-9][0-9][0-9]\\)-?\\([0-9][0-9][0-9]\\)") | ||
| 76 | (defconst iso8601--date-match | ||
| 77 | (iso8601--concat-regexps | ||
| 78 | (list iso8601--year-match | ||
| 79 | iso8601--full-date-match | ||
| 80 | iso8601--without-day-match | ||
| 81 | iso8601--outdated-date-match | ||
| 82 | iso8601--week-date-match | ||
| 83 | iso8601--ordinal-date-match))) | ||
| 84 | |||
| 85 | (defconst iso8601--time-match | ||
| 86 | "\\([0-9][0-9]\\):?\\([0-9][0-9]\\)?:?\\([0-9][0-9]\\)?\\.?\\([0-9][0-9][0-9]\\)?") | ||
| 87 | |||
| 88 | (defconst iso8601--zone-match | ||
| 89 | "\\(Z\\|\\([-+]\\)\\([0-9][0-9]\\):?\\([0-9][0-9]\\)?\\)") | ||
| 90 | |||
| 91 | (defconst iso8601--full-time-match | ||
| 92 | (concat "\\(" (replace-regexp-in-string "(" "(?:" iso8601--time-match) "\\)" | ||
| 93 | "\\(" iso8601--zone-match "\\)?")) | ||
| 94 | |||
| 95 | (defconst iso8601--combined-match | ||
| 96 | (concat "\\(" iso8601--date-match "\\)" | ||
| 97 | "\\(?:T\\(" | ||
| 98 | (replace-regexp-in-string "(" "(?:" iso8601--time-match) | ||
| 99 | "\\)" | ||
| 100 | "\\(" iso8601--zone-match "\\)?\\)?")) | ||
| 101 | |||
| 102 | (defconst iso8601--duration-full-match | ||
| 103 | "P\\([0-9]+Y\\)?\\([0-9]+M\\)?\\([0-9]+D\\)?\\(T\\([0-9]+H\\)?\\([0-9]+M\\)?\\([0-9]+S\\)?\\)?") | ||
| 104 | (defconst iso8601--duration-week-match | ||
| 105 | "P\\([0-9]+\\)W") | ||
| 106 | (defconst iso8601--duration-combined-match | ||
| 107 | (concat "P" iso8601--combined-match)) | ||
| 108 | (defconst iso8601--duration-match | ||
| 109 | (iso8601--concat-regexps | ||
| 110 | (list iso8601--duration-full-match | ||
| 111 | iso8601--duration-week-match | ||
| 112 | iso8601--duration-combined-match))) | ||
| 113 | |||
| 114 | (defun iso8601-parse (string) | ||
| 115 | "Parse an ISO 8601 date/time string and return a `decoded-time' structure. | ||
| 116 | |||
| 117 | The ISO 8601 date/time strings look like \"2008-03-02T13:47:30\", | ||
| 118 | but shorter, incomplete strings like \"2008-03-02\" are valid, as | ||
| 119 | well as variants like \"2008W32\" (week number) and | ||
| 120 | \"2008-234\" (ordinal day number)." | ||
| 121 | (if (not (iso8601-valid-p string)) | ||
| 122 | (signal 'wrong-type-argument string) | ||
| 123 | (let* ((date-string (match-string 1 string)) | ||
| 124 | (time-string (match-string 2 string)) | ||
| 125 | (zone-string (match-string 3 string)) | ||
| 126 | (date (iso8601-parse-date date-string))) | ||
| 127 | ;; The time portion is optional. | ||
| 128 | (when time-string | ||
| 129 | (let ((time (iso8601-parse-time time-string))) | ||
| 130 | (setf (decoded-time-hour date) (decoded-time-hour time)) | ||
| 131 | (setf (decoded-time-minute date) (decoded-time-minute time)) | ||
| 132 | (setf (decoded-time-second date) (decoded-time-second time)))) | ||
| 133 | ;; The time zone is optional. | ||
| 134 | (when zone-string | ||
| 135 | (setf (decoded-time-zone date) | ||
| 136 | ;; The time zone in decoded times are in seconds. | ||
| 137 | (* (iso8601-parse-zone zone-string) 60))) | ||
| 138 | date))) | ||
| 139 | |||
| 140 | (defun iso8601-parse-date (string) | ||
| 141 | "Parse STRING (which should be on ISO 8601 format) and return a time value." | ||
| 142 | (cond | ||
| 143 | ;; Just a year: [-+]YYYY. | ||
| 144 | ((iso8601--match iso8601--year-match string) | ||
| 145 | (iso8601--decoded-time | ||
| 146 | :year (iso8601--adjust-year (match-string 1 string) | ||
| 147 | (match-string 2 string)))) | ||
| 148 | ;; Calendar dates: YYYY-MM-DD and variants. | ||
| 149 | ((iso8601--match iso8601--full-date-match string) | ||
| 150 | (iso8601--decoded-time | ||
| 151 | :year (iso8601--adjust-year (match-string 1 string) | ||
| 152 | (match-string 2 string)) | ||
| 153 | :month (match-string 3 string) | ||
| 154 | :day (match-string 4 string))) | ||
| 155 | ;; Calendar date without day: YYYY-MM. | ||
| 156 | ((iso8601--match iso8601--without-day-match string) | ||
| 157 | (iso8601--decoded-time | ||
| 158 | :year (iso8601--adjust-year (match-string 1 string) | ||
| 159 | (match-string 2 string)) | ||
| 160 | :month (match-string 3 string))) | ||
| 161 | ;; Outdated date without year: --MM-DD | ||
| 162 | ((iso8601--match iso8601--outdated-date-match string) | ||
| 163 | (iso8601--decoded-time | ||
| 164 | :month (match-string 1 string) | ||
| 165 | :day (match-string 2 string))) | ||
| 166 | ;; Week dates: YYYY-Www-D | ||
| 167 | ((iso8601--match iso8601--week-date-match string) | ||
| 168 | (let* ((year (iso8601--adjust-year (match-string 1 string) | ||
| 169 | (match-string 2 string))) | ||
| 170 | (week (string-to-number (match-string 3 string))) | ||
| 171 | (day-of-week (and (match-string 4 string) | ||
| 172 | (string-to-number (match-string 4 string)))) | ||
| 173 | (jan-start (decoded-time-weekday | ||
| 174 | (decode-time | ||
| 175 | (iso8601--encode-time | ||
| 176 | (iso8601--decoded-time :year year | ||
| 177 | :month 1 | ||
| 178 | :day 4))))) | ||
| 179 | (correction (+ (if (zerop jan-start) 7 jan-start) | ||
| 180 | 3)) | ||
| 181 | (ordinal (+ (* week 7) (or day-of-week 0) (- correction)))) | ||
| 182 | (cond | ||
| 183 | ;; Monday 29 December 2008 is written "2009-W01-1". | ||
| 184 | ((< ordinal 1) | ||
| 185 | (setq year (1- year) | ||
| 186 | ordinal (+ ordinal (if (date-leap-year-p year) | ||
| 187 | 366 365)))) | ||
| 188 | ;; Sunday 3 January 2010 is written "2009-W53-7". | ||
| 189 | ((> ordinal (if (date-leap-year-p year) | ||
| 190 | 366 365)) | ||
| 191 | (setq ordinal (- ordinal (if (date-leap-year-p year) | ||
| 192 | 366 365)) | ||
| 193 | year (1+ year)))) | ||
| 194 | (let ((month-day (date-ordinal-to-time year ordinal))) | ||
| 195 | (iso8601--decoded-time :year year | ||
| 196 | :month (decoded-time-month month-day) | ||
| 197 | :day (decoded-time-day month-day))))) | ||
| 198 | ;; Ordinal dates: YYYY-DDD | ||
| 199 | ((iso8601--match iso8601--ordinal-date-match string) | ||
| 200 | (let* ((year (iso8601--adjust-year (match-string 1 string) | ||
| 201 | (match-string 2 string))) | ||
| 202 | (ordinal (string-to-number (match-string 3 string))) | ||
| 203 | (month-day (date-ordinal-to-time year ordinal))) | ||
| 204 | (iso8601--decoded-time :year year | ||
| 205 | :month (decoded-time-month month-day) | ||
| 206 | :day (decoded-time-day month-day)))) | ||
| 207 | (t | ||
| 208 | (signal 'wrong-type-argument string)))) | ||
| 209 | |||
| 210 | (defun iso8601--adjust-year (sign year) | ||
| 211 | (save-match-data | ||
| 212 | (let ((year (if (stringp year) | ||
| 213 | (string-to-number year) | ||
| 214 | year))) | ||
| 215 | (if (string= sign "-") | ||
| 216 | ;; -0001 is 2 BCE. | ||
| 217 | (1- (- year)) | ||
| 218 | year)))) | ||
| 219 | |||
| 220 | (defun iso8601-parse-time (string) | ||
| 221 | "Parse STRING, which should be an ISO 8601 time string, and return a time value." | ||
| 222 | (if (not (iso8601--match iso8601--full-time-match string)) | ||
| 223 | (signal 'wrong-type-argument string) | ||
| 224 | (let ((time (match-string 1 string)) | ||
| 225 | (zone (match-string 2 string))) | ||
| 226 | (if (not (iso8601--match iso8601--time-match time)) | ||
| 227 | (signal 'wrong-type-argument string) | ||
| 228 | (let ((hour (string-to-number (match-string 1 time))) | ||
| 229 | (minute (and (match-string 2 time) | ||
| 230 | (string-to-number (match-string 2 time)))) | ||
| 231 | (second (and (match-string 3 time) | ||
| 232 | (string-to-number (match-string 3 time)))) | ||
| 233 | ;; Hm... | ||
| 234 | (_millisecond (and (match-string 4 time) | ||
| 235 | (string-to-number (match-string 4 time))))) | ||
| 236 | (iso8601--decoded-time :hour hour | ||
| 237 | :minute (or minute 0) | ||
| 238 | :second (or second 0) | ||
| 239 | :zone (and zone | ||
| 240 | (* 60 (iso8601-parse-zone | ||
| 241 | zone))))))))) | ||
| 242 | |||
| 243 | (defun iso8601-parse-zone (string) | ||
| 244 | "Parse STRING, which should be an ISO 8601 time zone. | ||
| 245 | Return the number of minutes." | ||
| 246 | (if (not (iso8601--match iso8601--zone-match string)) | ||
| 247 | (signal 'wrong-type-argument string) | ||
| 248 | (if (match-string 2 string) | ||
| 249 | ;; HH:MM-ish. | ||
| 250 | (let ((hour (string-to-number (match-string 3 string))) | ||
| 251 | (minute (and (match-string 4 string) | ||
| 252 | (string-to-number (match-string 4 string))))) | ||
| 253 | (* (if (equal (match-string 2 string) "-") | ||
| 254 | -1 | ||
| 255 | 1) | ||
| 256 | (+ (* hour 60) | ||
| 257 | (or minute 0)))) | ||
| 258 | ;; "Z". | ||
| 259 | 0))) | ||
| 260 | |||
| 261 | (defun iso8601-valid-p (string) | ||
| 262 | "Say whether STRING is a valid ISO 8601 representation." | ||
| 263 | (iso8601--match iso8601--combined-match string)) | ||
| 264 | |||
| 265 | (defun iso8601-parse-duration (string) | ||
| 266 | "Parse ISO 8601 durations on the form P3Y6M4DT12H30M5S." | ||
| 267 | (cond | ||
| 268 | ((and (iso8601--match iso8601--duration-full-match string) | ||
| 269 | ;; Just a "P" isn't valid; there has to be at least one | ||
| 270 | ;; element, like P1M. | ||
| 271 | (> (length (match-string 0 string)) 2)) | ||
| 272 | (iso8601--decoded-time :year (or (match-string 1 string) 0) | ||
| 273 | :month (or (match-string 2 string) 0) | ||
| 274 | :day (or (match-string 3 string) 0) | ||
| 275 | :hour (or (match-string 5 string) 0) | ||
| 276 | :minute (or (match-string 6 string) 0) | ||
| 277 | :second (or (match-string 7 string) 0))) | ||
| 278 | ;; PnW: Weeks. | ||
| 279 | ((iso8601--match iso8601--duration-week-match string) | ||
| 280 | (let ((weeks (string-to-number (match-string 1 string)))) | ||
| 281 | ;; Does this make sense? Hm... | ||
| 282 | (iso8601--decoded-time :day (* weeks 7)))) | ||
| 283 | ;; P<date>T<time> | ||
| 284 | ((iso8601--match iso8601--duration-combined-match string) | ||
| 285 | (iso8601-parse (substring string 1))) | ||
| 286 | (t | ||
| 287 | (signal 'wrong-type-argument string)))) | ||
| 288 | |||
| 289 | (defun iso8601-parse-interval (string) | ||
| 290 | "Parse ISO 8601 intervals." | ||
| 291 | (let ((bits (split-string string "/")) | ||
| 292 | start end duration) | ||
| 293 | (if (not (= (length bits) 2)) | ||
| 294 | (signal 'wrong-type-argument string) | ||
| 295 | ;; The intervals may be an explicit start/end times, or either a | ||
| 296 | ;; start or an end, and an accompanying duration. | ||
| 297 | (cond | ||
| 298 | ((and (string-match "\\`P" (car bits)) | ||
| 299 | (iso8601-valid-p (cadr bits))) | ||
| 300 | (setq duration (iso8601-parse-duration (car bits)) | ||
| 301 | end (iso8601-parse (cadr bits)))) | ||
| 302 | ((and (string-match "\\`P" (cadr bits)) | ||
| 303 | (iso8601-valid-p (car bits))) | ||
| 304 | (setq duration (iso8601-parse-duration (cadr bits)) | ||
| 305 | start (iso8601-parse (car bits)))) | ||
| 306 | ((and (iso8601-valid-p (car bits)) | ||
| 307 | (iso8601-valid-p (cadr bits))) | ||
| 308 | (setq start (iso8601-parse (car bits)) | ||
| 309 | end (iso8601-parse (cadr bits)))) | ||
| 310 | (t | ||
| 311 | (signal 'wrong-type-argument string)))) | ||
| 312 | (unless end | ||
| 313 | (setq end (decoded-time-add start duration))) | ||
| 314 | (unless start | ||
| 315 | (setq start (decoded-time-add end | ||
| 316 | ;; We negate the duration so that | ||
| 317 | ;; we get a subtraction. | ||
| 318 | (mapcar (lambda (elem) | ||
| 319 | (if (numberp elem) | ||
| 320 | (- elem) | ||
| 321 | elem)) | ||
| 322 | duration)))) | ||
| 323 | (list start end | ||
| 324 | (or duration | ||
| 325 | (decode-time (time-subtract (iso8601--encode-time end) | ||
| 326 | (iso8601--encode-time start)) | ||
| 327 | (or (decoded-time-zone end) 0)))))) | ||
| 328 | |||
| 329 | (defun iso8601--match (regexp string) | ||
| 330 | (string-match (concat "\\`" regexp "\\'") string)) | ||
| 331 | |||
| 332 | (defun iso8601--value (elem &optional default) | ||
| 333 | (if (stringp elem) | ||
| 334 | (string-to-number elem) | ||
| 335 | (or elem default))) | ||
| 336 | |||
| 337 | (cl-defun iso8601--decoded-time (&key second minute hour | ||
| 338 | day month year | ||
| 339 | dst zone) | ||
| 340 | (list (iso8601--value second) | ||
| 341 | (iso8601--value minute) | ||
| 342 | (iso8601--value hour) | ||
| 343 | (iso8601--value day) | ||
| 344 | (iso8601--value month) | ||
| 345 | (iso8601--value year) | ||
| 346 | nil | ||
| 347 | dst | ||
| 348 | zone)) | ||
| 349 | |||
| 350 | (defun iso8601--encode-time (time) | ||
| 351 | "Like `encode-time', but fill in nil values in TIME." | ||
| 352 | (setq time (copy-sequence time)) | ||
| 353 | (unless (decoded-time-second time) | ||
| 354 | (setf (decoded-time-second time) 0)) | ||
| 355 | (unless (decoded-time-minute time) | ||
| 356 | (setf (decoded-time-minute time) 0)) | ||
| 357 | (unless (decoded-time-hour time) | ||
| 358 | (setf (decoded-time-hour time) 0)) | ||
| 359 | |||
| 360 | (unless (decoded-time-day time) | ||
| 361 | (setf (decoded-time-day time) 1)) | ||
| 362 | (unless (decoded-time-month time) | ||
| 363 | (setf (decoded-time-month time) 1)) | ||
| 364 | (unless (decoded-time-year time) | ||
| 365 | (setf (decoded-time-year time) 0)) | ||
| 366 | (encode-time time)) | ||
| 367 | |||
| 368 | (provide 'iso8601) | ||
| 369 | |||
| 370 | ;;; 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 @@ | |||
| 1 | ;;; iso8601-tests.el --- tests for calendar/iso8601.el -*- lexical-binding:t -*- | ||
| 2 | |||
| 3 | ;; Copyright (C) 2019 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | ;; This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | ;; GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | ;; it under the terms of the GNU General Public License as published by | ||
| 9 | ;; the Free Software Foundation, either version 3 of the License, or | ||
| 10 | ;; (at your option) any later version. | ||
| 11 | |||
| 12 | ;; GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | ;; GNU General Public License for more details. | ||
| 16 | |||
| 17 | ;; You should have received a copy of the GNU General Public License | ||
| 18 | ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. | ||
| 19 | |||
| 20 | ;;; Code: | ||
| 21 | |||
| 22 | (require 'ert) | ||
| 23 | (require 'iso8601) | ||
| 24 | |||
| 25 | (ert-deftest test-iso8601-date-years () | ||
| 26 | (should (equal (iso8601-parse-date "1985") | ||
| 27 | '(nil nil nil nil nil 1985 nil nil nil))) | ||
| 28 | (should (equal (iso8601-parse-date "-0003") | ||
| 29 | '(nil nil nil nil nil -4 nil nil nil))) | ||
| 30 | (should (equal (iso8601-parse-date "+1985") | ||
| 31 | '(nil nil nil nil nil 1985 nil nil nil)))) | ||
| 32 | |||
| 33 | (ert-deftest test-iso8601-date-dates () | ||
| 34 | (should (equal (iso8601-parse-date "1985-03-14") | ||
| 35 | '(nil nil nil 14 3 1985 nil nil nil))) | ||
| 36 | (should (equal (iso8601-parse-date "19850314") | ||
| 37 | '(nil nil nil 14 3 1985 nil nil nil))) | ||
| 38 | (should (equal (iso8601-parse-date "1985-02") | ||
| 39 | '(nil nil nil nil 2 1985 nil nil nil)))) | ||
| 40 | |||
| 41 | (ert-deftest test-iso8601-date-obsolete () | ||
| 42 | (should (equal (iso8601-parse-date "--02-01") | ||
| 43 | '(nil nil nil 1 2 nil nil nil nil))) | ||
| 44 | (should (equal (iso8601-parse-date "--0201") | ||
| 45 | '(nil nil nil 1 2 nil nil nil nil)))) | ||
| 46 | |||
| 47 | (ert-deftest test-iso8601-date-weeks () | ||
| 48 | (should (equal (iso8601-parse-date "2008W39-6") | ||
| 49 | '(nil nil nil 27 9 2008 nil nil nil))) | ||
| 50 | (should (equal (iso8601-parse-date "2009W01-1") | ||
| 51 | '(nil nil nil 29 12 2008 nil nil nil))) | ||
| 52 | (should (equal (iso8601-parse-date "2009W53-7") | ||
| 53 | '(nil nil nil 3 1 2010 nil nil nil)))) | ||
| 54 | |||
| 55 | (ert-deftest test-iso8601-date-ordinals () | ||
| 56 | (should (equal (iso8601-parse-date "1981-095") | ||
| 57 | '(nil nil nil 5 4 1981 nil nil nil)))) | ||
| 58 | |||
| 59 | (ert-deftest test-iso8601-time () | ||
| 60 | (should (equal (iso8601-parse-time "13:47:30") | ||
| 61 | '(30 47 13 nil nil nil nil nil nil))) | ||
| 62 | (should (equal (iso8601-parse-time "134730") | ||
| 63 | '(30 47 13 nil nil nil nil nil nil))) | ||
| 64 | (should (equal (iso8601-parse-time "1347") | ||
| 65 | '(0 47 13 nil nil nil nil nil nil)))) | ||
| 66 | |||
| 67 | (ert-deftest test-iso8601-combined () | ||
| 68 | (should (equal (iso8601-parse "2008-03-02T13:47:30") | ||
| 69 | '(30 47 13 2 3 2008 nil nil nil))) | ||
| 70 | (should (equal (iso8601-parse "2008-03-02T13:47:30Z") | ||
| 71 | '(30 47 13 2 3 2008 nil nil 0))) | ||
| 72 | (should (equal (iso8601-parse "2008-03-02T13:47:30+01:00") | ||
| 73 | '(30 47 13 2 3 2008 nil nil 3600))) | ||
| 74 | (should (equal (iso8601-parse "2008-03-02T13:47:30-01") | ||
| 75 | '(30 47 13 2 3 2008 nil nil -3600)))) | ||
| 76 | |||
| 77 | (ert-deftest test-iso8601-duration () | ||
| 78 | (should (equal (iso8601-parse-duration "P3Y6M4DT12H30M5S") | ||
| 79 | '(5 30 12 4 6 3 nil nil nil))) | ||
| 80 | (should (equal (iso8601-parse-duration "P1M") | ||
| 81 | '(0 0 0 0 1 0 nil nil nil))) | ||
| 82 | (should (equal (iso8601-parse-duration "PT1M") | ||
| 83 | '(0 1 0 0 0 0 nil nil nil))) | ||
| 84 | (should (equal (iso8601-parse-duration "P0003-06-04T12:30:05") | ||
| 85 | '(5 30 12 4 6 3 nil nil nil)))) | ||
| 86 | |||
| 87 | (ert-deftest test-iso8601-invalid () | ||
| 88 | (should-not (iso8601-valid-p " 2008-03-02T13:47:30-01")) | ||
| 89 | (should-not (iso8601-valid-p "2008-03-02T13:47:30-01:200")) | ||
| 90 | (should-not (iso8601-valid-p "2008-03-02T13:47:30-01 ")) | ||
| 91 | (should-not (iso8601-valid-p "2008-03-02 T 13:47:30-01 ")) | ||
| 92 | (should-not (iso8601-valid-p "20008-03-02T13:47:30-01"))) | ||
| 93 | |||
| 94 | (ert-deftest test-iso8601-intervals () | ||
| 95 | (should (equal | ||
| 96 | (iso8601-parse-interval "2007-03-01T13:00:00Z/2008-05-11T15:30:00Z") | ||
| 97 | '((0 0 13 1 3 2007 nil nil 0) | ||
| 98 | (0 30 15 11 5 2008 nil nil 0) | ||
| 99 | ;; Hm... can't really use decode-time for time differences... | ||
| 100 | (0 30 2 14 3 1971 0 nil 0)))) | ||
| 101 | (should (equal (iso8601-parse-interval "2007-03-01T13:00:00Z/P1Y2M10DT2H30M") | ||
| 102 | '((0 0 13 1 3 2007 nil nil 0) | ||
| 103 | (0 30 15 11 5 2008 nil nil 0) | ||
| 104 | (0 30 2 10 2 1 nil nil nil)))) | ||
| 105 | (should (equal (iso8601-parse-interval "P1Y2M10DT2H30M/2008-05-11T15:30:00Z") | ||
| 106 | '((0 0 13 1 3 2007 nil nil 0) | ||
| 107 | (0 30 15 11 5 2008 nil nil 0) | ||
| 108 | (0 30 2 10 2 1 nil nil nil))))) | ||
| 109 | |||
| 110 | (ert-deftest standard-test-dates () | ||
| 111 | (should (equal (iso8601-parse-date "19850412") | ||
| 112 | '(nil nil nil 12 4 1985 nil nil nil))) | ||
| 113 | (should (equal (iso8601-parse-date "1985-04-12") | ||
| 114 | '(nil nil nil 12 4 1985 nil nil nil))) | ||
| 115 | |||
| 116 | (should (equal (iso8601-parse-date "1985102") | ||
| 117 | '(nil nil nil 12 4 1985 nil nil nil))) | ||
| 118 | (should (equal (iso8601-parse-date "1985-102") | ||
| 119 | '(nil nil nil 12 4 1985 nil nil nil))) | ||
| 120 | |||
| 121 | (should (equal (iso8601-parse-date "1985W155") | ||
| 122 | '(nil nil nil 12 4 1985 nil nil nil))) | ||
| 123 | (should (equal (iso8601-parse-date "1985-W15-5") | ||
| 124 | '(nil nil nil 12 4 1985 nil nil nil))) | ||
| 125 | |||
| 126 | (should (equal (iso8601-parse-date "1985W15") | ||
| 127 | '(nil nil nil 7 4 1985 nil nil nil))) | ||
| 128 | (should (equal (iso8601-parse-date "1985-W15") | ||
| 129 | '(nil nil nil 7 4 1985 nil nil nil))) | ||
| 130 | |||
| 131 | (should (equal (iso8601-parse-date "1985-04") | ||
| 132 | '(nil nil nil nil 4 1985 nil nil nil))) | ||
| 133 | |||
| 134 | (should (equal (iso8601-parse-date "1985") | ||
| 135 | '(nil nil nil nil nil 1985 nil nil nil))) | ||
| 136 | |||
| 137 | (should (equal (iso8601-parse-date "+1985-04-12") | ||
| 138 | '(nil nil nil 12 4 1985 nil nil nil))) | ||
| 139 | (should (equal (iso8601-parse-date "+19850412") | ||
| 140 | '(nil nil nil 12 4 1985 nil nil nil)))) | ||
| 141 | |||
| 142 | (ert-deftest standard-test-time-of-day-local-time () | ||
| 143 | (should (equal (iso8601-parse-time "152746") | ||
| 144 | '(46 27 15 nil nil nil nil nil nil))) | ||
| 145 | (should (equal (iso8601-parse-time "15:27:46") | ||
| 146 | '(46 27 15 nil nil nil nil nil nil))) | ||
| 147 | |||
| 148 | (should (equal (iso8601-parse-time "1528") | ||
| 149 | '(0 28 15 nil nil nil nil nil nil))) | ||
| 150 | (should (equal (iso8601-parse-time "15:28") | ||
| 151 | '(0 28 15 nil nil nil nil nil nil))) | ||
| 152 | |||
| 153 | (should (equal (iso8601-parse-time "15") | ||
| 154 | '(0 0 15 nil nil nil nil nil nil)))) | ||
| 155 | |||
| 156 | (ert-deftest standard-test-time-of-day-fractions () | ||
| 157 | ;; decoded-time doesn't support sub-second times. | ||
| 158 | ;; (should (equal (iso8601-parse-time "152735,5") | ||
| 159 | ;; '(46 27 15 nil nil nil nil nil nil))) | ||
| 160 | ;; (should (equal (iso8601-parse-time "15:27:35,5") | ||
| 161 | ;; '(46 27 15 nil nil nil nil nil nil))) | ||
| 162 | ) | ||
| 163 | |||
| 164 | (ert-deftest standard-test-time-of-day-beginning-of-day () | ||
| 165 | (should (equal (iso8601-parse-time "000000") | ||
| 166 | '(0 0 0 nil nil nil nil nil nil))) | ||
| 167 | (should (equal (iso8601-parse-time "00:00:00") | ||
| 168 | '(0 0 0 nil nil nil nil nil nil))) | ||
| 169 | |||
| 170 | (should (equal (iso8601-parse-time "0000") | ||
| 171 | '(0 0 0 nil nil nil nil nil nil))) | ||
| 172 | (should (equal (iso8601-parse-time "00:00") | ||
| 173 | '(0 0 0 nil nil nil nil nil nil)))) | ||
| 174 | |||
| 175 | (ert-deftest standard-test-time-of-day-utc () | ||
| 176 | (should (equal (iso8601-parse-time "232030Z") | ||
| 177 | '(30 20 23 nil nil nil nil nil 0))) | ||
| 178 | (should (equal (iso8601-parse-time "23:20:30Z") | ||
| 179 | '(30 20 23 nil nil nil nil nil 0))) | ||
| 180 | |||
| 181 | (should (equal (iso8601-parse-time "2320Z") | ||
| 182 | '(0 20 23 nil nil nil nil nil 0))) | ||
| 183 | (should (equal (iso8601-parse-time "23:20Z") | ||
| 184 | '(0 20 23 nil nil nil nil nil 0))) | ||
| 185 | |||
| 186 | (should (equal (iso8601-parse-time "23Z") | ||
| 187 | '(0 0 23 nil nil nil nil nil 0)))) | ||
| 188 | |||
| 189 | |||
| 190 | (ert-deftest standard-test-time-of-day-zone () | ||
| 191 | (should (equal (iso8601-parse-time "152746+0100") | ||
| 192 | '(46 27 15 nil nil nil nil nil 3600))) | ||
| 193 | (should (equal (iso8601-parse-time "15:27:46+0100") | ||
| 194 | '(46 27 15 nil nil nil nil nil 3600))) | ||
| 195 | |||
| 196 | (should (equal (iso8601-parse-time "152746+01") | ||
| 197 | '(46 27 15 nil nil nil nil nil 3600))) | ||
| 198 | (should (equal (iso8601-parse-time "15:27:46+01") | ||
| 199 | '(46 27 15 nil nil nil nil nil 3600))) | ||
| 200 | |||
| 201 | (should (equal (iso8601-parse-time "152746-0500") | ||
| 202 | '(46 27 15 nil nil nil nil nil -18000))) | ||
| 203 | (should (equal (iso8601-parse-time "15:27:46-0500") | ||
| 204 | '(46 27 15 nil nil nil nil nil -18000))) | ||
| 205 | |||
| 206 | (should (equal (iso8601-parse-time "152746-05") | ||
| 207 | '(46 27 15 nil nil nil nil nil -18000))) | ||
| 208 | (should (equal (iso8601-parse-time "15:27:46-05") | ||
| 209 | '(46 27 15 nil nil nil nil nil -18000)))) | ||
| 210 | |||
| 211 | (ert-deftest standard-test-date-and-time-of-day () | ||
| 212 | (should (equal (iso8601-parse "19850412T101530") | ||
| 213 | '(30 15 10 12 4 1985 nil nil nil))) | ||
| 214 | (should (equal (iso8601-parse "1985-04-12T10:15:30") | ||
| 215 | '(30 15 10 12 4 1985 nil nil nil))) | ||
| 216 | |||
| 217 | (should (equal (iso8601-parse "1985102T235030Z") | ||
| 218 | '(30 50 23 12 4 1985 nil nil 0))) | ||
| 219 | (should (equal (iso8601-parse "1985-102T23:50:30Z") | ||
| 220 | '(30 50 23 12 4 1985 nil nil 0))) | ||
| 221 | |||
| 222 | (should (equal (iso8601-parse "1985W155T235030") | ||
| 223 | '(30 50 23 12 4 1985 nil nil nil))) | ||
| 224 | (should (equal (iso8601-parse "1985-W155T23:50:30") | ||
| 225 | '(30 50 23 12 4 1985 nil nil nil)))) | ||
| 226 | |||
| 227 | (ert-deftest standard-test-interval () | ||
| 228 | ;; A time interval starting at 20 minutes and 50 seconds past 23 | ||
| 229 | ;; hours on 12 April 1985 and ending at 30 minutes past 10 hours on | ||
| 230 | ;; 25 June 1985. | ||
| 231 | (should (equal (iso8601-parse-interval "19850412T232050/19850625T103000") | ||
| 232 | '((50 20 23 12 4 1985 nil nil nil) | ||
| 233 | (0 30 10 25 6 1985 nil nil nil) | ||
| 234 | (10 9 11 15 3 1970 0 nil 0)))) | ||
| 235 | (should (equal (iso8601-parse-interval | ||
| 236 | "1985-04-12T23:20:50/1985-06-25T10:30:00") | ||
| 237 | '((50 20 23 12 4 1985 nil nil nil) | ||
| 238 | (0 30 10 25 6 1985 nil nil nil) | ||
| 239 | (10 9 11 15 3 1970 0 nil 0)))) | ||
| 240 | |||
| 241 | ;; A time interval starting at 12 April 1985 and ending on 25 June | ||
| 242 | ;; 1985. | ||
| 243 | |||
| 244 | ;; This example doesn't seem valid according to the standard. | ||
| 245 | ;; "0625" is unambiguous, and means "the year 625". Weird. | ||
| 246 | ;; (should (equal (iso8601-parse-interval "19850412/0625") | ||
| 247 | ;; '((nil nil nil 12 4 1985 nil nil nil) | ||
| 248 | ;; (nil nil nil nil nil 625 nil nil nil) | ||
| 249 | ;; (0 17 0 22 9 609 5 nil 0)))) | ||
| 250 | |||
| 251 | ;; A time interval of 2 years, 10 months, 15 days, 10 hours, 20 | ||
| 252 | ;; minutes and 30 seconds. | ||
| 253 | (should (equal (iso8601-parse-duration "P2Y10M15DT10H20M30S") | ||
| 254 | '(30 20 10 15 10 2 nil nil nil))) | ||
| 255 | |||
| 256 | (should (equal (iso8601-parse-duration "P00021015T102030") | ||
| 257 | '(30 20 10 15 10 2 nil nil nil))) | ||
| 258 | (should (equal (iso8601-parse-duration "P0002-10-15T10:20:30") | ||
| 259 | '(30 20 10 15 10 2 nil nil nil))) | ||
| 260 | |||
| 261 | ;; A time interval of 1 year and 6 months. | ||
| 262 | (should (equal (iso8601-parse-duration "P1Y6M") | ||
| 263 | '(0 0 0 0 6 1 nil nil nil))) | ||
| 264 | (should (equal (iso8601-parse-duration "P0001-06") | ||
| 265 | '(nil nil nil nil 6 1 nil nil nil))) | ||
| 266 | |||
| 267 | ;; A time interval of seventy-two hours. | ||
| 268 | (should (equal (iso8601-parse-duration "PT72H") | ||
| 269 | '(0 0 72 0 0 0 nil nil nil))) | ||
| 270 | |||
| 271 | ;; Defined by start and duration | ||
| 272 | ;; A time interval of 1 year, 2 months, 15 days and 12 hours, | ||
| 273 | ;; beginning on 12 April 1985 at 20 minutes past 23 hours. | ||
| 274 | (should (equal (iso8601-parse-interval "19850412T232000/P1Y2M15DT12H") | ||
| 275 | '((0 20 23 12 4 1985 nil nil nil) | ||
| 276 | (0 20 11 28 6 1986 nil nil nil) | ||
| 277 | (0 0 12 15 2 1 nil nil nil)))) | ||
| 278 | (should (equal (iso8601-parse-interval "1985-04-12T23:20:00/P1Y2M15DT12H") | ||
| 279 | '((0 20 23 12 4 1985 nil nil nil) | ||
| 280 | (0 20 11 28 6 1986 nil nil nil) | ||
| 281 | (0 0 12 15 2 1 nil nil nil)))) | ||
| 282 | |||
| 283 | ;; Defined by duration and end | ||
| 284 | ;; A time interval of 1 year, 2 months, 15 days and 12 hours, ending | ||
| 285 | ;; on 12 April 1985 at 20 minutes past 23 hour. | ||
| 286 | (should (equal (iso8601-parse-interval "P1Y2M15DT12H/19850412T232000") | ||
| 287 | '((0 20 11 28 1 1984 nil nil nil) | ||
| 288 | (0 20 23 12 4 1985 nil nil nil) | ||
| 289 | (0 0 12 15 2 1 nil nil nil))))) | ||
| 290 | |||
| 291 | ;;; iso8601-tests.el ends here | ||