4

As an input I have a string which is a String in ISO 8601 to represent date. For example:

"2017-04-04T09:00:00-08:00"

The last part of String, which is "-08:00" denotes TimeZone Offset. I convert this string into a Calendar instance as shown below:

Calendar calendar = GregorianCalendar.getInstance();
Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).parse(iso8601Date);
calendar.setTime(date);

iso8601Date is "2017-04-04T09:00:00-08:00"

But this does not pick timezone and if I get timezone from Calendar instance, it gives currently set instance of the laptop and does not pick up timestamp from ISO 8601 String. I check for timezone via calendar instance as:

calendar.getTimeZone().getDisplayName()

Can someone show how to pick timezone also in the Calendar instance?

Tarun Deep Attri
  • 8,174
  • 8
  • 41
  • 56

3 Answers3

4

tl;dr

OffsetDateTime.parse( "2017-04-04T09:00:00-08:00" ) 

Details

The last part of String which is "-08:00" denotes TimeZone Offset.

Do not confuse offset with time zone.

The -08:00 represents an offset-from-UTC, not a time zone. A time zone is a history of various offsets used in the past, present, and future by the people of a particular region. A time zone is named with a continent, slash, and region such as America/Los_Angeles or Pacific/Auckland or Asia/Kolkata.

You are using troublesome old date-time classes now supplanted by the java.time classes. For Android, see the ThreeTen-Backport and ThreeTenABP projects.

Your input indicates only offset but not zone. So we parse as a OffsetDateTime.

OffsetDateTime odt = OffsetDateTime.parse( "2017-04-04T09:00:00-08:00" ) ;

If you are absolutely certain of the intended time zone, assign it.

ZoneId z = ZoneId.of( "America/Los_Angeles" ) ;
ZonedDateTime zdt = odt.atZoneSameInstant() ;

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?

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • I agree, stay away from `java.util.Date` (unless absolutely required by some other API *and* you're working in local time zone). – Eugen Pechanec Sep 05 '17 at 16:03
  • 2
    @EugenPechanec Actually, `java.util.Date` is always in UTC so your own time zone would be irrelevant. Both `Date` and `Calendar` classes are such a wretched mess that I advise *always* steering clear of them; always use java.time or its back-port. When the legacy classes are required by another API, do your own work in java.time, then convert results. To convert in Java 8 and later, look to new methods added to the old classes, in the back-port look to a utility class with conversion methods. – Basil Bourque Sep 05 '17 at 16:06
  • I read about java.time.OffsetDateTime but it is not available to android. – Tarun Deep Attri Sep 05 '17 at 16:07
  • 3
    @Terri See the back-port projects linked in my Answer. Well worth the bother of adding a library to your project. – Basil Bourque Sep 05 '17 at 16:16
  • @BasilBourque As part of returning individual fields (`getHour()`, `getDay()`, etc.) the calendar gets "normalized" to current time zone. So if you constructed the Date object with the millis constructor (i.e. in UTC), you'd get garbage, unless your local TZ is UTC. If you constructed the Date object using individual fields, you'd get garbage if your local TZ is different. In conclusion it's not UTC and not even local TZ. It's an indeterminate bloody mess. – Eugen Pechanec Sep 05 '17 at 17:01
3

When you create a Calendar, it takes the JVM's default timezone. And when you parse a String to a Date, it just sets one value: the number of milliseconds since epoch (1970-01-01T00:00Z). A Date doesn't have any timezone information, just this milliseconds value. So you need to set the timezone in the calendar.

In your formatter, you're treating Z as a literal, because it's inside quotes ('Z'). This ignores the offset and gets the date in the JVM default timezone (which will have a different value if the corresponding offset is not -08:00).

In JDK >= 7, you can use the X pattern to parse the offset:

Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.US).parse(iso8601Date);

But this doesn't set the timezone in the calendar (it will still use the JVM's default). So, a "better" way is to strip the offset from the input and handle it separately:

