From 6e81cfdbb4c156e53013af07023991987073eced Mon Sep 17 00:00:00 2001 From: Jesse Wilson Date: Thu, 10 Feb 2011 01:06:22 +0000 Subject: [PATCH] Locale-awareness for date parsing and formatting: - always format in en_US for best interchange - always parse in en_US, system locale and ISO-8601 --- .../com/google/gson/DefaultTypeAdapters.java | 54 ++++++--- .../gson/DefaultDateTypeAdapterTest.java | 110 +++++++++++++++++- 2 files changed, 144 insertions(+), 20 deletions(-) diff --git a/gson/src/main/java/com/google/gson/DefaultTypeAdapters.java b/gson/src/main/java/com/google/gson/DefaultTypeAdapters.java index 22a154fe..4b3fa423 100644 --- a/gson/src/main/java/com/google/gson/DefaultTypeAdapters.java +++ b/gson/src/main/java/com/google/gson/DefaultTypeAdapters.java @@ -44,6 +44,7 @@ import java.util.Properties; import java.util.Set; import java.util.SortedSet; import java.util.StringTokenizer; +import java.util.TimeZone; import java.util.TreeSet; import java.util.UUID; @@ -287,29 +288,40 @@ final class DefaultTypeAdapters { } static class DefaultDateTypeAdapter implements JsonSerializer, JsonDeserializer { - private final DateFormat format; + private final DateFormat enUsFormat; + private final DateFormat localFormat; + private final DateFormat iso8601Format; DefaultDateTypeAdapter() { - this.format = DateFormat.getDateTimeInstance(); + this(DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.US), + DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT)); } - DefaultDateTypeAdapter(final String datePattern) { - this.format = new SimpleDateFormat(datePattern); + DefaultDateTypeAdapter(String datePattern) { + this(new SimpleDateFormat(datePattern, Locale.US), new SimpleDateFormat(datePattern)); } - DefaultDateTypeAdapter(final int style) { - this.format = DateFormat.getDateInstance(style); + DefaultDateTypeAdapter(int style) { + this(DateFormat.getDateInstance(style, Locale.US), DateFormat.getDateInstance(style)); } - public DefaultDateTypeAdapter(final int dateStyle, final int timeStyle) { - this.format = DateFormat.getDateTimeInstance(dateStyle, timeStyle); + public DefaultDateTypeAdapter(int dateStyle, int timeStyle) { + this(DateFormat.getDateTimeInstance(dateStyle, timeStyle, Locale.US), + DateFormat.getDateTimeInstance(dateStyle, timeStyle)); + } + + public DefaultDateTypeAdapter(DateFormat enUsFormat, DateFormat localFormat) { + this.enUsFormat = enUsFormat; + this.localFormat = localFormat; + this.iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + this.iso8601Format.setTimeZone(TimeZone.getTimeZone("UTC")); } // These methods need to be synchronized since JDK DateFormat classes are not thread-safe // See issue 162 public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) { - synchronized (format) { - String dateFormatAsString = format.format(src); + synchronized (localFormat) { + String dateFormatAsString = enUsFormat.format(src); return new JsonPrimitive(dateFormatAsString); } } @@ -319,12 +331,20 @@ final class DefaultTypeAdapters { if (!(json instanceof JsonPrimitive)) { throw new JsonParseException("The date should be a string value"); } - try { - synchronized (format) { - return format.parse(json.getAsString()); + synchronized (localFormat) { + try { + return localFormat.parse(json.getAsString()); + } catch (ParseException ignored) { + } + try { + return enUsFormat.parse(json.getAsString()); + } catch (ParseException ignored) { + } + try { + return iso8601Format.parse(json.getAsString()); + } catch (ParseException e) { + throw new JsonSyntaxException(json.getAsString(), e); } - } catch (ParseException e) { - throw new JsonSyntaxException(e); } } @@ -332,7 +352,7 @@ final class DefaultTypeAdapters { public String toString() { StringBuilder sb = new StringBuilder(); sb.append(DefaultDateTypeAdapter.class.getSimpleName()); - sb.append('(').append(format.getClass().getSimpleName()).append(')'); + sb.append('(').append(localFormat.getClass().getSimpleName()).append(')'); return sb.toString(); } } @@ -454,7 +474,7 @@ final class DefaultTypeAdapters { throw new JsonParseException(e); } } - + public JsonElement serialize(InetAddress src, Type typeOfSrc, JsonSerializationContext context) { return new JsonPrimitive(src.getHostAddress()); diff --git a/gson/src/test/java/com/google/gson/DefaultDateTypeAdapterTest.java b/gson/src/test/java/com/google/gson/DefaultDateTypeAdapterTest.java index bdc1417d..e585531a 100644 --- a/gson/src/test/java/com/google/gson/DefaultDateTypeAdapterTest.java +++ b/gson/src/test/java/com/google/gson/DefaultDateTypeAdapterTest.java @@ -17,12 +17,12 @@ package com.google.gson; import com.google.gson.DefaultTypeAdapters.DefaultDateTypeAdapter; - -import junit.framework.TestCase; - import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import junit.framework.TestCase; /** * A simple unit test for the {@link DefaultDateTypeAdapter} class. @@ -31,6 +31,100 @@ import java.util.Date; */ public class DefaultDateTypeAdapterTest extends TestCase { + public void testFormattingInEnUs() { + testFormattingAlwaysEmitsUsLocale(Locale.US); + } + + public void testFormattingInFr() { + testFormattingAlwaysEmitsUsLocale(Locale.FRANCE); + } + + private void testFormattingAlwaysEmitsUsLocale(Locale locale) { + TimeZone defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + Locale defaultLocale = Locale.getDefault(); + Locale.setDefault(locale); + try { + assertFormatted("Jan 1, 1970 12:00:00 AM", new DefaultDateTypeAdapter()); + assertFormatted("1/1/70", new DefaultDateTypeAdapter(DateFormat.SHORT)); + assertFormatted("Jan 1, 1970", new DefaultDateTypeAdapter(DateFormat.MEDIUM)); + assertFormatted("January 1, 1970", new DefaultDateTypeAdapter(DateFormat.LONG)); + assertFormatted("1/1/70 12:00 AM", + new DefaultDateTypeAdapter(DateFormat.SHORT, DateFormat.SHORT)); + assertFormatted("Jan 1, 1970 12:00:00 AM", + new DefaultDateTypeAdapter(DateFormat.MEDIUM, DateFormat.MEDIUM)); + assertFormatted("January 1, 1970 12:00:00 AM UTC", + new DefaultDateTypeAdapter(DateFormat.LONG, DateFormat.LONG)); + assertFormatted("Thursday, January 1, 1970 12:00:00 AM UTC", + new DefaultDateTypeAdapter(DateFormat.FULL, DateFormat.FULL)); + } finally { + TimeZone.setDefault(defaultTimeZone); + Locale.setDefault(defaultLocale); + } + } + + public void testParsingDatesFormattedWithSystemLocale() { + TimeZone defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + Locale defaultLocale = Locale.getDefault(); + Locale.setDefault(Locale.FRANCE); + try { + assertParsed("1 janv. 1970 00:00:00", new DefaultDateTypeAdapter()); + assertParsed("01/01/70", new DefaultDateTypeAdapter(DateFormat.SHORT)); + assertParsed("1 janv. 1970", new DefaultDateTypeAdapter(DateFormat.MEDIUM)); + assertParsed("1 janvier 1970", new DefaultDateTypeAdapter(DateFormat.LONG)); + assertParsed("01/01/70 00:00", + new DefaultDateTypeAdapter(DateFormat.SHORT, DateFormat.SHORT)); + assertParsed("1 janv. 1970 00:00:00", + new DefaultDateTypeAdapter(DateFormat.MEDIUM, DateFormat.MEDIUM)); + assertParsed("1 janvier 1970 00:00:00 UTC", + new DefaultDateTypeAdapter(DateFormat.LONG, DateFormat.LONG)); + assertParsed("jeudi 1 janvier 1970 00 h 00 UTC", + new DefaultDateTypeAdapter(DateFormat.FULL, DateFormat.FULL)); + } finally { + TimeZone.setDefault(defaultTimeZone); + Locale.setDefault(defaultLocale); + } + } + + public void testParsingDatesFormattedWithUsLocale() { + TimeZone defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + Locale defaultLocale = Locale.getDefault(); + Locale.setDefault(Locale.US); + try { + assertParsed("Jan 1, 1970 0:00:00 AM", new DefaultDateTypeAdapter()); + assertParsed("1/1/70", new DefaultDateTypeAdapter(DateFormat.SHORT)); + assertParsed("Jan 1, 1970", new DefaultDateTypeAdapter(DateFormat.MEDIUM)); + assertParsed("January 1, 1970", new DefaultDateTypeAdapter(DateFormat.LONG)); + assertParsed("1/1/70 0:00 AM", + new DefaultDateTypeAdapter(DateFormat.SHORT, DateFormat.SHORT)); + assertParsed("Jan 1, 1970 0:00:00 AM", + new DefaultDateTypeAdapter(DateFormat.MEDIUM, DateFormat.MEDIUM)); + assertParsed("January 1, 1970 0:00:00 AM UTC", + new DefaultDateTypeAdapter(DateFormat.LONG, DateFormat.LONG)); + assertParsed("Thursday, January 1, 1970 0:00:00 AM UTC", + new DefaultDateTypeAdapter(DateFormat.FULL, DateFormat.FULL)); + } finally { + TimeZone.setDefault(defaultTimeZone); + Locale.setDefault(defaultLocale); + } + } + + public void testFormatUsesDefaultTimezone() { + TimeZone defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("America/Los_Angeles")); + Locale defaultLocale = Locale.getDefault(); + Locale.setDefault(Locale.US); + try { + assertFormatted("Dec 31, 1969 4:00:00 PM", new DefaultDateTypeAdapter()); + assertParsed("Dec 31, 1969 4:00:00 PM", new DefaultDateTypeAdapter()); + } finally { + TimeZone.setDefault(defaultTimeZone); + Locale.setDefault(defaultLocale); + } + } + public void testDateSerialization() throws Exception { int dateStyle = DateFormat.LONG; DefaultDateTypeAdapter dateTypeAdapter = new DefaultDateTypeAdapter(dateStyle); @@ -57,4 +151,14 @@ public class DefaultDateTypeAdapterTest extends TestCase { fail("Invalid date pattern should fail."); } catch (IllegalArgumentException expected) { } } + + private void assertFormatted(String formatted, DefaultDateTypeAdapter adapter) { + assertEquals(formatted, adapter.serialize(new Date(0), Date.class, null).getAsString()); + } + + private void assertParsed(String date, DefaultDateTypeAdapter adapter) { + assertEquals(date, new Date(0), adapter.deserialize(new JsonPrimitive(date), Date.class, null)); + assertEquals("ISO 8601", new Date(0), adapter.deserialize( + new JsonPrimitive("1970-01-01T00:00:00Z"), Date.class, null)); + } }