emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[nongnu] elpa/datetime 28e31cbe1c 065/147: Add support for timezone name


From: ELPA Syncer
Subject: [nongnu] elpa/datetime 28e31cbe1c 065/147: Add support for timezone names in timestamps formatted by the library.
Date: Fri, 31 Jan 2025 07:00:03 -0500 (EST)

branch: elpa/datetime
commit 28e31cbe1ca9dba54309ed31ae9cd250c3180149
Author: Paul Pogonyshev <pogonyshev@gmail.com>
Commit: Paul Pogonyshev <pogonyshev@gmail.com>

    Add support for timezone names in timestamps formatted by the library.
---
 Eldev                      |  16 +++-
 datetime.el                | 141 +++++++++++++++++++++++++-----
 dev/HarvestData.java       | 207 ++++++++++++++++++++++++++++++++++++++++-----
 test/ProcessTimestamp.java |   3 +-
 test/base.el               |   2 +-
 test/format.el             |  13 ++-
 test/parse.el              |   2 +-
 timezone-data.extmap       | Bin 840168 -> 867924 bytes
 timezone-name-data.extmap  | Bin 0 -> 3388371 bytes
 9 files changed, 330 insertions(+), 54 deletions(-)

diff --git a/Eldev b/Eldev
index f767e3d50c..e2f63a3813 100644
--- a/Eldev
+++ b/Eldev
@@ -40,12 +40,22 @@
   :collect        ":data"
   (datetime--build-extmap target "--timezones"))
 
-(defun datetime--build-extmap (target &rest command-line)
+(eldev-defbuilder datetime-builder-timezone-name-data-extmap (source target)
+  :short-name     "TZ-NAMES"
+  :message        target
+  :source-files   "/dev/HarvestData.class"
+  :targets        "timezone-name-data.extmap"
+  :collect        ":data"
+  (datetime--build-extmap target "--timezone-names" :share-values t 
:compress-values t))
+
+(defun datetime--build-extmap (target command-line &rest extmap-flags)
   (require 'extmap)
-  (eldev-call-process "java" `("-cp" "dev" "HarvestData" ,@command-line)
+  (eldev-trace "Collecting data using the Java helper...")
+  (eldev-call-process "java" `("-cp" "dev" "HarvestData" ,@(eldev-listify 
command-line))
     (unless (= exit-code 0)
       (signal 'eldev-error `("`HarvestData' tool exited with error code %d: 
%s" ,exit-code ,(buffer-string))))
-    (extmap-from-alist target (eldev-read-wholly (buffer-string) 
"`HarvestData' output") :overwrite t)))
+    (eldev-trace "Building an extmap out of it...")
+    (apply #'extmap-from-alist target (eldev-read-wholly (buffer-string) 
"`HarvestData' output") :overwrite t extmap-flags)))
 
 
 ;; Before testing we need to compile `test/ProcessTimestamp.java'.
diff --git a/datetime.el b/datetime.el
index faf8679a79..5e1f2690ab 100644
--- a/datetime.el
+++ b/datetime.el
@@ -1,13 +1,13 @@
 ;;; datetime.el --- Parsing, formatting and matching timestamps  -*- 
lexical-binding: t -*-
 
-;; Copyright (C) 2016-2019 Paul Pogonyshev
+;; Copyright (C) 2016-2020 Paul Pogonyshev
 
 ;; Author:     Paul Pogonyshev <pogonyshev@gmail.com>
 ;; Maintainer: Paul Pogonyshev <pogonyshev@gmail.com>
 ;; Version:    0.6.6
 ;; Keywords:   lisp, i18n
 ;; Homepage:   https://github.com/doublep/datetime
-;; Package-Requires: ((emacs "24.1") (extmap "1.0"))
+;; Package-Requires: ((emacs "24.4") (extmap "1.1.1"))
 
 ;; This program is free software; you can redistribute it and/or
 ;; modify it under the terms of the GNU General Public License as
@@ -55,7 +55,7 @@
 
 
 ;; Internally any date-time pattern is parsed to a list of value pairs
-;; (type . details).  Type is a symbol, while details are either nil,
+;; (TYPE . DETAILS).  Type is a symbol, while details are either nil,
 ;; another symbol or a number that represents minimum number of
 ;; characters in formatted number (left padded with zeros).  The only
 ;; exception is "as-is" part: it is just a string, not a cons cell.
@@ -107,7 +107,12 @@
 ;;   decimal-separator (PREFERRED)
 ;;       either dot or comma;
 ;;
-;;   timezone (?) -- currently not supported further than pattern parsing
+;;   timezone (SYMBOL)
+;;       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.
 
 
 (require 'extmap)
@@ -129,7 +134,7 @@
 ;; obviously of `java' type.
 ;;
 ;; There are many fallbacks involved to reduce size:
-;;   - for locale XX-YY value for any property defaults to that of
+;;   - for locale XX-YY value for any property defaults to that for
 ;;     locale XX;
 ;;   - `:decimal-separator' defaults to dot;
 ;;   - `:eras' and `:am-pm' default to English version;
@@ -148,12 +153,33 @@
 ;; Extracted from Java using `dev/HarvestData.java'.
 (defvar datetime--timezone-extmap (extmap-init (expand-file-name 
"timezone-data.extmap" datetime--directory) :weak-data t :auto-reload t))
 
+;; Extracted from Java using `dev/HarvestData.java'.
+;;
+;; Fallbacks:
+;;   - for locale XX-YY names defaults to those for locale XX;
+;;   - for locale XX names default to those in English locale;
+;;   - names themselves can be in several formats (individual values
+;;     are always strings):
+;;         FULL -- abbreviated name is taken from the English locale,
+;;                 no special for DST;
+;;         (FULL-STD . FULL-DST) -- abbreviated names are taken from the
+;;                                  English locale;
+;;         [ABBREVIATED FULL] -- no special for DST;
+;;         [ABBREVIATED-STD ABBREVIATED-DST FULL-STD FULL-DST].
+(defvar datetime--timezone-name-extmap (extmap-init (expand-file-name 
"timezone-name-data.extmap" datetime--directory) :weak-data t :auto-reload t))
+
 (defvar datetime--pattern-parsers '((parsed . (lambda (pattern options) 
pattern))
                                     (java   . datetime--parse-java-pattern)))
 
 (defvar datetime--pattern-formatters '((parsed . (lambda (parts options) 
parts))
                                        (java   . 
datetime--format-java-pattern)))
 
