1

Given a Date, a Time and a TimeZone, how can I implement a function that constructs and returns a new Datetime instance that represents the correct date and time in the specified time zone?

Below is the newDatetime(Date, Time, TimeZone) method as I have currently coded. As exposed by the two tests, the problem is that the offset calculation will be wrong at some point on the day of a transition in or out of Daylight Savings Time.

@isTest
private class DatetimeUtil2 {

    private static DateTime newDatetime(
            Date dateValue, Time timeValue, TimeZone zone) {

        // Calculate the offset as the number of seconds to add to the GMT
        // date and time defined by the `Date` and `Time` values.
        Integer offset = -1 * zone.getOffset(dateValue) / 1000 /* ms/sec */;

        return DateTime.newInstanceGmt(dateValue, timeValue).addSeconds(offset);
    }

    @isTest
    private static void newDatetime2017Mar12OneAmInAmericaNewYork() {

        // Given
        Datetime expected = Datetime.newInstanceGmt(2017, 3, 12, 6, 0, 0);

        Date givenDate = Date.newInstance(2017, 3, 12);
        Time givenTime = Time.newInstance(1, 0, 0, 0);
        TimeZone givenTimeZone = TimeZone.getTimeZone('America/New_York');

        // When
        Test.startTest();

        Datetime actual = DatetimeUtil2.newDatetime(
                givenDate, givenTime, givenTimeZone);

        // Then
        Test.stopTest();

        System.assertEquals(expected, actual);
    }

    @isTest
    private static void newDatetime2017Mar12ThreeAmInAmericaNewYork() {

        // Given
        Datetime expected = Datetime.newInstanceGmt(2017, 3, 12, 7, 0, 0);

        Date givenDate = Date.newInstance(2017, 3, 12);
        Time givenTime = Time.newInstance(3, 0, 0, 0);
        TimeZone givenTimeZone = TimeZone.getTimeZone('America/New_York');

        // When
        Test.startTest();

        Datetime actual = DatetimeUtil2.newDatetime(
                givenDate, givenTime, givenTimeZone);

        // Then
        Test.stopTest();

        System.assertEquals(expected, actual);
    }
}

What seems to be a catch 22 is that ...

  • I cannot determine the correct offset without the actual Datetime value. This assumes the use of TimeZone.getOffset to get the expected offset.
  • I cannot derive the correct Datetime value without the correct offset. This assumes a strategy of first constructing a GMT Datetime value and then applying the offset.
  • The only way you can do it correctly is to convert from the first time zone to GMT, then from GMT to the second time zone. Note that if the time is ambiguous in either the start or end time zone, there's no way to uniquely identify the time in GMT; this is a limitation of DST, not Salesforce specifically. – sfdcfox Feb 19 '18 at 20:27
  • @sfdcfox thanks for the quick comment, and can you clarify what you meant by the "first" time zone? In the code pasted in the question there is only one time zone in play, not counting GMT. – Marty C. Feb 19 '18 at 20:34
  • Any luck? I happen to be in the same scenario as you. – jmrjulian Jun 14 '18 at 0:48
  • @jmrjulian I haven't personally tested the answer against my failed unit tests but the approach there seems valid. If you can please give it a shot and upvote the answer if you find it to solve the same problem for you! – Marty C. Jun 14 '18 at 12:08
0

I can tell you that TimeZone.getOffset is capable of accepting both a Date and a Datetime argument. If you pass in a Date that is the transition day (at least as far as I've tested it in North American time zones) you get the offset of the start of the day which is pre-time change. As a result, I always add 1 day to the Date before constructing, so that I'm more often right on the first try. Excerpt from some code I use right now:

Datetime dtFrom (Date d, Time t, Timezone tz){
    // Please cite this thread if you use this snippet as the author may use this as part of a larger project 
    // https://salesforce.stackexchange.com/questions/208760/new-datetime-instance-on-daylight-savings-time-transition-date
    Datetime dt;
    // We want to ensure we get the timezone offset for the correct day due to DST.
    // Apex TimeZone.getOffset() returns offset for start of Timezone's day.
    // Approximation: Add 1 day before getting offset and most results are correct on day of time change
    Integer offset = tz.getOffset(d.addDays(1))/1000/60;
    dt = Datetime.newInstanceGmt(d, t).addMinutes(-offset);
    // Edge case: What if it's before the time change, on the day of time change?
    // If so, we will get a different offset and need to use that instead.
    Integer dtOffset = tz.getOffset(dt)/1000/60;
    if (offset != dtOffset) 
        dt = Datetime.newInstanceGmt(d,t).addMinutes(-dtOffset);
    // If the entered time is during the "skipped" hour when clocks go forward,
    // or the entered time is during the "repeated" hour when clocks go backward,
    // calculated Datetime will be during the first hour after the time change.
    return dt;
}  

So we make a "usually correct" assumption and obtain the offset for it. Then we check if the answer we got is self-consistent. If not, we calculate one more time passing the last attempt's resulting Datetime in. We don't keep going though because there are only two possible offsets on one given day.

If the first try gave us one offset and the second try didn't match, and the third try also didn't match the second, guaranteed we've flopped back to the first. And if this is happening we must be looking at a Date/Time with no self-consistent result: Either the "repeated" hour or the "skipped" hour.

Note that this code works predictably for the use cases of our current project and I cannot guarantee it will be robust for you. For the use cases I've tested, I believe edge case Date/Time inputs (times that happened twice or never) always resolve to being placed during the first hour after the time change. Test all your edge cases of course.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.