0

my problem in short: TDateTime A (03.09.2014 13:40) - TDateTime B (03.09.2014 13:40) = -1

I have two TDateTime values which i want to compare, first i used the = operator to check if they are the same but after a few tests i realized that this is not working in my case. The confusion in this is that it works great the most time but sometimes not.

I get one value out of the LastWriteTime attribute from an existing file and the other value is from a MySQL database.

Here is some code:

TDateTime a := FileList[loop].Lastwritetime.AsUTCDateTime; // TDateTime from MySQL
TDateTime b := GetLastwritetimeUtc(Sourcedirectory); // TDateTime from my local file

if (CompareDateTime(a, b) = 0) then
begin
   // do some stuff.
end;

Now as mentioned before this easy code is working most of the time but for some TDateTime values i get a negative result which should mean that my TDateTime value from the MySQL database is earlier then my local file TDateTime value.

So i started debugging:

double aTicks := a; // MySQL TDateTime
double bTicks := b; // Local file TDateTime

this provides me the days passed since 30.12.1899 and the decimal values the time.

Example values:

// a = 02.09.2014 11:42:01
// b = 02.09.2014 11:42:01
// aTicks = 41884,4875115741
// bTicks = 41884,4875153356

The decimals which are not same should be milliseconds or not (starting after xxxx,4875)? Now if i compare them (e.g. CompareDateTime(a,b) or a = b) i do not get 0/true (i do not compare the aTicks and bTicks values).

Do i have to make changes in my way i get the local file TDateTime (at the moment i am using the WinAPI, the GetLastWriteTimeUTC did not provide me the correct UTC time)?

I think it is not really a hard problem but i have no idea how to solve this. Does the `` TDateTime store the milliseconds hidden? In the debugging mode i do not see any milliseconds and i do not know how to get this value out of my TDateTime (using Delphi XE2).

Here are some extra details about my project

I get the TDateTime b value this way

function GetLastwritetimeUtc(source: String): TDateTime;
var
   fad: TWin32FileAttributeData;
   SystemTime: TSystemTime;
   lastwritetimeUtc: TDateTime;
begin
   GetFileAttributesEx(PWideChar(source),GetFileExInfoStandard,@fad);
   FileTimeToSystemTime(fad.ftLastWriteTime, SystemTime);
   lastwritetimeUtc := SystemTimeToDateTime(SystemTime);
   result := lastwritetimeUtc;
end;

If a file from the MySQL database is "newer" i replace it and set the LastWriteTime from my MySQL TDateTime a attribute this way: SetLastWriteTimeUTC(a) (and my TDateTime values from the MySQL (a) do not have any milliseconds value). So the problem should not occure again but it does.

The TDateTime value on my MySQL database is from this

XSDateTime c := DateTimeToXSDateTime(GetLastwritetimeUtc(sourceDirectory));
// i send this via WCF service to the MySQL database and store it in a `TDateTime` column (which does not include milliseconds)

I hope this is enough information and not too much.

Best regards,

Niclas

UPDATE:

The code does "the same" as my main program, as i said above the wrong DateTime comparision does not trigger all the time only on some files (in my case the $Default10.dsk).

uses
  SysUtils,
  Soap.SoapHttpTrans,
  DateUtils,
  Windows,
  System.IOUtils,
  Soap.XSBuiltIns;

var
  fad: TWin32FileAttributeData;
  SystemTime: TSystemTime;
  lastwritetimeUtcA: TDateTime;
  lastwritetimeUtcB: TDateTime;
  sourceFileA: string;
  sourceFileB: string;
  lastwritetimeXS: TXSDateTime;
begin
  while True do
  begin
    sourceFileA := 'Path to a file on your computer no matter which';
    sourceFileB := 'Path to another file on your computer no matter which';

    //GetLastWriteTime from local file
    GetFileAttributesEx(PWideChar(sourceFileA),GetFileExInfoStandard,@fad);
    FileTimeToSystemTime(fad.ftLastWriteTime, SystemTime);
    lastwritetimeUtcA := SystemTimeToDateTime(SystemTime);

    //Set the localfile lastwritetime to the theoretical mySQL file
    // in my main program there does not exist a mySQL file (only a value of TDateTime in the TDateTime column of my database where i store the lastwritetime from the local files
    TFile.SetLastWriteTimeUtc(sourceFileB, lastwritetimeUtcA);

    //Get the LastWriteTime from theoretical mySQL file
    // in my main program i get the lastwritetime value from the MySQL database via WCF that is the reason for the convertion of XSDateTime.AsUTCDateTime and DateTimeToXSDateTime
    GetFileAttributesEx(PWideChar(sourceFileB),GetFileExInfoStandard,@fad);
    FileTimeToSystemTime(fad.ftLastWriteTime, SystemTime);
    lastwritetimeUtcB := SystemTimeToDateTime(SystemTime);

    //Convert lastwritetime to XSDatetime - how i do it in my program
    lastwritetimeXS := DateTimeToXSDateTime(lastwritetimeUtcB);
    {Convert it back to DateTime}
    lastwritetimeUtcB := lastwritetimeXS.AsUTCDateTime;

    //Compare them
    if lastwritetimeUtcA = lastwritetimeUtcB then
      Writeln('Same time')
    else
      writeln('Not same time');
    Sleep(500);
  end;
