Is this somehow not a bug in the DateTime object exposed by JSONParser getDateTimeValue() method?

I opened a case with Salesforce (#08078994), but their Support Engineer has claimed repeatedly that, “I have reconfirmed and there is no issue with Date Time methods. The issue which you are notifying to us is related to coding done by you.”

Am I really doing something wrong here? I’m trying to show that when I use JSON.createParser() to build a DateTime value, it’s garbled. The formatGmt() method shows the correct time, but hourGmt() returns a different value (the local hour instead of GMT).

The code I wrote for this StackExchange answer to my question about parsing an ISO 8601 timestamp was affected by the bug.

String iso8601 = '2012-07-31T11:22:33.444Z';
JSONParser parser = JSON.createParser( '{"t":"' + iso8601 + '"}');
parser.nextToken();
parser.nextValue();
DateTime dt1 = parser.getDateTimeValue();

// dt2 should be identical to dt1
DateTime dt2 = DateTime.newInstance( dt1.getTime());
String timeformat = 'M/d/yyyy h:mm a';

System.Debug( iso8601 + ' parsed as ' + dt1);

System.Assert( dt1.format() == dt2.format()); // localtime is the same
System.Assert( dt1.getTime() == dt2.getTime()); // same number of milliseconds since epoch
System.Assert( dt1.formatGmt( timeformat) == dt2.formatGmt( timeformat));
System.Assert( dt1.hourGmt() == dt2.hourGmt()); // assertion fails when it should not

EDIT

This is still broken in Winter ’12, I have not had an opportunity to test it in Spring ’13 yet. Here’s a simplified example, that eliminates the dual DateTime objects and uses a single call to JSON.deserialize:

String iso8601 = '2012-07-31T11:22:33.000Z';
DateTime dt1 = (DateTime) JSON.deserialize( '"' + iso8601 + '"', DateTime.class);
String timeformat = 'M/d/yyyy h:mm a';
System.Debug( iso8601 + ' parsed as ' + dt1);
System.assertEquals( '7/31/2012 11:22 AM', dt1.formatGmt( timeformat), 'not parsed correctly');
System.assertEquals( 11, dt1.hourGmt(), 'hourGmt should be 11');

And the output:

USER_DEBUG|[7]|DEBUG|2012-07-31T11:22:33.000Z parsed as 2012-07-31 11:22:33
EXCEPTION_THROWN|[10]|System.AssertException: Assertion Failed: hourGmt should be 11: Expected: 11, Actual: 4

I think the key here is that formatGmt() prints an hour of 11, but hourGmt() returns 4, the same as hour().

And oddly, I can kind of “reset” the DateTime object using this construct:

dt1 = DateTime.newInstance( dt1.getTime());

If I add that before the assert in my test code, it passes.

EDIT

Still broken in Winter ’13.

Answer

Update for subsequent question edit

Here is what I think is happening:

  1. The DateTime dt1 returned by JSONParser.getDateTimeValue() internally stores values based on the users time zone offset. In my case, and I suspect yours, this is currently GMT – 7. This is why calls to dt1.dateGmt() include an offset.

  2. The time component that is stored internally in dt1 has been adjusted to the users time zone as well. So 11 AM becomes 4 AM due to the -7 hour offset.

  3. Calls to dt1.hourGmt() only take into account the time portion of the DateTime, and hence return 4 rather than 11. The other missing 7 hours are stored in the date portion of the DateTime (which can be seen in calls to dt1.dateGMT()).

An interesting experiment would be to alter the active users time zone to see if the result changes as expected.

The more I read over this the more I concur that this is a Salesforce bug in the hourGMT() method.


The DateTime dt1 returned by JSONParser.getDateTimeValue() has internally been adjusted to the active users time zone. This offset is tracked against the date component and the time component has been adjusted accordingly. For my test user on GMT-7 this only left 4 hours in the time component.

As Mike notes in his answer, DateTime.newInstance(Long) returns a DateTime in the GMT time zone.

Constructs a DateTime and initializes it to represent the specified number of milliseconds since January 1, 1970, 00:00:00 GMT. The returned date is in the GMT time zone.

Internally the dt2 date isn’t tracking the users time zone offset, so the time component has the full 11 hours.


I tried modifying your example to explicitly create dt2 using the GMT values.

String iso8601 = '2012-07-31T11:22:33.000Z'; // NB: Removed milliseconds as they weren't available in newInstanceGmt
JSONParser parser = JSON.createParser( '{"t":"' + iso8601 + '"}');
parser.nextToken();
parser.nextValue();
DateTime dt1 = parser.getDateTimeValue();

// dt2 should be identical to dt1
//DateTime dt2 = DateTime.newInstance( dt1.getTime());
//DateTime dt2= DateTime.newInstanceGmt(dt1.dateGMT(), dt1.timeGmt());
DateTime dt2= DateTime.newInstanceGmt(2012, 07, 31, 11, 22, 33);

String timeformat = 'M/d/yyyy h:mm a';

//2012-07-31T11:22:33.000Z parsed as 2012-07-31 11:22:33    
System.Debug( iso8601 + ' parsed as ' + dt1);

// The date values differ by 7 hours when tested with a user on PDT
System.Debug(dt1.dateGMT() + ', ' + dt1.timeGmt()); //2012-07-31 07:00:00, 04:22:33.000Z
System.Debug(dt2.dateGMT() + ', ' + dt2.timeGmt()); //2012-07-31 00:00:00, 11:22:33.000Z

System.assertEquals( dt1.format(), dt2.format(), 'localtime is the same');
System.assertEquals( dt1.getTime(), dt2.getTime(), 'same number of milliseconds since epoch');
System.assertEquals( dt1.formatGmt( timeformat), dt2.formatGmt( timeformat));
System.assertEquals( dt1.hourGmt(), dt2.hourGmt(), 'assertion fails when it should not');

The final assertion fails with the message:

System.AssertException: Assertion Failed: assertion fails when it
should not: Expected: 4, Actual: 11

Note the output of the latter two debug lines:

14:44:27.257 (257267000)|USER_DEBUG|[15]|DEBUG|2012-07-31 07:00:00,
04:22:33.000Z 14:44:27.257
(257435000)|USER_DEBUG|[16]|DEBUG|2012-07-31 00:00:00, 11:22:33.000Z

There is a difference in the internal representation of the DateTime. dt1 has a 7 hour offset in the GMT date and only 4 hours in the time component.

In contrast, dt2 has 0 hours in the GMT date and 11 hours in the time component.

The Salesforce user that executed the code had:

Time Zone (GMT-07:00) Pacific Daylight Time (America/Los_Angeles)

Attribution
Source : Link , Question Author : tomlogic , Answer Author : Daniel Ballinger

Leave a Comment