You are not logged in.
Pages: 1
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 incorrectUTC: 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) <-- incorrectUTC: 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) <-- incorrectUTC: 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) <-- incorrectUTC: 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
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
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
Offline
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
It seems like a good solution to me.
See https://synopse.info/fossil/info/26ecef466e
Also committed to mORMot 2.
Thanks for the feedback!
Offline
Pages: 1