Delphi: DateTimeToStr output with zero time (midnight) - delphi

I have found a similar question here, but it is unrelated to what I am trying to do. I have done a lot of research on the Internet and I have determined that Delphi is working as designed or intended, where it omits the time if the time is zero. I have an application which displays the date & time in a listview, and when the time is midnight, it doesn't show 00:00:00, and therefore making the results look uneven and out of place.
The way I've gotten around this which is still locale independant is to add a microsecond to the time, see sample code:
program Test11;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, Winapi.Windows;
begin
try
Writeln(DateTimeToStr(44167, TFormatSettings.Create(GetThreadLocale)));
Writeln(DateTimeToStr(44167.00000001, TFormatSettings.Create(GetThreadLocale)));
Readln;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
And subsequent output:
02/12/2020
02/12/2020 00:00:00
The question is - is there a better, more programatically correct way to do achieve this?
Running Delphi XE6

You can use FormatDateTime function for more control over date time formatting.
FormatDateTime('ddddd tt', 44167, TFormatSettings.Create);
Note: There is no need to call TFormatSettings.Create(GetThreadLocale) with locale parameter because plain TFormattSettings.Create call will internally use GetThreadLocale on Windows platform.

This is "normal" behavior of VCL. Look at the related code from System.SysUtils (it is from Seattle, but I think it hasn't much changes since XE6).
function DateTimeToStr(const DateTime: TDateTime;
const AFormatSettings: TFormatSettings): string;
begin
DateTimeToString(Result, '', DateTime, AFormatSettings);
end;
procedure DateTimeToString(var Result: string; const Format: string;
DateTime: TDateTime; const AFormatSettings: TFormatSettings);
begin
...
if Format <> '' then AppendFormat(Pointer(Format)) else AppendFormat('C');
...
//This is another related part of DateTimeToString.
case Token of
'C':
begin
GetCount;
AppendFormat(Pointer(AFormatSettings.ShortDateFormat));
GetTime;
if (Hour <> 0) or (Min <> 0) or (Sec <> 0) or (MSec <> 0) then
begin
AppendChars(' ', 1);
AppendFormat(Pointer(AFormatSettings.LongTimeFormat));
end;
end;
If time part of datetime value equals to zero, then only date will be in result string.
If you want always show datetime in single format, better use FormatDateTime as suggested by other answers.

Related

Date format in RecordSet

I need to store a date in a Recordset. I am doing it as follows:
rs: _Recordset;
dt: TDatetime;
Rs := CoRecordset.Create;
Rs.Fields.Append('Date', adDate, 4, adfldupdatable, Unassigned);
Rs.Fields.Item['Date'].Value := FormatDateTime('dd/mm/yyyy', TDatetime)
But, in the Recordset the date appears in format d/m/yyyy
How can I store dd/mm/yyyy in the recordset?
The adDate is not a string format, it is a date format. When you set value, it is performing a conversion from the string you entered into its internal format (which renders as d/m/yyyy when saved to a file).
If you want a specific string format for your date, use a string field to store the date. The disadvantage is that you are going to then need to make sure that you process the string field properly if you change your region settings to one where the date format is in M/D/Y order instead of what you stored as D/M/Y order.
I'm afraid I cannot get your code to function as you claim. In any case, it is clearly in error
because the line
Rs.Fields.Item['Date'].Value := FormatDateTime('dd/mm/yyyy', TDatetime)
is wrong, because you are passing TDateTime as the second argument, not your
dt variable you apparently intend.
So, instead, using Delphi 10.4.2. I have compiled and run this program on Win10
program RecordSetTest;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, System.Variants, System.Win.ComObj, WinAPI.ActiveX, WinAPI.AdoInt;
var
RS: _Recordset;
DT: TDatetime;
begin
CoInitialize(Nil);
DT := Now;
RS:= CoRecordset.Create;
RS.Fields.Append('Date', adDate, 4, adfldupdatable, Unassigned);
RS.Open('c:\temp\rsdata.xml', EmptyParam, adOpenStatic, adLockOptimistic, 0);
RS.AddNew('Date', FormatDateTime('dd/MM/yyyy', DT));
RS.Save('c:\temp\rsdata.xml', adPersistXML);
RS := Nil;
writeln('saved');
readln;
end.
Note the call to AddNew, which is necessary to add a data record to the RecordSet file.
On my system, which has short and long date formats of dd/MM/yyyy and dd MMMM yyyy, the file
written is as follows:
<xml xmlns:s='uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882'
xmlns:dt='uuid:C2F41010-65B3-11d1-A29F-00AA00C14882'
xmlns:rs='urn:schemas-microsoft-com:rowset'
xmlns:z='#RowsetSchema'>
<s:Schema id='RowsetSchema'>
<s:ElementType name='row' content='eltOnly' rs:updatable='true'>
<s:AttributeType name='Date' rs:number='1' rs:write='true'>
<s:datatype dt:type='dateTime' rs:dbtype='variantdate' dt:maxLength='16' rs:precision='0' rs:fixedlength='true'
rs:maybenull='false'/>
</s:AttributeType>
<s:extends type='rs:rowbase'/>
</s:ElementType>
</s:Schema>
<rs:data>
<z:row Date='2021-10-20T12:32:59'/>
</rs:data>
</xml>
Note the format of the date value
2021-10-20T12:32:59
despite the formatting of the date value in
RS.AddNew('Date', FormatDateTime('dd/MM/yyyy', DT));

TXSDateTime error "local time situated within the missing period prior to DST'

We convert our local dates (no time parts) to a external system expecting UTC datetime strings, by adding TTimeZone.Local.UTCOffset (now 2 hours) to a TDateTime.
This fails for the night we switch to DST (at 02:00).
Error from System.RTLConst:
SLocalTimeInvalid = 'The given "%s" local time is invalid (situated within the missing period prior to DST).';
occurring in System.DateUtils:
function TTimeZone.GetUtcOffsetInSeconds(const ADateTime: TDateTime; const ForceDaylight: Boolean): Int64;
var
LOffset, LDSTSave: Int64;
LType: TLocalTimeType;
begin
{ Obtain the information we require }
DoGetOffsetsAndType(ADateTime, LOffset, LDSTSave, LType);
{ Select the proper offset }
if (LType = lttInvalid) then
raise ELocalTimeInvalid.CreateResFmt(#SLocalTimeInvalid, [DateTimeToStr(ADateTime)])
else if (LType = lttDaylight) or ((LType = lttAmbiguous) and ForceDaylight) then
Result := LOffset + LDSTSave
else
Result := LOffset;
end;
Code to reproduce:
function DateTime2UTCString(ADateTime: TDateTime): String;
var XSD: TXSDateTime;
begin
XSD := TXSDateTime.Create;
try
try
XSD.AsDateTime := ADateTime;
Result := XSD.NativeToXS;
except
on E:Exception do
Result := E.Message;
end;
finally
XSD.Free;
end;
end;
function Date2UTCString(ADateTime: TDateTime): String;
// Input is guaranteed to have no time fraction
begin
ADateTime := ADateTime + TTimeZone.Local.UTCOffset;
Result := DateTime2UTCString(ADateTime);
end;
procedure TFrmUTCandDST.Button1Click(Sender: TObject);
var
lDT: TDateTime;
l : integer;
begin
lDT := EncodeDate(2016,3,25);
for l := 0 to 2 do
begin
lDT := lDT +1;
Memo1.Lines.Add(DateToStr(lDT) + ' -> ' + Date2UTCString(lDT));
end;
end;
(Don't forget to use SOAP.XSBuiltIns, System.DateUtils, System.TimeSpan).
Output:
26-3-2016 -> 2016-03-26T02:00:00.000+01:00
27-3-2016 -> The given "27-3-2016 2:00:00" local time is invalid (situated within the missing period prior to DST).
28-3-2016 -> 2016-03-28T02:00:00.000+02:00
How can I graciously circumvent this? I can use TTimeZone.Local.IsInvalidTime(ADateTime) to detect invalid dates, but
26-3-2016 2:00:00 would be wrong (that's exactly the time we moved to DST), not 27-3-2016 2:00:00 - so I don't know how to adjust in case of the 'invalid' date.
There is bug in unit System.DateUtils.pas (afaik still present in 10.1).
Function AdjustDateTime first takes date and time handling it as local time, and THEN tries to put offset into it. Since during daylight saving time there is a "missing hour" (in case of central Europe it was 26.03.2017), therefore after 1:59:59 A.M you've got 3:00:00 A.M.
If you accidentally use this period (like 2:17:35), you'll get an exception.
This is also present in other functions.
Simple code to reproduce the exception (C++):
ShowMessage(ISO8601ToDate("2017-03-26T02:22:50.000Z",false));
but this one runs ok:
ShowMessage(ISO8601ToDate("2017-03-26T02:22:50.000Z",true));`
For now to avoid the exception use XSD.AsUTCDateTime, then apply the local offset.
Example in c++ :
TTimeZone * localTz = TTimeZone::Local;
TDateTime TimeSomething = localTz->ToLocalTime(XSD->AsUTCDateTime);
In your case either local time is indeed invalid (there was no "2:00"),
or somewhere you're trying to treat UTC Time as local time, which of course is invalid. Solve this and you will solve your problem.
How can I graciously circumvent this? I can use TTimeZone.Local.IsInvalidTime(ADateTime) to detect invalid dates, but 26-3-2016 2:00:00 would be wrong (that's exactly the time we moved to DST), not 27-3-2016 2:00:00 - so I don't know how to adjust in case of the 'invalid' date.
Additionally i think you missing that in year 2016 we moved to DST at 27.03 at 2:00 but THIS year at 26-03, so 27-3-2016 2:00:00 is perfectly invalid date :)
As #Vancalar said, there is a bug in ISO8601ToDate when converting from UTC to local time near a DST transition. This bug persists in Rio 10.3.3.
A simple workaround is to avoid ISO8601ToDate's timezone conversion and let Microsoft do it. That is, replace the false value with true and follow with a call to UTCToTZLocalTime, as in the following Pascal snippet:
// Get UTC datetime from ISO8601 string
datetime := ISO8601ToDate(ISO8601UTCstring, true);
// Convert to local time w/DST conversion
datetime := UTCToTZLocalTime(zoneinfo,datetime);

Is DateTimeToString in Delphi XE5 doesn't work?

I have small piece of code:
DateTimeToString(DiffString, 't.zzz', TDT);
ShowMessage('TDT: ' + DateTimeToStr(TDT));
ShowMessage('DiffString: ' + DiffString);
which result with first ShowMessage gives random nice DateTime TDT value...
second where DiffString is exacly 00:00.000
Could anyone check it in other IDE?
In fact DateTimeToString works just fine and is behaving exactly as designed. It is doing precisely what you asked it to.
Here is the SSCCE that you should have provided:
{$APPTYPE CONSOLE}
uses
SysUtils;
var
DiffString: string;
TDT: TDateTime;
begin
TDT := Date;
DateTimeToString(DiffString, 't.zzz', TDT);
Writeln('TDT: ' + DateTimeToStr(TDT));
Writeln('DiffString: ' + DiffString);
end.
Output:
TDT: 04/02/2014
DiffString: 00:00.000
The reason is, and I am guessing here, that your date time comes from a call to Date. Or perhaps your date time is an uninitialized variable.
Whichever way, it is clear that the time part is zero. Into DiffString you put the time and not the date. That is what the t.zzz format string means.
Try again with a date time containing a non-zero time:
{$APPTYPE CONSOLE}
uses
SysUtils;
var
DiffString: string;
TDT: TDateTime;
begin
TDT := Now;
DateTimeToString(DiffString, 't.zzz', TDT);
Writeln('TDT: ' + DateTimeToStr(TDT));
Writeln('DiffString: ' + DiffString);
end.
Output
TDT: 04/02/2014 11:16:43
DiffString: 11:16.942
Of course, t.zzz is a bad choice of format. It combines the short time format with milliseconds. As you can see, on my machine, the default short time format omits seconds. So you get hours, minutes and milliseconds. You'll need to re-think your format string. Perhaps 'hh:nn:ss.zzz' is what you need.

Why DateTimeToMilliseconds in DateUtils.pas is marked as internal?

Why DateTimeToMilliseconds in DateUtils.pas is marked as internal?
Can I use it?
{ Internal, converts a date-time to milliseconds }
function DateTimeToMilliseconds(const ADateTime: TDateTime): Int64;
var
LTimeStamp: TTimeStamp;
begin
LTimeStamp := DateTimeToTimeStamp(ADateTime);
Result := LTimeStamp.Date;
Result := (Result * MSecsPerDay) + LTimeStamp.Time;
end;
[Delphi XE]
I have found this on About.com:
Experience shows that creating two TDateTime values using the function and EncodeDateTime that are distant from each other only a millisecond, the function returns a MillisecondsBetween not return as was expected, proving that it is not accurate.
So, if I don't care about few milisecs, I should use it.
The TDateTime is a floating point double. To minimize rounding errors when working with TDateTime values, most calculations in DateUtils converts the TDateTime to milliseconds.
Later when calculations are ready the Int64 value is converted back to a TDateTime value again.
The internal marking is to emphasize that this function is an implementation detail, not to be utilized outside the library. That is, when working with TDateTime values, use the public functions/procedures.
This is a little test of the function MilliSecondsBetween:
program TestMSecBetween;
{$APPTYPE CONSOLE}
uses
System.SysUtils,System.DateUtils;
var
d1,d2 : TDateTime;
i,iSec,iMin,iHour,iMSec;
isb : Int64;
begin
d1 := EncodeDateTime(2013,6,14,0,0,0,0);
for i := 0 to 1000*60*60*24-1 do
begin
iHour := (i div (1000*60*60)) mod 24;
iMin := (i div (1000*60)) mod 60;
iSec := (i div 1000) mod 60;
iMSec := i mod 1000;
d2 := EncodeDateTime(2013,6,14,iHour,iMin,iSec,iMSec);
isb := MilliSecondsBetween(d2,d1);
if (isb <> i) then
WriteLn(i:10,iHour:3,iMin:3,iSec:3,iMSec:4,isb:3);
end;
ReadLn;
end.
You can expand the test for more than one day to see if there are some anomalies.
There's no reason you could not use it, it is not deprecated and used internally.
It's just marked as 'internal' because the function header is not in the interface section. If you copy the header there it should work.
What we always do if we 'patch' a third-party unit like this, is copying it to a directory in our own search path (named PatchLibs) before modifying. That way you can't 'damage' the original file and you don't have to worry about how to rebuild the original units.

How do I work around Delphi's inability to accurately handle datetime manipulations?

I am new to Delphi (been programming in it for about 6 months now). So far, it's been an extremely frustrating experience, most of it coming from how bad Delphi is at handling dates and times. Maybe I think it's bad because I don't know how to use TDate and TTime properly, I don't know. Here is what is happening on me right now :
// This shows 570, as expected
ShowMessage(IntToStr(MinutesBetween(StrToTime('8:00'), StrToTime('17:30'))));
// Here I would expect 630, but instead 629 is displayed. WTF!?
ShowMessage(IntToStr(MinutesBetween(StrToTime('7:00'), StrToTime('17:30'))));
That's not the exact code I use, everything is in variables and used in another context, but I think you can see the problem. Why is that calculation wrong? How am I suppose to work around this problem?
Given
a := StrToTime('7:00');
b := StrToTime('17:30');
ShowMessage(FloatToStr(a));
ShowMessage(FloatToStr(b));
your code, using MinutesBetween, effectively does this:
ShowMessage(IntToStr(trunc(MinuteSpan(a, b)))); // Gives 629
However, it might be better to round:
ShowMessage(IntToStr(round(MinuteSpan(a, b)))); // Gives 630
What is actually the floating-point value?
ShowMessage(FloatToStr(MinuteSpan(a, b))); // Gives 630
so you are clearly suffering from traditional floating-point problems here.
Update:
The major benefit of Round is that if the minute span is very close to an integer, then the rounded value will guaranteed be that integer, while the truncated value might very well be the preceding integer.
The major benefit of Trunc is that you might actually want this kind of logic: Indeed, if you turn 18 in five days, legally you are still not allowed to apply for a Swedish driving licence.
So you if you'd like to use Round instead of Trunc, you can just add
function MinutesBetween(const ANow, AThen: TDateTime): Int64;
begin
Result := Round(MinuteSpan(ANow, AThen));
end;
to your unit. Then the identifier MinutesBetween will refer to this one, in the same unit, instead of the one in DateUtils. The general rule is that the compiler will use the function it found latest. So, for instance, if you'd put this function above in your own unit DateUtilsFix, then
implementation
uses DateUtils, DateUtilsFix
will use the new MinutesBetween, since DateUtilsFix occurss to the right of DateUtils.
Update 2:
Another plausible approach might be
function MinutesBetween(const ANow, AThen: TDateTime): Int64;
var
spn: double;
begin
spn := MinuteSpan(ANow, AThen);
if SameValue(spn, round(spn)) then
result := round(spn)
else
result := trunc(spn);
end;
This will return round(spn) is the span is within the fuzz range of an integer, and trunc(spn) otherwise.
For example, using this approach
07:00:00 and 07:00:58
will yield 0 minutes, just like the original trunc-based version, and just like the Swedish Trafikverket would like. But it will not suffer from the problem that triggered the OP's question.
This is an issue that is resolved in the latest versions of Delphi. So you could either upgrade, or simply use the new code in Delphi 2010. For example this program produces the output you expect:
{$APPTYPE CONSOLE}
uses
SysUtils, DateUtils;
function DateTimeToMilliseconds(const ADateTime: TDateTime): Int64;
var
LTimeStamp: TTimeStamp;
begin
LTimeStamp := DateTimeToTimeStamp(ADateTime);
Result := LTimeStamp.Date;
Result := (Result * MSecsPerDay) + LTimeStamp.Time;
end;
function MinutesBetween(const ANow, AThen: TDateTime): Int64;
begin
Result := Abs(DateTimeToMilliseconds(ANow) - DateTimeToMilliseconds(AThen))
div (MSecsPerSec * SecsPerMin);
end;
begin
Writeln(IntToStr(MinutesBetween(StrToTime('7:00'), StrToTime('17:30'))));
Readln;
end.
The Delphi 2010 code for MinutesBetween looks like this:
function SpanOfNowAndThen(const ANow, AThen: TDateTime): TDateTime;
begin
if ANow < AThen then
Result := AThen - ANow
else
Result := ANow - AThen;
end;
function MinuteSpan(const ANow, AThen: TDateTime): Double;
begin
Result := MinsPerDay * SpanOfNowAndThen(ANow, AThen);
end;
function MinutesBetween(const ANow, AThen: TDateTime): Int64;
begin
Result := Trunc(MinuteSpan(ANow, AThen));
end;
So, MinutesBetween effectively boils down to a floating point subtraction of the two date/time values. Because of the inherent in-exactness of floating point arithmetic, this subtraction can yield a value that is slightly above or below the true value. When it is below the true value, the use of Trunc will take you all the way down to the previous minute. Simply replacing Trunc with Round would resolve the problem.
As it happens the latest Delphi versions, completely overhaul the date/time calculations. There are major changes in DateUtils. It's a little harder to analyse, but the new version relies on DateTimeToTimeStamp. That converts the time portion of the value to the number of milliseconds since midnight. And it does so like this:
function DateTimeToTimeStamp(DateTime: TDateTime): TTimeStamp;
var
LTemp, LTemp2: Int64;
begin
LTemp := Round(DateTime * FMSecsPerDay);
LTemp2 := (LTemp div IMSecsPerDay);
Result.Date := DateDelta + LTemp2;
Result.Time := Abs(LTemp) mod IMSecsPerDay;
end;
Note the use of Round. The use of Round rather than Trunc is the reason why the latest Delphi code handles MinutesBetween in a robust fashion.
Assuming that you cannot upgrade right now, I would deal with the problem like this:
Leave your code unchanged. Continue to call MinutesBetween etc.
When you do upgrade, your code that calls MinutesBetween etc. will now work.
In the meantime fix MinutesBetween etc. with code hooks. When you do come to upgrade, you can simply remove the hooks.

Resources