2

I'm working on an app, part of which is making an HTTP request to a server and then parses the server date and time from the headers. This is done in the following fashion:

//The format used here typically looks like this: "Tue, 08 Jul 2124 13:34:21 GMT-8"
final SimpleDateFormat serverDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss");
String string = response.headers.get("Date"); // this is where we get the Date and Time value
Date date = serverDateFormat.parse(string); // this is where we parse that string into the Date format
long result = date.getTime(); // this is where we translate it into millisec format
Log.e(TAG,"Date in milliseconds: "+result);

Note that my SimpleDateFormat ignores the timezone in the parsed string. This is necessary in order to retain the original server time. If we parse the timezone, the Date variable will automatically adjust the time to the device's timezone (so if he got 21pm GMT, and the device's timezone is GMT-2, then the output will be 19pm).

Now this works perfectly fine on a few devices I have tested it with... except for one. It always produces a result which is an hour or two bigger than the input (so when the server time is 21pm, the function will return 22pm or 23pm). Since the device doesn't belong to me and, for that matter, is located in a totally different state, it is problematic for me to obtain a logcat.

The problem, honestly, makes little sense to me. I can't even think of a possible reason for the time to be inconsistently off on a single device.

The device specs, in case if that makes any difference, are as follows: Pixel XL, Android 7.1.2, PST timezone. The application was tested on Nexus 5x (7.1.2, PST), Galaxy S4 (6.0.1, CST), Galaxy S7 (7.1.2, CST) which all produced the correct results.

If anyone would have an idea of what could possibly cause this, I'd appreciate your answers!

altskop
  • 311
  • 2
  • 12

2 Answers2

4

As explained in @Matt's answer, it's not a good thing to just ignore the offset (GMT-8), because SimpleDateFormat will use the system's default timezone - and this timezone can be different in each device/environment, and you have no control over it. Even if the default timezone is correct, it can be changed, even at runtime, so you can't assume that it'll always be what you need.

Example: my default timezone is America/Sao_Paulo, and the current offset is -03:00 (or GMT-3, or 3 hours behind UTC). If I use your code, it will assume it is 13:34 in São Paulo (which is 16:34 in UTC - the millis value is 4876130061000, which is wrong because it's not equivalent to the original input (13:34 in GMT-8)).

The code only gives the correct values if I change my default timezone to be GMT-8. To not depend on that, you can set the timezone of the formatter.

SimpleDateFormat has some patterns to parse timezone/offset, but none of them worked with GMT-8 (it seems that it accepts only GMT-08:00), so one solution is to remove this from the input and use it to set the correct timezone to the formatter:

String input = "Tue, 08 Jul 2124 13:34:21 GMT-8";
String[] v = input.split(" GMT"); // split the string, before and after "GMT"
SimpleDateFormat serverDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.ENGLISH);
// set the formatter timezone to the offset (in this case, to "GMT-8")
serverDateFormat.setTimeZone(TimeZone.getTimeZone("GMT" + v[1]));
// parse the date/time part
Date date = serverDateFormat.parse(v[0]);

The date millis value will be 4876148061000, which is equivalent to 2124-07-08T21:34:21Z (21:34 UTC = 13:34 in GMT-8).


Another detail is that, when you do:

System.out.println(date);

It calls Date::toString method, which gets the millis value and convert to system's default timezone, giving the false impression that the date object has a timezone - but that's wrong: the date contains only the number of milliseconds since 1970-01-01T00:00Z.

toString is usually called also when you see the date in a debugger

If you want to print the equivalent date and time values for some specific timezone, you can use a SimpleDateFormat and set the timezone you want in this formatter. Using Date::toString always misleads you about the values.

To print the date/time using the same offset used by the server, you can create a SimpleDateFormat and set the GMT-8 to it, as above:

// display the date in a specific format
SimpleDateFormat outputFormat = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss");
// use the same offset used by the server (GMT-8)
outputFormat.setTimeZone(TimeZone.getTimeZone("GMT-8"));
System.out.println(outputFormat.format(date)); // 08/07/2124 13:34:21

The output will be:

08/07/2124 13:34:21

If you don't specify a timezone, it'll use the device/system's default, giving different results depending on the device's configuration.

