GetActualStartDateTimeByEnd seems to add instead of subtract when WorkTimePolicy = whAllTime

Hi,

I think I may have found a small issue in TTMSFNCGanttWorkTime, but I'm not entirely certain I'm reading it correctly — so please take this as a question rather than a bug report.

Environment: Delphi 13.1, VCL, TMS FNC Gantt Chart 1.3.3.6 (tms.fnc.ganttchart), unit VCL.TMSFNCGanttClasses.pas

GetActualStartDateTimeByEnd ends like this:

    tempD := tempD - tl;
  end
  else
    tempD := ADateTime + ADuration.AsTime;   // taken when AWorkTimePolicy = whAllTime

  Result := tempD;
end;

The else branch is the one used for whAllTime, and it adds the duration. Since this function is the backward counterpart of GetActualEndDateTime, I would have expected a subtraction here. The line is identical to the corresponding one in GetActualEndDateTime, where adding is of course correct.

A small repro:

var
Dur: TTMSFNCGanttDuration;
Res: TDateTime;
begin
Dur := TTMSFNCGanttDuration.Create(nil);
try
Dur.SetDuration(gdtWorkDays, 3);
Res := GanttChart1.Project.WorkTime.GetActualStartDateTimeByEnd(
EncodeDate(2026, 7, 29), Dur, whAllTime);
// expected: 2026-07-26
// returned: 2026-08-01
finally
Dur.Free;
end;
end;

With whWorkDaysOnly and whWorkTimeAndDays the function behaves exactly as documented — only the whAllTime path is affected, which may be why it hasn't come up before.

One thing worth adding for anyone else running into this: the component's own scheduling does not appear to be affected. TTMSFNCGanttTask.GetPlannedStartForDependency handles whAllTime separately before it ever calls into WorkTime, so the behaviour only shows up when calling the method directly. I've taken the same approach as a workaround in my own code, so there's no urgency on my side.

Could you confirm whether this is indeed unintended, or whether the whAllTime case is meant to behave differently here?

Thanks!

Thank you for letting us know.
This was indeed unintended and should be subtracted.
We fixed it and it will be solved in the next release of TMS FNC Gantt Chart.

Hi,

thanks again for the quick fix on the whAllTime sign issue in GetActualStartDateTimeByEnd - much appreciated.

While testing around that one, I ran into a second thing in the same function that I can't quite explain, and I'd like to ask whether I'm using it correctly before calling it a bug.

Setup: TTMSFNCGanttWorkTime with working days Mon-Fri, no holidays anywhere in the tested range, duration type gdtWorkDays, policy whWorkDaysOnly.

The test is a round trip - subtract a duration, then add the same duration back, on the same calendar:

Dur.SetDuration(gdtWorkDays, n);
Back  := WT.GetActualStartDateTimeByEnd(Ref, Dur, whWorkDaysOnly);
Again := WT.GetActualEndDateTime(Back, Dur, whWorkDaysOnly);
// I would expect Again to be Ref again, for every n

I ran n = 1..15 against two reference points.

Ref = Wednesday 2026-08-05 00:00 - all 15 are fine. "Back" matches what I count by hand day by day, and "Again" comes back to exactly the reference point.

Ref = Monday 2026-08-03 00:00 - all 15 are off:

 n   expected (counted by hand)   Back (returned)             Again (round trip)
 1   Fri 31.07.2026 00:00         Sun 02.08.2026 00:00        Mon 03.08.2026 23:59:59
 2   Thu 30.07.2026 00:00         Sat 01.08.2026 00:00        Tue 04.08.2026 23:59:59
 3   Wed 29.07.2026 00:00         Fri 31.07.2026 00:00        Wed 05.08.2026 00:00
 4   Tue 28.07.2026 00:00         Thu 30.07.2026 00:00        Wed 05.08.2026 00:00
 5   Mon 27.07.2026 00:00         Wed 29.07.2026 00:00        Wed 05.08.2026 00:00
 6   Fri 24.07.2026 00:00         Tue 28.07.2026 00:00        Wed 05.08.2026 00:00
 8   Wed 22.07.2026 00:00         Fri 24.07.2026 00:00        Wed 05.08.2026 00:00
15   Mon 13.07.2026 00:00         Wed 15.07.2026 00:00        Wed 05.08.2026 00:00

Two things stand out to me:

  • For n = 1 and n = 2 the returned start is a Sunday and a Saturday — non-working days, although the policy is whWorkDaysOnly.
  • From n = 3 onwards the result is consistently the correct answer for n − 2, i.e. two working days short, and the round trip is stuck at Wednesday 00:00, which is exactly the reference point plus two working days. It does not grow with n.

So the deviation doesn't seem to depend on the duration at all, but on where the reference point sits: it shows up when the end date directly follows non-working time (Monday 00:00 being, in the exclusive notation, the end of Sunday). My guess from the outside is that the initial normalisation of such an end date moves forward into the next working days instead of back to the end of the previous one - but that's speculation, I can't see the code.

As a side note, for n = 1 and 2 the forward call returns 23:59:59 timestamps, while ends normally come back as the following midnight. That may well be part of the same thing.

So my question: is a Monday 00:00 a legitimate argument here, or should end dates be passed in a different form (for example as the previous working day's end)? If it is supposed to work the way I assumed, it might be worth a look while the other fix is still fresh.

Thanks a lot!

Hi,

Thanks for the detailed write-up — the day-by-day table made it easy to pinpoint the issue.

I found this inconsistency while investigating the AllTime work-time policy, and it has been addressed by the fix as well.

With the fix in place, GetActualStartDateTimeByEnd counts working days only. Using Monday 2026-08-03 00:00 as the reference end date gives:

  • n=1 → Fri 31.07
  • n=2 → Thu 30.07
  • n=3 → Wed 29.07
  • n=5 → Mon 27.07
  • n=8 → Wed 22.07
  • n=15 → Mon 13.07

So passing Monday 00:00 as the end date is perfectly valid; there is no need to convert it to the previous working day's end first.

Regarding the round trip not returning the exact same value:

With the fix, this should now behave correctly. However, there is still one subtle difference. At a weekend boundary, the end of Friday has two equally valid representations: Saturday 00:00 and Monday 00:00. These represent the same point in time because no work occurs during the weekend. GetActualEndDateTime intentionally returns the actual end of the work (the earlier boundary) instead of moving it forward across the non-working period.

This behavior is intentional. The end date of a task is used when scheduling dependent tasks, and each dependency is evaluated according to its own work-time policy. If we were to normalize the end date forward over the weekend, that shifted value would propagate to successor tasks that may use a different policy, potentially altering their schedules. By keeping the end date at the actual end-of-work moment, we avoid introducing that drift into the dependency chain.

So the round trip returns the correct point in time, although it may not always return the exact same calendar representation that was originally provided.

I hope this clarifies the behavior.

Thank you, that clears it up completely.

The six values you listed are exactly what my independent hand count produces for the same reference point, so the fix looks right from here - I'll confirm it against the full test once the release is out.

The explanation for the round trip makes sense to me, and it's a good argument. I hadn't considered that a forward-normalised end date would propagate into successors with a different work-time policy - that's precisely the kind of drift I've been guarding against elsewhere in my own code, so I'm glad it's handled at your end too. I've adjusted my test accordingly: it now compares the two points after pulling both to the next actual start of work, and treats a differing calendar representation as expected rather than as a failure.

Thanks again for looking into both of these so quickly.