Repository: commons-lang Updated Branches: refs/heads/master 38bafd283 -> 3c92830ec
LANG-1219: FastDateFormat doesn't respect summer daylight in some localized strings Project: http://git-wip-us.apache.org/repos/asf/commons-lang/repo Commit: http://git-wip-us.apache.org/repos/asf/commons-lang/commit/3c92830e Tree: http://git-wip-us.apache.org/repos/asf/commons-lang/tree/3c92830e Diff: http://git-wip-us.apache.org/repos/asf/commons-lang/diff/3c92830e Branch: refs/heads/master Commit: 3c92830ec51fb6b6e9a7ca2fb8fda324cd33e48a Parents: 38bafd2 Author: Chas Honton <[email protected]> Authored: Sat Apr 23 13:02:54 2016 -0700 Committer: Chas Honton <[email protected]> Committed: Sat Apr 23 13:02:59 2016 -0700 ---------------------------------------------------------------------- src/changes/changes.xml | 2 + .../commons/lang3/time/FastDateParser.java | 118 +++++++++++-------- .../FastDateParser_TimeZoneStrategyTest.java | 11 ++ 3 files changed, 84 insertions(+), 47 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/commons-lang/blob/3c92830e/src/changes/changes.xml ---------------------------------------------------------------------- diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 44458a8..35c9185 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -22,6 +22,8 @@ <body> <release version="3.5" date="tba" description="tba"> + + <action issue="LANG-1219" type="fix" dev="chas" due-to="Jarek">FastDateFormat doesn't respect summer daylight in some localized strings</action> <action issue="LANG-1201" type="add" dev="ggregory" due-to="Gary Gregory">Add a TimeUnit-like classes for base 2 and base 10 digital conversions (bits, bytes, KB, MB, and so on)</action> <action issue="LANG-1146" type="add" dev="ggregory" due-to="Gabor Liptak">z/OS identification in SystemUtils</action> <action issue="LANG-1210" type="update" dev="ggregory" due-to="Matthias Niehoff">StringUtils#startsWithAny has error in Javadoc</action> http://git-wip-us.apache.org/repos/asf/commons-lang/blob/3c92830e/src/main/java/org/apache/commons/lang3/time/FastDateParser.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/lang3/time/FastDateParser.java b/src/main/java/org/apache/commons/lang3/time/FastDateParser.java index 7b20903..62f3577 100644 --- a/src/main/java/org/apache/commons/lang3/time/FastDateParser.java +++ b/src/main/java/org/apache/commons/lang3/time/FastDateParser.java @@ -31,7 +31,6 @@ import java.util.List; import java.util.ListIterator; import java.util.Locale; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; import java.util.TimeZone; import java.util.TreeSet; @@ -92,7 +91,16 @@ public class FastDateParser implements DateParser, Serializable { // derived fields private transient List<StrategyAndWidth> patterns; - + // comparator used to sort regex alternatives + // alternatives should be ordered longer first, and shorter last. ('february' before 'feb') + // all entries must be lowercase by locale. + private static final Comparator<String> LONGER_FIRST_LOWERCASE = new Comparator<String>() { + @Override + public int compare(String left, String right) { + return right.compareTo(left); + } + }; + /** * <p>Constructs a new FastDateParser.</p> * @@ -453,42 +461,28 @@ public class FastDateParser implements DateParser, Serializable { } /** - * alternatives should be ordered longer first, and shorter last. comparisons should be case insensitive. - */ - private static final Comparator<Map.Entry<String, Integer>> ALTERNATIVES_ORDERING = new Comparator<Map.Entry<String, Integer>>() { - @Override - public int compare(Map.Entry<String, Integer> left, Map.Entry<String, Integer> right) { - int v = left.getValue() - right.getValue(); - if (v != 0) { - return v; - } - return right.getKey().compareToIgnoreCase(left.getKey()); - } - }; - - /** * Get the short and long values displayed for a field * @param cal The calendar to obtain the short and long values * @param locale The locale of display names * @param field The field of interest * @param regex The regular expression to build - * @param vales The map to fill + * @return The map of string display names to field values */ - private static void appendDisplayNames(Calendar cal, Locale locale, int field, - StringBuilder regex, Map<String, Integer> values) { - - Set<Entry<String, Integer>> displayNames = cal.getDisplayNames(field, Calendar.ALL_STYLES, locale).entrySet(); - TreeSet<Map.Entry<String, Integer>> sort = new TreeSet<Map.Entry<String, Integer>>(ALTERNATIVES_ORDERING); - sort.addAll(displayNames); - - for (Map.Entry<String, Integer> entry : sort) { - String symbol = entry.getKey(); - if (symbol.length() > 0) { - if (values.put(symbol.toLowerCase(locale), entry.getValue()) == null) { - simpleQuote(regex, symbol).append('|'); - } + private static Map<String, Integer> appendDisplayNames(Calendar cal, Locale locale, int field, StringBuilder regex) { + Map<String, Integer> values = new HashMap<String, Integer>(); + + Map<String, Integer> displayNames = cal.getDisplayNames(field, Calendar.ALL_STYLES, locale); + TreeSet<String> sorted = new TreeSet<String>(LONGER_FIRST_LOWERCASE); + for (Map.Entry<String, Integer> displayName : displayNames.entrySet()) { + String key = displayName.getKey().toLowerCase(locale); + if (sorted.add(key)) { + values.put(key, displayName.getValue()); } } + for (String symbol : sorted) { + simpleQuote(regex, symbol).append('|'); + } + return values; } /** @@ -703,7 +697,7 @@ public class FastDateParser implements DateParser, Serializable { private static class CaseInsensitiveTextStrategy extends PatternStrategy { private final int field; final Locale locale; - private final Map<String, Integer> lKeyValues = new HashMap<String,Integer>(); + private final Map<String, Integer> lKeyValues; /** * Construct a Strategy that parses a Text field @@ -717,7 +711,7 @@ public class FastDateParser implements DateParser, Serializable { StringBuilder regex = new StringBuilder(); regex.append("((?iu)"); - appendDisplayNames(definingCalendar, locale, field, regex, lKeyValues); + lKeyValues = appendDisplayNames(definingCalendar, locale, field, regex); regex.setLength(regex.length()-1); regex.append(")"); createPattern(regex); @@ -826,8 +820,18 @@ public class FastDateParser implements DateParser, Serializable { private static final String GMT_OPTION= "GMT[+-]\\d{1,2}:\\d{2}"; private final Locale locale; - private final Map<String, TimeZone> tzNames= new HashMap<String, TimeZone>(); - + private final Map<String, TzInfo> tzNames= new HashMap<String, TzInfo>(); + + private static class TzInfo { + TimeZone zone; + int dstOffset; + + TzInfo(TimeZone tz, boolean useDst) { + zone = tz; + dstOffset = useDst ?tz.getDSTSavings() :0; + } + } + /** * Index of zone id */ @@ -839,30 +843,48 @@ public class FastDateParser implements DateParser, Serializable { * @param locale The Locale */ TimeZoneStrategy(Calendar cal, final Locale locale) { - this.locale = locale; final StringBuilder sb = new StringBuilder(); - sb.append('(' + RFC_822_TIME_ZONE + "|(?iu)" + GMT_OPTION ); + sb.append("((?iu)" + RFC_822_TIME_ZONE + "|" + GMT_OPTION ); + + final Set<String> sorted = new TreeSet<String>(LONGER_FIRST_LOWERCASE); final String[][] zones = DateFormatSymbols.getInstance(locale).getZoneStrings(); for (final String[] zoneNames : zones) { + // offset 0 is the time zone ID and is not localized final String tzId = zoneNames[ID]; if (tzId.equalsIgnoreCase("GMT")) { continue; } final TimeZone tz = TimeZone.getTimeZone(tzId); - for(int i= 1; i<zoneNames.length; ++i) { - String zoneName = zoneNames[i]; - if (zoneName == null) { + // offset 1 is long standard name + // offset 2 is short standard name + TzInfo standard = new TzInfo(tz, false); + TzInfo tzInfo = standard; + for (int i = 1; i < zoneNames.length; ++i) { + switch (i) { + case 3: // offset 3 is long daylight savings (or summertime) name + // offset 4 is the short summertime name + tzInfo = new TzInfo(tz, true); + break; + case 5: // offset 5 starts additional names, probably standard time + tzInfo = standard; break; } - if (tzNames.put(zoneName.toLowerCase(locale), tz) == null) { - simpleQuote(sb.append('|'), zoneName); + String key = zoneNames[i].toLowerCase(locale); + // ignore the data associated with duplicates supplied in + // the additional names + if (sorted.add(key)) { + tzNames.put(key, tzInfo); } } } - + // order the regex alternatives with longer strings first, greedy + // match will ensure longest string will be consumed + for (String zoneName : sorted) { + simpleQuote(sb.append('|'), zoneName); + } sb.append(")"); createPattern(sb); } @@ -872,15 +894,17 @@ public class FastDateParser implements DateParser, Serializable { */ @Override void setCalendar(final FastDateParser parser, final Calendar cal, final String value) { - TimeZone tz; if (value.charAt(0) == '+' || value.charAt(0) == '-') { - tz = TimeZone.getTimeZone("GMT" + value); + TimeZone tz = TimeZone.getTimeZone("GMT" + value); + cal.setTimeZone(tz); } else if (value.regionMatches(true, 0, "GMT", 0, 3)) { - tz = TimeZone.getTimeZone(value.toUpperCase()); + TimeZone tz = TimeZone.getTimeZone(value.toUpperCase()); + cal.setTimeZone(tz); } else { - tz = tzNames.get(value.toLowerCase(locale)); + TzInfo tzInfo = tzNames.get(value.toLowerCase(locale)); + cal.set(Calendar.DST_OFFSET, tzInfo.dstOffset); + cal.set(Calendar.ZONE_OFFSET, tzInfo.zone.getRawOffset()); } - cal.setTimeZone(tz); } } http://git-wip-us.apache.org/repos/asf/commons-lang/blob/3c92830e/src/test/java/org/apache/commons/lang3/time/FastDateParser_TimeZoneStrategyTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/commons/lang3/time/FastDateParser_TimeZoneStrategyTest.java b/src/test/java/org/apache/commons/lang3/time/FastDateParser_TimeZoneStrategyTest.java index be08454..2ed0c7f 100644 --- a/src/test/java/org/apache/commons/lang3/time/FastDateParser_TimeZoneStrategyTest.java +++ b/src/test/java/org/apache/commons/lang3/time/FastDateParser_TimeZoneStrategyTest.java @@ -17,6 +17,8 @@ package org.apache.commons.lang3.time; import java.text.DateFormatSymbols; +import java.text.ParseException; +import java.util.Date; import java.util.Locale; import java.util.TimeZone; @@ -52,4 +54,13 @@ public class FastDateParser_TimeZoneStrategyTest { } } } + + @Test + public void testLang1219() throws ParseException { + FastDateParser parser = new FastDateParser("dd.MM.yyyy HH:mm:ss z", TimeZone.getDefault(), Locale.GERMAN); + + Date summer = parser.parse("26.10.2014 02:00:00 MESZ"); + Date standard = parser.parse("26.10.2014 02:00:00 MEZ"); + Assert.assertNotEquals(summer.getTime(), standard.getTime()); + } }