+(defvar datetime--last-conversion-was-in-dst nil)
+
+(defvar datetime--locale-timezone-name-lookup-cache nil)
+(defvar datetime--locale-timezone-name-lookup-cache-version 0)
+
+
 ;; `datetime-list-*' must be defined here, since they are used in
 ;; `defcustom' forms below.
 (defun datetime-list-locales (&optional include-variants)
@@ -288,7 +314,7 @@ form:
 (defmacro datetime--extend-as-is-part (parts text)
   `(let ((text ,text))
      (if (stringp (car ,parts))
-         (setcar parts (concat (car ,parts) text))
+         (setf (car parts) (concat (car ,parts) text))
        (push text ,parts))))
 
 
@@ -358,12 +384,16 @@ form:
                        (?m (cons 'minute            num-repetitions))
                        (?s (cons 'second            num-repetitions))
                        (?S (cons 'second-fractional num-repetitions))
-                       (?z (cons 'timezone          'general))
+                       (?z (cons 'timezone          (if (>= num-repetitions 4) 
'full 'abbreviated)))
                        (?Z (cons 'timezone          'rfc-822))
                        (?X (cons 'timezone          'iso-8601))
                        (_
                         (error "Illegal pattern character `%c'" character)))
                      parts))
+              ;; FIXME: Optional pattern sections are currently treated the 
same as
+              ;;        mandatory (brackets are just discarded).  May want to 
treat them
+              ;;        as optional at least for parsing purposes later.
+              ((or (= character ?\[) (= character ?\])))
               (t
                (if (and (or (= character ?.) (= character ?,))
                         (plist-get options :any-decimal-separator)
@@ -480,6 +510,9 @@ form:
 (defsubst datetime--digits-format (num-repetitions)
   (if (> num-repetitions 1) (format "%%0%dd" num-repetitions) "%d"))
 
+(defsubst datetime--format-escape-string (string)
+  (replace-regexp-in-string "%" "%%" string t t))
+
 (defun datetime-float-formatter (type pattern &rest options)
   "Return a function that formats date-time expressed as a float.
 The returned function accepts single argument---a floating-point
@@ -516,7 +549,7 @@ to this function.
          format-arguments)
     (dolist (part (datetime--parse-pattern type pattern options))
       (if (stringp part)
-          (push (replace-regexp-in-string "%" "%%" part t t) format-parts)
+          (push (datetime--format-escape-string part) format-parts)
         (let ((type    (car part))
               (details (cdr part)))
           (pcase type
@@ -536,7 +569,7 @@ to this function.
                      (error "Formatting `%s' is currently not implemented" 
type))
                    format-arguments)
              (when (eq details 'always-two-digits)
-               (setcar format-arguments `(mod ,(car format-arguments) 100))))
+               (setf (car format-arguments) `(mod ,(car format-arguments) 
100))))
             (`year-for-week
              (error "Formatting `%s' is currently not implemented" type))
             (`month
@@ -606,7 +639,18 @@ to this function.
              (let ((scale (expt 10 details)))
                (push `(mod (* time ,scale) ,scale) format-arguments)))
             (`timezone
-             (signal 'datetime-unsupported-timezone nil))
+             (pcase details
+               ((or `abbreviated `full)
+                (let* ((name     (datetime-locale-timezone-name locale 
timezone nil (eq details 'full)))
+                       (dst-name (pcase timezone-data
+                                   (`(,_constant-offset) name)
+                                   (_ (datetime-locale-timezone-name locale 
timezone t (eq details 'full))))))
+                  (if (string= name dst-name)
+                      (push (datetime--format-escape-string name) format-parts)
+                    (push "%s" format-parts)
+                    (push `(if datetime--last-conversion-was-in-dst ,dst-name 
,name) format-arguments))))
+               (_
+                (signal 'datetime-unsupported-timezone details))))
             (_ (error "Unexpected value %s" type))))))
     ;; 400 is the size of Gregorian calendar leap year loop.
     (let* ((days-in-400-years datetime--gregorian-days-in-400-years)
@@ -686,8 +730,12 @@ to this function.
               (while (and (>= offset-in-year (car year-transitions))
                           (setq offset           (cadr year-transitions)
                                 year-transitions (cddr year-transitions))))))
+          ;; Floating-point offset is our internal mark of a transition to 
DST.  Its value
+          ;; is really an integer anyway.
+          (setf datetime--last-conversion-was-in-dst (floatp offset))
           (+ date-time offset))
       ;; Offset before the very first transition.
+      (setf datetime--last-conversion-was-in-dst nil)
       (+ date-time (car (aref all-year-transitions 0))))))
 
 ;; 146097 is the value of `datetime--gregorian-days-in-400-years'.
@@ -707,7 +755,7 @@ to this function.
          (num-years            (length all-year-transitions))
          transitions)
     (when (>= year-offset num-years)
-      (setcar (cdr timezone-data) (setq all-year-transitions (vconcat 
all-year-transitions (make-vector (max (1+ (- year-offset num-years)) (/ 
num-years 2) 10) nil)))))
+      (setf (cadr timezone-data) (setq all-year-transitions (vconcat 
all-year-transitions (make-vector (max (1+ (- year-offset num-years)) (/ 
num-years 2) 10) nil)))))
     (let ((year      (+ (nth 2 timezone-data) year-offset))
           (year-base (+ (nth 0 timezone-data) (* year-offset 
datetime--average-seconds-in-year))))
       (dolist (rule (nth 3 timezone-data))
@@ -726,15 +774,17 @@ to this function.
               (setq year-day (if (< day-of-month 0) (- year-day (mod (- 
day-of-week current-weekday) 7)) (+ year-day (mod (- day-of-week 
current-weekday) 7))))))
           (when (plist-get rule :end-of-day)
             (setq year-day (1+ year-day)))
-          (push (- (+ (datetime--start-of-day year year-day) (plist-get rule 
:time))
-                   (pcase (plist-get rule :time-definition)
-                     (`utc      0)
-                     (`standard (plist-get rule :standard-offset))
-                     (`wall     offset-before)
-                     (type      (error "Unhandled time definition type `%s'" 
type)))
-                   year-base)
+          (push (round (- (+ (datetime--start-of-day year year-day) (plist-get 
rule :time))
+                          (pcase (plist-get rule :time-definition)
+                            (`utc      0)
+                            (`standard (plist-get rule :standard-offset))
+                            (`wall     offset-before)
+                            (type      (error "Unhandled time definition type 
`%s'" type)))
+                          year-base))
                 transitions)
-          (push (plist-get rule :after) transitions))))
+          (let ((after (plist-get rule :after)))
+            ;; Mark transitions to DST by making offset a float.
+            (push (if (plist-get rule :dst) (float after) after) 
transitions)))))
     (aset all-year-transitions year-offset (nreverse transitions))))
 
 
@@ -1584,20 +1634,67 @@ Supported fields:
           (:eras              datetime--english-eras)
           (:am-pm             datetime--english-am-pm)))))
 
