How to add only some days to date-time variable? - delphi

When I need to increase date 1/1/2016 by 7 days, I can simply do:
IncDay(myDate, 7); // Result 8/1/2016
What do I do, if I need to ignore some days (e.g. Saturdays), so the Result is 10/1/2016 ?

Apparently, based on the somewhat cryptic comments, you wish to increment a date by a number of days, excluding Saturday. You can do that by making use of the the DayOfTheWeek function in DateUtils. This will tell you which day of the week a specified date falls on.
So your function is something like this:
function IncExcludingSaturday(FromDate: TDateTime; IncDays: Integer): TDateTime;
begin
Assert(IncDays >= 0);
Result := FromDate;
if DayOfTheWeek(Result) = DaySaturday then
Result := IncDay(Result);
while IncDays > 0 do
begin
Result := IncDay(Result);
if DayOfTheWeek(Result) = DaySaturday then
Result := IncDay(Result);
dec(IncDays);
end;
end;
This is a rather crude way to achieve your goal. You can find more interesting ideas here: AddBusinessDays and GetBusinessDays
Now, in the question you suggest that 7 days from 01/01/2016, excluding Saturdays, takes you to 09/01/2016. But that is surely wrong since that date is a Saturday. The correct answer is surely 10/01/2016 which is a Sunday. In other words we need to skip over two Saturdays, on the 2nd and the 9th.

This adds a day to the calculation for each Saturday found in the ANumberOfDays range:
{.$DEFINE UNCLEAR_WHAT_YOU_R_ASKING}
function IncDayIgnoringSaturdays(const AValue: TDateTime; const ANumberOfDays: Integer = 1): TDateTime;
var
i, j: Integer;
begin
i := ANumberOfDays;
j := 0;
Result := AValue;
while i > 0 do begin
Result := IncDay(Result);
if DayOfTheWeek(Result) = DaySaturday then
Inc(j);
Dec(i);
end;
Result := IncDay(Result, j);
{$IFDEF UNCLEAR_WHAT_YOU_R_ASKING}
if DayOfTheWeek(Result) = DaySaturday then
Result := IncDay(Result);
{$ENDIF}
end;
begin
WriteLn(DateTimeToStr(IncDayIgnoringSaturdays(StrToDateTime('1/1/2016'), 7)));
WriteLn(DateTimeToStr(IncDayIgnoringSaturdays(StrToDateTime('1/1/2016'), 14)));
ReadLn;
end.
EDIT
The above may return a date on Saturday or not, depending on the UNCLEAR_WHAT_YOU_R_ASKING conditional define.

You've been given a couple of answers for the special case of excluding a single specific weekday. This is not particularly flexible.
You could try for implementing a function that takes a set of weekdays that are 'valid' as one of its parameters. The number of days to increment the date by is approximately AIncByDays * 7 / NoOfDaysInSet. But it gets rather tricky adjusting the result correctly for valid/invalid weekdays 'near' the start date. Even after all this complexity, you still wouldn't have a way to deal with special dates, like public holidays.
Fortunately there's different approach that's much simpler to implement and far more flexible. It's only drawback is that it's inefficient for 'large' increments.
The general approach is to increment 1 day at a time.
And on each increment check the validity of the new date.
Only if the new date is valid, reduce the increment counter by 1.
Repeat the above in a loop until the increment counter is reduced to 0.
The following uses a callback function to check the validity of each date.
type
TValidDateFunc = function (ADate: TDateTime): Boolean;
function IncValidDays(AStartDate: TDateTime; AIncBy: Integer; AIsValid: TValidDateFunc): TDateTime;
var
LIncDirection: Integer;
begin
// Support dec using negative AIncBy
if AIncBy >= 0 then
LIncDirection := 1
else
LIncDirection := -1;
Result := AStartDate;
while (AIncBy <> 0) do
begin
IncDay(Result, LIncDirection);
if (AIsValid(Result)) then
Dec(AIncBy, LIncDirection);
end;
end;
Now you can simply write whatever function you desire to determine a valid date and use it in the above function. E.g.
function DateNotSaturday(ADate: TDateTime): Boolean;
begin
Result := (DayOfTheWeek(ADate) <> DaySaturday);
end;
NewDate := IncValidDays(SomeDate, 10, DateNotSaturday);
Note that it now becomes quite easy to write a function that uses only work days that aren't public holidays. E.g.
function IsWorkDay(ADate: TDateTime): Boolean;
var
LDay, LMonth, LYear: Word;
begin
DecodeDate(ADate, LYear, LMonth, LDay);
Result := True;
Result := Result and (DayOfTheWeek(ADate) <> DaySaturday);
Result := Result and (DayOfTheWeek(ADate) <> DaySunday);
Result := Result and ((LDay <> 1) or (LMonth <> 1)); //Excludes New Years day.
...
end;
The biggest advantage of this approach is that you don't have to deal with the risk of 'double-ignoring' a date because it's both a weekend day and a public holiday.
NOTE: In recent versions of Delphi you could replace the callback function with an anonymous method.

