#1 2021-10-15 00:20:59

missionhq
Member
From: Australia
Registered: 2019-06-11
Posts: 33

Problem with TSynTimeZone UTCtoLocal

Hi all...
Some feedback on a bug I think I've found with converting UTC to Local with TSynTimeZone when a timezone has Daylight Savings and we're crossing a DST start date.

TSynTimeZone.Default.UtcToLocal is determining whether Daylight Savings should be applied based on the given UTC datetime, but should be determining it based on the final Local DateTime

My example scenario...

  • My TimeZone is "AUS Eastern Standard Time". It is UTC+10:00 and supports daylight savings

  • Daylight saving started on Sunday 3rd October at 2am local time

Calling (psudo code)

TSynTimeZone.Default.UtcToLocal("saturday 2nd Oct at 10pm", "AUS Eastern Standard Time")

should calculate the bias with daylight saving applied as the final local time is on Sunday 3rd @ 9am (UTC+11:00 for DaylightSaving) ), but the bias is being calculated based on the original UTC date-time as so is without DaylightSaving. It seems in this 10 hour window the bias+DaylightSaving is being calculated incorrectly.

As soon  as the UTC time rolls past 2am on Sunday 3rd October everything is calculated correctly.

I haven't checked but I guess the LocalToUTC would be incorrect as we come out of daylight savings too.

I did a little test app to identify the problem. Here's the results

UTC: 2/10/2021 10:00:00 PM    is LOCAL: 3/10/2021 8:00:00 AM (E. Australia Standard Time)  <-- This timezone doesn't use DST. I'm using it as a reference
UTC: 2/10/2021 10:00:00 PM    is LOCAL: 3/10/2021 8:00:00 AM (AUS Eastern Standard Time) <-- This timezone does have DST. Local date conversion is incorrect

UTC: 2/10/2021 10:00:00 AM    is LOCAL: 2/10/2021 8:00:00 PM (E. Australia Standard Time)
UTC: 2/10/2021 10:00:00 AM    is LOCAL: 2/10/2021 8:00:00 PM (AUS Eastern Standard Time) <-- incorrect

UTC: 3/10/2021 12:00:00 PM    is LOCAL: 3/10/2021 10:00:00 PM (E. Australia Standard Time)
UTC: 3/10/2021 12:00:00 PM    is LOCAL: 3/10/2021 11:00:00 PM (AUS Eastern Standard Time) <-- incorrect

UTC: 3/10/2021 1:00:00 AM    is LOCAL: 3/10/2021 11:00:00 AM (E. Australia Standard Time)
UTC: 3/10/2021 1:00:00 AM    is LOCAL: 3/10/2021 11:00:00 AM (AUS Eastern Standard Time) <-- incorrect

UTC: 3/10/2021 2:00:00 AM    is LOCAL: 3/10/2021 12:00:00 PM (E. Australia Standard Time)
UTC: 3/10/2021 2:00:00 AM    is LOCAL: 3/10/2021 1:00:00 PM (AUS Eastern Standard Time) <-- correct. As soon UTC passes the local timezone DST start time everything is OK

Offline

#2 2021-10-15 06:53:43

ab
Administrator
From: France
Registered: 2010-06-21
Posts: 14,182
Website

Re: Problem with TSynTimeZone UTCtoLocal

Have you any patch to propose?

Offline

#3 2021-10-15 07:02:18

missionhq
Member
From: Australia
Registered: 2019-06-11
Posts: 33

Re: Problem with TSynTimeZone UTCtoLocal

Not at the moment. I'll give it some thought over the weekend and see...
The current implementation is fast! I didn't want to compromise speed with a sort of "special case" scenario. A bit of investigating I think....

Offline

#4 2021-10-15 07:22:28

missionhq
Member
From: Australia
Registered: 2019-06-11
Posts: 33

Re: Problem with TSynTimeZone UTCtoLocal

I'm looking at applying the main bias first,  then using the result to determin if the DST bias should also be applied. The downside is that it involves a double query of the timezone sad

Offline

#5 2021-10-15 09:03:47

ab
Administrator
From: France
Registered: 2010-06-21
Posts: 14,182
Website

Re: Problem with TSynTimeZone UTCtoLocal

