Ollie C
Ollie C

Reputation: 28519

Why is JodaTime timezone shifting a date time?

When the string "2017-04-21T17:46:00Z" is passed into the first method the resulting formatted date string is "06:46 21 Apr 2017". Why is the hour moving by eleven hours? The input strings are being provided by an HTTP server application in JSON. I thought the Z suffix referred to Zulu, ie GMT.

private static final String DATE_TIME_FORMAT = "hh:mm dd MMM yyyy";

public static String formatTimestamp(String dateTimestamp) {
    DateTime dateTime = getDateTimeFromTimestamp(dateTimestamp);
    DateTimeFormatter fmt = DateTimeFormat.forPattern(DATE_TIME_FORMAT);
    return fmt.print(dateTime);
}

private static DateTime getDateTimeFromTimestamp(String dateTimestamp) {
    return new DateTime(dateTimestamp);
}

I suspect it relates to timezones but it's not clear how or where. The code is running on an Android device in the UK, in the GMT timezone.

Upvotes: 5

Views: 2294

Answers (2)

Basil Bourque
Basil Bourque

Reputation: 340350

Using java.time

The Answer by Hugo seems to be correct and informative. But FYI, the Joda-Time project is now in maintenance mode, with the team advising migration to the java.time classes. For Android, see the last bullet at bottom below.

Your input string is in standard ISO 8601 format. The java.time classes use standard formats when parsing & generating strings. So no need to specify a formatting pattern.

The Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds (up to nine (9) digits of a decimal fraction).

String input = "2017-04-21T17:46:00Z" ;
Instant instant = Instant.parse( input ) ;

instant.toString(): 2017-04-21T17:46:00Z

For more flexible formatting such as you desire, convert to an OffsetDateTime object were you can specify any offset-from-UTC in hours and minutes. We want UTC itself (an offset of zero) so we can use the constant ZoneOffset.UTC.

OffsetDateTime odt = instant.atOffset( ZoneOffset.UTC ) ;

odt.toString(): 2017-04-21T17:46Z

Define a formatting pattern to match your desired format. Note that you must specify a Locale to determine (a) the human language for translation of name of day, name of month, and such, and (b) the cultural norms deciding issues of abbreviation, capitalization, punctuation, separators, and such.

DateTimeFormatter f = DateTimeFormatter.ofPattern( "hh:mm dd MMM yyyy" , Locale.US ) ;
String output = odt.format( f ) ;

output: 05:46 21 Apr 2017

If you want to see this same moment through the lens of a region’s wall-clock time such as Europe/London or Pacific/Auckland, apply a time zone to get a ZonedDateTime.

Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 3-4 letter abbreviation such as EST or IST or BST as they are not true time zones, not standardized, and not even unique(!).

ZoneId z = ZoneId.of( "Europe/London" ) ;
ZonedDateTime zdt = instant.atZone( z ) ;

Note the time-of-day is an hour off because of Daylight Saving Time (DST).

zdt.toString(): 2017-04-21T18:46+01:00[Europe/London]

See this code run live at IdeOne.com.

About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

Where to obtain the java.time classes?

Upvotes: 1

user7605325
user7605325

Reputation:

I've made a test with java 7 and joda-time 2.7 (but not the Android's version)

That's how I could reproduce the problem:

// changing my default timezone (because I'm not in UK)
DateTimeZone.setDefault(DateTimeZone.forID("Europe/London"));
// calling your method
System.out.println(formatTimestamp("2017-04-21T17:46:00Z"));

The output is

06:46 21 Abr 2017

To check what's wrong, I've changed the date format to:

DATE_TIME_FORMAT2 = "hh:mm a dd MMM yyyy Z z zzzz";

Where a means "AM or PM", Z is the timezone offset/id, z is the timezone "short" name and zzzz is the timezone "long" name. Using this format, the output is:

06:46 PM 21 Abr 2017 +0100 BST British Summer Time

So the datetime created is 6PM, just one hour ahead of input, not eleven hours as you thought (actually if you change the format to HH instead of hh, the hours will be 18 instead of 06).

Also note the timezone fields: +0100 BST British Summer Time. The first part (+0100) means that this DateTime is one hour ahead of GMT, and BST British Summer Time means it's in British's Daylight Saving Time.


So, to have your output equals to your input, you have 2 alternatives:

1. Change your default timezone to UTC:

DateTimeZone.setDefault(DateTimeZone.UTC);
System.out.println(formatTimestamp("2017-04-21T17:46:00Z"));

The output will be:

05:46 21 Apr 2017

If you want to change the hours to be 17:46, change your date format, replacing hh by HH

2. Use the DateTime constructor that receives a DateTimeZone:

private static DateTime getDateTimeFromTimestamp(String dateTimestamp) {
    // creates a DateTime in UTC
    return new DateTime(dateTimestamp, DateTimeZone.UTC);
}

The output will be the same as alternative 1, but in this case you don't need to change the default timezone.

For me, alternative 2 makes more sense, because:

  • you don't need to change the default timezone (which can cause some mess in other parts of the application)
  • you already know that all dates handled by this code are in UTC time (because of the "Z" in the end)

Upvotes: 4

Related Questions