Hey,
I know that negative slack is a result of scheduling error, but its calculation is present in MS Project. Would be great if TotalSlack would show negative values instead of 0 if the slack is negative for a task. As of now I’m calculating this myself beased on the min value of late - early finish and late - early start dates, this works for 80% of the cases, but sometimes MS Project does backwards passes and has a constraint on project end, which is really hard to calculate myself. Is it possible to add negative slack calculations in Aspose.Tasks?
File to use for a reference:
Project1.7z (13.8 KB)
The code I use to create this:
using Aspose.Tasks;
using Aspose.Tasks.Util;
using Task = Aspose.Tasks.Task;
var project = new Project();
project.Set(Prj.StartDate, new DateTime(2024, 10, 7, 8, 0, 0));
project.Set(Prj.MinutesPerDay, 480);
project.Set(Prj.MinutesPerWeek, 2400);
project.Set(Prj.DaysPerMonth, 20);
project.CustomProps.Add("SlackCoefficient", 24 / (480 / 60));
AddDefaultCalendar(project);
project.CalculationMode = CalculationMode.Automatic;
var task1 = project.RootTask.Children.Add("Task 1");
task1.Set(Tsk.Start, new DateTime(2024, 10, 9, 8, 0, 0));
task1.Set(Tsk.Duration, project.GetDuration(8, TimeUnitType.Day));
var task2 = project.RootTask.Children.Add("Task 2");
task2.Set(Tsk.Start, new DateTime(2024, 10, 21, 8, 0, 0));
task2.Set(Tsk.Duration, project.GetDuration(1, TimeUnitType.Day));
var task3 = project.RootTask.Children.Add("Task 3");
task3.Set(Tsk.Start, new DateTime(2024, 10, 22, 8, 0, 0));
task3.Set(Tsk.Duration, project.GetDuration(1, TimeUnitType.Day));
var task4 = project.RootTask.Children.Add("Task 4");
task4.Set(Tsk.Start, new DateTime(2024, 10, 23, 8, 0, 0));
task4.Set(Tsk.Duration, project.GetDuration(1, TimeUnitType.Day));
var task5 = project.RootTask.Children.Add("Task 5");
task5.Set(Tsk.Start, new DateTime(2024, 10, 24, 8, 0, 0));
task5.Set(Tsk.Duration, project.GetDuration(1, TimeUnitType.Day));
var projectEndMilestone = project.RootTask.Children.Add("Project End");
projectEndMilestone.Set(Tsk.Start, new DateTime(2024, 10, 21, 8, 0, 0));
projectEndMilestone.Set(Tsk.Duration, project.GetDuration(0, TimeUnitType.Day));
projectEndMilestone.Set(Tsk.ConstraintDate, new DateTime(2024, 10, 21, 8, 0, 0));
projectEndMilestone.Set(Tsk.ConstraintType, ConstraintType.MustStartOn);
var task6 = project.RootTask.Children.Add("Task 6");
task6.Set(Tsk.Start, new DateTime(2024, 10, 8, 8, 0, 0));
task6.Set(Tsk.Duration, project.GetDuration(1, TimeUnitType.Day));
task6.Set(Tsk.ConstraintDate, new DateTime(2024, 10, 8, 8, 0, 0));
task6.Set(Tsk.ConstraintType, ConstraintType.MustStartOn);
var projectStartMilestone = project.RootTask.Children.Add("Project Start");
projectStartMilestone.Set(Tsk.Start, new DateTime(2024, 10, 10, 8, 0, 0));
projectStartMilestone.Set(Tsk.Duration, project.GetDuration(0, TimeUnitType.Day));
task6.Set(Tsk.ConstraintDate, new DateTime(2024, 10, 10, 8, 0, 0));
task6.Set(Tsk.ConstraintType, ConstraintType.MustStartOn);
var link1 = project.TaskLinks.Add(projectStartMilestone, task6, TaskLinkType.FinishToStart);
var link2 = project.TaskLinks.Add(task6, task1, TaskLinkType.FinishToStart);
var link3 = project.TaskLinks.Add(task1, task2, TaskLinkType.FinishToStart);
var link4 = project.TaskLinks.Add(task2, task3, TaskLinkType.FinishToStart);
var link5 = project.TaskLinks.Add(task3, task4, TaskLinkType.FinishToStart);
var link6 = project.TaskLinks.Add(task4, task5, TaskLinkType.FinishToStart);
var link7 = project.TaskLinks.Add(task5, projectEndMilestone, TaskLinkType.FinishToStart);
var collector = new ChildTasksCollector();
TaskUtils.Apply(project.RootTask, collector, 0);
foreach (var task in collector.Tasks)
{
var finishSlack = task.Get(Tsk.LateFinish) - task.Get(Tsk.EarlyFinish);
var startSlack = task.Get(Tsk.LateStart) - task.Get(Tsk.EarlyStart);
Console.WriteLine($"Task: {task.Get(Tsk.Name)}");
Console.WriteLine($"Calculated Slack: {CalculateSlack(finishSlack, startSlack, project, task)}");
}
double CalculateSlack(TimeSpan finishSlack, TimeSpan startSlack, Project project, Task task)
{
var slack = finishSlack.CompareTo(startSlack);
var minSlack = slack >= 0 ? (startSlack, true) : (finishSlack, false);
WorkUnit duration;
if (minSlack.Item2)
{
var cal = GetCalendar(project, task);
if (minSlack.Item1.TotalSeconds < 0)
{
duration = cal.GetWorkingHours(task.Start + minSlack.Item1, task.Start);
return GetSlackByDurationUnits(project, task, duration.WorkingHours) * -1;
}
else
{
duration = cal.GetWorkingHours(task.Start, task.Start + minSlack.Item1);
return GetSlackByDurationUnits(project, task, duration.WorkingHours);
}
}
else
{
var cal = GetCalendar(project, task);
if (minSlack.Item1.TotalSeconds < 0)
{
duration = cal.GetWorkingHours(task.Finish + minSlack.Item1, task.Finish);
return GetSlackByDurationUnits(project, task, duration.WorkingHours) * -1;
}
else
{
duration = cal.GetWorkingHours(task.Finish, task.Finish + minSlack.Item1);
return GetSlackByDurationUnits(project, task, duration.WorkingHours);
}
}
}
Calendar GetCalendar(Project project, Task task)
{
return task.Get(Tsk.Calendar) ?? project.Get(Prj.Calendar);
}
double GetSlackByDurationUnits(Project project, Task task, TimeSpan duration)
{
var slackCoefficient = (double)project.CustomProps.First(x => x.Name == "SlackCoefficient").Value;
var minutesPerDay = project.MinutesPerDay;
switch (task.Get(Tsk.Duration).TimeUnit)
{
case TimeUnitType.Minute:
return duration.TotalMinutes * slackCoefficient;
case TimeUnitType.Hour:
return duration.TotalHours * slackCoefficient;
case TimeUnitType.Day:
return duration.TotalDays * slackCoefficient;
case TimeUnitType.Week:
var daysPerWeek = project.MinutesPerWeek / minutesPerDay;
return duration.TotalDays / daysPerWeek * slackCoefficient;
case TimeUnitType.Month:
return duration.TotalDays / project.DaysPerMonth * slackCoefficient;
default:
return duration.TotalDays * slackCoefficient;
}
}
void AddDefaultCalendar(Project project)
{
var cal = project.Calendars.Add("Default");
cal.WeekDays.Clear();
CreateDefaultCalendar(cal);
cal.Uid = 0;
project.Set(Prj.Calendar, cal);
}
void CreateDefaultCalendar(Calendar cal)
{
var wt1 = new WorkingTime(new DateTime(1, 1, 1, 8, 0, 0), new DateTime(1, 1, 1, 16, 0, 0));
var monday = new WeekDay(DayType.Monday);
var tuesday = new WeekDay(DayType.Tuesday);
var wednesday = new WeekDay(DayType.Wednesday);
var thursday = new WeekDay(DayType.Thursday);
var friday = new WeekDay(DayType.Friday);
monday.WorkingTimes.Add(wt1);
tuesday.WorkingTimes.Add(wt1);
wednesday.WorkingTimes.Add(wt1);
thursday.WorkingTimes.Add(wt1);
friday.WorkingTimes.Add(wt1);
monday.DayWorking = true;
tuesday.DayWorking = true;
wednesday.DayWorking = true;
thursday.DayWorking = true;
friday.DayWorking = true;
cal.WeekDays.Add(monday);
cal.WeekDays.Add(tuesday);
cal.WeekDays.Add(wednesday);
cal.WeekDays.Add(thursday);
cal.WeekDays.Add(friday);
}