Apex Class – Day Light Savings – If statement to adjust for daylight savings based on dates

apexdatetime

We have Apex classes that trigger and process a CSV file with rows of data. One of the columns is a time column and it is set up below. We implemented this Apex class in September/October and it worked as expected.

Once we tested in January (after day light savings – Nov 6), the time was off by 1 hour. So I went into the Apex class and fixed it.

The main line of the coding that I updated before Spring Forward (March 13) was the ("-06:00:'). (line 8)
Now after daylight savings again, my time is off by another hour.

Instead of doing this twice a year, since updating Apex Classes is kind of struggle of mine. Is there code I can add to make this work all year round? For example: If the day is before this day, use (-06:00), if not use (-05:00)

Needing code to adjust for daylight savings twice a year.

    // 15 START TIME --> STL_Start_Time__c
reqString = inputvalues[15];
if(reqString != '') {
//system.debug('reqString value from csv: '+reqString);
String dtStartStr = reqString;
List<String> dateVals = dtStartStr.split(' ')[0].split('/');
List<String> timeVals = dtStartStr.split(' ')[1].split(':');
String jsonStartDt = dateVals[2]+'-'+dateVals[0]+'-'+dateVals[1]+'T'+timeVals[0]+':'+timeVals[1]+':'+timeVals[2]+'-06:00';
//system.debug('JSON formatted date: '+jsonStartDt);
Datetime startDt = (Datetime)JSON.deserialize('"'+jsonStartDt+'"',Datetime.class);
//system.debug('converted DT'+startDt);
tempReimReqLine.STL_Start_Time__c = startDt;}
else { tempReimReqLine.STL_Start_Time__c = null;}

Best Answer

I'd like to take @sfdcfox's response and extend it. When you consider Daylight Savings Time, this has two transitions in the year (unless DST is being made permanent, as The Fox says). You can visualize these something like the following (the exact time and DST offset can vary depending on the time zone):

Daylight Savings Time Transitions

You can see there are some oddities;

  • When transitioning "to DST", there are local times that don't actually exist. Here (for this example time zone) that's times between 02:00 and 02:59:59.999, inclusive (upper timeline).
  • When transitioning "from DST", there are local times that effectively happen twice. Here (for this example time zone) that's any time between 01:00 and 01:59:59.999, inclusive (lower timeline).

The fundamental algorithm that @sfdcfox's answer contains is sound. However, the following extended version addresses getting consistent behaviour in DST transitions.

In this example, I've used dates and times that are around and cover the transition date/times for the selected time zone.

Note that this test code can be pasted as anonymous apex and run yourself:

// Define the local dates and local times
Date[] dates = new Date[] {
        Date.newInstance(2022, 03, 12),
        Date.newInstance(2022, 03, 13),
        Date.newInstance(2022, 03, 14),
        Date.newInstance(2022, 11, 5),
        Date.newInstance(2022, 11, 6),
        Date.newInstance(2022, 11, 7)
};

Time[] times = new Time[] {
        Time.newInstance(0, 0, 0, 0),
        Time.newInstance(1, 0, 0, 0),
        Time.newInstance(1, 59, 59, 0),
        Time.newInstance(2, 0, 0, 0),
        Time.newInstance(2, 59, 59, 0),
        Time.newInstance(3, 0, 0, 0),
        Time.newInstance(3, 59, 59, 0)
};

// Iterate them all
for (Date d : dates) {
    for (Time t : times) {
        // Convert this into a UTC date/time value
        Datetime dt = Datetime.newInstanceGmt(d, t);

        // Identify the target time zone
        TimeZone target = TimeZone.getTimeZone('America/Chicago');

        System.debug('Input local:  ' + d.format().left(10) + ' ' + ('0' + t.hour().format()).right(2) + ':' + ('0' + t.minute().format()).right(2) + ' (' + target.getID() + ')');

        // Figure out the offset at this UTC "moment"
        Integer offsetAtUTC = target.getOffset(dt);

        // Adjust the date/time value to be in the
        // target time zone
        Datetime targetDatetime = dt.addSeconds(-offsetAtUTC / 1000);

        // Now it is in the target time zone, we have
        // a new "moment". If the UTC "moment" was
        // before a DST transition but the "moment" in
        // the target time zone is after that
        // transition the calculation will be adrift
        Integer offsetAtLocal = target.getOffset(targetDatetime);

        if (offsetAtLocal != offsetAtUTC) {
            // There's a drift because of a DST
            // transition. Correct it
            System.debug('Adjusting for DST transition');
            Datetime adjustedDatetime = targetDatetime.addSeconds((offsetAtUTC - offsetAtLocal) / 1000);
            
            Integer offsetAtAdjusted = target.getOffset(adjustedDatetime);
            
            // The correction is conditional; if the corrected time is
            // on the same side of the transition as the correction then
            // we need to correct, but we must also correct if leaping
            // forward regardless
            if (offsetAtAdjusted == offsetAtLocal ||
                    adjustedDatetime > targetDatetime) {
                targetDatetime = adjustedDatetime;
            }
        }
        
        System.debug('Output local: ' + targetDatetime.format('MM/dd/yyyy HH:ss a z', target.getID()));
    }
}

Importantly this tries to provide a consistent or at least valid UTC that represents the input local time. (My local times are built from separate Date and Time components; these have no time zone. It's only when you turn it into a Datetime that you then have a time zone in play.)

The debug output for the above is:

Input local:  12/03/2022 00:00 (America/Chicago)
Output local: 03/12/2022 00:00 AM CST
Input local:  12/03/2022 01:00 (America/Chicago)
Output local: 03/12/2022 01:00 AM CST
Input local:  12/03/2022 01:59 (America/Chicago)
Output local: 03/12/2022 01:59 AM CST
Input local:  12/03/2022 02:00 (America/Chicago)
Output local: 03/12/2022 02:00 AM CST
Input local:  12/03/2022 02:59 (America/Chicago)
Output local: 03/12/2022 02:59 AM CST
Input local:  12/03/2022 03:00 (America/Chicago)
Output local: 03/12/2022 03:00 AM CST
Input local:  12/03/2022 03:59 (America/Chicago)
Output local: 03/12/2022 03:59 AM CST
Input local:  13/03/2022 00:00 (America/Chicago)
Output local: 03/13/2022 00:00 AM CST
Input local:  13/03/2022 01:00 (America/Chicago)
Output local: 03/13/2022 01:00 AM CST
Input local:  13/03/2022 01:59 (America/Chicago)
Output local: 03/13/2022 01:59 AM CST
Input local:  13/03/2022 02:00 (America/Chicago)
Adjusting for DST transition
Output local: 03/13/2022 03:00 AM CDT
Input local:  13/03/2022 02:59 (America/Chicago)
Adjusting for DST transition
Output local: 03/13/2022 03:59 AM CDT
Input local:  13/03/2022 03:00 (America/Chicago)
Adjusting for DST transition
Output local: 03/13/2022 03:00 AM CDT
Input local:  13/03/2022 03:59 (America/Chicago)
Adjusting for DST transition
Output local: 03/13/2022 03:59 AM CDT
Input local:  14/03/2022 00:00 (America/Chicago)
Output local: 03/14/2022 00:00 AM CDT
Input local:  14/03/2022 01:00 (America/Chicago)
Output local: 03/14/2022 01:00 AM CDT
Input local:  14/03/2022 01:59 (America/Chicago)
Output local: 03/14/2022 01:59 AM CDT
Input local:  14/03/2022 02:00 (America/Chicago)
Output local: 03/14/2022 02:00 AM CDT
Input local:  14/03/2022 02:59 (America/Chicago)
Output local: 03/14/2022 02:59 AM CDT
Input local:  14/03/2022 03:00 (America/Chicago)
Output local: 03/14/2022 03:00 AM CDT
Input local:  14/03/2022 03:59 (America/Chicago)
Output local: 03/14/2022 03:59 AM CDT
Input local:  05/11/2022 00:00 (America/Chicago)
Output local: 11/05/2022 00:00 AM CDT
Input local:  05/11/2022 01:00 (America/Chicago)
Output local: 11/05/2022 01:00 AM CDT
Input local:  05/11/2022 01:59 (America/Chicago)
Output local: 11/05/2022 01:59 AM CDT
Input local:  05/11/2022 02:00 (America/Chicago)
Output local: 11/05/2022 02:00 AM CDT
Input local:  05/11/2022 02:59 (America/Chicago)
Output local: 11/05/2022 02:59 AM CDT
Input local:  05/11/2022 03:00 (America/Chicago)
Output local: 11/05/2022 03:00 AM CDT
Input local:  05/11/2022 03:59 (America/Chicago)
Output local: 11/05/2022 03:59 AM CDT
Input local:  06/11/2022 00:00 (America/Chicago)
Output local: 11/06/2022 00:00 AM CDT
Input local:  06/11/2022 01:00 (America/Chicago)
Output local: 11/06/2022 01:00 AM CDT
Input local:  06/11/2022 01:59 (America/Chicago)
Output local: 11/06/2022 01:59 AM CDT
Input local:  06/11/2022 02:00 (America/Chicago)
Adjusting for DST transition
Output local: 11/06/2022 02:00 AM CST
Input local:  06/11/2022 02:59 (America/Chicago)
Adjusting for DST transition
Output local: 11/06/2022 02:59 AM CST
Input local:  06/11/2022 03:00 (America/Chicago)
Adjusting for DST transition
Output local: 11/06/2022 03:00 AM CST
Input local:  06/11/2022 03:59 (America/Chicago)
Adjusting for DST transition
Output local: 11/06/2022 03:59 AM CST
Input local:  07/11/2022 00:00 (America/Chicago)
Output local: 11/07/2022 00:00 AM CST
Input local:  07/11/2022 01:00 (America/Chicago)
Output local: 11/07/2022 01:00 AM CST
Input local:  07/11/2022 01:59 (America/Chicago)
Output local: 11/07/2022 01:59 AM CST
Input local:  07/11/2022 02:00 (America/Chicago)
Output local: 11/07/2022 02:00 AM CST
Input local:  07/11/2022 02:59 (America/Chicago)
Output local: 11/07/2022 02:59 AM CST
Input local:  07/11/2022 03:00 (America/Chicago)
Output local: 11/07/2022 03:00 AM CST
Input local:  07/11/2022 03:59 (America/Chicago)
Output local: 11/07/2022 03:59 AM CST

Note the following:

  • I'm running in the UK, so the simple date output is dd/mm/yyyy while the time zone formatted date output is mm/dd/yyyy since it is a US time zone.
  • On non-transition dates or away from transition times, it's very simple and basically the same as The Fox's algorithm.
  • On transition dates and around transition times the algorithm has to decide on whether or not an adjustment needs to be made. Sometimes it decides it's needed, other times not.
  • On transition dates for "Spring Forward", "non-existent local times" are mapped to the following hour (e.g. 02:59 becomes 03:59). It's either that or decide to throw an exception.
  • On transition date for "Fall Back", the algorithm consistently maps to a UTC that represents one of the "duplicate times".
Related Topic