diff --git a/Server/appsettings.json b/Server/appsettings.json index bf1cd6c..a32e56e 100644 --- a/Server/appsettings.json +++ b/Server/appsettings.json @@ -12,7 +12,9 @@ "EndStation": "Basel", "ApiBaseUrl": "http://transport.opendata.ch/v1", "SafetyBufferMinutes": 30, - "MinBreakMinutes": 60 + "MinBreakMinutes": 60, + "MaxEarlyArrivalMinutes": 60, + "MaxLateArrivalMinutes": 15 }, "Shifts": [ diff --git a/Services/TransportConnectionCalculator.cs b/Services/TransportConnectionCalculator.cs index 874cec2..ee0364a 100644 --- a/Services/TransportConnectionCalculator.cs +++ b/Services/TransportConnectionCalculator.cs @@ -4,10 +4,20 @@ namespace ShiftScheduler.Services; public static class TransportConnectionCalculator { - public static TransportConnection? FindBestConnection(IReadOnlyList connections, DateTime latestArrivalTime) + public static TransportConnection? FindBestConnection( + IReadOnlyList 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(); - + var lateValidConnections = new List(); + foreach (var connection in connections) { if (DateTime.TryParse(connection.ArrivalTime, out var arrivalTime)) @@ -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(); + } + + return bestValidConnection; } - // If no valid connections, return the earliest available - return connections - .OrderBy(c => DateTime.Parse(c.ArrivalTime ?? "23:59")) - .FirstOrDefault(); + // 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")) + .ToList(); + + return sortedLateValid.First(); + } + + // No suitable connections found + return null; } } \ No newline at end of file diff --git a/Services/TransportService.cs b/Services/TransportService.cs index c6f4a54..63de945 100644 --- a/Services/TransportService.cs +++ b/Services/TransportService.cs @@ -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; diff --git a/Shared/TransportModels.cs b/Shared/TransportModels.cs index aa7de4d..b93f83c 100644 --- a/Shared/TransportModels.cs +++ b/Shared/TransportModels.cs @@ -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 diff --git a/ShiftScheduler.Services.Tests/TransportConnectionCalculatorTests.cs b/ShiftScheduler.Services.Tests/TransportConnectionCalculatorTests.cs new file mode 100644 index 0000000..d5c9a42 --- /dev/null +++ b/ShiftScheduler.Services.Tests/TransportConnectionCalculatorTests.cs @@ -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 + { + 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 + { + 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 + { + 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 + { + 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 + { + 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(); + 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 + { + 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"); + } +} \ No newline at end of file diff --git a/ShiftScheduler.Services.Tests/TransportServiceTests.cs b/ShiftScheduler.Services.Tests/TransportServiceTests.cs index 005ecdc..63c2c19 100644 --- a/ShiftScheduler.Services.Tests/TransportServiceTests.cs +++ b/ShiftScheduler.Services.Tests/TransportServiceTests.cs @@ -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);