TTMSFNCGanttChart 1.3.3.6: resizing a task corrupts PlannedDuration (whWorkDaysOnly)

TTMSFNCGanttChart — duration corrupted on resize (workDaysOnly), plus two smaller items

Component: tms.fnc.ganttchart 1.3.3.6 IDE: Delphi 13.1 Florence, VCL, Win64 Line numbers refer to the sources shipped with 1.3.3.6.

Configuration (relevant for all three items):

  • Task.WorkTimePolicy = whWorkDaysOnly
  • Working days Mon–Fri, no holidays in the test range
  • No WorkingHours configured, so FWorkingHours.FDaySpan stays at its default of 86400. Durations are whole working days (gdtWorkDays).

1. GetDurationBetweenDateTimes does not compensate the 23:59:59 boundary

What happens

GetNextEnd deliberately ends a working stretch one second before midnight (VCL.TMSFNCGanttClasses.pas, line 7149):

wc := Int(IncDay(wc, -1)) + EncodeTime(23,59,59,0);

GetActualEndDateTime knows about this and compensates explicitly (line 6875):

if (HourOf(ed) = 23) and (MinuteOf(ed) = 59) and (SecondOf(ed) = 59)
   and (AWorkTimePolicy = whWorkDaysOnly) then
  tempTl := IncSecond(tempTl);

GetDurationBetweenDateTimes (line 7080) performs the inverse conversion but has no such compensation. One second is lost per non-working boundary crossed, and the result no longer hits any of the mod branches cleanly.

The forward direction (start + duration → end) is therefore correct, the backward direction (start + end → duration) is not.

How it reaches user data

TTMSFNCGanttTask.SetPlannedEnd (line 3353) recomputes the duration from the two dates:

FOwnerProject.FWorkTime.GetDurationBetweenDateTimes(
  FPlannedStart, AValue, FPlannedDuration, WorkTimePolicy, False)

and PlannedEnd is assigned by every resize path in VCL.TMSFNCGanttChart.pas:

Path Line Assigns
DoOnAfterSizeItem 4106 PlannedStart and PlannedEnd
TimeLineSizeEndTimeEarlier 7611 PlannedStart and PlannedEnd
TimeLineSizeEndTimeLater 7661 PlannedStart and PlannedEnd
DoOnAfterMoveItem 4080 only PlannedStart — not affected

Moving a bar is fine. Resizing corrupts the duration.

Reproduction

  1. New project, working days Mon–Fri, no WorkingHours configured.
  2. Add a task starting Friday 14.08.2026, duration 1 working day.
  3. Drag the right edge to Tuesday 18.08.2026. Duration reads 3d — but see the measurement below, the stored type is already wrong.
  4. Drag the right edge back to Friday 14.08.2026. The bar returns to Friday correctly, the duration column now shows 23h 59m 59s instead of 1 AT.

Measured values

Logged in OnAfterSizeTimeLineTask, immediately on entry, before any code of ours ran:

Action PlannedDuration.DurationType .Value .ToString ScheduledEnd
enlarge Fri → Tue gdtSeconds 259198 3d 18.08.2026 23:59:58
shrink Tue → Fri gdtWorkDays 0.999988 23h 59m 59s 14.08.2026 23:59:59

Two points worth noting:

  • 259198 = 259200 − 2, one second per weekend boundary crossed.
  • The corruption appears in two different shapes. In the shrink case the DurationType is already gdtWorkDays; only the value is fractional. Any sanitising code that checks the type alone will miss it — ours did, which is how we found the second shape.

Suggested fix

Apply the same compensation in GetDurationBetweenDateTimes that GetActualEndDateTime already contains, i.e. add the lost second back when the second date sits on a 23:59:59 working-day boundary and AWorkTimePolicy = whWorkDaysOnly.

Unrelated but noticed while reading: GetDurationBetweenDateTimes calls

dt := GetWorkTimeDifference(AFirstDateTime, ASecondDateTime);

without passing AWorkTimePolicy, so the default whWorkTimeAndDays applies even when the caller asked for whWorkDaysOnly. Possibly intentional, but it looks inconsistent with the rest of the method.


2. Mouse cursor is not kept in sync when dragging a task across non-working days

Drag a task to the right across one or more weekends. The bar correctly skips the non-working days, but the mouse cursor is not moved along with it. The gap between cursor and bar grows with every weekend skipped and becomes large enough over a few weeks that the drag is hard to control.

Purely visual, but noticeable in daily use. Same configuration as above.


3. Question rather than a bug: AddDependency does not trigger CalculateTaskTimes

We add dependencies programmatically and then read ScheduledStart. That returned stale values, and the reason surprised us.

TTMSFNCGanttTaskDependencies.AddDependency (line 3862) assigns the fields directly, bypassing the property setters:

Result.FDependencyType := ADependencyType;
Result.FDelay.FDurationValue := ADelayValue;
Result.FDelay.FDurationType := ADelayType;
Result.DependsOnTask := ATask;

ChangeDependsOnTask (line 3932) then raises DoDependencyAdded, and the receiving task (line 2892) only does:

ATaskDependency.DependsOnTask.AddToTaskUpdateList(Self);

No rescheduling. Whereas changing an existing dependency (line 2898) does:

procedure TTMSFNCGanttTask.DoOnInternalTaskDependencyChanged(...);
begin
  CalculateTaskTimes;
end;

TaskDependencies.BeginUpdate / EndUpdate does not help either, since TTMSFNCGanttTaskDependencies does not override Update.

Is calling CalculateTaskTimes (or Project.CalculateTasks) explicitly after AddDependency the intended usage? If so, a note in the documentation would help — the asymmetry between adding and changing a dependency is easy to trip over. If not, raising DoDependencyChanged from AddDependency would make the two paths consistent.


4. Status of the two previously confirmed fixes

You confirmed two issues earlier this year:

  • GetActualStartDateTimeByEnd, whAllTime: sign error in the else branch (tempD := ADateTime + ADuration.AsTime).
  • GetActualStartDateTimeByEnd, whWorkDaysOnly: wrong result when the given end date directly follows a non-working stretch.

Is there a build available that contains them? We are running local workarounds for both and would like to remove them once the fixes ship.

Thanks in advance.

Thank you for your analysis.

Your first point was indeed an issue. The suggested fix was a good approach to solving it, but it didn't take the full problem into account.

This has now been resolved and will be fixed in the next release of TMS FNC Gantt Chart.

Regarding point 2, this is a small hiccup in the Timeline that could have a significant impact if changed. Therefore, we need some time to investigate whether it is feasible to make this change without breaking existing applications that are currently in production.

With regard to your question, both calls eventually go back to AddToTaskUpdateList. However, it would sometimes skip the calculation if more than one task was dependent. CalculateTaskTimes should have been outside the if statement to provide more consistent and optimized behavior.

We aim to have a release containing these fixes to be available by the end of this month.

Thank you. Regarding point 2, one addition that may help you weigh it: in a construction schedule spanning several months, dragging a task by several weeks is routine. The offset grows with every skipped weekend, so after about eight weeks the cursor is more than two weeks away from the bar and the drag becomes impractical to control. It is not a cosmetic detail for this use case.

If the regression risk is the concern, would an opt-in property be an option — existing behaviour as the default, the synchronised cursor switched on explicitly? That would keep production applications untouched.