The query could be very fast in practice - it is a no-op if the year is the same I guess.

Offline

#6 2021-10-18 23:07:09

missionhq
Member
From: Australia
Registered: 2019-06-11
Posts: 33

Re: Problem with TSynTimeZone UTCtoLocal

Hi AB
Had the idea of shifting the DST Start/End times in GetBiasForDateTime if the value is in UTC

function TSynTimeZone.GetBiasForDateTime(const Value: TDateTime;
  const TzId: TTimeZoneID; out Bias: integer; out HaveDaylight: boolean; 
  const DateIsUTC: boolean = false): boolean;                   <======== NEW ===
var ndx: integer;
    d: TSynSystemTime;
    tzi: PTimeZoneInfo;
    std,dlt: TDateTime;
begin
  if (self=nil) or (TzId='') then
    ndx := -1 else
    if TzID=fLastZone then
      ndx := fLastIndex else begin
      ndx := fZones.FindHashed(TzID);
      fLastZone := TzID;
      flastIndex := ndx;
    end;
  if ndx<0 then begin
    Bias := 0;
    HaveDayLight := false;
    result := TzID='UTC'; // e.g. on XP
    exit;
  end;
  d.FromDate(Value); // faster than DecodeDate
  tzi := fZone[ndx].GetTziFor(d.Year);
  if tzi.change_time_std.IsZero then begin
    HaveDaylight := false;
    Bias := tzi.Bias+tzi.bias_std;
  end else begin
    HaveDaylight := true;
    std := tzi.change_time_std.EncodeForTimeChange(d.Year);
    dlt := tzi.change_time_dlt.EncodeForTimeChange(d.Year);

    // === NEW === shift the DST start and end times to convert to UTC      
    if DateIsUTC then
    begin
      std:= ((std*MinsPerDay)+tzi.Bias+tzi.bias_dlt)/MinsPerDay;   // Std shifts by the DST bias
      dlt:= ((dlt*MinsPerDay)+tzi.Bias+tzi.bias_std)/MinsPerDay;   // Dst shifts by the STD bias
    end;

    if std<dlt then
      if (std<=Value) and (Value<dlt) then
        Bias := tzi.Bias+tzi.bias_std else
        Bias := tzi.Bias+tzi.bias_dlt else
      if (dlt<=Value) and (Value<std) then
        Bias := tzi.Bias+tzi.bias_dlt else
        Bias := tzi.Bias+tzi.bias_std;
  end;
  result := true;
end;

Then in UtcToLocal when we get the Bias we specify it's a UTC time

function TSynTimeZone.UtcToLocal(const UtcDateTime: TDateTime;
  const TzId: TTimeZoneID): TDateTime;
var Bias: integer;
    HaveDaylight: boolean;
begin
  if (self=nil) or (TzId='') then
    result := UtcDateTime else begin
    GetBiasForDateTime(UtcDateTime,TzId,Bias,HaveDaylight, true);  //<======= NEW specify it's a UTC time ===
    result := ((UtcDateTime*MinsPerDay)-Bias)/MinsPerDay;
  end;
end;

Tested this on a few timezones and seems to work OK (but I do find working with Timezones, DTS and bias always a bit of a struggle!)


Interestingly I noticed a slight difference testing with "IncMinute" instead of the Multiply/Divide solution. I guess it's a rounding/precision issue but couldn't work out why...

With "std:= ((std*MinsPerDay)+tzi.Bias+tzi.bias_dlt)/MinsPerDay;"
  UTC4:00pm = Aus EST 2:00AM
  UTC4:01pm = Aus EST 3:01AM

With "std:= incMinute(std, tzi.Bias+tzi.bias_dlt);"
  UTC4:00pm = Aus EST 3:00AM
  UTC4:01pm = Aus EST 3:01AM

Offline

#7 2021-10-19 16:55:10

ab
Administrator
From: France
Registered: 2010-06-21
Posts: 14,182
Website

Re: Problem with TSynTimeZone UTCtoLocal

It seems like a good solution to me.
See https://synopse.info/fossil/info/26ecef466e

Also committed to mORMot 2. smile

Thanks for the feedback!

Offline

Board footer

Powered by FluxBB