Reputation: 231
I have a need to store the Last Write Access time of a file on Windows as a string. Need to avoid any Daylight Savings Time or users in a different time zone issues. I think I have a solution, but it seems there are many issues with dates.
I don't have any need to compare a date to previously stored date. Only need to know if it changed.
Storing the raw TFileTime record (represented as an Int64) seemed best as this is what is actually used by Windows as 2 DWORDS. Delphi seems to want to use TDateTime (FileAge) or an integer (FileSetDate). Both of these seem to translate to local times and only use 32 bits vs. 64 bits.
I do have a need to display a "user friendly" string and did a UTC display string to double check stored values. For these, I did use a TDateTime to translate out of the TFileTime format.
The helper unit looks like this:
unit FileTimeHelperUnt;
interface
uses
Winapi.Windows, System.SysUtils;
type
TFileTimeHelper = record helper for TFileTime
function ToString: String; //Use to export TFileTime as Int64 String.
function FromString( AString: String ): Boolean; //Use to restore
TFileTime from Int64 String
function GetLastWriteTime( AFilePathStr: String ): Boolean;
function SetLastWriteTime( AFilePathStr: String ): Boolean;
function UTCString: String;
function UserFriendlyString: String; //Like Windows Explorer and Local.
end;
implementation
{ TFileTimeHelper }
function TFileTimeHelper.ToString: String;
var
TmpInt64: Int64 absolute Self;
begin
Result := TmpInt64.ToString;
end;
function TFileTimeHelper.FromString(AString: String): Boolean;
begin
Result := False;
try
Int64(Self) := StrToInt64( AString );
Result := True;
except on E: Exception do
end;
end;
function TFileTimeHelper.GetLastWriteTime(AFilePathStr: String): Boolean;
var
TmpSearchRec: TSearchRec;
begin
Result := False;
if FileExists( AFilePathStr )=False then
Exit;
if FindFirst( AFilePathStr, faAnyFile, TmpSearchRec )=0 then
begin
Self := TmpSearchRec.FindData.ftLastWriteTime;
Result := True;
end;
end;
function TFileTimeHelper.SetLastWriteTime(AFilePathStr: String): Boolean;
var
TmpHandle: THandle;
begin
Result := False;
if FileExists( AFilePathStr )=False then
Exit;
try
TmpHandle := FileOpen(AFilePathStr, fmOpenWrite);
if TmpHandle = THandle(-1) then
Exit;
try
SetFileTime(TmpHandle, nil, nil, @Self);
Result := (GetLastError=0);
finally
FileClose( TmpHandle );
end;
except on E: Exception do
end;
end;
function TFileTimeHelper.UTCString: String;
var
TmpSystemTime: TSystemTime;
TmpDateTime: TDateTime;
begin
FileTimeToSystemTime( Self, TmpSystemTime );
TmpDateTime := SystemTimeToDateTime(TmpSystemTime);
Result := DateTimeToStr( TmpDateTime );
end;
function TFileTimeHelper.UserFriendlyString: String;
var
TmpSystemTime: TSystemTime;
TmpLocalLastWriteFileTime: TFileTime;
TmpDateTime: TDateTime;
begin
FileTimeToLocalFileTime( Self, TmpLocalLastWriteFileTime );
FileTimeToSystemTime( TmpLocalLastWriteFileTime, TmpSystemTime );
TmpDateTime := SystemTimeToDateTime(TmpSystemTime);
Result := FormatDateTime( 'm/d/yyyy h:nn ampm', TmpDateTime );
end;
end.
The calling unit looks like this:
procedure TForm12.btnGetFileDate2Click(Sender: TObject);
var
TmpFileTime: TFileTime;
begin
TmpFileTime.GetLastWriteTime( 'File.txt' );
edtFileDateTime.Text := TmpFileTime.ToString;
edtLocalFileDateTime.Text := TmpFileTime.UserFriendlyString;
edtUTCDateTime.Text := TmpFileTime.UTCString;
end;
procedure TForm12.btnSetFileDate2Click(Sender: TObject);
var
TmpFileTime: TFileTime;
begin
TmpFileTime.FromString( edtFileDateTime.Text );
TmpFileTime.SetLastWriteTime( 'File.txt' );
end;
Everything seems to work well. I'm not worried at this point about TFileTime being changed from 64-bits. Hoping I didn't miss any scenarios which could cause problems.
Also, hopefully someone else may find this useful if there aren't many problems.
The question is: is this code going to run into any time zone or daylight savings time issues? I think this code should avoid a "save now and load after daylight savings time changes" problem. Or a "Save in my timezone and then gets loaded by someone else in another time zone" problem. The TFileTime structure should stay the same and my program will recognize it didn't change. Not certain I've got all of the potential problems listed. Basically, is there any case where storing the string and loading later or in a different place will make my program think there is a change?
Thanks.
Upvotes: 0
Views: 1544
Reputation: 231
For whatever reason, my original research didn't turn up this article:MS FileTime Structure
2 things:
In the interest of completeness, here is the final production code:
unit FileTimeHelperUnt;
interface
uses
Winapi.Windows, System.SysUtils;
type
TFileTimeCompare = ( ftNewer, ftOlder, ftEqual );
TFileTimeHelper = record helper for TFileTime
function ToString: String;
function FromString( AString: String ): Boolean;
function GetLastWriteTime( AFilePathStr: String ): Boolean;
function SetLastWriteTime( AFilePathStr: String ): Boolean;
function Compare( AFileTime: TFileTime ): TFileTimeCompare;
function UTCString: String;
function UserFriendlyString: String; //Like Windows Explorer and Local.
end;
implementation
{ TFileTimeHelper }
function TFileTimeHelper.ToString: String;
begin
Result := IntToStr( Integer(Self.dwLowDateTime) ) + ',' + IntToStr( Integer(Self.dwHighDateTime) );
end;
function TFileTimeHelper.FromString( AString: String ): Boolean;
var
TmpLowDateTimeStr: String;
TmpHighDateTimeStr: String;
TmpPos: Integer;
begin
Result := False;
try
if AString.IsEmpty then
begin
Exit;
end;
TmpPos := Pos( ',', AString );
if TmpPos=0 then
begin
Exit;
end;
TmpLowDateTimeStr := Copy( AString, 1, Pos( ',', AString )-1 );
TmpHighDateTimeStr := Copy( AString, Pos( ',', AString )+1, MaxInt );
Self.dwLowDateTime := DWORD( StrToInt( TmpLowDateTimeStr ) );
Self.dwHighDateTime := DWORD( StrToInt( TmpHighDateTimeStr ) );
Result := True;
except on E: Exception do
end;
end;
function TFileTimeHelper.GetLastWriteTime(AFilePathStr: String): Boolean;
var
TmpSearchRec: TSearchRec;
begin
Result := False;
if FileExists( AFilePathStr )=False then
Exit;
if FindFirst( AFilePathStr, faAnyFile, TmpSearchRec )=0 then
begin
try
Self.dwLowDateTime := TmpSearchRec.FindData.ftLastWriteTime.dwLowDateTime;
Self.dwHighDateTime := TmpSearchRec.FindData.ftLastWriteTime.dwHighDateTime;
Result := True;
finally
FindClose( TmpSearchRec );
end;
end;
end;
function TFileTimeHelper.SetLastWriteTime(AFilePathStr: String): Boolean;
var
TmpHandle: THandle;
begin
Result := False;
if FileExists( AFilePathStr )=False then
Exit;
try
TmpHandle := FileOpen(AFilePathStr, fmOpenWrite);
if TmpHandle = THandle(-1) then
Exit;
try
SetFileTime(TmpHandle, nil, nil, @Self);
Result := (GetLastError=0);
finally
FileClose( TmpHandle );
end;
except on E: Exception do
end;
end;
//Given the imprecision of certain file systems, only compare to the minute.
function TFileTimeHelper.Compare( AFileTime: TFileTime ): TFileTimeCompare;
var
TmpSelfSystemTime: TSystemTime;
TmpArgSystemTime: TSystemTime;
function CompareWord( A, B: Word ): TFileTimeCompare;
begin
if A = B then
Result := ftEqual
else if A < B then
Result := ftNewer
else
Result := ftOlder;
end;
begin
Result := ftEqual;
FileTimeToSystemTime( Self, TmpSelfSystemTime );
FileTimeToSystemTime( AFileTime, TmpArgSystemTime );
//Compare Year
case CompareWord( TmpSelfSystemTime.wYear, TmpArgSystemTime.wYear ) of
ftNewer: Exit( ftNewer );
ftOlder: Exit( ftOlder );
end;
//Compare Month
case CompareWord( TmpSelfSystemTime.wMonth, TmpArgSystemTime.wMonth ) of
ftNewer: Exit( ftNewer );
ftOlder: Exit( ftOlder );
end;
//Compare Day
case CompareWord( TmpSelfSystemTime.wMonth, TmpArgSystemTime.wMonth ) of
ftNewer: Exit( ftNewer );
ftOlder: Exit( ftOlder );
end;
//Compare Hour
case CompareWord( TmpSelfSystemTime.wHour, TmpArgSystemTime.wHour ) of
ftNewer: Exit( ftNewer );
ftOlder: Exit( ftOlder );
end;
//Compare Minute
case CompareWord( TmpSelfSystemTime.wMinute, TmpArgSystemTime.wMinute ) of
ftNewer: Exit( ftNewer );
ftOlder: Exit( ftOlder );
end;
end;
function TFileTimeHelper.UTCString: String;
var
TmpSystemTime: TSystemTime;
TmpDateTime: TDateTime;
begin
FileTimeToSystemTime( Self, TmpSystemTime );
TmpDateTime := SystemTimeToDateTime(TmpSystemTime);
Result := DateTimeToStr( TmpDateTime );
end;
function TFileTimeHelper.UserFriendlyString: String;
var
TmpSystemTime: TSystemTime;
TmpLocalLastWriteFileTime: TFileTime;
TmpDateTime: TDateTime;
begin
try
FileTimeToLocalFileTime( Self, TmpLocalLastWriteFileTime );
FileTimeToSystemTime( TmpLocalLastWriteFileTime, TmpSystemTime );
TmpDateTime := SystemTimeToDateTime(TmpSystemTime);
Result := FormatDateTime( 'm/d/yyyy h:nn ampm', TmpDateTime );
except on E: Exception do
Result := 'Unknown.';
end;
end;
end.
Thanks for the help. I will check this off as the answer mainly because it's the solution used.
Upvotes: 0