Skip to content

Commit

Permalink
feat: Redid calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
Jonathanio123 committed Dec 13, 2024
1 parent 4ebbe0c commit 0db7ba0
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,101 +49,119 @@ private static bool IsSupportPosition(ApiPositionV2 position)

public static List<ExpiringPosition> GetPositionAllocationsEndingNextThreeMonths(IEnumerable<ApiPositionV2> allProjectPositions)
{
/*
* Remember that it's the position*Allocation* that is expiring, not the position itself.
* So a position allocation can be considered expiring if:
* 1. The position is active with an assigned person, and the next split within the next 3 months does not have a person assigned
* - We want to notify task owners that an upcoming split is missing an allocation and that they should assign someone
*
* 2. The last split is expiring within the next 3 months.
* There can be more splits after this but if they're not starting (appliesFrom) within the next 3 months (from NowDate), we consider the position expiring.
* Once the later split comes within the 3-month window, the position will fall under TBNPositionsStartingWithinThreeMonths
* - We want to notify task owners that the position is expiring
*
* Note: If there is a gap between two splits and the gap/time-period is fully within the 3-month window,
* then we DO NOT consider the position expiring. This is also an unusual case.
*/

var nowDate = NowDate;
var expiringDate = nowDate.AddMonths(3);

var expiringPositions = new List<ExpiringPosition>();

foreach (var position in allProjectPositions)
{
position.Instances = position.Instances.OrderBy(i => i.AppliesFrom).ToList();
if (position.Instances.Count == 0) // No instances, skip
continue;

var activeInstance = position.Instances.FirstOrDefault(i => i.AppliesFrom <= nowDate && i.AppliesTo >= nowDate && i.AssignedPerson is not null);
var instancesWithinPeriod = FindInstancesWithinPeriod(position, new DateTimeRange() { Start = nowDate, Stop = expiringDate });


// Find future instances with a start date within the 3-month window that may or may not end within the 3-month window
var futureInstances = position.Instances
.Where(i => i.AppliesFrom >= nowDate && i.AppliesFrom < expiringDate)
.OrderBy(i => i.AppliesFrom)
.ToList();
if (instancesWithinPeriod.EndingWithinPeriod.Count == 0
&& instancesWithinPeriod.ContainedWithinPeriod.Count == 0
&& instancesWithinPeriod.StartingWithinPeriod.Any())
{
continue;
}

// Handle case where the position is not currently active
if (activeInstance is null)
foreach (var instance in instancesWithinPeriod.Instances)
{
if (futureInstances.Count == 0)
continue; // This is a past position
if ((instancesWithinPeriod.ContainedWithinPeriod.Contains(instance) || instancesWithinPeriod.StartingWithinPeriod.Contains(instance))
&& instance.AssignedPerson is null)
{
expiringPositions.Add(new ExpiringPosition(position, instance.AppliesTo));
break;
}

if (instancesWithinPeriod.AnyInstancesAfter(instance) == false)
{
expiringPositions.Add(new ExpiringPosition(position, instance.AppliesTo));
break;
}
}
}

var endingPositionAllocation = FindFirstTBNOrLastExpiringInstance(futureInstances);

if (endingPositionAllocation is null)
// No TBN/expiring instances found within the 3-month window, could be an instance that starts within the 3-month window
// But ends after the 3-month window
continue;
return expiringPositions;
}

// If the first TBN/expiring instance found is not the last instance, then there are more instances after it
var isEndingInstanceLast = futureInstances.Last() == endingPositionAllocation;
private static InstancesWithinPeriod FindInstancesWithinPeriod(ApiPositionV2 position, DateTimeRange period)
{
var instancesEndingWithinPeriod = position.Instances
.Where(i => period.Contains(i.AppliesTo) && !period.Contains(i.AppliesFrom))
.ToList();

if ((isEndingInstanceLast || endingPositionAllocation.AssignedPerson is null) && endingPositionAllocation.AppliesTo < expiringDate)
expiringPositions.Add(new ExpiringPosition(position, endingPositionAllocation.AppliesTo));
var instancesContainingPeriod = position.Instances
.Where(i => period.Contains(i.AppliesFrom) && period.Contains(i.AppliesTo))
.ToList();

continue;
}
var instancesStartingWithinPeriod = position.Instances
.Except(instancesContainingPeriod)
.Where(i => period.Contains(i.AppliesFrom) && !period.Contains(i.AppliesTo))
.ToList();

var instancesStartingAfterPeriod = position.Instances
.Where(i => i.AppliesFrom > period.Stop)
.ToList();

// Handle case where the position is currently active
var lastInstance = instancesStartingAfterPeriod.LastOrDefault();

var isActiveInstanceExpiring = activeInstance.AppliesTo < expiringDate;

if (isActiveInstanceExpiring && futureInstances.Count == 0)
{
expiringPositions.Add(new ExpiringPosition(position, activeInstance.AppliesTo));
continue;
}
return new InstancesWithinPeriod()
{
EndingWithinPeriod = instancesEndingWithinPeriod,
ContainedWithinPeriod = instancesContainingPeriod,
StartingWithinPeriod = instancesStartingWithinPeriod,
LastInstance = lastInstance
};
}

