Optimize Transport Connection Selection with Enhanced Timing Logic (#24)

* Initial plan

* Implement enhanced transport connection optimization logic

Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com>

* Add configuration for new transport optimization parameters and complete implementation

Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com>

* Address PR feedback: remove comments, old logic, and unnecessary tests

Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com>

* Fix looking for connection

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: clayschaad <11883505+clayschaad@users.noreply.github.com>
Co-authored-by: clayschaad <c.schaad@pog.ch>
This commit is contained in:
Copilot 2025-08-23 20:16:04 +02:00 committed by GitHub
parent f5387e7b41
commit 68e2fa4927
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 210 additions and 11 deletions

View file

@ -12,7 +12,9 @@
"EndStation": "Basel",
"ApiBaseUrl": "http://transport.opendata.ch/v1",
"SafetyBufferMinutes": 30,
"MinBreakMinutes": 60
"MinBreakMinutes": 60,
"MaxEarlyArrivalMinutes": 60,
"MaxLateArrivalMinutes": 15
},
"Shifts": [

View file

@ -4,9 +4,19 @@ namespace ShiftScheduler.Services;
public static class TransportConnectionCalculator
{
public static TransportConnection? FindBestConnection(IReadOnlyList<TransportConnection> connections, DateTime latestArrivalTime)
public static TransportConnection? FindBestConnection(
IReadOnlyList<TransportConnection> connections,
DateTime shiftStartTime,
int safetyBufferMinutes,
int maxEarlyArrivalMinutes,
int maxLateArrivalMinutes)
{
var latestArrivalTime = shiftStartTime.AddMinutes(-safetyBufferMinutes);
var earliestAcceptableTime = shiftStartTime.AddMinutes(-maxEarlyArrivalMinutes);
var latestAcceptableTime = shiftStartTime.AddMinutes(maxLateArrivalMinutes);
var validConnections = new List<TransportConnection>();
var lateValidConnections = new List<TransportConnection>();
foreach (var connection in connections)
{
@ -16,21 +26,48 @@ public static class TransportConnectionCalculator
{
validConnections.Add(connection);
}
else if (arrivalTime <= latestAcceptableTime)
{
lateValidConnections.Add(connection);
}
}
}
// If we have valid connections (arriving before latest arrival time)
if (validConnections.Count > 0)
{
var sortedValid = validConnections
.OrderBy(c => DateTime.Parse(c.ArrivalTime ?? "00:00"))
.ToList();
return sortedValid.Last();
var bestValidConnection = sortedValid.Last();
var bestValidArrivalTime = DateTime.Parse(bestValidConnection.ArrivalTime!);
// Check if the best valid connection arrives too early (more than maxEarlyArrivalMinutes before shift)
if (bestValidArrivalTime < earliestAcceptableTime && lateValidConnections.Count > 0)
{
// Return the earliest connection that arrives after latest arrival time but within acceptable range
var sortedLateValid = lateValidConnections
.OrderBy(c => DateTime.Parse(c.ArrivalTime ?? "23:59"))
.ToList();
return sortedLateValid.First();
}
// If no valid connections, return the earliest available
return connections
return bestValidConnection;
}
// If no connections arrive before latest arrival time, check if any arrive within acceptable late range
if (lateValidConnections.Count > 0)
{
var sortedLateValid = lateValidConnections
.OrderBy(c => DateTime.Parse(c.ArrivalTime ?? "23:59"))
.FirstOrDefault();
.ToList();
return sortedLateValid.First();
}
// No suitable connections found
return null;
}
}

View file

@ -9,7 +9,10 @@ namespace ShiftScheduler.Services
{
var latestArrivalTime = shiftStartTime.AddMinutes(-config.SafetyBufferMinutes);
var searchDate = shiftStartTime.ToString("yyyy-MM-dd");
var searchTime = shiftStartTime.ToString("HH:mm");
// To allow connections that arrive after shift starts, we search from earlier time
// and request more connections to cover the full range
var searchTime = shiftStartTime.AddMinutes(config.MaxLateArrivalMinutes).ToString("HH:mm");
var url = $"{config.ApiBaseUrl}/connections?from={Uri.EscapeDataString(config.StartStation)}&to={Uri.EscapeDataString(config.EndStation)}&date={searchDate}&time={searchTime}&isArrivalTime=1&limit=5";
var response = await httpClient.GetStringAsync(url);
@ -21,7 +24,12 @@ namespace ShiftScheduler.Services
if (apiResponse?.Connections.Count > 0)
{
var allConnections = apiResponse.Connections.Select(MapToTransportConnection).ToList();
return TransportConnectionCalculator.FindBestConnection(allConnections, latestArrivalTime);
return TransportConnectionCalculator.FindBestConnection(
allConnections,
shiftStartTime,
config.SafetyBufferMinutes,
config.MaxEarlyArrivalMinutes,
config.MaxLateArrivalMinutes);
}
return null;

View file

@ -7,6 +7,8 @@ namespace ShiftScheduler.Shared
public string ApiBaseUrl { get; set; } = string.Empty;
public int SafetyBufferMinutes { get; set; } = 30;
public int MinBreakMinutes { get; set; } = 60;
public int MaxEarlyArrivalMinutes { get; set; } = 60;
public int MaxLateArrivalMinutes { get; set; } = 15;
}
public class TransportConnection

View file