+(defun datetime-locale-timezone-name (locale timezone dst &optional full)
+  "Get name of TIMEZONE in given LOCALE.
+For timezones that don't have daylight saving time, parameter DST
+is ignored.
+
+By default, abbreviated name (like \"UTC\") suitable for use in
+date-time strings is returned.  However, if FULL is non-nil, a
+non-abbreviated name (e.g. \"Coordinated Universal Time\") is
+returned instead."
+  ;; See `datetime--timezone-name-extmap' for description of fallbacks.
+  (let ((names (plist-get (extmap-get datetime--timezone-name-extmap locale t) 
timezone)))
+    (cond ((vectorp names)
+           (aref names (if (= (length names) 4)
+                           (+ (if full 2 0) (if dst 1 0))
+                         (if full 1 0))))
+          ((consp names)
+           (if full
+               (if dst (cdr names) (car names))
+             (datetime-locale-timezone-name 'en timezone dst)))
+          ((stringp names)
+           (if full
+               names
+             (datetime-locale-timezone-name 'en timezone nil)))
+          (t
+           (let ((locale-data (extmap-get datetime--locale-extmap locale t)))
+             (when locale-data
+               (datetime-locale-timezone-name (or (plist-get locale-data 
:parent) 'en) timezone dst full)))))))
+
 
 (defun datetime-locale-database-version ()
   "Return locale database version, a simple integer.
 This version will be incremented each time locale database of the
 package is updated.  It can be used e.g. to invalidate caches you
-create based on locales `datetime' knows about."
+create based on locales `datetime' knows about.
+
+Note that this database doesn't include timezone names.  See
+`datetime-timezone-name-database-version'."
   4)
 
 (defun datetime-timezone-database-version ()
   "Return timezone database version, a simple integer.
 This version will be incremented each time timezone database of the
 package is updated.  It can be used e.g. to invalidate caches you
-create based on timezones `datetime' knows about and their rules."
-  4)
+create based on timezones `datetime' knows about and their rules.
+
+Locale-specific timezone names are contained in a different
+database.  See `datetime-timezone-name-database-version'."
+  5)
+
+(defun datetime-timezone-name-database-version ()
+  "Return timezone name database version, a simple integer.
+This version will be incremented each time timezone name database
+of the package is updated.  It can be used e.g. to invalidate
+caches.
+
+This database includes only locale-specific timezone names.
+Other locale-specific data as well as locale-independent data
+about timezones is contained in different databases.  See
+`datetime-locale-database-version' and
+`datetime-timezone-database-version'."
+  1)
 
 
 (provide 'datetime)
diff --git a/dev/HarvestData.java b/dev/HarvestData.java
index 5456eeb20d..4221be4b5b 100644
--- a/dev/HarvestData.java
+++ b/dev/HarvestData.java
@@ -24,26 +24,48 @@ public class HarvestData
 
         if (Arrays.asList (args).contains ("--timezones"))
             printTimezoneData ();
+
+        if (Arrays.asList (args).contains ("--timezone-names"))
+            printTimezoneNameData ();
     }
 
-    protected static void printLocaleData () throws Exception
+
+    protected static List <Locale> getAllLocales ()
     {
         List <Locale>  locales = new ArrayList <> (Arrays.asList 
(Locale.getAvailableLocales ()));
+
+        locales.removeIf ((locale) -> {
+                // This way we discard a few locales that can otherwise lead 
to duplicate keys
+                // because of use of toLanguageTag().  E.g. `no_NO_NY' is 
problematic.
+                if (locale.getVariant ().length () > 0)
+                    return true;
+
+                if (!Chronology.ofLocale (locale).getId ().equals ("ISO")) {
+                    // Ignore such locales for now.
+                    return true;
+                }
+
+                return false;
+            });
+
         locales.sort ((a, b) -> a.toLanguageTag ().compareToIgnoreCase 
(b.toLanguageTag ()));
+        return locales;
+    }
 