Determine the day of the week using DayOfTheWeek function. Now you know when will be next Saturday and whether it will get inside your period. If your period is larger than one week, then you can multiple number of Saturdays in your period by a number of full weeks. If your period is larger than 7 weeks, then you will have to add one more day for each 7 weeks.

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.

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.

insert number of days of year, months, days of month

How can I insert in a database the number of days of the year and at the same time insert in the same record the month, day of the month, and the day of the week?
This is my table:
tabela/coluna.Dias_ano(registo 1...365)
Year:=StrToInt(ano.Text);
diasano.Text:= IntToStr( DaysInAYear(Year) );
diasAno| Mes |diames |dia semana |
1 | janeiro | 1 |Segunda |
2 | janeiro | 2 | Terça |
...
365 | Dezembro | 31 | Segunda
Probably I'm missing the question but in case I'm not, you can find what you need in "DateUtils.pas". It has functions like "DayOfTheYear", "MonthOfTheYear", "DayOfTheMonth", "DayOfTheWeek" and many more. I think you're gonna store them in different fields, but there might be a probability that you don't need to store them at all; the database you're using might supply similar functionality, in that case you can construct your queries to supply the filtering/ordering you need.
edit: code for the 3rd comment below;
procedure TForm1.Button1Click(Sender: TObject);
var
Year, DaysInYear: Word;
FirstDay, i: Integer;
begin
Year := StrToInt(ano.Text);
DaysInYear := DaysInAYear(Year);
diasano.Text := IntToStr(DaysInYear);
FirstDay := Trunc(EncodeDate(Year, 1, 1));
for i := FirstDay to FirstDay + DaysInYear - 1 do begin
Planeamento.Append;
Planeamento.FieldByName('diasAno').Value := DayOfTheYear(i);
Planeamento.FieldByName('Month').Value := LongMonthNames[MonthOfTheYear(i)];
Planeamento.FieldByName('DayOfMonth').Value := DayOfTheMonth(i);
Planeamento.FieldByName('DayOfWeek').Value := LongDayNames[DayOfTheWeek(i)];
Planeamento.Post;
end;
end;
edit: With calculated fields;
For the below example the table has five columns instead of four. Let's name the first column 'Date'. This column is the only column to store data and will hold the Date (as per ldsandon's answer, since by storing the date instead of day-number, you won't have to keep track of what table represents what year, and calculations will be simpler).
The other four columns are exactly the same as in the question, except that they all are "calculated fields".
procedure TForm1.Button1Click(Sender: TObject);
var
Year, DaysInYear: Word;
FirstDay, i: Integer;
begin
Year := StrToInt(ano.Text);
DaysInYear := DaysInAYear(Year);
diasano.Text := IntToStr(DaysInYear);
FirstDay := Trunc(EncodeDate(Year, 1, 1));
for i := FirstDay to FirstDay + DaysInYear - 1 do begin
Planeamento.Append;
Planeamento.FieldByName('Date').Value := i;
Planeamento.Post;
end;
end;
procedure TForm1.PlaneamentoCalcFields(DataSet: TDataSet);
var
Date: TDateTime;
begin
Date := DataSet.FieldByName('Date').AsDateTime;
DataSet.FieldByName('diasAno').AsInteger := DayOfTheYear(Date);
DataSet.FieldByName('Month').AsString := LongMonthNames[MonthOfTheYear(Date)];
DataSet.FieldByName('DayOfMonth').AsInteger := DayOfTheMonth(Date);
DataSet.FieldByName('DayOfWeek').AsString := LongDayNames[DayOfTheWeek(Date)];
end;
You could simply calculate what value has 1/1/<year>, than check if <year> is leap or not and then with a simple for loop calculate the TDateTime value for each day (just add the day number to the January 1st value), extract the info you need with the DateUtils functions and write them to a record for each day.
But I would advise you agains such a solution. Those all are informations already encoded into a datetime value. I would simply store each item with its date, the whole calendar can be easily built client side when needed (and only the needed parts), without having to store it wholly in the database.

Resources