Apex: The DateTime Class & Fun With Time Zones

There is quite a bit of good information and resources out there on using the DateTime class in Salesforce. In fact, the Salesforce Apex Developer Guide itself has published great examples on using this class.

However if there’s one thing I’ve learned over the past 5 years of professional development, it’s this: writing code using dates and time zones is a developer’s worst nightmare. From fixing unit tests on January 1st because a date was hardcoded and somehow made it past code review, to a client in another country complaining that something is broken because they told the system to exclude certain dates and it didn’t listen… I can’t possibly remember all of the stories I’ve heard.

So let’s take a deeper dive into the DateTime class with a few additional examples. Hopefully you’ll learn a thing or two – I know I have.

UTC vs. User’s Time Zone

Time zones are confusing. Salesforce makes this a little less confusing for you, but there’s still definitely room for error if you don’t know what all of the various DateTime methods are doing.

Salesforce stores all DateTime values in the database in UTC. When you’re viewing a record in Salesforce, the Created Date will be adjusted based on your time zone preferences. However, the UTC value is the value retrieved in Apex.

Lead testLead = new Lead(LastName = 'Tester', Company = 'Test Company');
insert testLead;
testLead = [SELECT CreatedDate FROM Lead WHERE Id = :testLead.Id];

The above code will insert a dummy Lead record and then output the record, which includes the value of its CreatedDate. Regardless of the time zone you are in, the CreatedDate will be in UTC.

What if I want to know the time for the current user?

Given a DateTime, you can convert the instance into a string using the format() method, which will convert the DateTime into something like “2/3/2017 1:20 PM”. If a different output is desired, the format() method also accepts additional parameters so you can specify a different Java simple date format.

There are other methods that also output various components of the DateTime in the user’s time zone, like date() and time(). It’s important to read through the documentation to understand the value returned.

DateTime dt = DateTime.newInstance(2017,2,3,13,20,30);
System.debug(dt.date()); // Returns Date: 2017-02-03 00:00:00
System.debug(dt.time()); // Returns Time: 13:20:30.000Z
System.debug(dt.format()); // Returns String: 2/3/2017 1:20 PM
System.debug(dt); // Returns UTC DateTime
System.debug(DateTime.now()); // Returns UTC DateTime

newInstance vs newInstanceGmt

So, we just reviewed that the date() and time() methods return the Date and Time in the user’s time zone. Two other methods – aptly named dateGmt() and timeGmt() – return the Date and Time in the GMT time zone.

What if you need to construct a new DateTime instance from a given Date and a Time? Well, you’d better determine whether or not the Date and Time is in GMT or in your user’s current time zone, otherwise your results are not going to be as expected!

DateTime dt = DateTime.newInstance(2017,2,3,13,20,30);
Date localDate = dt.date();
Time localTime = dt.time();
Date gmtDate = dt.dateGmt();
Time gmtTime = dt.timeGmt();
DateTime newDateTime1 = DateTime.newInstance(localDate, localTime);
System.debug(newDateTime1); // Returns UTC DateTime as expected
DateTime newDateTime2 = DateTime.newInstance(gmtDate, gmtTime);
System.debug(newDateTime2); // Returns UTC DateTime + local time offset!

In this example, since I live 8 hours behind UTC time the output of newDateTime2 is 8 hours ahead of UTC time. This is because I’m passing the Date and Time as GMT time into the newInstance method and the system is expecting me to pass in the user’s local time.

If I knew the date/time in GMT that I wanted, I would instead use the newInstanceGmt method:

DateTime dt = DateTime.newInstance(2017,2,3,13,20,30);
Date gmtDate = dt.dateGmt();
Time gmtTime = dt.timeGmt();
DateTime gmtTime1 = DateTime.newInstanceGmt(gmtDate, gmtTime);
System.debug(gmtTime1); // Returns UTC DateTime as expected
DateTime gmtTime2 = DateTime.newInstanceGmt(2017,1,1,4,5,6);
System.debug(gmtTime2); // Returns UTC DateTime: 2017-01-01 04:05:06

The DST Time Warp

Congratulations for making it this far! Let’s talk about time zones and Daylight Savings Time.

Many different parts of the world participate in Daylight Savings Time. In 2016, Daylights Savings Time ended for the United States on November 6th. This can have unintended consequences if you are manipulating DateTime instances in Apex code.

For example, let’s say that it is currently December 18th, 2016 and users are running your application that performs a process that filters out all records created this month or last month – so any records created after October 31st at 11:59:59pm in the user’s current time zone. (Yes – you can do this through SOQL – but let’s assume that SOQL is not possible for this use case.)

Because the system needs to do this calculation dynamically based on the current user and the current date, you might have been tempted to write Apex similar to the following:

Date today = Date.newInstance(2016, 12, 18); // To illustrate the problem, let's not use Date.today()
DateTime beginningOfMonth = DateTime.newInstance(today.toStartOfMonth(), Time.newInstance(0,0,0,0));
DateTime brokenDateTimeFilter = beginningOfMonth.addMonths(-1).addSeconds(-1);
System.debug(brokenDateTimeFilter); // My Result: 2016-11-01 07:59:59
System.debug(brokenDateTimeFilter.date()); // Result: 2016-11-01 00:00:00

Now, at first glance this DateTime might appear to be fine. (Note: the result of the debug statement on line 4 will be different for you depending on your time zone.) However, the result of line 5 should be somewhat baffling. Our DateTime instance should represent October 31st at 11:59:59pm in the user’s current time zone, yet the date() method (which returns a Date in the user’s local time) returns November 1st. What’s going on here?

Let’s first take a look at code that works:

Date today = Date.newInstance(2016, 12, 18); // To illustrate the problem, let's not use Date.today()
DateTime dateTimeFilter = DateTime.newInstance(today.toStartOfMonth().addMonths(-1), Time.newInstance(0,0,0,0));
dateTimeFilter = dateTimeFilter.addSeconds(-1);
System.debug(dateTimeFilter); // My Result: 2016-11-01 06:59:59
System.debug(dateTimeFilter.date()); // Result: 2016-10-31 00:00:00

When comparing the results of this code versus the other code you’ll notice that while it is similar, the first example produces a DateTime that is an hour later than in the second example.

So what happened: on November 6th, the user’s local time switched from Daylight Savings Time. However, simply adding months or days to an already constructed DateTime instance isn’t going to take Daylight Savings Time into consideration. There is nothing technically wrong with the first example – the code literally did what you told it to do.

In the second example, notice that we are only working with one DateTime instance. Never manipulate a DateTime instance to switch it to another date. Instead, figure out the exact date that you want before constructing the DateTime instance.

Okay, okay. So I am at fault for breaking my own rule here, since I used the addSeconds method to subtract a second from the DateTime. This should be relatively safe to do, but a better approach would be:

Date today = Date.newInstance(2016, 12, 18);
DateTime dateTimeFilter = DateTime.newInstance(today.toStartOfMonth().addMonths(-1).addDays(-1), Time.newInstance(23,59,59,0));
System.debug(dateTimeFilter); // My Result: 2016-11-01 06:59:59
System.debug(dateTimeFilter.date()); // Result: 2016-10-31 00:00:00

And now, your calculations will work as expected regardless of that annoying DST.

I hope you found these examples to be helpful. If you have any questions or suggestions after reading through this, please feel free to leave feedback as a comment below.

Until next time,

— Robert

3 thoughts on “Apex: The DateTime Class & Fun With Time Zones

  1. Super duper hit post. Had been smashing my head over something the past 2 days. Your article solved my code’s bug. Thank you Robert.

Post Reply