end;
TryToSolveItSimple
  • 873
  • 1
  • 12
  • 23
  • Why don't you ignore the milliseconds? – David Heffernan Sep 03 '14 at 12:46
  • I do not know how and did not find any ways to do this - it could solve my problem and i would be very happy if you could provide me code for this so i can check it out. But it also would be very nice to know why this is happening? – TryToSolveItSimple Sep 03 '14 at 12:52
  • Please can you give a short complete program to demonstrate the problem. – David Heffernan Sep 03 '14 at 12:54
  • i will create one and upload it on a filehoster or cloud space but i think this will not represent my own existing "special" problem. As mentioned above in most of the cases it IS working well. I compare `TDateTime` values from my MySQL and my local files. But a few files (everytime the same in one case) are not "compareable", then the situation is as explained above. I deleted this files and recreated them with the data from my MySQL database but it still occures. EDIT: i think i know how to "recreate" it and will provide you the same files etc. please wait a few minutes or more. – TryToSolveItSimple Sep 03 '14 at 13:01
  • You could use [MilliSecondsBetween()](http://docwiki.embarcadero.com/Libraries/en/System.DateUtils.MilliSecondsBetween) for the comparison and if it is > x, you can assume they are different. – LU RD Sep 03 '14 at 13:05
  • Please don't upload anything. Just add a short, complete console app to the question. – David Heffernan Sep 03 '14 at 13:09
  • @DavidHeffernan i updated my post. But there is a strange exception^^. The core of the code is the same as in my main program. The difference: my lastwritetimeA is from a local file and the lastwritetimeB is from the MySQL database and i think here is the real difference – TryToSolveItSimple Sep 03 '14 at 14:03
  • 1
    That program is no use to us because we don't have your files. Can't you give us something that we can run? – David Heffernan Sep 03 '14 at 14:08
  • how could i do this? With no uploading ... you could replace the path with paths of you files. It happens with one file all the time and some files random. I can give you one or more of these files. But i think the important thing is the MySQL storing and getting the lastwritetime not the files itself. – TryToSolveItSimple Sep 03 '14 at 14:12
  • Program is fine, just replace `TDirectory.SetLastWriteTimeUtc` with `TFile.SetLastWriteTimeUtc` and remove the `;` before the else, compiler tells where. Use any two files. Problem is floating point comparison. – Sertac Akyuz Sep 03 '14 at 14:14
  • @SertacAkyuz floating point comparision exactly - i figured this out in my post above (see the example values with aTicks and bTicks in the comments). Sorry for the wrong semicolon i edited my code in the post and did not see this. I will try the comparision with the individual parts like you said but i still wonder why the comparison works most of the time. – TryToSolveItSimple Sep 03 '14 at 14:20
  • @Ask - You removed the wrong semicolumn, it should be the one *before* the else, ;), not really important... If you're bound to use floating point, 'math' unit should have some CompareValue which you can use an epsilon you define, there should be some questions here on SO too.. – Sertac Akyuz Sep 03 '14 at 14:22
  • I am not bound to use floating point that was only a way to figure out the issue. The easiest way is to ignore the milliseconds or set them to zero for both `TDateTime` values. Is there a smarter way than `DecodeDateTime` and `EncodeDateTime` the `DateTime`? Or must i use the floating point and compare it with Math unit? – TryToSolveItSimple Sep 03 '14 at 14:54
  • @Ask - By not using a floating point, I meant storing the times in the database as timestamp or something like it, and not using a datetime on the client at all. – Sertac Akyuz Sep 03 '14 at 15:08

3 Answers3

1

If you want to compare two TDateTime values and match to the second, ignoring millisecond differences, use SecondsBetween from the DateUtils unit:

program Project1;

uses
  SysUtils, DateUtils;

var
  dtOne, dtTwo: TDateTime;

begin
  dtOne := 41884.4875115741;
  dtTwo := 41884.4875153356;

  if SecondsBetween(dtOne, dtTwo) = 0 then
    WriteLn('Dates the same without MS')
  else
    WriteLn('Not the same dates.');

  ReadLn;
end.

There are similar functions for other differences, such as DaysBetween, MinutesBetween, and MilliSecondsBetween if you need to match to other resolutions. Here's a utility function that works for various resolutions (exact match, day, hour, minute, or second):

type
  TDiffResolution = (tdrExact, tdrDay, tdrHour, tdrMin, tdrSec);

function IsSameDateTime(dValOne, dValTwo: TDateTime;
  const Resolution: TDiffResolution = tdrSec): Boolean;
