2

I have used the following code to set the time for GMT:

Time TIME_6_PM = Time.newInstance(18, 0, 0, 0);
Datetime fixedTime = Datetime.newInstanceGmt(oldTime.dateGMT(), TIME_6_PM);

However, I am wondering what is the best way to set the time to be exactly 6:00 pm in a specific time zone regardless of the current user's one. Need to consider day saving time as well.

2 Answers 2

4

EDIT: Robust Solution

This approach accounts for the issue that Autonomy brought up in the comments.


// We need 2 Datetimes to go between local and non-local
// - One Datetime which functions as input.
// - Another "reference" Datetime we use to get the TimeZone offset.
//
// We will initially estimate the "reference" but our
// goal is to find an "input" which can function as its 
// own "reference"
//

Datetime offsetFromLocal(
    Datetime inputLocalAsGmt,
    TimeZone tz,
    Datetime reference
) {
    return Datetime.newInstance(
        inputLocalAsGmt.getTime() - tz.getOffset(reference)
    );
}

Datetime offsetToLocal(Datetime input, TimeZone tz) {
    return Datetime.newInstance(
        input.getTime() + tz.getOffset(input)
    );
}


// If we use the input as its own "reference",
// as intended, how close do we get to the 
// expected value?
//
Long error(
    Date expectedLocalDate,
    Time expectedLocalTime,
    TimeZone localTimezone,
    Datetime actual
) {
    Datetime expectedAsGmt = Datetime.newInstanceGmt(
        expectedLocalDate, expectedLocalTime);

    return (
        offsetToLocal(actual, localTimezone).getTime()
        -
        expectedAsGmt.getTime()
    );
}

// This will return a Datetime which will work out to 
// the specified local Date and Time (if they exist) 
// after we adjust it for the given TimeZone.
//
// This accounts for all DST transitions known to 
// Salesforce.
//
// we use a guess-and-check approach which is guaranteed 
// to stabilize in one iteration, provided the Local time 
// we give it exists. Otherwise, we adjust the time to 
// something equivalent.
//

Datetime datetimeFromLocal(Date localDate, Time localTime, TimeZone localTimeZone) {

    // 1. Estimate a reference. Can be anything. 
    //    Choosing a time in the middle of the same year.
    Datetime firstReference =  Datetime.newInstanceGmt(
        Date.newInstance(localDate.year(), 6, 15),
        Time.newInstance(12, 0, 0, 0)
    );

    Datetime targetLocalAsGmt = Datetime.newInstanceGmt(localDate, localTime);

    // 2. Compute an estimate and its error.
    Datetime estimate = offsetFromLocal(targetLocalAsGmt, localTimeZone, firstReference);
    Long error = error(localDate, localTime, localTimeZone, estimate);
    
    // 3. Subtract the error from the estimate, to get a 
    // new, adjusted (improved) estimate.
    //
    Datetime adjustedEstimate = Datetime.newInstance(
        estimate.getTime() - error
    );

    // 4. Inspect the error on our final estimate.
    // 
    Long adjustedEstimateError = error(localDate, localTime, localTimeZone, adjustedEstimate);

    // If still non-zero ...
    if(adjustedEstimateError != 0) {

        // ... we are in the interval skipped by a 
        // "Spring Forward" transition.
        //
        // In this case, the magnitude of the error 
        // tells us the size of the skipped interval.

        // To get us out of the invalid zone, we 
        // jump ahead by the error amount ... 
        //
        Datetime roundedUp = Datetime.newInstance(
            adjustedEstimate.getTime() + Math.abs(adjustedEstimateError)
        );
        Datetime roundedUpLocalAsGmt = offsetToLocal(roundedUp, localTimeZone);

        // ... and try again with the new value.
        return datetimeFromLocal(
            roundedUpLocalAsGmt.dateGmt(),
            roundedUpLocalAsGmt.timeGmt(),
            localTimeZone
        );
        
    }
    
    // The value we have is valid, with zero error.
    //
    return adjustedEstimate;
}

// Examples
// ---------------

String targetTimeZoneId = 'Australia/Brisbane';

String formatLocal(Datetime input) {
    return input.format('yyyy-MM-dd HH:mm:ssXXX', targetTimeZoneId);
}

String formatGmt(Datetime input) {
    return input.formatGmt('yyyy-MM-dd HH:mm:ssXXX');
}

// The original example