@ -0,0 +1,148 @@
using ShiftScheduler.Services;
using ShiftScheduler.Shared;
using Shouldly;
namespace ShiftScheduler.Services.Tests;
public class TransportConnectionCalculatorTests
{
// Tests for new enhanced logic
[Fact]
public void FindBestConnectionEnhanced_WithGoodTimingConnection_ShouldReturnLatestValidConnection()
{
// Arrange - Shift starts at 8:00, safety buffer 30 min, so latest arrival is 7:30
var connections = new List<TransportConnection>
{
new TransportConnection { ArrivalTime = "2023-12-15T07:15:00" },
new TransportConnection { ArrivalTime = "2023-12-15T07:25:00" }, // Should be selected (latest valid)
new TransportConnection { ArrivalTime = "2023-12-15T08:05:00" } // Too late
};
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
// Act
var result = TransportConnectionCalculator.FindBestConnection(
connections, shiftStartTime, 30, 60, 15);
// Assert
result.ShouldNotBeNull();
result.ArrivalTime.ShouldBe("2023-12-15T07:25:00");
}
[Fact]
public void FindBestConnectionEnhanced_WithTooEarlyConnection_ShouldPreferLaterConnection()
{
// Arrange - Shift starts at 8:00, max early arrival 60 min, so earliest acceptable is 7:00
var connections = new List<TransportConnection>
{
new TransportConnection { ArrivalTime = "2023-12-15T06:45:00" }, // Too early (more than 60 min before 8:00)
new TransportConnection { ArrivalTime = "2023-12-15T08:05:00" } // Within acceptable late range (15 min after 8:00)
};
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
// Act
var result = TransportConnectionCalculator.FindBestConnection(
connections, shiftStartTime, 30, 60, 15);
// Assert
result.ShouldNotBeNull();
result.ArrivalTime.ShouldBe("2023-12-15T08:05:00");
}
[Fact]
public void FindBestConnectionEnhanced_WithMultipleLateConnections_ShouldReturnEarliest()
{
// Arrange - Shift starts at 8:00, no valid early connections
var connections = new List<TransportConnection>
{
new TransportConnection { ArrivalTime = "2023-12-15T08:10:00" },
new TransportConnection { ArrivalTime = "2023-12-15T08:05:00" }, // Should be selected (earliest late)
new TransportConnection { ArrivalTime = "2023-12-15T08:12:00" }
};
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
// Act
var result = TransportConnectionCalculator.FindBestConnection(
connections, shiftStartTime, 30, 60, 15);
// Assert
result.ShouldNotBeNull();
result.ArrivalTime.ShouldBe("2023-12-15T08:05:00");
}
[Fact]
public void FindBestConnectionEnhanced_WithConnectionsBeyondAcceptableRange_ShouldReturnNull()
{
// Arrange - All connections are too late (beyond max late arrival)
var connections = new List<TransportConnection>
{
new TransportConnection { ArrivalTime = "2023-12-15T08:20:00" },
new TransportConnection { ArrivalTime = "2023-12-15T08:25:00" },
new TransportConnection { ArrivalTime = "2023-12-15T08:30:00" }
};
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
// Act
var result = TransportConnectionCalculator.FindBestConnection(
connections, shiftStartTime, 30, 60, 15);
// Assert
result.ShouldBeNull();
}
[Fact]
public void FindBestConnectionEnhanced_WithAcceptableEarlyConnection_ShouldNotPreferLate()
{
// Arrange - Early connection is acceptable (not too early), should prefer it over late
var connections = new List<TransportConnection>
{
new TransportConnection { ArrivalTime = "2023-12-15T07:20:00" }, // 40 min before shift - acceptable
new TransportConnection { ArrivalTime = "2023-12-15T08:05:00" } // 5 min after shift
};
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
// Act
var result = TransportConnectionCalculator.FindBestConnection(
connections, shiftStartTime, 30, 60, 15);
// Assert
result.ShouldNotBeNull();
result.ArrivalTime.ShouldBe("2023-12-15T07:20:00");
}
[Fact]
public void FindBestConnectionEnhanced_WithEmptyConnections_ShouldReturnNull()
{
// Arrange
var connections = new List<TransportConnection>();
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
// Act
var result = TransportConnectionCalculator.FindBestConnection(
connections, shiftStartTime, 30, 60, 15);
// Assert
result.ShouldBeNull();
}
[Fact]
public void FindBestConnectionEnhanced_WithMixedValidAndLateConnections_ShouldChooseBasedOnTiming()
{
// Arrange - Test scenario where both valid and late connections exist
var connections = new List<TransportConnection>
{
new TransportConnection { ArrivalTime = "2023-12-15T06:30:00" }, // Too early (90 min before shift)
new TransportConnection { ArrivalTime = "2023-12-15T07:25:00" }, // Valid but would be considered too early
new TransportConnection { ArrivalTime = "2023-12-15T08:05:00" }, // Late but acceptable
new TransportConnection { ArrivalTime = "2023-12-15T08:20:00" } // Too late
};
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
// Act - MaxEarlyArrivalMinutes = 30, so earliest acceptable is 7:30
var result = TransportConnectionCalculator.FindBestConnection(
connections, shiftStartTime, 30, 30, 15);
// Assert - 7:25 arrives at 7:25, which is 35 min before shift (more than 30 min), so should prefer late connection
result.ShouldNotBeNull();
result.ArrivalTime.ShouldBe("2023-12-15T08:05:00");
}
}

View file

@ -25,7 +25,9 @@ public class TransportServiceTests
EndStation = "Bern",
ApiBaseUrl = "https://transport.opendata.ch/v1",
SafetyBufferMinutes = 30,
MinBreakMinutes = 60
MinBreakMinutes = 60,
MaxEarlyArrivalMinutes = 60,
MaxLateArrivalMinutes = 15
};
_transportService = new TransportService(_httpClient, config);