-        Map <Locale, Map <String, String>>  data = new LinkedHashMap <> ();
+    protected static List <ZoneId> getAllTimezones ()
+    {
+        List <ZoneId>  timezones = ZoneId.getAvailableZoneIds ().stream ().map 
((id) -> ZoneId.of (id)).collect (Collectors.toList ());
+        timezones.sort ((a, b) -> a.getId ().compareToIgnoreCase (b.getId ()));
+        return timezones;
+    }
 
-        for (Locale locale : locales) {
-            // This way we discard a few locales that can otherwise lead to 
duplicate keys
-            // because of use of toLanguageTag().  E.g. `no_NO_NY' is 
problematic.
-            if (locale.getVariant ().length () > 0)
-                continue;
 
+    protected static void printLocaleData () throws Exception
+    {
+        Map <Locale, Map <String, String>>  data = new LinkedHashMap <> ();
+
+        for (Locale locale : getAllLocales ()) {
             Chronology  chronology = Chronology.ofLocale (locale);
-            if (!chronology.getId ().equals ("ISO")) {
-                // Ignore such locales for now.
-                continue;
-            }
 
             Map <String, String>  map = new LinkedHashMap <> ();
             data.put (locale, map);
@@ -135,10 +157,8 @@ public class HarvestData
             }
         }
 
-        for (Locale locale : locales) {
-            if (data.containsKey (locale))
-                removeUnnecessaryLocaleData (data, locale);
-        }
+        for (Locale locale : getAllLocales ())
+            removeUnnecessaryLocaleData (data, locale);
 
         System.out.println ("(");
         for (Map.Entry <Locale, Map <String, String>> entry : data.entrySet ())
@@ -177,7 +197,7 @@ public class HarvestData
     protected static void removeUnnecessaryLocaleData (Map <Locale, Map 
<String, String>> data, Locale locale)
     {
         Map <String, String>  locale_data = data.get (locale);
-        Locale                parent = new Locale (locale.getLanguage ());
+        Locale                parent      = new Locale (locale.getLanguage ());
         Map <String, String>  parent_data;
 
         if (Objects.equals (locale, parent))
@@ -218,14 +238,12 @@ public class HarvestData
             locale_data.remove (main_key);
     }
 
+
     protected static void printTimezoneData () throws Exception
     {
-        List <ZoneId>  timezones = ZoneId.getAvailableZoneIds ().stream ().map 
((id) -> ZoneId.of (id)).collect (Collectors.toList ());
-        timezones.sort ((a, b) -> a.getId ().compareToIgnoreCase (b.getId ()));
-
         Map <ZoneId, List <Object>>  data = new LinkedHashMap <> ();
 
-        for (ZoneId timezone : timezones) {
+        for (ZoneId timezone : getAllTimezones ()) {
             ZoneRules  rules = timezone.getRules ();
 
             if (rules.isFixedOffset ())
@@ -246,13 +264,19 @@ public class HarvestData
                 for (ZoneOffsetTransition transition : transitions) {
                     int  year_offset = (int) ((transition.getInstant 
().getEpochSecond () - base) / AVERAGE_SECONDS_IN_YEAR);
                     if ((transition.getInstant ().getEpochSecond () + 1 - 
base) % AVERAGE_SECONDS_IN_YEAR < 1)
-                        System.err.println (String.format ("*Warning*: 
timezone '%s', offset transition at %s would be a potential rounding error", 
timezone.getId (), transition.getInstant ()));
+                        System.err.printf ("*Warning*: timezone '%s', offset 
transition at %s would be a potential rounding error\n", timezone.getId (), 
transition.getInstant ());
 
                     while (year_offset >= transition_data.size ())
                         transition_data.add (new ArrayList <> (Arrays.asList 
(last_offset)));
 
                     transition_data.get (year_offset).add 
(transition.getInstant ().getEpochSecond () - (base + year_offset * 
AVERAGE_SECONDS_IN_YEAR));
-                    transition_data.get (year_offset).add (last_offset = 
transition.getOffsetAfter ().getTotalSeconds ());
+                    last_offset = transition.getOffsetAfter ().getTotalSeconds 
();
+
+                    // Floating-point offset is our internal mark of a 
transition to DST.
+                    // Java is over-eager to convert ints to float for us, so 
we format
+                    // them as strings manually now and add '.0' if 
appropriate.
+                    boolean  to_dst = !Objects.equals 
(transition.getOffsetAfter (), rules.getStandardOffset (transition.getInstant 
()));
+                    transition_data.get (year_offset).add (String.format 
(to_dst ? "%d.0" : "%d", last_offset));
                 }
 
                 List <Object>  transition_rule_data = new ArrayList <> ();
@@ -288,6 +312,9 @@ public class HarvestData
                     rule.put (":before", String.valueOf 
(transition_rule.getOffsetBefore ().getTotalSeconds ()));
                     rule.put (":after",  String.valueOf 
(transition_rule.getOffsetAfter  ().getTotalSeconds ()));
 
+                    if (!Objects.equals (transition_rule.getOffsetAfter (), 
transition_rule.getStandardOffset ()))
+                        rule.put (":dst", "t");
+
                     transition_rule_data.add (toLispPlist (rule, false));
                 }
 
@@ -306,6 +333,141 @@ public class HarvestData
         System.out.println (")");
     }
 
+
+    protected static void printTimezoneNameData () throws Exception
+    {
+        Map <Locale, Map <String, String[]>>  data = new LinkedHashMap <> ();
+
+        for (Locale locale : getAllLocales ()) {
+            Map <String, String[]>  map = new LinkedHashMap <> ();
+
+            DateTimeFormatter  abbreviation_retriever = 
DateTimeFormatter.ofPattern ("z",    locale);
+            DateTimeFormatter  full_name_retriever    = 
DateTimeFormatter.ofPattern ("zzzz", locale);
+
+            for (ZoneId timezone : getAllTimezones ()) {
+                ZonedDateTime  dummy_date = ZonedDateTime.ofInstant 
(Instant.ofEpochSecond (0), timezone);
+                String[]       names      = new String[] { 
abbreviation_retriever.format (dummy_date), null,
+                                                           full_name_retriever 
  .format (dummy_date), null };
+                ZoneRules      rules      = timezone.getRules ();
+
+
+                if (!rules.isFixedOffset ()) {
+                    // They are probably already ordered, but I cannot find a 
confirmation in
+                    // the documentation.
+                    List <ZoneOffsetTransition>  transitions = new ArrayList 
<> (rules.getTransitions ());
+                    transitions.sort ((a, b) -> a.getInstant ().compareTo 
(b.getInstant ()));
+
+                    Instant  switch_to_dst = null;
+                    for (ZoneOffsetTransition transition : transitions) {
+                        if (rules.isDaylightSavings (transition.getInstant ()) 
&& !rules.isDaylightSavings (Instant.ofEpochSecond (transition.getInstant 
().getEpochSecond () - 1))) {
+                            switch_to_dst = transition.getInstant ();
+                            break;
+                        }
+                    }
+
+                    // I would give a warning, but this seems to be a frequent 
occasion.
+                    // Maybe it's supposed to be like that.
+                    if (switch_to_dst != null) {
+                        ZonedDateTime  dst = ZonedDateTime.ofInstant 
(switch_to_dst, timezone);
+                        ZonedDateTime  std = ZonedDateTime.ofInstant 
(Instant.ofEpochSecond (switch_to_dst.getEpochSecond () - 1), timezone);
+
+                        names = new String[] { abbreviation_retriever.format 
(std),
+                                               abbreviation_retriever.format 
(dst),
+                                               full_name_retriever   .format 
(std),
+                                               full_name_retriever   .format 
(dst) };
+
+                        if (Objects.equals (names[0], names[1]) && 
Objects.equals (names[2], names[3])) {
+                            // Another not quite understandable, but frequent 
thing.
+                            // There seem to also be some timezone/locale 
pairs where
+                            // abbreviations match, but full names don't.  
Those we
+                            // ignore.
+                            names[1] = names[3] = null;
+                        }
+                    }
+                }
+
+                map.put (timezone.getId (), names);
+            }
+
+            data.put (locale, map);
+        }
+
+        for (Locale locale : getAllLocales ())
+            removeUnnecessaryTimezoneNameData (data, locale);
+
+        // Many timezones in Java are broken in that instants formatted/parsed 
in them get
+        // shifted around.  Not much we can do about this, but at least we'll 
keep the
+        // list internally so that we can avoid testing in them.
+        List <String>      broken    = new ArrayList <> ();
+        DateTimeFormatter  formatter = DateTimeFormatter.ofPattern 
("yyyy-MM-dd HH:mm:ss z");
+        Instant[]          check_at  = { Instant.from (formatter.parse 
("2020-01-01 00:00:00 UTC")),
+                                         Instant.from (formatter.parse 
("2020-07-01 00:00:00 UTC")) };
+
+        for (ZoneId timezone : getAllTimezones ()) {
+            if (Arrays.stream (check_at)
+                .anyMatch ((instant) -> !Objects.equals (instant, Instant.from 
(formatter.parse (formatter.format (ZonedDateTime.ofInstant (instant, 
timezone)))))))
+                broken.add (timezone.getId ());
+        }
+
+        System.out.println ("(");
+
+        for (Map.Entry <Locale, Map <String, String[]>> entry : data.entrySet 
()) {
+            Map <String, String>  values = new LinkedHashMap <> ();
+            for (Map.Entry <String, String[]> name_entry : entry.getValue 
().entrySet ()) {
+                String[]  names = name_entry.getValue ();
+
+                // See description of fallbacks in `datetime.el'.
+                values.put (name_entry.getKey (), (names[0] != null || 
names[1] != null
+                                                   ? (names[3] != null
+                                                      ? String.format ("[%s %s 
%s %s]", quoteString (names[0]), quoteString (names[1]), quoteString 
(names[2]), quoteString (names[3]))
+                                                      : String.format ("[%s 
%s]",       quoteString (names[0]),                         quoteString 
(names[2])))
+                                                   : (names[3] != null
+                                                      ? String.format ("(%s . 
%s)", quoteString (names[2]), quoteString (names[3]))
+                                                      : quoteString 
(names[2]))));
+            }
+
+            System.out.println (toLispPlist (entry.getKey ().toLanguageTag (), 
values, false));
+        }
+
+        if (!broken.isEmpty ()) {
+            broken.add (0, ":broken");
+            System.out.println (toLispList (broken));
+        }
+
+        System.out.println (")");
+    }
+
+    protected static void removeUnnecessaryTimezoneNameData (Map <Locale, Map 
<String, String[]>> data, Locale locale)
+    {
+        if (Objects.equals (locale, Locale.ENGLISH))
+            return;
+
+        Map <String, String[]>  locale_data  = data.get (locale);
+        Map <String, String[]>  english_data = data.get (Locale.ENGLISH);
+        Locale                  parent       = new Locale (locale.getLanguage 
());
+        Map <String, String[]>  parent_data;
+
+        if (Objects.equals (locale, parent))
+            parent_data = Collections.emptyMap ();
+        else {
+            removeUnnecessaryTimezoneNameData (data, parent);
+            parent_data = data.get (parent);
+        }
+
+        // Discard abbreviated names that match those for English locale and 
leave only
+        // the full names.
+        locale_data.entrySet ().stream ().forEach ((entry) -> {
+                String[]  names = entry.getValue ();
+                if (Objects.equals (names[0], english_data.get (entry.getKey 
()) [0]) && Objects.equals (names[1], english_data.get (entry.getKey ()) [1]))
+                    names[0] = names[1] = null;
+            });
+
+        // Fall back to the parent locale where possible.
+        locale_data.entrySet ().removeIf ((entry) -> (Objects   .equals 
(entry.getValue (), english_data.get (entry.getKey ()))
+                                                      || Objects.equals 
(entry.getValue (), parent_data .get (entry.getKey ()))));
+    }
+
+
     protected static String toLispList (List <?> list)
     {
         if (list == null || list.isEmpty ())
@@ -364,6 +526,7 @@ public class HarvestData
         return string != null ? String.format ("\"%s\"", string.replaceAll 
("\\\\", "\\\\").replaceAll ("\"", "\\\"")) : "nil";
     }
 