targetTimeZoneId = 'Australia/Brisbane';

Datetime targetDatetime = datetimeFromLocal(
    Date.newInstance(2018, 3, 18),
    Time.newInstance(18, 0, 0, 0),
    TimeZone.getTimeZone(targetTimeZoneId)
);

Assert.areEqual('2018-03-18 18:00:00+10:00', formatLocal(targetDatetime));
Assert.areEqual('2018-03-18 08:00:00Z',      formatGmt(targetDatetime));

// Here are some trickier examples.
//
// Brisbane has not observed DST since 1992.
// Here we select a place which has.

targetTimeZoneId = 'Australia/Sydney';

// - Spring forward on Oct 7, 2018 at 2AM standard time

Datetime preSpringForward = datetimeFromLocal(
    Date.newInstance(2018, 10, 7),
    Time.newInstance(1, 45, 0, 0),
    TimeZone.getTimeZone(targetTimeZoneId)
);

Assert.areEqual('2018-10-07 01:45:00+10:00', formatLocal(preSpringForward));
Assert.areEqual('2018-10-06 15:45:00Z',      formatGmt(preSpringForward));

Datetime postSpringForward = datetimeFromLocal(
    Date.newInstance(2018, 10, 7),
    Time.newInstance(3, 15, 0, 0),
    TimeZone.getTimeZone(targetTimeZoneId)
);
Assert.areEqual('2018-10-07 03:15:00+11:00', formatLocal(postSpringForward));
Assert.areEqual('2018-10-06 16:15:00Z',      formatGmt(postSpringForward));

// 2:30 AM is skipped by the "Spring Forward".
// So our method bumps the value up to 3:30 AM.
//

Datetime midSpringForward = datetimeFromLocal(
    Date.newInstance(2018, 10, 7),
    Time.newInstance(2, 30, 0, 0),
    TimeZone.getTimeZone(targetTimeZoneId)
);
Assert.areEqual('2018-10-07 03:30:00+11:00', formatLocal(midSpringForward));
Assert.areEqual('2018-10-06 16:30:00Z',      formatGmt(midSpringForward));

Original, Less Robust Solution

This approach does not work in some cases.

Example for March 18, 2018 at 6:00 PM in the Australia/Brisbane time zone:

Date targetDate = Date.newInstance(2018, 3, 18);
Time targetTime = Time.newInstance(18, 0, 0, 0);

TimeZone targetTimezone = TimeZone.getTimeZone('Australia/Brisbane');

// The targetDate is used here to decide whether 
// Daylight Savings should apply.
Integer offsetSeconds = targetTimezone.getOffset(targetDate) / 1000;

Datetime targetDatetime = Datetime.newInstanceGmt(targetDate, targetTime)
                                  .addSeconds(-offsetSeconds);

System.assertEquals(
    '2018-03-18 18:00:00+10:00',
    targetDatetime.format('yyyy-MM-dd HH:mm:ssXXX', targetTimezone.getID()),
    'Wrong local time.'
);

System.assertEquals(
    '2018-03-18 08:00:00Z',
    targetDatetime.formatGmt('yyyy-MM-dd HH:mm:ssXXX'),
    'Wrong UTC time.'
);
2
  • The problem with this is that the offset is incorrect on the DST change day; the get offset parameter does not take a time so it checks the offset at midnight compared to GMT of the given date, which is no good if later that day it changes to DST.
    – Autonomy
    Commented Feb 21, 2020 at 0:33
  • @Autonomy: I think I fixed it.
    – quietopus
    Commented Dec 15, 2023 at 3:09
0

Not sure if there is a better way of achieving this.

So I have found this solution first post by Ken Koellner here: https://developer.salesforce.com/forums/?id=906F00000008zv0IAA . This approach is not very elegant, but works for me:

    string dateTimeStr = inDateTime.format('yyyy-MM-dd HH:mm:ss',  timeZoneStr);
    string dateGmtStr  = inDateTime.formatGMT('yyyy-MM-dd HH:mm:ss');
    Datetime localDateTime = DateTime.valueOf(dateTimeStr);
    Datetime baseGMTTime = DateTime.valueOf(dateGMTStr);
    Long milliSecDiff =  baseGMTTime.getTime() - localDateTime.getTime();
    Long minDiff = milliSecDiff / 1000 / 60;
    Datetime outDateTime = inDateTime.addMinutes(minDiff.intValue());

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .