branch: elpa/datetime commit 4c422b6f9dbee8e090edc288d5e6a6d10a2be313 Author: Paul Pogonyshev <pogonys...@gmail.com> Commit: Paul Pogonyshev <pogonys...@gmail.com>
Implement every of the million subtly different ways Java can format timezone offsets. --- datetime.el | 128 +++++++++++++++++++++++++++++++++++++++++++++++++-------- test/format.el | 25 +++++++++-- 2 files changed, 133 insertions(+), 20 deletions(-) diff --git a/datetime.el b/datetime.el index fb9eebe8ae..a208857919 100644 --- a/datetime.el +++ b/datetime.el @@ -112,8 +112,8 @@ ;; abbreviated, full --- timezone name, as reported by Java ;; (abbreviated is by far more useful, as full is too ;; verbose for most usecases); -;; rfc-822, iso-8601 -- currently not supported further than -;; pattern parsing. +;; offset-* -- different representations of timezone (search +;; the source code for a full list) offset to GMT. (require 'extmap) @@ -394,8 +394,24 @@ form: (?s (cons 'second num-repetitions)) (?S (cons 'second-fractional num-repetitions)) (?z (cons 'timezone (if (>= num-repetitions 4) 'full 'abbreviated))) - (?Z (cons 'timezone 'rfc-822)) - (?X (cons 'timezone 'iso-8601)) + (?O (cons 'timezone (pcase num-repetitions + (1 'offset-localized-short) + (4 'offset-localized-full) + (_ (error "Pattern character `%c' must come in exactly 1 or 4 repetitions" character))))) + ((or ?x ?X) + (cons 'timezone (let ((details (pcase num-repetitions + (1 'offset-hh?mm) + (2 'offset-hhmm) + (3 'offset-hh:mm) + (4 'offset-hhmm?ss) + (5 'offset-hh:mm?:ss) + (_ (error "Pattern character `%c' must come in 1-5 repetitions" character))))) + (if (= character ?x) details (intern (format "%s-or-z" (symbol-name details))))))) + (?Z (cons 'timezone (pcase num-repetitions + ((or 1 2 3) 'offset-hhmm) + (4 'offset-localized-full) + (5 'offset-hh:mm?:ss-or-z) + (_ (error "Pattern character `%c' must come in 1-5 repetitions" character))))) (_ (error "Illegal pattern character `%c'" character))) parts)) @@ -529,6 +545,83 @@ form: index)) +;; In functions below we rely on form arguments being evaluated from left to right. This +;; is documented in Elisp manual. Important as we use `(setf offset ...)' in the first +;; argument's of `format'. + +(defsubst datetime--format-offset-hhmm (offset) + (format (if (>= offset 0) + "+%02d%02d" + (setf offset (- offset)) + "-%02d%02d") + (/ offset (* 60 60)) (/ (% offset (* 60 60)) 60))) + +(defsubst datetime--format-offset-hh?mm (offset) + (let ((sign (if (>= offset 0) ?+ ?-)) + (hours (/ (if (>= offset 0) offset (setf offset (- offset))) (* 60 60))) + (minutes (/ (% offset (* 60 60)) 60))) + (if (= minutes 0) + (format "%c%02d" sign hours) + (format "%c%02d%02d" sign hours minutes)))) + +(defsubst datetime--format-offset-hhmm?ss (offset) + (let ((sign (if (>= offset 0) ?+ ?-)) + (seconds (% (if (>= offset 0) offset (setf offset (- offset))) 60))) + (if (= seconds 0) + (format "%c%02d%02d" sign (/ offset (* 60 60)) (/ (% offset (* 60 60)) 60)) + (format "%c%02d%02d%02d" sign (/ offset (* 60 60)) (/ (% offset (* 60 60)) 60) seconds)))) + +(defsubst datetime--format-offset-hh:mm (offset) + (format (if (>= offset 0) + "+%02d:%02d" + (setf offset (- offset)) + "-%02d:%02d") + (/ offset (* 60 60)) (/ (% offset (* 60 60)) 60))) + +(defsubst datetime--format-offset-hh:mm?:ss (offset) + (let ((sign (if (>= offset 0) ?+ ?-)) + (seconds (% (if (>= offset 0) offset (setf offset (- offset))) 60))) + (if (= seconds 0) + (format "%c%02d:%02d" sign (/ offset (* 60 60)) (/ (% offset (* 60 60)) 60)) + (format "%c%02d:%02d:%02d" sign (/ offset (* 60 60)) (/ (% offset (* 60 60)) 60) seconds)))) + +(defsubst datetime--format-offset-hhmm-or-z (offset) + (if (= offset 0) "Z" (datetime--format-offset-hhmm offset))) + +(defsubst datetime--format-offset-hh?mm-or-z (offset) + (if (= offset 0) "Z" (datetime--format-offset-hh?mm offset))) + +(defsubst datetime--format-offset-hhmm?ss-or-z (offset) + (if (= offset 0) "Z" (datetime--format-offset-hhmm?ss offset))) + +(defsubst datetime--format-offset-hh:mm-or-z (offset) + (if (= offset 0) "Z" (datetime--format-offset-hh:mm offset))) + +(defsubst datetime--format-offset-hh:mm?:ss-or-z (offset) + (if (= offset 0) "Z" (datetime--format-offset-hh:mm?:ss offset))) + +(defsubst datetime--format-offset-localized-short (offset) + (if (= offset 0) + "GMT" + (let ((sign (if (>= offset 0) ?+ ?-)) + (minutes-and-seconds (% (if (>= offset 0) offset (setf offset (- offset))) (* 60 60)))) + (if (= minutes-and-seconds 0) + (format "GMT%c%d" sign (/ offset (* 60 60))) + (let ((seconds (% minutes-and-seconds 60))) + (if (= seconds 0) + (format "GMT%c%d:%02d" sign (/ offset (* 60 60)) (/ minutes-and-seconds 60)) + (format "GMT%c%d:%02d:%02d" sign (/ offset (* 60 60)) (/ minutes-and-seconds 60) seconds))))))) + +(defsubst datetime--format-offset-localized-full (offset) + (if (= offset 0) + "GMT" + (let ((sign (if (>= offset 0) ?+ ?-)) + (seconds (% (if (>= offset 0) offset (setf offset (- offset))) 60))) + (if (= seconds 0) + (format "GMT%c%02d:%02d" sign (/ offset (* 60 60)) (/ (% offset (* 60 60)) 60)) + (format "GMT%c%02d:%02d:%02d" sign (/ offset (* 60 60)) (/ (% offset (* 60 60)) 60) seconds))))) + + (defsubst datetime--digits-format (num-repetitions) (if (> num-repetitions 1) (format "%%0%dd" num-repetitions) "%d")) @@ -679,19 +772,20 @@ to this function. (push "%s" format-parts) ;; See comments for the variable for explanation of `floatp'. (push `(if (floatp datetime--last-conversion-offset) ,dst-name ,name) format-arguments)))) - (`rfc-822 - (pcase timezone-data - (`(,constant-offset) - (push (format "%c%02d%02d" - (if (>= constant-offset 0) ?+ ?-) - (/ (abs constant-offset) (* 60 60)) - (/ (mod (abs constant-offset) (* 60 60)) 60)) - format-parts)) - (_ - (push "%c%02d%02d" format-parts) - (push `(if (>= datetime--last-conversion-offset 0) ?+ ?-) format-arguments) - (push `(/ (abs (round datetime--last-conversion-offset)) (* 60 60)) format-arguments) - (push `(/ (mod (abs (round datetime--last-conversion-offset)) (* 60 60)) 60) format-arguments)))) + ((or `offset-localized-short `offset-localized-full + `offset-hh?mm `offset-hhmm `offset-hh:mm `offset-hhmm?ss `offset-hh:mm?:ss + `offset-hh?mm-or-z `offset-hhmm-or-z `offset-hh:mm-or-z `offset-hhmm?ss-or-z `offset-hh:mm?:ss-or-z + `offset-hhmm) + (let ((formatter-function (intern (format "datetime--format-%s" (symbol-name details))))) + (pcase timezone-data + (`(,constant-offset) + (push (funcall formatter-function constant-offset) format-parts)) + (_ + ;; At least `offset-hhmm' and `offset-hh:mm' could in principle be + ;; inlined since they use (or could use) fixed format substring. + ;; Hardly terribly important. + (push "%s" format-parts) + (push `(,formatter-function (round datetime--last-conversion-offset)) format-arguments))))) (_ (signal 'datetime-unsupported-timezone details)))) (_ (error "Unexpected value %s" type)))))) diff --git a/test/format.el b/test/format.el index b7141f42c6..89ee7b3a20 100644 --- a/test/format.el +++ b/test/format.el @@ -142,10 +142,29 @@ ;; Exact numbers don't matter much, we just need to skip a few months each time. (datetime--test-formatter (mapcar (lambda (k) (* k 7000000)) (number-sequence -300 400)))))) -(ert-deftest datetime-formatting-with-timezone-name-3 () + +;; Spaces are included only for readability where needed. They don't affect anything otherwise (or, +;; rather, should affect the library and Java benchmark in the same way). +(defvar datetime--test-offset-format-specifiers + '("Z" "ZZ" "ZZZ" " ZZZZ" "ZZZZZ" + " O" " OOOO" + "x" "xx" "xxx" "xxxx" "xxxxx" + "X" "XX" "XXX" "XXXX" "XXXXX")) + +(ert-deftest datetime-formatting-with-timezone-offset-1 () (dolist (timezone (datetime-list-timezones)) - (datetime--test-set-up-formatter timezone 'en "yyyy-MM-dd HH:mm:ssZ" - (datetime--test-formatter-around-transition 1414285200)))) + (dolist (offset-format-specifier datetime--test-offset-format-specifiers) + (datetime--test-set-up-formatter timezone 'en (format "yyyy-MM-dd HH:mm:ss%s" offset-format-specifier) + (datetime--test-formatter-around-transition 1414285200))))) + +;; Test with offsets that include seconds. This was true for most real timezones in ye older times. +(ert-deftest datetime-formatting-with-timezone-offset-2 () + (dolist (timezone '(Africa/Lusaka America/Asuncion Asia/Dushanbe Asia/Tehran Atlantic/Bermuda Australia/Sydney + Brazil/East Canada/Pacific Europe/Athens Europe/Rome Europe/Zurich Indian/Antananarivo + Mexico/General Pacific/Samoa US/Central)) + (dolist (offset-format-specifier datetime--test-offset-format-specifiers) + (datetime--test-set-up-formatter timezone 'en (format "yyyy-MM-dd HH:mm:ss%s" offset-format-specifier) + (datetime--test-formatter -3000000000))))) (ert-deftest datetime-formatting-day-periods () (let (times)