I understand that doubles have a sign (1 bit), exponent (11 bits) and fraction (52 bits) (according to Wikipedia anyway).
I have three questions.
1) How do I use these three parts to convert them to a number I can use (both the Integral and fractional parts)?
2) How are the three parts stored in a double within Delphi?
3) Once I know the integral and fractional parts, I'm really using them as a date and time with the date being the integral part being an offset in days and the fractional being an offset, I think, in milliseconds from midnight. So really its two numbers I am trying to derive from the double, days and milliseconds. The question is, is there a way to separate the two into ints? that is, day offset into int (or longint) and milliseconds into another int (or longint) using standard math functions?
I found out earlier that hex representations of dates I have stored were indeed Delphi doubles (see below). But I don't know how the double is represented
----------- answer to an earlier question --------------------------
Effectively these strings looks like the Hex representation for the Delphi TDatetime type which is an alias for the Double type (8 bytes), Where the integral part of the TDateTime value is the number of days that have passed since 12/30/1899 and the fractional part of the TDateTime value is fraction of a 24 hour day that has elapsed.
in Delphi you can parse such values using the HexToBin function like so
{$APPTYPE CONSOLE}
uses
System.Classes,
System.SysUtils;
function Decode(const HexStr: AnsiString) : TDateTime;
begin
Assert(Length(HexStr)=16, 'Lenght must be 16');
HexToBin(PAnsiChar(HexStr), #Result, SizeOf(TDateTime));
end;
begin
try
Writeln(DateTimeToStr(Decode('a297780add3ee440')));
Writeln(DateTimeToStr(Decode('5c6c320bdd3ee440')));
Writeln(DateTimeToStr(Decode('67b176e0dd3ee440')));
Writeln(DateTimeToStr(Decode('38a7e155bc42e440')));
Writeln(DateTimeToStr(Decode('d94ee458bc42e440')));
Writeln(DateTimeToStr(Decode('d22af9134989dc40')));
Writeln(DateTimeToStr(Decode('d4fb7863c542e440')));
Writeln(DateTimeToStr(Decode('e501c962c542e440')));
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
ReadLn;
end.
This will return
07-07-2013 21:46:50
07-07-2013 21:46:58
07-07-2013 22:24:27
07-08-2013 21:15:05
07-08-2013 21:15:37
01-01-1980 3:24:15
08-08-2013 4:02:29
08-08-2013 4:02:21
You're misunderstanding what the relevant parts of a TDateTime contain.
The integral part (the part to the left of the decimal point) is the number of days that have elapsed since December 30, 1899, as you state. However, the fractional part (the part to the right of the decimal), is simply the decimal representation of the time that has passed since 00:00:00 0001 (the start of that day).
In other words,
0.25 12/30/1899 06:00:00 (6:00 AM)
0.5 12/30/1899 12:00:00 (noon)
0.75 12/30/1899 18:00:00 (6:00 PM)
The simplest way to work with Delphi's TDateTime and it's various parts is to let the RTL handle it for you. To separate out the individual portions of the time, use DecodeTime. To retrieve the individual parts of a date, use DecodeDate (both from the 'SysUtils' unit:
var
Hr, Min, Sec: Word;
Yr, Mon, Day: Word;
DecodeTime(Now, Hr, Min, Sec);
DecodeDate(Now, Yr, Mon, Day);
To retrieve them all at once, use the DateUtils unit and DecodeDateTime instead:
DecodeDateTime(Now, Yr, Mon, Day, Hr, Min, Sec);
If you really do want just the individual parts (the left and right sides of the decimal point), then as David says you just use Int and Frac
A Delphi double value is a standard IEEE754 double, using the standard representation. All languages do this for various reasons. Not least of which is that the underlying hardware operates of IEEE754 types.
You can pull a double value apart using the TDoubleHelper introduced in XE3. The documentation shows you how: http://docwiki.embarcadero.com/Libraries/en/System.TDoubleHelper. For older Delphi versions you would use TDoubleRec instead.
Now, splitting the value into its significand and exponent will not help you. You want the integer part and the fractional part which are entirely different things. Get them using the standard functions Int() and Frac().
However, when you do this, you still won't have what you need. The fractional part is a floating point number between 0 and 1. That doesn't sound much like a number of milliseconds. So I believe that you do not yet fully understand this representation.
Perhaps what you have is nothing more than a TDateTime! In which case you use the standard functions from the RTL to decode it.
I've just read your earlier question. You are indeed on the wrong track. Read again the answer. Specifically:
Where the integral part of the TDateTime
value is the number of days that have passed since
12/30/1899 and the fractional part of the TDateTime value is
fraction of a 24 hour day that has elapsed.
So, in your VB code, convert the hex string, or binary, whichever you have, into a double. Then get the integer part and the fractional part and you are done..
Related
I am trying to get the time stamp of a file when it was last modified to the millisecond. Currently if I use:
FileAge(Filename);
I will get the last modified date/time to a second. For example: 12:00:54. Is it possible to get the last modified date and time with milliseconds?
Use TFile.GetLastWriteTime or TFile.GetLastWriteTimeUtc from the System.IOUtils unit.
If the system supports your desired accuracy then these functions will provide dates to that accuracy. Of course it all depends on the file system in use.
There are two versions of FileAge():
function FileAge(const FileName: string): Integer;
This version is deprecated.
On Windows, it uses GetFileAttributesEx() and/or FindFirstFile() to retrieve the timestamp as a FILETIME (which has 100-nanosecond precision), but then coverts it to a DOS-encoded integer using FileTimeToDosDateTime(), which has seconds precision.
On POSIX, it uses stat(), which returns timestamps as seconds elapsed since the epoch.
function FileAge(const FileName: string; out FileDateTime: TDateTime; FollowLink: Boolean = True): Boolean;
On Windows, it retreives the same timestamp as the deprecated version, but coverts the FILETIME to a SYSTEMTIME and then to a TDateTime using milliseconds precision.
On POSIX, it uses stat(), and thus has seconds precision.
Im using Delphi 7 and now FloatToStr(64) returns
64,0000017441
when a different module is loaded in the same process that uses Direct3D code.
The return value is
64
as expected, when the other module is not active in the process.
Why does the Output of FloatToStr() change based on other unrelated code executed and what to do to always get a reliable and consistent string representation of a floating point value?
The difference in behaviour of FloatToStr() can be seen in the following sample code:
{$APPTYPE CONSOLE}
program Project1;
uses
SysUtils;
begin
Writeln('FloatToStr(64) = ', FloatToStr(64));
Set8087CW(Get8087CW and $FCFF);
Writeln('FloatToStr(64) = ', FloatToStr(64));
end.
In the foreign plugin code, in "display.cpp" in the code
hr = d3d->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd,
D3DCREATE_NOWINDOWCHANGES | D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_PUREDEVICE,
&d3dpp, &d3ddev );
add the flag "D3DCREATE_FPU_PRESERVE" so that the code is:
hr = d3d->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd,
D3DCREATE_NOWINDOWCHANGES | D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_PUREDEVICE | D3DCREATE_FPU_PRESERVE,
&d3dpp, &d3ddev );
If you leave out this code, Direct 3D changes the precision used by the FPU for the current thread and you will see less precise values.
See also http://blogs.msdn.com/b/tmiller/archive/2004/06/01/145596.aspx
The alternative (when you can't or don't want to change the other plugin code ) is to use explicit formating when displaying floating point values.
Use
s:= FormatFloat('0.##', x);
instead of
s:= FloatToStr(X);
This will always round the value to two decimal places and not show trailing zeros after the decimal separator. And it will not show any decimal separator, when the two highest digits are 0.
Another alternative would be to have your code run in a separate thread, since the FPU precision settings are per thread.
Another alternative would be to execute
Reset8087CW;
in your code before calling FloatToStr(). However, this could mess with Direct3D and produce errors there.
To give a simple console application that replicates the behaviour you're seeing, as well as an explanation with some more details:
{$APPTYPE CONSOLE}
program Project1;
uses
SysUtils;
begin
Set8087CW(Get8087CW and $FCFF);
Writeln('FloatToStr(64) = ', FloatToStr(64));
end.
Output:
FloatToStr(64) = 64.000001744411
Stepping through the debugger, the code for converting 64 to a decimal representation ends up calling
function _Pow10(val: Extended; Power: Integer): Extended;
with val = 64, Power = 16 to calculate 64 * 10**16. This then gets converted to an integer 640000000000000000, from which decimal digits get extracted, and in which a decimal point gets placed. This would be just fine in the default FPU mode, where it would produce 640000000000000000 exactly, but in the FPU mode used here, less precision is available, not enough precision to represent that number. It gets rounded up to the nearest representable number, and the subsequent code to extract the decimals correctly sees that the last digits aren't 0.
This issue is the subject of an unresolved QC report from 2004: QC#7275.
I'm using FloatToText this way:
function ExFloatToStr(Value: Extended): string;
var
Buffer: array[0..63] of Char;
FormatSettings: TFormatSettings;
begin
GetLocaleFormatSettings(GetUserDefaultLCID, FormatSettings);
SetString(Result, Buffer, FloatToText(Buffer, Value, fvExtended, ffGeneral,
18, 0, FormatSettings));
end;
If I pass to this function value 9229.99, it returns a string value 9229.9900000000016, but it's not what I want.
When I create a new project and just copy the above code there, it works well. It returns 9229.99.
What might be the reason they work different in different projects ?
This is in fact an issue of representability. Here's the SSCCE:
{$APPTYPE CONSOLE}
uses
SysUtils, Windows;
function ExFloatToStr(Value: Extended): string;
var
Buffer: array[0..63] of Char;
FormatSettings: TFormatSettings;
begin
GetLocaleFormatSettings(GetUserDefaultLCID, FormatSettings);
SetString(Result, Buffer, FloatToText(Buffer, Value, fvExtended, ffGeneral,
18, 0, FormatSettings));
end;
var
X: Double;
Y: Extended;
begin
X := 9229.99;
Y := 9229.99;
Writeln(ExFloatToStr(X));
Writeln(ExFloatToStr(Y));
Readln;
end.
Output
9229.98999999999978
9229.99
You ask for 18 digits. When you store the value as a double precision, the value stored becomes imprecise to 18 decimal digit precision. A double precision value has 15-16 significant decimal digits of precision. And that is fewer than 18. When you store the value as an extended value, there is more precision available, sufficient to store your value accurately to the 18 decimal digits of precision that your requested.
I always refer to Rob Kennedy's excellent page on this matter: http://pages.cs.wisc.edu/~rkennedy/exact-float?number=9229.99
This tells us that your value, when converted to extended and double is represented as:
9229.99 = + 9229.99000 00000 00000 21316 28207 28030 05576 13372 80273 4375
9229.99 = + 9229.98999 99999 99781 72127 15744 97222 90039 0625
And this tallies precisely with the output above.
So, I expect that you will find that in your existing project you will, at some point, be storing the value to a double precision variable. And at that point you lose the extra precision of Extended.
For what it is worth, I regard the 80 bit extended type as an anachronism. It is only supported on Intel chips, and only used by 32 bit compilers. In my experience it nevers offers any real benefit over double precision. And its performance is poor because of memory alignment. As the author of a lot of floating point code, I never use extended.
There is a difference when compiling for x86 vs x64 Windows targets.
For x86 platform the 'Extended' type has a different 'size' then on the x64 platform.
See the Delphi help (XE2):
System.Extended offers greater precision than other real types, but is less
portable. Be careful using System.Extended if you are creating data files to
share across platforms.
On Win32 systems, the size of System.Extended is 10 bytes.
On Win64 systems, however, the System.Extended type is an alias for System.Double,
which is only 8 bytes. This difference can adversely affect numeric precision in
floating-point operations. For more information, see Delphi Considerations for
Cross-Platform Applications.
Writeln(IntToStr(SizeOf(Extended))); // displays 10 on Win32 and 8 on Win64
Essentially this means on x64 platforms, you have less significant floating point values, which might result in a different conversion to a textual value.
I would like to add seconds to a TDateTime variable, so that the result is the top of the minute. For example, if it's 08:30:25, I want change the TDateTime variable to store 08:31:00.
I see that TDateTime has a Decode function, which I could use. There isn't, however, an encode function to put the altered time back into a TDateTime variable.
Using DateUtils it's possible to do it like this:
Uses
DateUtils;
var
Seconds : Word;
Seconds := SecondOfTheMinute(MyTime); // Seconds from last whole minute
// Seconds := SecondOf(MyTime); is equivalent to SecondOfTheMinute()
if (Seconds > 0) then
MyTime := IncSecond(MyTime,60 - Seconds);
There sure is, at least in the recent versions - see the DateUtils unit, especially all the Recode* routines and EncodeDateTime. The DateUtils unit is already available in Delphi 2010, perhaps even in earlier version.
Theory
The TDateTime data type represents number of days since 30 Dec 1899 as a real number. That is, the integral part of TDateTime is an amount of whole days, and the fractional part represents a time of day.
Practical
Therefore, your problem could be solved using simple arithmetics:
var
Days: TDateTime;
Mins: Extended; { widen TDateTime's mantissa by 11 bits to accommodate division error }
begin
Days := Date + StrToTime('08:30:25');
Writeln(DateTimeToStr(Days));
Mins := Days * 24 * 60 ; // compute minutes
Mins := Math.Ceil(Mins); // round them up
Days := Mins / (24 * 60); // and back to days
{ or as simple and concise expression as: }
// Days := Ceil(Days * MinsPerDay) / MinsPerDay;
Writeln(DateTimeToStr(Days));
I would like a code sample for a function that takes a tDateTime and an integer as input and sets the system time using setlocaltime after advancing that tDateTime by (int) months. The time should stay the same.
pseudo code example
SetNewTime(NOW,2);
The issues I'm running into are rather frustrating. I cannot use incmonth or similar with a tDateTime, only a tDate, etc.
Below is a complete command-line program that works for me. Tested in Delphi 5 and 2007. Why do you say IncMonth does not work for TDateTime?
program OneMonth;
{$APPTYPE CONSOLE}
uses
SysUtils,
Windows,
Messages;
procedure SetLocalSystemTime(settotime: TDateTime);
var
SystemTime : TSystemTime;
begin
DateTimeToSystemTime(settotime,SystemTime);
SetLocalTime(SystemTime);
//tell windows that the time changed
PostMessage(HWND_BROADCAST,WM_TIMECHANGE,0,0);
end;
begin
try
SetLocalSystemTime(IncMonth(Now,1));
except on E:Exception do
Writeln(E.Classname, ': ', E.Message);
end;
end.
IncMonth should work with a TDateTime:
function IncMonth ( const StartDate : TDateTime {; NumberOfMonths : Integer = 1} ) : TDateTime;
Keep in mind a TDate is really just a TDateTime that by convention your ignore the fraction on.
Based on your pseudocode:
procedure SetNewTime(aDateTime: TDateTime; aMonths: Integer);
var
lSystemTime: TSystemTime;
begin
DateTimeToSystemTime(aDateTime, lSystemTime);
Inc(lSystemTime.wMonth, aMonths);
setSystemTime(lSystemTime);
end;
setSystemTime uses UTC time, so you have to adjust for your time zone. The bias is the number of minutes your machine's timezone differs from UTC. This adjusts the date properly on my system:
procedure SetNewTime(aDateTime: TDateTime; aMonths: Integer);
var
lSystemTime: TSystemTime;
lTimeZone: TTimeZoneInformation;
begin
GetTimeZoneInformation(lTimeZone);
aDateTime := aDateTime + (lTimeZone.Bias / 1440);
DateTimeToSystemTime(aDateTime, lSystemTime);
Inc(lSystemTime.wMonth, aMonths);
setSystemTime(lSystemTime);
end;
There isn't enough information to provide a definitive answer to your question.
Consider what you would want to happen if the day of the current month doesn't exist in your future month. Say, January 31 + 1 month. (7 months of the year have 31 days and the rest have fewer.) You have the same problem if you increment the year and the starting date is February 29 on a leap year. So there can't be a universal IncMonth or IncYear function that will work consistantly on all dates.
For anyone interested, I heartily recommend Julian Bucknall's article on the complexities that are inherent in this type of calculation
on how to calculate the number of months and days between two dates.
The following is the only generic date increment functions possible that do not introduce anomolies into generic date math. But it only accomplishes this by shifting the responsibility back onto the programmer who presumably has the exact requirements of the specific application he/she is programming.
IncDay - Add a or subtract a number of days.
IncWeek - Add or subtract a number of weeks.
But if you must use the built in functions then at least be sure that they do what you want them to do. Have a look at the DateUtils and SysUtils units. Having the source code to these functions is one of the coolest aspects of Delphi. Having said that, here is the complete list of built in functions:
IncDay - Add a or subtract a number of days.
IncWeek - Add or subtract a number of weeks.
IncMonth - Add a or subtract a number of months.
IncYear - Add a or subtract a number of years.
As for the second part of your question, how to set the system date & time using a TDatetime, the following shamelessly stolen code from another post will do the job once you have a TDatetime that has the value you want:
procedure SetSystemDateTime(aDateTime: TDateTime);
var
lSystemTime: TSystemTime;
lTimeZone: TTimeZoneInformation;
begin
GetTimeZoneInformation(lTimeZone);
aDateTime := aDateTime + (lTimeZone.Bias / 1440);
DateTimeToSystemTime(aDateTime, lSystemTime);
setSystemTime(lSystemTime);
end;