1

Jargon:

CET : Central European Time.
Daylight saving time : UTC+1 in winter, UTC+2 in summer.

In the CET region, Android and iOS libraries behave as if there never was a daylight saving time before 1996 while .Net behaves as it has always existed.

To illustrate this behaviour, here's some code written in .NET/Java executed on CET machines.

In .Net :

static void PrintDate(String input)
{        
    String format = "yyyy-MM-ddTHH:mm:ss.fffffffzzz";         
    var date = DateTime.ParseExact(input, format, CultureInfo.InvariantCulture);
    var output = date.ToString(format, CultureInfo.InvariantCulture);
    System.Diagnostics.Debug.WriteLine(input + " => " + output);
}

In Android and iOS (just the java example but both behave in the same manner).

static void printDate(String input)
{
    String format = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSZZZZZ";
    Date date =  new SimpleDateFormat(format).parse(input);
    String output = new SimpleDateFormat(format).format(date);
    Log.i("MyTag", input  + " => " + output);        
}

A simple call to the method :

public static void Main()
{
    PrintDate("1993-10-06T00:00:00.0000000+02:00");
    PrintDate("1993-12-06T00:00:00.0000000+02:00");
    PrintDate("1996-10-06T00:00:00.0000000+02:00");
    PrintDate("1996-12-06T00:00:00.0000000+02:00");
}

Here's the output in .Net :

1993-10-06T00:00:00.0000000+02:00 => 1993-10-06T00:00:00.0000000+02:00
1993-12-06T00:00:00.0000000+02:00 => 1993-12-05T23:00:00.0000000+01:00
1996-10-06T00:00:00.0000000+02:00 => 1996-10-06T00:00:00.0000000+02:00
1996-12-06T00:00:00.0000000+02:00 => 1996-12-05T23:00:00.0000000+01:00

And here's the output in Android/iOs

1993-10-06T00:00:00.0000000+02:00 => 1993-10-05T23:00:00.0000000+01:00
1993-12-06T00:00:00.0000000+02:00 => 1993-12-05T23:00:00.0000000+01:00
1996-10-06T00:00:00.0000000+02:00 => 1996-10-06T00:00:00.0000000+02:00
1996-12-06T00:00:00.0000000+02:00 => 1996-12-05T23:00:00.0000000+01:00

How could I homogenize the behaviour between these three polatforms?

