Add decimal value to a date time in firemonkey delphi XE4? - delphi

Is there anyone who can help me with, for example: Adding decimal 0,5 or 1 to a time?
eg. 0,5 + 15:30:50 = 16:00:50

If you have a time represented as a TDateTime, and wish to add a number of hours to it then you would do so like this:
uses
System.SysUtils, System.DateUtils;
function IncrementTimeBySpecifiedNumberOfHours(Time: TDateTime;
Hours: Double): TDateTime;
begin
Result := TimeOf(Time + Hours/HoursPerDay);
end;
I'm assuming that you are concerned only with times here and wish to implement a 24 hour wrap-around. Hence the use of TimeOf.

Related

Work out number of days using Month Calendar Delphi

I'm using two Month calendar on my form. I select a date on each one and I want to calculate the number of days between these two days. I only code I have is
procedure TForm1.Button1Click(Sender: TObject);
Var
N, m: TDate;
d: Real;
l,k:String;
begin
N := (MonthCalendar1.Date);
m := MonthCalendar2.Date;
L := formatdatetime('dd', N);
K:=formatdatetime('dd',M);
d := StrToFloat(L)-StrToFloat(K);
ShowMessage(FloatToStr(d));
end;
You can use DaysBetween() from System.DateUtils to obtain the difference, in days, between two TDateTime values.
See Docs: http://docwiki.embarcadero.com/Libraries/Rio/en/System.DateUtils.DaysBetween
Edit, see code:
uses System.DateUtils;
[..]
procedure TForm1.Button1Click(Sender: TObject);
var
NumDays: Integer;
begin
NumDays := DaysBetween(MonthCalendar1.Date, MonthCalendar2.Date);
ShowMessage('Days between selected dates: ' + NumDays.ToString);
end;
You don't need a MonthCalendar to do this. The part of a TDateTime to the RHS of the decimal point is effectively a "day number" so if you substract one of those from another, you find the "days between". Apply a Trunc to both to discard the time-of-day parts. See http://docwiki.embarcadero.com/Libraries/Rio/en/System.TDateTime for definition of TDateTime.
So, for example, given
var
D1,
D2 : TDateTime;
DaysBetween : Integer;
and D1 and D2 are two TDateTimes (that you could enter if you wish using a TMonthCalendar), then
DaysBetween := Trunc(D2) - Trunc(D1);
The call to the built-in Trunc function discards the fractional, time-of-day part of a TDateTime value (that is, the partr to the right of the decimal point), so that 23:59 on one day and 00:01 on the next are calculated to be one day apart. This may, or may not, be the result you want, depending on your application, which is why I have suggested calculating it yourself rather than using the built-in DaysBetween function.

Convert Extended to Time

I need to convert extended values to time format. For example :
3.50 represents 00:03:50
62.02 represents 01:02:02
73.70 represents 01:14:10
I have tried the following function to convert the Hour and Minutes part but I don't have any idea on how to convert the Seconds' part.
function ConvertToTime(AValue: Extended): TDateTime;
begin
Result:= EncodeTime(trunc(ArticleRec.Quantity) div 60,trunc(ArticleRec.Quantity) mod 60,0,0);
end;
Thanking you in anticipation for your help.
The fractional part is obtained like this:
var
SecondsFrac: Double;
....
SecondsFrac := Frac(Value);
And then you can convert from a floating point fractional value in the range 0 to 1 to an integer in the range 0 to 100 like this:
var
Seconds: Integer;
....
Seconds := Round(SecondsFrac*100);
This is a pretty weird way to store time though. You have to deal with the fact that when Seconds >= 60 you need to increment the minutes, and decrement Seconds by 60.
I guess I'd do that by converting the time into seconds, and going from there:
function ConvertWeirdTimeFormatToSeconds(const Value: Double): Integer;
var
SecondsFrac: Double;
begin
SecondsFrac := Frac(Value);
Result := Round(SecondsFrac*100) + Trunc(Value)*60;
end;
You can then decode the seconds into distinct parts like this:
procedure DecodeSeconds(Value: Integer; out Hours, Minutes, Seconds: Integer);
begin
Seconds := Value mod 60;
Value := Value div 60;
Minutes := Value mod 60;
Value := Value div 60;
Hours := Value;
end;
Which makes me think it might be better to just store the time in an integer number seconds from midnight. It makes far more sense to use a standard format, in my view.
I see no reason to use Extended here, or indeed anywhere for that matter. It's a non-standard type that due to its strange size and consequent alignment issues tends to perform poorly. And it's only supported on x86.

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.

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.

Change today's date, advancing one month and setting the systemtime

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;

Resources