if (isActiveInstanceExpiring)
{
var endingPositionAllocation = FindFirstTBNOrLastExpiringInstance(futureInstances);

if (endingPositionAllocation is not null)
expiringPositions.Add(new ExpiringPosition(position, endingPositionAllocation.AppliesTo));
}
private class InstancesWithinPeriod
{
public ApiPositionInstanceV2? LastInstance { get; set; }
public required List<ApiPositionInstanceV2> EndingWithinPeriod { get; set; }
public required List<ApiPositionInstanceV2> ContainedWithinPeriod { get; set; }
public required List<ApiPositionInstanceV2> StartingWithinPeriod { get; set; } // Starting within period and ending after period

// The instance is active and not expiring, continue to next position
}
public List<ApiPositionInstanceV2> Instances => EndingWithinPeriod.Concat(ContainedWithinPeriod).Concat(StartingWithinPeriod).Distinct().ToList();

public bool AnyInstancesAfter(ApiPositionInstanceV2 instance)
{
// In this case the instance outlasts the period and is outside of scope
if (StartingWithinPeriod.Contains(instance))
return true;

return expiringPositions;
return Instances.Except([instance]).Any(i => i.AppliesFrom > instance.AppliesTo);
}
}

private static ApiPositionInstanceV2? FindFirstTBNOrLastExpiringInstance(IEnumerable<ApiPositionInstanceV2> futureOrderedInstances)

private class DateTimeRange
{
ApiPositionInstanceV2? lastExpiringInstance = null;
foreach (var instance in futureOrderedInstances)
public DateTime Start { get; set; }
public DateTime Stop { get; set; }

public bool Overlaps(DateTimeRange other)
{
if (instance.AssignedPerson is null)
return instance; // We found a TBN instance
return Start < other.Stop && Stop > other.Start;
}

if (instance.AppliesTo < NowDate.AddMonths(3))
lastExpiringInstance = instance; // We found an expiring instance
public bool Contains(DateTime dateTime)
{
return Start <= dateTime && dateTime <= Stop;
}

return lastExpiringInstance;
public TimeSpan Duration()
{
return Stop - Start;
}
}

// https://github.com/equinor/fusion-resource-allocation-apps/blob/0c8477f48021c594af20c0b1ba7b549b187e2e71/apps/org-admin/src/pages/ProjectPage/utils.ts#L53
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest()


var futureInstanceThatIsAlsoExpiring = new PositionBuilder()
.WithInstance(now.AddDays(30), now.AddDays(30 * 2), person: personA)
.WithInstance(now.AddDays(30), now.AddDays(30 * 1.5), person: personA)
.Build();
testData.AddPosition(futureInstanceThatIsAlsoExpiring, shouldBeIncludedInReportList: true);

Expand Down Expand Up @@ -165,6 +165,16 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest()

testData.AddPosition(nonActivePositionWithPastAllocationAndFutureTBN);


var testPos = new PositionBuilder()
.WithInstance(now.AddMonths(-6), now.AddMonths(1), person: personA)
.AddNextInstance(TimeSpan.FromDays(48), personA)
.AddNextInstance(TimeSpan.FromDays(90), personA)
.Build();

testData.AddPosition(testPos);


var shouldBeIncludedInReport = testData.ShouldBeIncludedInReport;
var positionsToTest = testData.PositionsToTest;
var instanceToBeIncluded = testData.InstanceToBeIncluded;
Expand Down Expand Up @@ -192,7 +202,7 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest()
}

// Check that there are no extra positions that should not be included
data.Should().HaveSameCount(shouldBeIncludedInReport, $"Exactly these positions should be included in the report, {string.Join(", ", shouldBeIncludedInReport)}, these should not be included {string.Join(", ", positionsToTest.Select(p => p.Name).Except(shouldBeIncludedInReport))}");
data.Should().HaveSameCount(shouldBeIncludedInReport, $"Exactly these positions should be included in the report: {string.Join(", ", shouldBeIncludedInReport)}. These should not be included: {string.Join(", ", data.Select(p => p.Position.Name).Where(p => !shouldBeIncludedInReport.Contains(p)))}");
}

[Fact]
Expand Down Expand Up @@ -278,7 +288,7 @@ public void GetTBNPositionsStartingWithinThreeMonthsTests()
// Check that there are no extra positions that should not be included
data.Should().HaveSameCount(testData.ShouldBeIncludedInReport,
$"Exactly these positions should be included in the report, {string.Join(", ", testData.ShouldBeIncludedInReport)}," +
$" these should not be included {string.Join(", ", testData.PositionsToTest.Select(p => p.Name).Except(testData.ShouldBeIncludedInReport))}");
$" these should not be included {string.Join(", ", data.Select(p => p.Position.Name).Where(p => !testData.ShouldBeIncludedInReport.Contains(p)))}");
}


Expand Down

0 comments on commit 0db7ba0

Please sign in to comment.