How to calculate count of business days between two dates? - delphi

I need to calculate count of business days (working days) between two given dates. Business days are all days of the week except Saturday and Sunday. I'm not considering holidays into this count.
How to calculate count of business days between two dates ?

function BusinessDaysSinceFixedDate ( const nDate : tDateTime ) : integer;
const
Map : array [ -6 .. 6 ] of integer
= ( 0, 0, 1, 2, 3, 4, 5, 5, 5, 6, 7, 8, 9 );
var
X : integer;
begin
X := trunc ( nDate );
Result := 5 * ( X div 7 ) + Map [ X mod 7 ];
end;
function BusinessDaysBetweenDates ( const nStartDate : tDateTime;
const nEndDate : tDateTime ) : integer;
begin
Result := BusinessDaysSinceFixedDate ( nEndDate )
- BusinessDaysSinceFixedDate ( nStartDate );
end;
The routine BusinessDaysSinceFixedDate calculates the number of business days since a fixed date. The specific date, which is irrelevant, is Monday, 25 December, 1899.
It simply counts the number of weeks that have passed (X div 7) and multiplies that by 5.
Then it adds an offset to correct based on the day of the week.
Note that (X mod 7) will return a negative value for a negative date, i.e. a date before 30 December, 1899.
The routine BusinessDaysBetweenDates simply calls BusinessDaysSinceFixedDate for the start and end date and subtracts one from the other.

Without looping all days and input parameters not depending on order.
Uses DateUtils,Math;
function WorkingDaysBetween( const firstDate,secondDate : TDateTime) : Integer;
var
startDate,stopDate : TDateTime;
startDow,stopDow : Integer;
begin
if (firstDate < secondDate) then
begin
startDate := firstDate;
stopDate := secondDate;
end
else
begin
startDate := secondDate;
stopDate := firstDate;
end;
startDow := DayOfTheWeek(startDate);
stopDow := DayOfTheWeek(stopDate);
if (stopDow >= startDow) then
stopDow := Min(stopDow,6)
else
Inc(stopDow,5);
Result :=
5*WeeksBetween(stopDate,startDate) +
(stopDow - Min(startDow,6));
end;