Calendar calendar = GregorianCalendar.getInstance();
String iso8601Date = "2017-04-04T09:00:00-08:00";
// get the offset (-08:00)
String offset = iso8601Date.substring(19);
TimeZone tz = TimeZone.getTimeZone("GMT" + offset);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
// set the offset in the formatter
sdf.setTimeZone(tz);
// parse just date and time (without the offset)
Date date = sdf.parse(iso8601Date.substring(0, 19));
// set the offset in the calendar
calendar.setTimeZone(tz);
calendar.setTime(date);

With this, the calendar will have the offset -08:00 set. As @BasilBourque's answer already said, -08:00 is an offset, not a timezone (the TimeZone class treats offsets just like they were timezones, which is a workaround/bad design choice).


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. You'll also need the ThreeTenABP to make it work (more on how to use it here).

@BasilBourque's answer already tells you about OffsetDateTime. But to convert to a Calendar, you can use a org.threeten.bp.ZonedDateTime and convert it using the org.threeten.bp.DateTimeUtils class:

String iso8601Date = "2017-04-04T09:00:00-08:00";
ZonedDateTime zdt = ZonedDateTime.parse(iso8601Date);
Calendar cal = DateTimeUtils.toGregorianCalendar(zdt);

The calendar will be already set with the -08:00 offset.


If you want to get the timezone from the offset, I'm afraid it's not that simple. More than one timezone can use the same offset, so you can't know for sure which timezone to use (the best you can do is to get a list of possible candidates).


java.util.Date

Just a more detailed note about java.util.Date. This link explains a lot about it, so I really recommend you to read it.

As already said above, a Date has no timezone information. It just keeps the number of milliseconds since epoch (which is 1970-01-01T00:00Z, or January 1st 1970 at midnight in UTC).

This value is the same everywhere in the world. Example: at the moment I'm writing this, the millis value for the current time is 1504632865935. This number is the same for anyone in the world who gets the current time at the same instant I did, regardless of what timezone they're using.

What is different is the local date and time that corresponds to this millis value. In UTC, it corresponds to 2017-09-05T17:34:25.935Z, in New York, the date is the same (September 5th 2017) but the time is different (13:34), and in Tokyo is September 6th 2017 at 02:34 AM.

Although the Date object is the same (because its millis value is 1504632865935 for everyone), the corresponding date and time changes according to the timezone used.

People tend to think that a Date has a timezone because when printing it (with System.out.println or by loggging) or when inspecting in a debugger, it implicity uses the toString() method, and this converts the date to the JVM's default timezone (and it also prints the zone name). This gives the impression that a Date has a format and a timezone set to it, but it doesn't.

1

One Key understanding I want to share from Hugo's answer and my further search is following. Please correct me if I am wrong:

Date does not care about timezone. It represents milliseconds passed since epoch.

Regarding finding the Timezone from provided ISO 8061 format is there, Date class can not tell that and we have to use some alternate methods as specified by @Hugo and @Basil Bourque.

Tarun Deep Attri
  • 8,174
  • 8
  • 41
  • 56
  • 1
    Not exactly. A `Date` has the number of milliseconds since `1970-01-01T00:00Z` (also called "unix epoch", or *Jan 1st 1970 midnight in UTC*). Suppose this number of milliseconds is 1491325200000. This number represents a different date and time in each timezone. It can be `2017-04-04 at 5 PM in UTC`, or 1 PM in New York or the next day (`2017-04-05`) at 2 AM in Tokyo. A `Date` is not aware of the date/time fields (it doesn't have day/month/year, nor hour/minute/second - those fields will have different values in different timezones - as the date has no timezone, it doesn't have the fields). –  Sep 05 '17 at 17:26
  • 1
    When you say *"milliseconds represents for example: 1st January 2017 02:00"*, you must also ask: "where?" (in which timezone?). This date and time can represent a different number of milliseconds since epoch, depending on the timezone you use. If you consider Jan 1st 2017 at 2 AM in UTC, the millis value is `1483236000000`. But the same date/time in New York corresponds to millis `1483254000000`, and in Tokyo is `1483203600000`. The millis value is the only "absolute" value (it's the same everywhere), but the local date/time that corresponds to it depends on the timezone. –  Sep 05 '17 at 17:32
  • 1
    I've added some info about it in my answer. –  Sep 05 '17 at 17:45