Akli
  • 1,351
  • 1
  • 15
  • 28
  • 2
    In Java, `SimpleDateFormat` uses the JVM default timezone when formatting (check with `TimeZone.getDefault()`). So `1993-10-06T00:00+02:00` is converted to `1993-10-05T23:00+01:00` probably because the default timezone is one with `+01:00` offset in October 1993, while in October 1996 it was in DST (`+02:00`). My guess is [Europe/Paris](https://www.timeanddate.com/time/zone/france/paris?year=1990), but it can be other, as [lots of timezones uses CET as a short name](https://www.timeanddate.com/time/zones/cet). Setting the timezone in `SimpleDateFormat` before calling `format` solves it. –  Sep 27 '17 at 17:41
  • `+02:00` is an [offset, not a timezone](https://stackoverflow.com/tags/timezone/info). Just being `+02:00` doesn't necessarily mean that it's CET during DST, [there's more than 1 timezone that uses this offset](https://en.wikipedia.org/wiki/List_of_UTC_time_offsets#UTC.2B02:00.2C_B). And short names like CET are [ambiguous and not standard](https://stackoverflow.com/a/18407231/7605325), so you should consider using [IANA timezones names](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (always in the format `Region/City`, like `America/Sao_Paulo` or `Europe/Paris`) –  Sep 27 '17 at 19:47
  • As you suggested setting SimpleDateFormat with setTimeZone(TimeZone.getTimeZone("GMT")) fixes the different behaviour between periods after and before 1996, I'm just loosing the offset but that's not an issue. Could you plz post your first comment as an answer? – Akli Sep 28 '17 at 09:19
  • I didn't answer before because you asked for a way to fix it in .net, java and ios. But I can only tell about the java part, are you OK with an incomplete answer? –  Sep 28 '17 at 09:22
  • And what should be the output? Always with `+02:00`? –  Sep 28 '17 at 09:30
  • The answer does not need to be with code, for iOS it's the same solution as you suggested : [dateFormat setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]] – Akli Sep 28 '17 at 09:30
  • The offset is irrelevent as long as it adds up to the same UTC time. I did not have that behaviour on Android and iOS for summer dates before 1996 in CET device. – Akli Sep 28 '17 at 09:33
  • 1
    You are using troublesome, confusing, and flawed old date-time classes in Java. These are now supplanted by the java.time classes built into Java 8 and later. For Android, see the ThreeTen-Backport and ThreeTenABP projects. – Basil Bourque Sep 28 '17 at 14:30

1 Answers1

1

In Java, SimpleDateFormat uses the JVM default timezone if you don't set one in it. (check what's yours with TimeZone.getDefault()).

So 1993-10-06T00:00+02:00 is converted to 1993-10-05T23:00+01:00 probably because the default timezone is one with +01:00 offset in October 1993, while in October 1996 it was in DST (+02:00). My guess is Europe/Paris, but it can be other, as lots of timezones uses CET as a short name.

Anyway, just check the history of DST in Paris and note that in October 1993 the offset was +01:00 while in October 6th 1996 it was +02:00. So it's a good guess, but any timezone with the same rules will have the same behaviour.

Also, +02:00 is an offset, not a timezone. Just being +02:00 doesn't necessarily mean that it's CET during DST, because there's more than one timezone that uses this offset. And short names like CET are ambiguous and not standard, so you should consider using IANA timezones names (always in the format Region/City, like America/Sao_Paulo or Europe/Paris).

Anyway, if you don't want to have variable offsets, you shouldn't rely on the JVM default timezone, because it can have DST effects and the offset will vary according to the date (and the default timezone can be changed without notice, even at runtime). One way to avoid it, is to set a fixed offset in the formatter:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSZZZZZ");
// set the offset +02:00, so all dates will be formatted using this
// (instead of the current offset for the JVM default timezone)
sdf.setTimeZone(TimeZone.getTimeZone("GMT+02:00"));
Date date = sdf.parse("1993-10-06T00:00:00.0000000+02:00");
System.out.println(sdf.format(date));

Just some quick notes:

  • SimpleDateFormat doesn't work well with more than 3 digits after the decimal point. In the case above, it works fine because it's all zeroes, but if you have any value different from zero and more than 3 digits, you can have strange, wrong, unexpected results. In this case, you should remove the extra digits, because this class simply can't handle more than 3 (and it also doesn't work well for formatting).
  • I'm testing with JDK 7, so the pattern ZZZZZ doesn't work for parsing. Instead, I've used yyyy-MM-dd'T'HH:mm:ss.SSSSSSSXXX, which parses the inputs above, and formats the date to 1993-10-06T00:00:00.0000000+02:00 (but note that the X is not available in JDK 6)
  • If you want the output in another offset, just change it accordingly in the getTimeZone method. If you want UTC, use getTimeZone("UTC")

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 the ThreeTenABP (more on how to use it here).

One improvement in this new API is the support to nanoseconds (up to 9 digits after the decimal point), so it can handle your inputs without the problems of SimpleDateFormat.

This new API also has lots of new date/time types suited for different situations. In this case, you have a date and time in a specific offset, and wish to maintain it. So, the best class is a org.threeten.bp.OffsetDateTime:

OffsetDateTime odt = OffsetDateTime.parse("1993-10-06T00:00:00.0000000+02:00");
System.out.println(odt.toString()); // 1993-10-06T00:00+02:00

Note that the toString() method omits the seconds and nanoseconds if they are zero. If you want the output exactly like the input (with 7 digits after the decimal point), just create a org.threeten.bp.format.DateTimeFormatter:

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSXXX");
System.out.println(fmt.format(odt)); // 1993-10-06T00:00:00.0000000+02:00

To change this to another offset (or to UTC), use a org.threeten.bp.ZoneOffset:

// convert to UTC
odt = odt.withOffsetSameInstant(ZoneOffset.UTC);
System.out.println(fmt.format(odt)); // 1993-10-05T22:00:00.0000000Z

// convert to another offset (+01:00)
odt = odt.withOffsetSameInstant(ZoneOffset.ofHours(1));
System.out.println(fmt.format(odt)); // 1993-10-05T23:00:00.0000000+01:00