begin
  case Resolution of
    tdrExact: Result := MillisecondsBetween(dValOne, dValTwo) = 0;
    tdrDay: Result := IsSameDay(dValOne, dValTwo);
    tdrHour: Result := HoursBetween(dValOne, dValTwo) = 0;
    tdrMin: Result := MinutesBetween(dValOne, dValTwo) = 0;
    tdrSec: Result := SecondsBetween(dValOne, dValTwo) = 0;
  else
    raise Exception.CreateFmt('Invalid resolution value (%d) provided.',
                              [Ord(Resolution)]);
  end;
end;

Sample use:

  dtOne := 41884.4875115741;
  dtTwo := 41884.4875153356;

  if IsSameDateTime(dtOne, dtTwo, tdrSec) then
    WriteLn('Dates are the same.')
  else
    WriteLn('Dates are different.');
  ReadLn;    
Ken White
  • 123,280
  • 14
  • 225
  • 444
  • Or, you could simply recode the `TDateTime` values to remove the milliseconds. Look at `System.DateUtils.RecodeMilliSecond()` for that. Or use `EncodeDate()` and `EncodeTime()` directly instead of `SystemTimeToDateTime()` so you can set milliseconds to 0 from the beginning. – Remy Lebeau Sep 03 '14 at 17:06
  • @Remy: Yes, those are also viable alternatives. I chose this way because it's easy to extend to work with ignoring minute differences or hour differences as well (by adding a resolution type as a parameter) and because it was easy to write. :-) – Ken White Sep 03 '14 at 17:36
  • that is a smart way. Thanks for that, it is working well for me! – TryToSolveItSimple Sep 04 '14 at 07:18
  • Sorry i have a question for the comparison with `SecondsBetween` am i right that this method does not compare the hours, minutes and days, etc.? only the seconds? In my test case it does give me the result that my file from yesterday is newer then my current file because the seconds are greater. When i commented your post a few hours ago i only tested it with files from today. – TryToSolveItSimple Sep 04 '14 at 10:15
  • @Askinator: It compares the seconds between, which if the seconds don't match will be greater than zero; if the minutes don't match will be greater (a full minute would return 60 seconds between), an hour will be greater still (60 * 60 = 3600 seconds between), etc. A full 24 hours difference would result in 86400 seconds between. So it compares every part of the values down to the seconds, but ignores milliseconds. If you want to include milliseconds, use `MillisecondsBetween` instead. – Ken White Sep 04 '14 at 12:32
  • @KenWhite Ah! Do i understand it right that it only shows me that it does not match and not which is "older" or "newer"? I have rewritten my `GetLastWriteTimeUtc` so it does not include ms that solved my problem now but i thought i could solve it a bit easier (it was no more difficult after all responds here but no oneliner :P) – TryToSolveItSimple Sep 04 '14 at 13:08
  • It only shows you the difference, not which one is older/newer. You can figure that out yourself, though; if the files are not the same date, you then compare FileA's date to FileB's date, and if FileA's date is greater then it's newer, otherwise FileB is. The first test (that they're not the same date to begin with) tells you whether you need to do the second test or not. It's still just a couple of lines of code: `if SecondsBetween(DateA, DateB) <> 0 then if DateA > DateB then FileA is newest else FileB is newest.` – Ken White Sep 04 '14 at 13:18
  • @KenWhite you are missing the point now. My problem exactly was that i can not compare so easy by checking DateA > DateB or DateA = DateB because of the ms. That was my topic about - ignoring the ms and not the comparison itself but you posted a method to compare without checking the ms that was the code i was looking for but in the end it did not solve the problem because only saying: its not the same was not everything. But we should not discuss here a solved problem. If you want to have an extended discussion we could have a chat. – TryToSolveItSimple Sep 04 '14 at 14:43
0

You can also play by converting TDateTime to string and comparing them. This way you can make much complex conditions buy setting the time format. For example, you can check if dates are within the same hour, ignoring minutes and seconds:

If FormatDateTime('yyyymmddhh', Date1) = FormatDateTime('yyyymmddhh', Date2) ...

The other way is to decode them and compare parts you are interested in, as in:

DecodeDateTime (Date1, Y1, M1, D1, H1, N1, S1, mS1);
DecodeDateTime (Date2, Y2, M2, D2, H2, N2, S2, mS2);
If (Y1 = Y2) and (M1 = M2) and (D1 = D2) and (H1 = H2) then ...
adlabac
  • 416
  • 4
  • 12
0
    dt := Now;
    dtWithoutMilliseconds := SecondsBetween(0,dt)*OneSecond;
    //or
    dtWithoutMilliseconds := dt-MillisecondOf(dt)*OneMillisecond;
    //or
    dtWithoutMilliseconds := Trunc(dt)+Trunc(Frac(dt)*SecsPerDay)/SecsPerDay;
    //or simply
    dtWithoutMilliseconds := Trunc(dt)+Trunc(Frac(dt)*3600)/3600;
Sławek
  • 11
  • 2