Java DateTimeFormatterBuilder with Verbose Time Zone
Asked Answered
P

1

5

Suppose I have a date such as:

Nov 30, 2013 19:00:00.001930000 Eastern Standard Time

I'm trying to parse the input using DateTimeFormatterBuilder, but I can't figure out what to put for the Set of generic type ZoneId which is null below.

String basePattern = "MMM dd, yyyy HH:mm:ss";
new DateTimeFormatterBuilder()
        .appendPattern(basePattern)
        .appendFraction(ChronoField.NANO_OF_SECOND,0,9, true)
        .appendZoneText(TextStyle.FULL, null)
        .toFormatter();
Pachston answered 20/10, 2017 at 2:52 Comment(0)
A
7

Pseudo zones

As the class documentation explains briefly, the 3-4 letters commonly seen in mainstream media to indicate a time zone are not actually official time zones. These pseudo-zones are not standardized, and are not even unique! Many codes are re-used around the globe. For example, IST is both India Standard Time and Irish Standard Time. And CST is both China Standard Time and Central Standard Time (in North America).

Never use these pseudo-zones. Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland.

Resolving ambiguity

If your input does have these pseudo zones, in the abbreviated format, you have to deal with the ambiguity. By default, the formatter builder will attempt to resolve the ambiguity by considering the Locale of formatter. In the case of CST if the Locale is Locale.CHINA, then perhaps the CST means China Standard Time rather than Central Standard Time.

Unfortunately this is a crude approach. The issues of Locale and time zone are orthogonal. You could have an Chinese-speaking user handling data for deliveries in Chicago in which case the Locale may be China but the CST found in the data means Central Standard Time. So in such cases, you can specify one or more time zones such as America/Chicago and America/Winnipeg to be considered by the formatter when trying to parse CST to override the default of considering the Locale.

Set< ZoneID > zones = new TreeSet<>() ;
zones.add( ZoneId.of( "America/Chicago" ) ;
zones.add( ZoneId.of( "America/Manitoba" ) ;
…
.appendZoneText( TextStyle.SHORT , zones ) 
…

Here is a full example, parsing CST as Central Standard Time on my macOS MacBook set to default time zone of America/Los_Angeles and default locale of Locale.US. Notice that we pass only a single argument to appendZoneText (no Set passed).

String input = "Nov 30, 2013 19:00:00.001930000 CST";  
String basePattern = "MMM dd, yyyy HH:mm:ss";
DateTimeFormatter f = new DateTimeFormatterBuilder( )
        .appendPattern( basePattern )
        .appendFraction( ChronoField.NANO_OF_SECOND , 0 , 9 , true )
        .appendPattern( " " )
        .appendZoneText( TextStyle.SHORT  )
        .toFormatter( );

ZonedDateTime zdt = ZonedDateTime.parse( input , f );

System.out.println( "input: " + input );
System.out.println( "zdt.toString(): " + zdt );

input: Nov 30, 2013 19:00:00.001930000 CST

zdt.toString(): 2013-11-30T19:00:00.001930-06:00[America/Chicago]

Let's pass the Set of ZoneId objects to override that behavior, hinting that CST means China Standard Time. Here we do pass a Set of ZoneId objects. We use the same input to get a very different output.

Set < ZoneId > zones = new HashSet <>( );
zones.add( ZoneId.of( "Asia/Shanghai" ) ) ;

String input = "Nov 30, 2013 19:00:00.001930000 CST";  
String basePattern = "MMM dd, yyyy HH:mm:ss";
DateTimeFormatter f = new DateTimeFormatterBuilder( )
        .appendPattern( basePattern )
        .appendFraction( ChronoField.NANO_OF_SECOND , 0 , 9 , true )
        .appendPattern( " " )
        .appendZoneText( TextStyle.SHORT , zones )
        .toFormatter( );

ZonedDateTime zdt = ZonedDateTime.parse( input , f );

System.out.println( "input: " + input );
System.out.println( "zdt.toString(): " + zdt );

input: Nov 30, 2013 19:00:00.001930000 CST

zdt.toString(): 2013-11-30T19:00:00.001930+08:00[Asia/Shanghai]

Now, in your case, you have the full name of the pseudo-zone rather than the abbreviation. So there is likely no ambiguity. So you probably could get away with the overloaded method not taking a second argument.

.appendZoneText( TextStyle.FULL ) 

Example:

String input = "Nov 30, 2013 19:00:00.001930000 Eastern Standard Time";  
String basePattern = "MMM dd, yyyy HH:mm:ss";
DateTimeFormatter f = new DateTimeFormatterBuilder( )
        .appendPattern( basePattern )
        .appendFraction( ChronoField.NANO_OF_SECOND , 0 , 9 , true )
        .appendPattern( " " )
        .appendZoneText( TextStyle.FULL )
        .toFormatter( );

ZonedDateTime zdt = ZonedDateTime.parse( input , f );

System.out.println( "input: " + input );
System.out.println( "zdt.toString(): " + zdt );

input: Nov 30, 2013 19:00:00.001930000 Eastern Standard Time

zdt.toString(): 2013-11-30T19:00:00.001930-05:00[America/New_York]

Yet there is a usefulness to passing a Set of ZoneId objects here too. The Set is used in assigning the time zone to the instantiated ZonedDateTime object. Notice in output above that America/New_York was assigned by default. But there are many other time zones that are also implied by the pseudo-zone “Eastern Standard Time”, such as the Bahamas, America/Nassau, and Cancun in Mexico, and so on.

Which element in the set is chosen to be applied is a mystery to me however. I tried to use a SortedSet thinking the first found in the natural order of the Set may be chosen. Alas, ZoneId does not implement Comparable interface so a SortedSet such as TreeSet cannot be used.

Set < ZoneId > zones = new HashSet <>( );
zones.add( ZoneId.of( "America/Detroit" ) );
zones.add( ZoneId.of( "America/New_York" ) );
zones.add( ZoneId.of( "America/Nassau" ) );
zones.add( ZoneId.of( "America/Cancun" ) );

String input = "Nov 30, 2013 19:00:00.001930000 Eastern Standard Time";  
String basePattern = "MMM dd, yyyy HH:mm:ss";
DateTimeFormatter f = new DateTimeFormatterBuilder( )
        .appendPattern( basePattern )
        .appendFraction( ChronoField.NANO_OF_SECOND , 0 , 9 , true )
        .appendPattern( " " )
        .appendZoneText( TextStyle.FULL , zones )
        .toFormatter( );

ZonedDateTime zdt = ZonedDateTime.parse( input , f );

System.out.println( "input: " + input );
System.out.println( "zdt.toString(): " + zdt );

input: Nov 30, 2013 19:00:00.001930000 Eastern Standard Time

zdt.toString(): 2013-11-30T19:00:00.001930-06:00[America/Cancun]

Achilles answered 20/10, 2017 at 3:39 Comment(1)
Just one detail: it's better to specify the locale (such as toFormatter(Locale.US)) because those zone names are locale-sensitive. In my JVM the default is pt_BR (Brazilian Portuguese) and the parsing fails (it works only if the zone name is Horário de luz natural oriental). The locale is also required due to month name (in pt_BR, they're all lowercase and it also fails for this case - I had to change the input to nov). And in my JVM, the last example is resolved to America/Nassau. Maybe it just gets the first element returned by the set (which has no guaranteed order), who knows...Balfour

© 2022 - 2024 — McMap. All rights reserved.