branch: elpa/datetime commit 28e31cbe1ca9dba54309ed31ae9cd250c3180149 Author: Paul Pogonyshev <pogonys...@gmail.com> Commit: Paul Pogonyshev <pogonys...@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 <pogonys...@gmail.com> ;; Maintainer: Paul Pogonyshev <pogonys...@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