1

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;}

2 Answers 2

3

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".
2
  • Thank you for replying to my question. I will look into this and see how I can add this to the Apex Class.
    – Avatar618
    Jun 1 at 15:21
  • The important part is that within the two loops. How you initialize the "dt" value depends on how the value is obtained from your CSV file. See sfdcfox's answer for an example of parsing a fixed format into parts using a regex. His "gmtTime" variable is the equivalent of my "dt" variable.
    – Phil W
    Jun 1 at 15:55
2

Don't hard-code the timezone offset; parse the time as a GMT, then use the TimeZone class to calculate the correct offset.

// If using this in a loop, compile once for performance
Pattern p = Pattern.compile('(\\d+)/(\\d+)/(\\d+) (\\d+):(\\d+):(\\d+)');


String reqString = '1/15/2022 11:15:00';
Matcher m = p.matcher(reqString);
m.find();
// Construct as GMT
DateTime gmtTime = DateTime.newInstanceGmt(
    Integer.valueOf(m.group(3)),
    Integer.valueOf(m.group(1)),
    Integer.valueOf(m.group(2)),
    Integer.valueOf(m.group(4)),
    Integer.valueOf(m.group(5)),
    Integer.valueOf(m.group(6))
);
// Get desired time zone
TimeZone tz = TimeZone.getTimeZone('America/Denver');
// getOffset returns the value in ms, we need it in seconds.
// We also negate the value since we're converting from GMT -> MST/MDT.
DateTime localTime = gmtTime.addSeconds(-tz.getOffset(gmtTime)/1000);

This code assumes you're using 'MM/DD/YYYY HH:mm:ss' in 24-hour time. You can make appropriate changes as you desire. Note that date/time values very nearly on the border of a DST change may still be an hour off. Due to ambiguity with specifying a time zone offset and overlaps, there's no easy way to account for this.

This code will also account for future DST savings time (notably, DST may become permanent in the US in 2023; in November 2023, the US may no longer "fall back" as it used to, if the new bill is passed).

1
  • 1
    Thank you for responding. To be honest, I am not good or even decent at tweaking the current apex code we have in our system. This will take a while for me to test in our sandbox.
    – Avatar618
    Jun 1 at 15:23

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.