+
     protected static boolean isLeapYear (int year)
     {
         return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
diff --git a/test/ProcessTimestamp.java b/test/ProcessTimestamp.java
index 16202b688f..200ccd194b 100644
--- a/test/ProcessTimestamp.java
+++ b/test/ProcessTimestamp.java
@@ -43,7 +43,7 @@ public class ProcessTimestamp
             switch (command) {
             case "format":
                 System.out.println (DateTimeFormatter.ofPattern (pattern, 
locale)
-                                    .format (LocalDateTime.ofInstant 
(Instant.ofEpochSecond ((long) Math.floor (timestamp),
+                                    .format (ZonedDateTime.ofInstant 
(Instant.ofEpochSecond ((long) Math.floor (timestamp),
                                                                                
              (int) Math.floor ((timestamp - Math.floor (timestamp))  * 
1_000_000_000)),
                                                                       
timezone)));
                 break;
@@ -53,6 +53,7 @@ public class ProcessTimestamp
                                                      .parseCaseInsensitive ()
                                                      // Commented out since it 
triggers bugs in obscure locales in Java.
                                                      // We don't use this for 
testing anyway.
+                                                     // See: 
https://bugs.openjdk.java.net/browse/JDK-8211306
                                                      // .parseLenient ()  
                                                      .appendPattern (pattern));
 
diff --git a/test/base.el b/test/base.el
index e4ffb0e1a3..269341cc9e 100644
--- a/test/base.el
+++ b/test/base.el
@@ -1,6 +1,6 @@
 ;;; -*- lexical-binding: t -*-
 
-;; Copyright (C) 2018-2019 Paul Pogonyshev
+;; Copyright (C) 2018-2020 Paul Pogonyshev
 
 ;; This program is free software; you can redistribute it and/or
 ;; modify it under the terms of the GNU General Public License as
diff --git a/test/format.el b/test/format.el
index deb14f52a1..2e49763195 100644
--- a/test/format.el
+++ b/test/format.el
@@ -1,6 +1,6 @@
 ;;; -*- lexical-binding: t -*-
 
-;; Copyright (C) 2018-2019 Paul Pogonyshev
+;; Copyright (C) 2018-2020 Paul Pogonyshev
 
 ;; This program is free software; you can redistribute it and/or
 ;; modify it under the terms of the GNU General Public License as
@@ -56,9 +56,8 @@
     (dolist (locale (datetime-list-locales t))
       (dolist (variant '(:short :medium :long :full))
         (let ((pattern (datetime-locale-date-time-pattern locale variant)))
-          (unless (datetime-pattern-includes-timezone-p 'java pattern)
-            (datetime--test-set-up-formatter 'UTC locale pattern
-              (datetime--test-formatter now))))))))
+          (datetime--test-set-up-formatter 'UTC locale pattern
+            (datetime--test-formatter now)))))))
 
 (ert-deftest datetime-test-formatting-various-timestamps-1 ()
   (datetime--test-set-up-formatter 'UTC 'en "yyyy-MM-dd HH:mm:ss.SSS"
@@ -114,3 +113,9 @@
   (datetime--test-set-up-formatter 'Australia/Hobart 'en "yyyy-MM-dd 
HH:mm:ss.SSS"
     ;; Rule-based transition on 2014-10-05.
     (datetime--test-formatter-around-transition 1412438400)))
+
+(ert-deftest datetime-test-formatting-with-timezone-name-1 ()
+  (datetime--test-set-up-formatter 'Europe/Berlin 'en "yyyy-MM-dd HH:mm:ss z"
+    ;; Rule-based transition on 2014-10-26.  Should also result in
+    ;; timezone name changing between CEST and CET.
+    (datetime--test-formatter-around-transition 1414285200)))
diff --git a/test/parse.el b/test/parse.el
index b8fcf95e89..edf8b81127 100644
--- a/test/parse.el
+++ b/test/parse.el
@@ -1,6 +1,6 @@
 ;;; -*- lexical-binding: t -*-
 
-;; Copyright (C) 2018-2019 Paul Pogonyshev
+;; Copyright (C) 2018-2020 Paul Pogonyshev
 
 ;; This program is free software; you can redistribute it and/or
 ;; modify it under the terms of the GNU General Public License as
diff --git a/timezone-data.extmap b/timezone-data.extmap
index bfc9e17e68..0d40d943bf 100644
Binary files a/timezone-data.extmap and b/timezone-data.extmap differ
diff --git a/timezone-name-data.extmap b/timezone-name-data.extmap
new file mode 100644
index 0000000000..ab910b2085
Binary files /dev/null and b/timezone-name-data.extmap differ



reply via email to

[Prev in Thread] Current Thread [Next in Thread]