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
WorkingHoursconfigured, soFWorkingHours.FDaySpanstays 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
- New project, working days Mon–Fri, no
WorkingHoursconfigured. - Add a task starting Friday 14.08.2026, duration 1 working day.
- Drag the right edge to Tuesday 18.08.2026. Duration reads
3d— but see the measurement below, the stored type is already wrong. - Drag the right edge back to Friday 14.08.2026. The bar returns to Friday correctly, the duration column now shows
23h 59m 59sinstead of1 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
DurationTypeis alreadygdtWorkDays; 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 theelsebranch (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.