branch: elpa/datetime
commit 4c422b6f9dbee8e090edc288d5e6a6d10a2be313
Author: Paul Pogonyshev <[email protected]>
Commit: Paul Pogonyshev <[email protected]>
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)