Note that I also used Locale.ENGLISH to parse the input. That's needed because the month and day of week are in English. If I don't specify a java.util.Locale, the formatter will use the system's default and it's not guaranteed to always be English. And this also can be changed at runtime, so it's better to use an explicit one instead of relying on the default.


Java new Date/Time API

The old classes (Date, Calendar and SimpleDateFormat) have lots of problems and design issues, and they're being replaced by the new APIs.

In Android you can use the ThreeTen Backport, a great backport for Java 8's new date/time classes. To make it work, you'll also need ThreeTenABP (more on how to use it here).

As the input has a date, time and offset, you can parse it to a org.threeten.bp.OffsetDateTime, using a org.threeten.bp.format.DateTimeFormatter.

One detail is that July 8th 2124 is a Saturday, so I had to change the input string, otherwise I get an error. SimpleDateFormat doesn't give this error because it's known to be too lenient and ignore lots of errors and try to "fix" in not-so-smart ways (that might be a little opinion-based, but many consider this a bad thing, and that's why the new API is more strict about this).

String input = "Sat, 08 Jul 2124 13:34:21 GMT-8";
DateTimeFormatter parser = DateTimeFormatter.ofPattern("EE, dd MMM yyyy HH:mm:ss O", Locale.ENGLISH);
OffsetDateTime odt = OffsetDateTime.parse(input, parser);

odt will be equivalent to 2124-07-08T13:34:21-08:00. There's no need to set a timezone in the formatter, because it parses the offset from the input correctly. To get the millis value, just do:

// the same  value as date.getTime()
long millis = odt.toInstant().toEpochMilli();

To convert to a java.util.Date, you can use the org.threeten.bp.DateTimeUtils class:

// convert to java.util.Date
Date date = DateTimeUtils.toDate(odt.toInstant());

To display the to the users, you can use a different DateTimeFormatter. As the OffsetDateTime keeps the correct values, there's no need to set a timezone in the formatter:

// display date in a specific format
DateTimeFormatter outputFormat = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss");
System.out.println(odt.format(outputFormat)); // 08/07/2124 13:34:21

The output will be:

08/07/2124 13:34:21

To convert to another timezone or offset, you can use the classes org.threeten.bp.ZoneId and org.threeten.bp.ZoneOffset:

// convert to UTC
System.out.println(outputFormat.format(odt.withOffsetSameInstant(ZoneOffset.UTC)));
// convert to offset +05:00
System.out.println(outputFormat.format(odt.withOffsetSameInstant(ZoneOffset.ofHours(5))));
// convert to Europe/Berlin timezone
System.out.println(outputFormat.format(odt.atZoneSameInstant(ZoneId.of("Europe/Berlin"))));

The output is:

08/07/2124 21:34:21
09/07/2124 02:34:21
08/07/2124 23:34:21

Note that I used the timezone Europe/Berlin. The API uses IANA timezones names (always in the format Continent/City, like America/Sao_Paulo or Europe/Berlin). Avoid using the 3-letter abbreviations (like CST or PST) because they are ambiguous and not standard.

You can get a list of all available timezones (and choose accordingly) with ZoneId.getAvailableZoneIds().

2

Note that my SimpleDateFormat ignores the timezone in the parsed string. This is necessary in order to retain the original server time.

This is a false assumption and not a good strategy. It's probably the reason for your observations.

By not parsing the time zone, you're relying on SimpleDateFormat's default time zone, which in your case will be the time zone of the device. Since the Date object and resulting timestamp are both in terms of UTC (always!) then you are going to get a different timestamp depending on the device's time zone.

Since you said the device is in another state, it's likely the time zone is different. You can simply change one of your working device's time zone and you should see the same result.

You are correct that you will have to deal with the discrepancy between server and client time zone somewhere. As long as you're using Date, you would have to format it somewhere, probably using SimpleDateFormat again. In that code, you would set the time zone.

This is cumbersome though, especially if you're trying to retain the offset that was parsed. A better approach is to use the OffsetDateTime class from the java.time package.. For Android, you'll need to use the JSR-310 Android Backport (ThreeTenABP) library to take advantage of this.

See also: SimpleDateFormat Pitfalls

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575