You need to use DayOfTheWeek (from the DateUtils unit) and a counter, iterating through from the starting date to the ending date. (You'll also probably need a table of holidays, to exclude those from your count also.)
function BusinessDaysBetween(const StartDate, EndDate: TDateTime): Integer;
var
CurrDate : TDateTime;
begin
CurrDate := StartDate;
Result := 0;
while (CurrDate <= EndDate) do
begin
// DayOfTheWeek returns 1-5 for Mon-Fri, so 6 and 7 are weekends
if DayOfTheWeek(CurrDate) < 6 then
Inc(Result);
CurrDate := CurrDate + 1;
end;
end;
You can enhance this a little by not worrying about the order of the parameters (in other words, it doesn't matter if start is before end or end is before start, the function will still work):
function BusinessDaysBetween(const FirstDate, SecondDate: TDateTime): Integer;
var
CurrDate : TDateTime;
StartDate, EndDate: TDateTime;
begin
if SecondDate > FirstDate then
begin
StartDate := FirstDate;
EndDate := SecondDate;
end
else
begin
StartDate := SecondDate;
EndDate := FirstDate;
end;
CurrDate := StartDate;
Result := 0;
while (CurrDate <= EndDate) do
begin
if DayOfTheWeek(CurrDate) < 6 then
Inc(Result);
CurrDate := CurrDate + 1;
end;
end;

Related

Counting down to a time?

I'm trying to count down to a time of the day (24-hour clock format). This is my solution so far:
function TimeDiffStr(const s1, s2: string): string;
var
t1, t2: TDateTime;
secs: Int64;
begin
t1 := StrToDateTime(s1);
t2 := StrToDateTime(s2);
secs := SecondsBetween(t1, t2);
Result := Format('%2.2d:%2.2d:%2.2d', [secs div 3600, (secs div 60) mod 60, secs mod 60]);
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
TargetTime: TTime;
s: string;
begin
s := TimeDiffStr(TimeToStr(Now), TimeToStr(TargetTime));
end;
If Now is, for example, 15:35:02 and the target time is 21:44:59, the output is correct (06:09:57). However, if Now is 15:35:02 and the target time is 01:32:23, instead of counting down from 09:57:21, it will count upwards, because the function does not know that the target time is on a different day.
How can I work out the difference between two times when the times are on different days?
First off, there is no need to pass strings around. If you start with TTime and convert to TTime, then simply pass TTime around.
Second, since you are dealing with just time values, if the target time is meant to be on the next day, you need to add 24 hours so that you have a TDateTime that actually represents the next day.
Try this:
uses
..., DateUtils;
function TimeDiffStr(const t1, t2: TTime): string;
var
d1, d2: TDateTime;
secs: Int64;
begin
d1 := t1;
if t2 < t1 then
d2 := IncDay(t2) // or IncHour(t2, 24)
else
d2 := t2;
secs := SecondsBetween(d1, d2);
Result := Format('%2.2d:%2.2d:%2.2d', [secs div 3600, (secs div 60) mod 60, secs mod 60]);
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
TargetTime: TTime;
s: string;
begin
TargetTime := ...;
s := TimeDiffStr(Time(), TargetTime);
end;

How to get the year when bolding month days in a TMonthCalendar?

I have a log that use the Calendar and I want to bold the days that have recorded info. I have them in an 3D array TDiaryLog = array[1900..2399] of array[1..12] of array[1..31] of POneDay;. But in the OnGetMonthInfo, when I must build the list with the bold days, it gives me only the Month, and not the Year. How should I know for what month I must pass the day if I don't have the year ? When December is displayed as the current month in the Calendar, there are a few days shown from the January next year !
procedure TMainForm.CalendarGetMonthInfo(Sender: TObject; Month: Cardinal;
var MonthBoldInfo: Cardinal);
begin
end;
I made a new component where I intercepted the MCN_GETDAYSTATE message and I extracted the year too from the message info... It was there all the time, but Delphi decided that year is not useful.
TOnGetMonthInfoExEvent = procedure(Sender: TObject; Year, Month: Word;
var MonthBoldInfo: LongWord) of object;
TNewMonthCalendar = class(TMonthCalendar)
private
FOnGetMonthInfoEx: TOnGetMonthInfoExEvent;
procedure CNNotify(var Msg: TWMNotifyMC); message CN_NOTIFY;
published
property OnGetMonthInfoEx: TOnGetMonthInfoExEvent read FOnGetMonthInfoEx write FOnGetMonthInfoEx;
end;
procedure TNewMonthCalendar.CNNotify(var Msg: TWMNotifyMC);
var
I: Integer;
Month, Year: Word;
DS: PNMDayState;
CurState: PMonthDayState;
begin
if (Msg.NMHdr.code = MCN_GETDAYSTATE) and Assigned(FOnGetMonthInfoEx) then begin
DS:= Msg.NMDayState;
FillChar(DS.prgDayState^, DS.cDayState * SizeOf(TMonthDayState), 0);
CurState:= DS.prgDayState;
for I:= 0 to DS.cDayState - 1 do begin
Year:= DS.stStart.wYear;
Month:= DS.stStart.wMonth + I;
if Month > 12 then begin Inc(Year); Dec(Month, 12); end;
FOnGetMonthInfoEx(Self, Year, Month, CurState^);
Inc(CurState);
end;
end
else inherited;
end;
BONUS
And, as a bonus, you need this to update the changes you made to the bold info of the current month view... because it doesn't work with Invalidate.
procedure TNewMonthCalendar.RefreshDayState;
var N: Cardinal;
Range: array[0..1] of TSystemTime;
Year, Month: Word;
States: array of TMonthDayState;
I: Integer;
begin
if not Assigned(FOnGetMonthInfoEx) then Exit;
N:= SendMessage(Handle, MCM_GETMONTHRANGE, GMR_DAYSTATE, LPARAM(#Range));
Year:= Range[0].wYear;
Month:= Range[0].wMonth;
SetLength(States, N);
FillChar(States[0], N * SizeOf(TMonthDayState), 0);
for I:= 0 to N-1 do begin
FOnGetMonthInfoEx(Self, Year, Month, States[I]);
Inc(Month);
if Month > 12 then
begin Dec(Month, 12); Inc(Year); end;
end;
SendMessage(Handle, MCM_SETDAYSTATE, N, LPARAM(#States[0]));
end;

Subtract two TDATETIME variables in Delphi and return the result in minutes

I have two TDateTime variables, like this:
s := StrToDateTime('03/03/2017 10:10:12');
e := StrToDateTime('04/04/2017 10:10:12');
I need to find out the difference between them, in hh:mm:ss format.
The ...Between() functions are not helping me here.
Use the DateUtils.SecondsBetween function:
Uses
DateUtils,SysUtils;
function TimeDiffStr(const s1,s2: String): String;
var
t1,t2: TDateTime;
secs: Int64;
begin
t1 := StrToDateTime(s1);
t2 := StrToDateTime(s2);
secs := SecondsBetween(t1,t2);
Result := Format('%2.2d:%2.2d:%2.2d',[secs div SecsPerHour,(secs div SecsPerMin) mod SecPerMin,secs mod SecsPerMin]);
end;
begin
WriteLn(TimeDiffStr('03/03/2017 10:10:12','04/04/2017 10:10:12'));
ReadLn;
end.
From the number of seconds, calculate the hours,minutes and remaining seconds.
If you want the difference in minutes, use the DateUtils.MinutesBetween function:
function TimeDiffStr(const s1,s2: String): String;
var
t1,t2: TDateTime;
minutes: Int64;
begin
t1 := StrToDateTime(s1);
t2 := StrToDateTime(s2);
minutes := MinutesBetween(t1,t2);
Result := Format('%2.2d:%2.2d:%2.2d',[minutes div MinsPerHour,minutes mod MinsPerHour,0]);
end;
You can use TTimeSpan (from the System.TimeSpan unit).
program Project1;
{$APPTYPE CONSOLE}
uses
System.SysUtils, System.TimeSpan;
var
StartDate, EndDate: TDateTime;
TS: TTimeSpan;
Temp: string;
begin
StartDate := StrToDateTime('03/03/2017 10:10:12');
EndDate := StrToDateTime('04/04/2017 10:10:12');
TS := TTimeSpan.Subtract(EndDate, StartDate);
Temp := TS;
WriteLn(Temp); // Outputs 32.00:00:00
// The next line outputs the same as the one above
WriteLn(Format('%.2d:%.2d:%.2d:%.2d', [TS.Days, TS.Hours, TS.Minutes, TS.Seconds]));
WriteLn(TS.TotalMinutes); // Outputs 4.60800000000000E+0004
WriteLn(Trunc(TS.TotalMinutes)); // Outputs 46080
// This one will give the output you want (768:00:00)
WriteLn(Format('%.2d:%.2d:%.2d', [TS.Days * 24 + TS.Hours, TS.Minutes, TS.Seconds]));
ReadLn;
end.
First off, don't use hard-coded strings for date/time values. That is subject to localization issues, and it is just wasted overhead anyway. Use the SysUtils.EncodeDate() and SysUtils.EncodeTime() functions, or the DateUtils.EncodeDateTime() function.
Second, the ...Between() functions can indeed be usedneed, in particular SecondsBetween(). You can calculate the individual components from that return value.
Try something like this:
uses
..., SysUtils, DateUtils;
var
s, e: TDateTime;
diff: Int64;
days, hours, mins, secs: Integer;
s: string;
begin
s := EncodeDateTime(2017, 3, 3, 10, 10, 12, 0);
e := EncodeDateTime(2017, 4, 4, 10, 10, 12, 0);
diff := SecondsBetween(e, s);
days := diff div SecsPerDay;
diff := diff mod SecsPerDay;
hours := diff div SecsPerHour;
diff := diff mod SecsPerHour;
mins := diff div SecsPerMin;
diff := diff mod SecsPerMin;
secs := diff;
s := Format('%d:%d:%d:%d', [days, hours, mins, secs]);
end;

delphi get hour&minutes difference in specific format

Was playing with dateutils and did some experimenting.
procedure TForm1.Button1Click(Sender: TObject);
var
fromDate, toDate : TDateTime;
begin
fromDate := cxDateEdit1.Date ;
toDate := cxDateEdit2.Date ;
Label1.Caption := 'Hour difference '+IntToStr(HoursBetween(toDate, fromDate))+' hours';
Label2.Caption := 'Minute difference '+IntToStr(MinutesBetween(toDate, fromDate))+' minutes';
end;
How can I get a time difference result in a label caption like hh/mm (example 01:05) ???
A TDateTime is intended to be used with absolute dates and times. Instead you might consider TTimeSpan from the System.TimeSpan unit.
uses
System.TimeSpan;
....
var
d1, d2: TDateTime;
Span: TTimeSpan;
str: string;
....
d1 := ...;
d2 := ...;
Span := TTimeSpan.Subtract(d2, d1);
str := Format('%.2d:%.2d', [Span.Hours, Span.Minutes]));
This assumes that the span is less than a day. But then the format of your output seems to build in that very assumption.
Whether or not this is really any better than simply subtracting two date time values I am not so sure.
SysUtils.FormatDateTime has many useful TDateTime to string conversions:
Label3.Caption := 'Time difference [hh:mm] '+FormatDateTime('hh:nn',toDate-fromDate);
As an alternative, use the result from MinutesBetween:
var
minutes: Integer;
...
minutes := MinutesBetween(toDate,FromDate);
Label3.Caption :=
'Time difference [hh:mm] '+Format('%.2d:%.2d',[minutes div 60,minutes mod 60]);

Date/Time manipulation - friendly countdown string

I'm building something which has a countdown to a certain date/time. I have it working - at least the Hours, Minutes, and Seconds work fine. My problem is when I try to implement Days, it does not give the correct result. I know about the DateUtils unit, but there's so much stuff there and I don't know how to do this, especially since I'm horrible at math.
I have a timer with interval at 100. Then I have a global fDestDT for the destination date/time to base the countdown off of. In the timer, I have a local TDateTime called DT. I then break it into multiple strings and put them back together into 1 'friendly' string...
procedure TForm1.TmrTimer(Sender: TObject);
var
DT: TDateTime;
D, H, N, S: String;
Str: String;
begin
DT:= fDestDT - Now; //fDest = destination date/time of countdown
//Need to format only plural numbers with 's'
D:= FormatDateTime('d', DT)+' Days'; //Get number of days
H:= FormatDateTime('h', DT)+' Hours'; //Get number of hours
N:= FormatDateTime('n', DT)+' Minutes'; //Get number of minutes
S:= FormatDateTime('s', DT)+' Seconds'; //Get number of seconds
Str:= D+', '+H+', '+N+', '+S; //Build friendly string
if lblTitle.Caption <> Str then
lblTitle.Caption:= Str; //Update caption only if it's changed
end;
It should come out something like...
0 Days, 3 Hours, 1 Minute, 12 Seconds
But instead the days are showing wrong, when the Date/Time of the countdown is on today's date, it is showing 30 Days...
30 Days, 3 Hours, 1 Minute, 12 Seconds
I presume that if I were to put it more than 1 month in advance, it would also not show correctly either. How do I get the number of days properly? And is there anything in the DateUtils unit that can automate most of this work better than I already am?
EDIT:
FIXED! The problem was I was stupidly subtracting with DT:= fDestDT - Now; which was correct in my first code snippet, but after converting to use DateUtils.DaysBetween instead, I needed to remove that subtraction, and just set DT:= Now;.
Working code:
procedure TForm1.TmrTimer(Sender: TObject);
var
DT: TDateTime;
Days, Hours, Mins, Secs: Word;
SDays, SHours, SMins, SSecs: String;
Str: String;
begin
DT:= Now;
Days:= DaysBetween(DT, fDestDT);
Hours:= HoursBetween(fDestDT, DT) mod 24; // Remove total days
Mins:= MinutesBetween(DT, fDestDT) mod 60;
Secs := SecondsBetween(DT, fDestDT) mod 60;
if Days = 1 then SDays:= 'Day' else SDays:= 'Days';
if Hours = 1 then SHours:= 'Hour' else SHours:= 'Hours';
if Mins = 1 then SMins:= 'Minute' else SMins:= 'Minutes';
if Secs = 1 then SSecs:= 'Second' else SSecs:= 'Seconds';
Str:= Format('%d '+SDays+' %d '+SHours+' %d '+SMins+' %d '+SSecs,
[Days, Hours, Mins, Secs]);
if lblTime.Caption <> Str then
lblTime.Caption:= Str;
end;
See DaysBetween, HoursBetween, MinutesBetween, and SecondsBetween in DateUtils. You have to do some minor math. :)
Here's a sample console app to demonstrate:
program Project2;
{$APPTYPE CONSOLE}
uses
SysUtils, DateUtils;
procedure ShowTimeDiff(const StartDate, OldDate: TDateTime);
var
Days, Hours, Mins, Secs: Word;
OutputText: string;
begin
Writeln(Format('Start: %s, Old: %s',
[FormatDateTime('mm/dd/yyyy hh:nn:ss', StartDate),
FormatDateTime('mm/dd/yyyy hh:nn:ss', OldDate)]));
Days := DaysBetween(StartDate, OldDate);
Hours := HoursBetween(OldDate, StartDate) mod 24; // Remove total days
Mins := MinutesBetween(StartDate, OldDate) mod 60;
Secs := SecondsBetween(StartDate, OldDate) mod 60;
OutputText := Format(' %d days, %d hours, %d min, %d secs',
[Days, Hours, Mins, Secs]);
WriteLn(OutputText);
end;
var
BeginDate, EndDate: TDateTime;
begin
BeginDate := Now;
EndDate := BeginDate - 0.5; // about 12 hours earlier
ShowTimeDiff(BeginDate, EndDate);
EndDate := BeginDate - 2.53724; // Create date about 2 1/2 days earlier
ShowTimeDiff(EndDate, BeginDate);
EndDate := BeginDate - 5.75724; // Create date about 5 3/4 days earlier
ShowTimeDiff(BeginDate, EndDate);
ReadLn;
end.
Produces the following output:
Note that the reversal of parameter order between DaysBetween and HoursBetween is intentional to demonstrate that the functions always return positive values, so the order of the parameters isn't important. This is mentioned in the documentation.
The problem is that when you subtract Now from fDestDT you expect to get difference between two dates, but you actually get another datetime value. As the values youre using are nearly the same, you get the "zero date" of the Delphi's datetime system, the 30. dets 1899. Thats why you get "30 Days" for FormatDateTime('d', DT)+' Days'.
Since the smallest amount youre intrested in is second I suggest you use SecondsBetween to get the difference between two timestamps and then divide it into parts like
diff := SecondsBetween(Now, fDestDT);
S:= IntToStr(diff mod 60)+' Seconds';
diff := diff div 60;
N:= IntToStr(diff mod 60)+' Minutes';
diff := diff div 60;
H:= IntToStr(diff mod 24)+' Hours';
diff := diff div 24;
D:= IntToStr(diff)+' Days';
If you are using Delphi 2010 (I believe) or above, you can likely simplify your code and make it more clear by using the TimeSpan.pas unit, which contains a record that you can use to break out the amount of time in a given span of time.
I needed something more flexible that covers different formats, so I implemented TTimeDiff as:
uses
SysUtils,
DateUtils,
StrUtils,
Math;
type
TTimeDiff = record
type TTimeDiffFormat = (tdfFull, tdfSignificant, tdfAllNonZeros, tdfXNonZeros);
procedure Init(const ANow, AThen: TDateTime);
class function TimeDiff(const ANow, AThen: TDateTime): TTimeDiff; static;
function ToString(const TimeDiffFormat: TTimeDiffFormat; const Delimiter: string = ', ';
const NonZerosCount: Byte = 1): string;
case Integer of
0: (Years, Months, Days, Houres, Minutes, Seconds: Word);
1: (Values: array[0..5] of Word);
end;
{ TTimeDiff }
class function TTimeDiff.TimeDiff(const ANow, AThen: TDateTime): TTimeDiff;
begin
Result.Init(ANow, AThen);
end;
procedure TTimeDiff.Init(const ANow, AThen: TDateTime);
begin
Years := YearsBetween(ANow, AThen);
Months := MonthsBetween(ANow, AThen) mod 12;
Days := DaysBetween(IncMonth(Min(ANow, AThen), Years * 12 + Months), Max(ANow, AThen));
Houres := HoursBetween(ANow, AThen) mod 24;
Minutes := MinutesBetween(ANow, AThen) mod 60;
Seconds := SecondsBetween(ANow, AThen) mod 60;
end;
function TTimeDiff.ToString(const TimeDiffFormat: TTimeDiffFormat; const Delimiter: string = ', ';
const NonZerosCount: Byte = 1): string;
const
Captions: array [0..5] of string = ('year', 'month', 'day', 'hour', 'minute', 'second');
var
I: Integer;
VisitedNonZeros: Byte;
begin
Result := '';
VisitedNonZeros := 0;
for I := 0 to 5 do
begin
if Values[I] > 0 then
Inc(VisitedNonZeros);
if
(TimeDiffFormat = tdfFull) or
((TimeDiffFormat = tdfSignificant) and (VisitedNonZeros > 0)) or
((TimeDiffFormat in [tdfAllNonZeros, tdfXNonZeros]) and (Values[I] > 0))
then
begin
Result := Result + Format('%d %s%s%s', [Values[I], Captions[I], IfThen(Values[I] = 1, '', 's'), Delimiter]);
if (TimeDiffFormat = tdfXNonZeros) and (VisitedNonZeros = NonZerosCount) then
Break;
end;
end;
Result := Copy(Result, 1, Length(Result) - Length(Delimiter));
end;
TTimeDiffFormat explanation:
tdfFull: includes all parts regardless of their values (years, months, days, hours, minutes and seconds respectively).
tdfSignificant: excludes LEADING zero-valued parts
tdfAllNonZeros: excludes ALL zero-valued parts
tdfXNonZeros: includes only first X non-zero valued parts, where X is set to 1 by default
How to use:
var
ANow, AThen: TDateTime;
Diff: TTimeDiff;
begin
try
ANow := DateUtils.EncodeDateTime(1993, 11, 3, 21, 22, 18, 0);
AThen := DateUtils.EncodeDateTime(1993, 9, 21, 6, 21, 34, 0);
Writeln('Difference between ');
Writeln(FormatDateTime('YYYY/MM/DD HH:NN:SS', ANow), ' and');
Writeln(FormatDateTime('YYYY/MM/DD HH:NN:SS', AThen), ' is:');
Writeln('');
Diff.Init(ANow, AThen);
with Diff do
begin
Writeln(ToString(tdfFull));
Writeln(ToString(tdfSignificant, ' and '));
Writeln(TTimeDiff.TimeDiff(Athen, ANow).ToString(tdfSignificant), ' (inverted)');
Writeln(ToString(tdfAllNonZeros));
Writeln(ToString(tdfXNonZeros, ', ', 2));
Writeln(ToString(tdfXNonZeros));
readln;
end;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
Results:
Difference between
1993/11/03 21:22:18 and
1993/09/21 06:21:34 is:
0 years, 1 month, 13 days, 15 hours, 0 minutes, 43 seconds
1 month and 13 days and 15 hours and 0 minutes and 43 seconds
1 month, 13 days, 15 hours, 0 minutes, 43 seconds (inverted)
1 month, 13 days, 15 hours, 43 seconds
1 month, 13 days
1 month

Resources