Need the ability to be able to get negative slack

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);
}

@KornelijusS,
thank you for the example.
We will fix calculation of slack properties to allow negative values.
The following new ticket is opened in our internal issue tracking system and we will deliver the fix according to the terms mentioned in Free Support Policies.

Issue ID(s): TASKSNET-11306

You can obtain Paid Support Services if you need support on a priority basis, along with the direct access to our Paid Support management team.