From a1f9f1313e862e433d4f4a9832edd4c15b29bc41 Mon Sep 17 00:00:00 2001 From: Claudio Schaad Date: Wed, 3 Sep 2025 22:17:53 +0200 Subject: [PATCH] Fix find best connection with using of local date time offset (#46) * Add logger * Change strings to date time offset * Fix logic for best connection * Fix parsing shift times * set time zone --- Client/Pages/Index.razor.cs | 2 +- Server/Controllers/ShiftController.cs | 53 ++------ Services/ConfigurationService.cs | 49 ++++++- Services/ITransportService.cs | 4 +- Services/IcsExportService.cs | 15 +-- Services/PdfExportService.cs | 9 +- Services/TransportApiService.cs | 51 ++------ Services/TransportConnectionCalculator.cs | 76 +++++------ Services/TransportService.cs | 2 +- Shared/ShiftWithTransport.cs | 13 +- Shared/TransportModels.cs | 44 ++----- .../ConfigurationServiceTests.cs | 47 +++++++ .../TransportApiServiceTests.cs | 28 ++-- .../TransportConnectionCalculatorTests.cs | 120 ++++++++++++------ .../TransportServiceTests.cs | 25 ++-- 15 files changed, 280 insertions(+), 258 deletions(-) create mode 100644 ShiftScheduler.Services.Tests/ConfigurationServiceTests.cs diff --git a/Client/Pages/Index.razor.cs b/Client/Pages/Index.razor.cs index b771e92..55c70ec 100644 --- a/Client/Pages/Index.razor.cs +++ b/Client/Pages/Index.razor.cs @@ -15,7 +15,7 @@ namespace ShiftScheduler.Client.Pages private Dictionary SelectedSchedule { get; set; } = new(); private Dictionary SelectedShiftsWithTransport { get; set; } = new(); - private bool _isCurrentMonth = false; + private bool _isCurrentMonth = true; private bool _isLoadingTransport = false; private bool _isLoadingInitial = false; private bool _showConfigDialog = false; diff --git a/Server/Controllers/ShiftController.cs b/Server/Controllers/ShiftController.cs index a051151..065e7de 100644 --- a/Server/Controllers/ShiftController.cs +++ b/Server/Controllers/ShiftController.cs @@ -45,14 +45,15 @@ namespace ShiftScheduler.Server.Controllers var transportConfig = _configurationService.GetTransportConfiguration(); TransportConnection? morningTransport = null; TransportConnection? afternoonTransport = null; + + var shifTimes = _configurationService.ParseShiftTimes(request.Date, shift); // Get transport for morning shift if it has morning time if (!string.IsNullOrEmpty(shift.MorningTime)) { - var morningStartTime = ParseShiftTime(request.Date, shift.MorningTime); - if (morningStartTime.HasValue) + if (shifTimes.MorningStart.HasValue) { - morningTransport = await _transportService.GetConnectionAsync(morningStartTime.Value); + morningTransport = await _transportService.GetConnectionAsync(shifTimes.MorningStart.Value); } } @@ -65,22 +66,18 @@ namespace ShiftScheduler.Server.Controllers // If both morning and afternoon shifts exist, check break duration if (!string.IsNullOrEmpty(shift.MorningTime) && !string.IsNullOrEmpty(shift.AfternoonTime)) { - var morningEndTime = ParseShiftEndTime(request.Date, shift.MorningTime); - var afternoonStartTime = ParseShiftTime(request.Date, shift.AfternoonTime); - - if (morningEndTime.HasValue && afternoonStartTime.HasValue) + if (shifTimes.MorningEnd.HasValue && shifTimes.AfternoonStart.HasValue) { - var breakDurationMinutes = (afternoonStartTime.Value - morningEndTime.Value).TotalMinutes; + var breakDurationMinutes = (shifTimes.AfternoonStart.Value - shifTimes.MorningEnd.Value).TotalMinutes; shouldLoadAfternoonTransport = breakDurationMinutes >= transportConfig.MinBreakMinutes; } } if (shouldLoadAfternoonTransport) { - var afternoonStartTime = ParseShiftTime(request.Date, shift.AfternoonTime); - if (afternoonStartTime.HasValue) + if (shifTimes.AfternoonStart.HasValue) { - afternoonTransport = await _transportService.GetConnectionAsync(afternoonStartTime.Value); + afternoonTransport = await _transportService.GetConnectionAsync(shifTimes.AfternoonStart.Value); } } } @@ -96,40 +93,6 @@ namespace ShiftScheduler.Server.Controllers return Ok(shiftWithTransport); } - private static DateTime? ParseShiftTime(DateTime date, string timeRange) - { - try - { - var times = timeRange.Split('-'); - if (times.Length > 0 && TimeSpan.TryParse(times[0], out var startTime)) - { - return date.Add(startTime); - } - } - catch - { - // Ignore parsing errors - } - return null; - } - - private static DateTime? ParseShiftEndTime(DateTime date, string timeRange) - { - try - { - var times = timeRange.Split('-'); - if (times.Length > 1 && TimeSpan.TryParse(times[1], out var endTime)) - { - return date.Add(endTime); - } - } - catch - { - // Ignore parsing errors - } - return null; - } - [HttpPost("export_ics")] public IActionResult ExportIcsWithTransport([FromBody] List shiftsWithTransport) { diff --git a/Services/ConfigurationService.cs b/Services/ConfigurationService.cs index 03901b5..e0d54aa 100644 --- a/Services/ConfigurationService.cs +++ b/Services/ConfigurationService.cs @@ -12,12 +12,15 @@ namespace ShiftScheduler.Services List GetShifts(); TransportConfiguration GetTransportConfiguration(); - // Schedule persistence methods Task SaveScheduleAsync(int year, int month, Dictionary schedule); Task> LoadScheduleAsync(int year, int month); Task DeleteScheduleAsync(int year, int month); + + ShiftTimes ParseShiftTimes(DateTime date, Shift shift); } + public record ShiftTimes(DateTimeOffset? MorningStart, DateTimeOffset? MorningEnd, DateTimeOffset? AfternoonStart, DateTimeOffset? AfternoonEnd); + public class ConfigurationService : IConfigurationService { private ApplicationConfiguration _configuration; @@ -26,6 +29,8 @@ namespace ShiftScheduler.Services private readonly string _shiftsFilePath; private readonly string _transportFilePath; + readonly TimeZoneInfo localTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Zurich"); + public ConfigurationService(ApplicationConfiguration initialConfiguration) { _configDirectory = Path.Combine(Directory.GetCurrentDirectory(), "config"); @@ -201,7 +206,47 @@ namespace ShiftScheduler.Services // Log error if needed, but don't throw } } - + + public ShiftTimes ParseShiftTimes(DateTime date, Shift shift) + { + DateTimeOffset? morningStart = null; + DateTimeOffset? morningEnd = null; + DateTimeOffset? afternoonStart = null; + DateTimeOffset? afternoonEnd = null; + + TimeSpan offset = localTimeZone.GetUtcOffset(DateTime.UtcNow); + + if (!string.IsNullOrEmpty(shift.MorningTime)) + { + var times = shift.MorningTime.Split('-'); + if (times.Length == 2) + { + var start = $"{date:yyyy-MM-dd}T{times[0]}:00+{offset.Hours}:{offset.Minutes}"; + var end = $"{date:yyyy-MM-dd}T{times[1]}:00+{offset.Hours}:{offset.Minutes}"; + morningStart = DateTimeOffset.Parse(start); + morningEnd = DateTimeOffset.Parse(end); + } + } + + if (!string.IsNullOrEmpty(shift.AfternoonTime)) + { + var times = shift.AfternoonTime.Split('-'); + if (times.Length == 2) + { + var start = $"{date:yyyy-MM-dd}T{times[0]}:00+{offset.Hours}:{offset.Minutes}"; + var end = $"{date:yyyy-MM-dd}T{times[1]}:00+{offset.Hours}:{offset.Minutes}"; + afternoonStart = DateTimeOffset.Parse(start); + afternoonEnd = DateTimeOffset.Parse(end); + } + } + + return new ShiftTimes( + MorningStart: morningStart, + MorningEnd: morningEnd, + AfternoonStart: afternoonStart, + AfternoonEnd: afternoonEnd); + } + private ApplicationConfiguration LoadConfigurationFromFiles(ApplicationConfiguration fallbackConfiguration) { try diff --git a/Services/ITransportService.cs b/Services/ITransportService.cs index ee3d593..0d84930 100644 --- a/Services/ITransportService.cs +++ b/Services/ITransportService.cs @@ -4,11 +4,11 @@ namespace ShiftScheduler.Services { public interface ITransportService { - Task GetConnectionAsync(DateTime shiftStartTime); + Task GetConnectionAsync(DateTimeOffset shiftStartTime); } public interface ITransportApiService { - Task GetConnectionAsync(DateTime shiftStartTime); + Task GetConnectionAsync(DateTimeOffset shiftStartTime); } } \ No newline at end of file diff --git a/Services/IcsExportService.cs b/Services/IcsExportService.cs index d725df4..8ab3ab4 100644 --- a/Services/IcsExportService.cs +++ b/Services/IcsExportService.cs @@ -26,7 +26,7 @@ namespace ShiftScheduler.Services var summary = $"{shift.Name} (Morning)"; var description = ""; - if (shiftWithTransport.MorningTransport != null && !string.IsNullOrEmpty(shiftWithTransport.MorningTransport.DepartureTime)) + if (shiftWithTransport.MorningTransport != null) { var transportSummary = FormatTransportInfo(shiftWithTransport.MorningTransport); description = $"Transport: {transportSummary}"; @@ -47,7 +47,7 @@ namespace ShiftScheduler.Services var summary = $"{shift.Name} (Afternoon)"; var description = ""; - if (shiftWithTransport.AfternoonTransport != null && !string.IsNullOrEmpty(shiftWithTransport.AfternoonTransport.DepartureTime)) + if (shiftWithTransport.AfternoonTransport != null) { var transportSummary = FormatTransportInfo(shiftWithTransport.AfternoonTransport); description = $"Transport: {transportSummary}"; @@ -68,15 +68,8 @@ namespace ShiftScheduler.Services private string FormatTransportInfo(TransportConnection transport) { - var departure = DateTime.TryParse(transport.DepartureTime, out var dep) ? dep.ToString("HH:mm") : transport.DepartureTime; - var arrival = DateTime.TryParse(transport.ArrivalTime, out var arr) ? arr.ToString("HH:mm") : transport.ArrivalTime; - - var mainJourney = transport.Sections?.FirstOrDefault()?.Journey; - if (mainJourney != null) - { - return $"{mainJourney.Category} {mainJourney.Number}: {departure} → {arrival}"; - } - + var departure =transport.DepartureTime.ToString("HH:mm"); + var arrival = transport.ArrivalTime.ToString("HH:mm"); return $"{departure} → {arrival}"; } } diff --git a/Services/PdfExportService.cs b/Services/PdfExportService.cs index c0ab460..ae81441 100644 --- a/Services/PdfExportService.cs +++ b/Services/PdfExportService.cs @@ -176,13 +176,13 @@ namespace ShiftScheduler.Services var transportLines = new List(); - if (shiftWithTransport.MorningTransport != null && !string.IsNullOrEmpty(shiftWithTransport.MorningTransport.DepartureTime)) + if (shiftWithTransport.MorningTransport != null) { var morningInfo = FormatTransportConnection(shiftWithTransport.MorningTransport); transportLines.Add(morningInfo); } - if (shiftWithTransport.AfternoonTransport != null && !string.IsNullOrEmpty(shiftWithTransport.AfternoonTransport.DepartureTime)) + if (shiftWithTransport.AfternoonTransport != null) { var afternoonInfo = FormatTransportConnection(shiftWithTransport.AfternoonTransport); transportLines.Add(afternoonInfo); @@ -193,9 +193,8 @@ namespace ShiftScheduler.Services private static string FormatTransportConnection(TransportConnection transport) { - var departure = DateTime.TryParse(transport.DepartureTime, out var dep) ? dep.ToString("HH:mm") : transport.DepartureTime; - var arrival = DateTime.TryParse(transport.ArrivalTime, out var arr) ? arr.ToString("HH:mm") : transport.ArrivalTime; - + var departure = transport.DepartureTime.ToString("HH:mm"); + var arrival = transport.ArrivalTime.ToString("HH:mm"); return $"🚂 {departure}→{arrival}"; } diff --git a/Services/TransportApiService.cs b/Services/TransportApiService.cs index c6e579e..c06e959 100644 --- a/Services/TransportApiService.cs +++ b/Services/TransportApiService.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Text.Json; using Microsoft.Extensions.Logging; using ShiftScheduler.Shared; @@ -6,7 +7,7 @@ namespace ShiftScheduler.Services { public class TransportApiService(HttpClient httpClient, IConfigurationService configurationService, ILogger logger) : ITransportApiService { - public async Task GetConnectionAsync(DateTime shiftStartTime) + public async Task GetConnectionAsync(DateTimeOffset shiftStartTime) { var config = configurationService.GetTransportConfiguration(); @@ -28,17 +29,11 @@ namespace ShiftScheduler.Services foreach (var connection in allConnections) { - logger.LogInformation($"Found connection {connection}"); + logger.LogDebug($"Found connection {connection}"); } var bestConnection = TransportConnectionCalculator.FindBestConnection( - allConnections, - shiftStartTime, - config.SafetyBufferMinutes, - config.MaxEarlyArrivalMinutes, - config.MaxLateArrivalMinutes); - - logger.LogInformation($"Found best {bestConnection}"); + allConnections, new ConnectionPickArgument(shiftStartTime, config.SafetyBufferMinutes, config.MaxEarlyArrivalMinutes, config.MaxLateArrivalMinutes), logger); return bestConnection; } @@ -51,43 +46,13 @@ namespace ShiftScheduler.Services if (apiConnection == null) return new TransportConnection(); + var formats = new[] { @"dd'd'hh\:mm\:ss", @"d'd'hh\:mm\:ss" }; return new TransportConnection { - DepartureTime = apiConnection.From?.Departure ?? string.Empty, - ArrivalTime = apiConnection.To?.Arrival ?? string.Empty, - Duration = apiConnection.Duration, + DepartureTime = DateTimeOffset.Parse(apiConnection.From!.Departure), + ArrivalTime = DateTimeOffset.Parse(apiConnection.To!.Arrival), + Duration = TimeSpan.ParseExact(apiConnection.Duration, formats, CultureInfo.InvariantCulture), Platform = apiConnection.From?.Platform ?? string.Empty, - Sections = apiConnection.Sections.Select(s => new TransportSection - { - Journey = s.Journey != null ? new TransportJourney - { - Name = s.Journey.Name, - Category = s.Journey.Category, - Number = s.Journey.Number - } : null, - Departure = s.Departure != null ? new TransportCheckpoint - { - Station = s.Departure.Station != null ? new TransportStation - { - Name = s.Departure.Station.Name, - Id = s.Departure.Station.Id - } : null, - Departure = s.Departure.Departure, - Arrival = s.Departure.Arrival, - Platform = s.Departure.Platform - } : null, - Arrival = s.Arrival != null ? new TransportCheckpoint - { - Station = s.Arrival.Station != null ? new TransportStation - { - Name = s.Arrival.Station.Name, - Id = s.Arrival.Station.Id - } : null, - Departure = s.Arrival.Departure, - Arrival = s.Arrival.Arrival, - Platform = s.Arrival.Platform - } : null - }).ToList() }; } } diff --git a/Services/TransportConnectionCalculator.cs b/Services/TransportConnectionCalculator.cs index ee0364a..500b875 100644 --- a/Services/TransportConnectionCalculator.cs +++ b/Services/TransportConnectionCalculator.cs @@ -1,73 +1,65 @@ +using Microsoft.Extensions.Logging; using ShiftScheduler.Shared; namespace ShiftScheduler.Services; +public record ConnectionPickArgument( + DateTimeOffset ShiftStartTime, + int SafetyBufferMinutes, + int MaxEarlyArrivalMinutes, + int MaxLateArrivalMinutes) +{ + public override string ToString() + { + return $"ShiftStartTime: {ShiftStartTime}, SafetyBufferMinutes: {SafetyBufferMinutes}, MaxEarlyArrivalMinutes{MaxEarlyArrivalMinutes}, MaxLateArrivalMinutes:{MaxLateArrivalMinutes}"; + } +} + public static class TransportConnectionCalculator { public static TransportConnection? FindBestConnection( - IReadOnlyList connections, - DateTime shiftStartTime, - int safetyBufferMinutes, - int maxEarlyArrivalMinutes, - int maxLateArrivalMinutes) + IReadOnlyList connections, ConnectionPickArgument args, ILogger logger) { - var latestArrivalTime = shiftStartTime.AddMinutes(-safetyBufferMinutes); - var earliestAcceptableTime = shiftStartTime.AddMinutes(-maxEarlyArrivalMinutes); - var latestAcceptableTime = shiftStartTime.AddMinutes(maxLateArrivalMinutes); + var latestArrivalTime = args.ShiftStartTime.AddMinutes(-args.SafetyBufferMinutes); + var earliestAcceptableTime = args.ShiftStartTime.AddMinutes(-args.MaxEarlyArrivalMinutes); + var latestAcceptableTime = args.ShiftStartTime.AddMinutes(args.MaxLateArrivalMinutes); + + logger.LogDebug(args.ToString()); + logger.LogDebug($"latestArrivalTime: {latestArrivalTime}"); + logger.LogDebug($"earliestAcceptableTime: {earliestAcceptableTime}"); + logger.LogDebug($"latestAcceptableTime: {latestAcceptableTime}"); var validConnections = new List(); var lateValidConnections = new List(); - foreach (var connection in connections) + foreach (var connection in connections.OrderBy(c => c.ArrivalTime)) { - if (DateTime.TryParse(connection.ArrivalTime, out var arrivalTime)) + if (connection.ArrivalTime <= latestArrivalTime) { - if (arrivalTime <= latestArrivalTime) - { - validConnections.Add(connection); - } - else if (arrivalTime <= latestAcceptableTime) - { - lateValidConnections.Add(connection); - } + validConnections.Add(connection); + } + else if (connection.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(); - - 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) + var bestValidConnection = validConnections.Last(); + if (bestValidConnection.ArrivalTime > earliestAcceptableTime) { - // 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; } - - 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")) - .ToList(); - - return sortedLateValid.First(); + return lateValidConnections.First(); } - // No suitable connections found - return null; + return validConnections.FirstOrDefault(); } } \ No newline at end of file diff --git a/Services/TransportService.cs b/Services/TransportService.cs index 91dd0f0..5605014 100644 --- a/Services/TransportService.cs +++ b/Services/TransportService.cs @@ -5,7 +5,7 @@ namespace ShiftScheduler.Services { public class TransportService(ITransportApiService transportService, IConfigurationService configurationService, IMemoryCache cache) : ITransportService { - public async Task GetConnectionAsync(DateTime shiftStartTime) + public async Task GetConnectionAsync(DateTimeOffset shiftStartTime) { var config = configurationService.GetTransportConfiguration(); var searchDate = shiftStartTime.ToString("yyyy-MM-dd"); diff --git a/Shared/ShiftWithTransport.cs b/Shared/ShiftWithTransport.cs index ad11ace..1cf620c 100644 --- a/Shared/ShiftWithTransport.cs +++ b/Shared/ShiftWithTransport.cs @@ -1,6 +1,5 @@ namespace ShiftScheduler.Shared { - // View model that combines shift information with transport data for a specific date public record ShiftWithTransport { public DateTime Date { get; init; } @@ -20,15 +19,9 @@ namespace ShiftScheduler.Shared private string FormatTransportSummary(TransportConnection connection) { - if (string.IsNullOrEmpty(connection.DepartureTime)) return string.Empty; - - var departure = DateTime.TryParse(connection.DepartureTime, out var dep) ? dep.ToString("HH:mm") : connection.DepartureTime; - var arrival = DateTime.TryParse(connection.ArrivalTime, out var arr) ? arr.ToString("HH:mm") : connection.ArrivalTime; - - var mainJourney = connection.Sections?.FirstOrDefault()?.Journey; - var trainInfo = mainJourney != null ? $"{mainJourney.Category}{mainJourney.Number}" : "Train"; - - return $"{trainInfo} {departure}→{arrival}"; + var departure = connection.DepartureTime.ToString("HH:mm"); + var arrival = connection.ArrivalTime.ToString("HH:mm"); + return $"{departure}→{arrival}"; } } } \ No newline at end of file diff --git a/Shared/TransportModels.cs b/Shared/TransportModels.cs index 66fa378..d4ccafd 100644 --- a/Shared/TransportModels.cs +++ b/Shared/TransportModels.cs @@ -14,43 +14,23 @@ namespace ShiftScheduler.Shared public class TransportConnection { - public string DepartureTime { get; set; } = string.Empty; - public string ArrivalTime { get; set; } = string.Empty; - public string Duration { get; set; } = string.Empty; + public TransportConnection() + { + } + + public TransportConnection(string arrivalTime) + { + ArrivalTime = DateTimeOffset.Parse(arrivalTime); + } + + public DateTimeOffset DepartureTime { get; set; } + public DateTimeOffset ArrivalTime { get; set; } + public TimeSpan Duration { get; set; } public string? Platform { get; set; } = string.Empty; - public List Sections { get; set; } = new(); public override string ToString() { return $"{Platform}: {DepartureTime} - {ArrivalTime} ({Duration})"; } } - - public class TransportSection - { - public TransportJourney? Journey { get; set; } - public TransportCheckpoint? Departure { get; set; } - public TransportCheckpoint? Arrival { get; set; } - } - - public class TransportJourney - { - public string Name { get; set; } = string.Empty; - public string Category { get; set; } = string.Empty; - public string Number { get; set; } = string.Empty; - } - - public class TransportCheckpoint - { - public TransportStation? Station { get; set; } - public string? Departure { get; set; } = string.Empty; - public string? Arrival { get; set; } = string.Empty; - public string? Platform { get; set; } = string.Empty; - } - - public class TransportStation - { - public string Name { get; set; } = string.Empty; - public string Id { get; set; } = string.Empty; - } } \ No newline at end of file diff --git a/ShiftScheduler.Services.Tests/ConfigurationServiceTests.cs b/ShiftScheduler.Services.Tests/ConfigurationServiceTests.cs new file mode 100644 index 0000000..7470216 --- /dev/null +++ b/ShiftScheduler.Services.Tests/ConfigurationServiceTests.cs @@ -0,0 +1,47 @@ +using ShiftScheduler.Shared; +using Shouldly; + +namespace ShiftScheduler.Services.Tests; + +public class ConfigurationServiceTests +{ + [Fact] + public void ParseShiftTimes_WithMorningAndAfternoon_ShouldReturnValid() + { + var applicationConfig = new ApplicationConfiguration(); + var configurationService = new ConfigurationService(applicationConfig); + var shift = new Shift + { + MorningTime = "06:00-12:30", + AfternoonTime = "13:00-17:30" + }; + + var shiftTimes = configurationService.ParseShiftTimes(DateTime.Parse("2025-07-13T13:00:00"), shift); + shiftTimes.MorningStart.ShouldBe(T("2025-07-13T06:00:00+02:00")); + shiftTimes.MorningEnd.ShouldBe(T("2025-07-13T12:30:00+02:00")); + shiftTimes.AfternoonStart.ShouldBe(T("2025-07-13T13:00:00+02:00")); + shiftTimes.AfternoonEnd.ShouldBe(T("2025-07-13T17:30:00+02:00")); + } + + [Fact] + public void ParseShiftTimes_WithMorning_ShouldReturnValid() + { + var applicationConfig = new ApplicationConfiguration(); + var configurationService = new ConfigurationService(applicationConfig); + var shift = new Shift + { + MorningTime = "06:00-12:30" + }; + + var shiftTimes = configurationService.ParseShiftTimes(DateTime.Parse("2025-07-13T13:00:00"), shift); + shiftTimes.MorningStart.ShouldBe(T("2025-07-13T06:00:00+02:00")); + shiftTimes.MorningEnd.ShouldBe(T("2025-07-13T12:30:00+02:00")); + shiftTimes.AfternoonStart.ShouldBeNull(); + shiftTimes.AfternoonEnd.ShouldBeNull(); + } + + private DateTimeOffset T(string dateTimeString) + { + return DateTimeOffset.Parse(dateTimeString); + } +} \ No newline at end of file diff --git a/ShiftScheduler.Services.Tests/TransportApiServiceTests.cs b/ShiftScheduler.Services.Tests/TransportApiServiceTests.cs index e9487ca..dbe0a32 100644 --- a/ShiftScheduler.Services.Tests/TransportApiServiceTests.cs +++ b/ShiftScheduler.Services.Tests/TransportApiServiceTests.cs @@ -58,17 +58,10 @@ public class TransportApiServiceTests // Assert result.ShouldNotBeNull(); - result.DepartureTime.ShouldBe("2023-12-15T06:45:00"); - result.ArrivalTime.ShouldBe("2023-12-15T07:30:00"); - result.Duration.ShouldBe("00:45:00"); + result.DepartureTime.ShouldBe(T("2023-12-15T06:45:00")); + result.ArrivalTime.ShouldBe(T("2023-12-15T07:30:00")); + result.Duration.ShouldBe(TimeSpan.Parse("00:45:00")); result.Platform.ShouldBe("5"); - result.Sections.ShouldNotBeEmpty(); - result.Sections.Count.ShouldBe(1); - - var section = result.Sections.First(); - section.Journey?.Name.ShouldBe("IC 1"); - section.Journey?.Category.ShouldBe("IC"); - section.Journey?.Number.ShouldBe("1"); } [Fact] @@ -122,7 +115,7 @@ public class TransportApiServiceTests // Latest acceptable arrival: 8:00 - 30 min = 7:30 // Valid connections: 07:15 and 07:25 (both arrive before 7:30) // Algorithm should return the latest valid: 07:25 - result.ArrivalTime.ShouldBe("2023-12-15T07:25:00"); + result.ArrivalTime.ShouldBe(T("2023-12-15T07:25:00")); } private void SetupHttpMockResponse(HttpStatusCode statusCode, string content) @@ -158,7 +151,7 @@ public class TransportApiServiceTests Arrival = "2023-12-15T07:30:00", Platform = "3" }, - Duration = "00:45:00", + Duration = "00d00:45:00", Sections = new List { new TransportApiSection @@ -209,7 +202,7 @@ public class TransportApiServiceTests Arrival = "2023-12-15T07:15:00", Platform = "3" }, - Duration = "00:45:00" + Duration = "00d00:45:00" }, // Later valid connection - should be selected as best new TransportApiConnection @@ -226,7 +219,7 @@ public class TransportApiServiceTests Arrival = "2023-12-15T07:25:00", Platform = "2" }, - Duration = "00:45:00" + Duration = "00d00:45:00" }, // Too late connection - invalid (arrives after 07:30 which is shift start - 30 min buffer) new TransportApiConnection @@ -243,9 +236,14 @@ public class TransportApiServiceTests Arrival = "2023-12-15T07:35:00", Platform = "1" }, - Duration = "00:45:00" + Duration = "00d00:45:00" } } }; } + + private DateTimeOffset T(string dateTimeString) + { + return DateTimeOffset.Parse(dateTimeString); + } } \ No newline at end of file diff --git a/ShiftScheduler.Services.Tests/TransportConnectionCalculatorTests.cs b/ShiftScheduler.Services.Tests/TransportConnectionCalculatorTests.cs index d5c9a42..892ae2d 100644 --- a/ShiftScheduler.Services.Tests/TransportConnectionCalculatorTests.cs +++ b/ShiftScheduler.Services.Tests/TransportConnectionCalculatorTests.cs @@ -1,4 +1,5 @@ -using ShiftScheduler.Services; +using Microsoft.Extensions.Logging; +using Moq; using ShiftScheduler.Shared; using Shouldly; @@ -6,26 +7,29 @@ namespace ShiftScheduler.Services.Tests; public class TransportConnectionCalculatorTests { - // Tests for new enhanced logic + private readonly Mock loggerMock = new(); + [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 + new(arrivalTime: "2023-12-15T07:15:00"), + new(arrivalTime: "2023-12-15T07:25:00"), // Should be selected (latest valid) + new(arrivalTime: "2023-12-15T08:05:00") // Too late }; - var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); + var shiftStartTime = T("2023-12-15T08:00:00"); // Act var result = TransportConnectionCalculator.FindBestConnection( - connections, shiftStartTime, 30, 60, 15); + connections, + new ConnectionPickArgument(shiftStartTime, 30, 60, 15), + loggerMock.Object); // Assert result.ShouldNotBeNull(); - result.ArrivalTime.ShouldBe("2023-12-15T07:25:00"); + result.ArrivalTime.ShouldBe(T("2023-12-15T07:25:00")); } [Fact] @@ -34,18 +38,20 @@ public class TransportConnectionCalculatorTests // 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) + new(arrivalTime: "2023-12-15T06:45:00"), // Too early (more than 60 min before 8:00) + new(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); + var shiftStartTime = T("2023-12-15T08:00:00"); // Act var result = TransportConnectionCalculator.FindBestConnection( - connections, shiftStartTime, 30, 60, 15); + connections, + new ConnectionPickArgument(shiftStartTime, 30, 60, 15), + loggerMock.Object); // Assert result.ShouldNotBeNull(); - result.ArrivalTime.ShouldBe("2023-12-15T08:05:00"); + result.ArrivalTime.ShouldBe(T("2023-12-15T08:05:00")); } [Fact] @@ -54,19 +60,21 @@ public class TransportConnectionCalculatorTests // 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" } + new(arrivalTime: "2023-12-15T08:10:00"), + new(arrivalTime: "2023-12-15T08:05:00"), // Should be selected (earliest late) + new(arrivalTime: "2023-12-15T08:12:00") }; - var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); + var shiftStartTime = T("2023-12-15T08:00:00"); // Act var result = TransportConnectionCalculator.FindBestConnection( - connections, shiftStartTime, 30, 60, 15); + connections, + new ConnectionPickArgument(shiftStartTime, 30, 60, 15), + loggerMock.Object); // Assert result.ShouldNotBeNull(); - result.ArrivalTime.ShouldBe("2023-12-15T08:05:00"); + result.ArrivalTime.ShouldBe(T("2023-12-15T08:05:00")); } [Fact] @@ -75,15 +83,17 @@ public class TransportConnectionCalculatorTests // 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" } + new(arrivalTime: "2023-12-15T08:20:00"), + new(arrivalTime: "2023-12-15T08:25:00"), + new(arrivalTime: "2023-12-15T08:30:00") }; - var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); + var shiftStartTime = T("2023-12-15T08:00:00"); // Act var result = TransportConnectionCalculator.FindBestConnection( - connections, shiftStartTime, 30, 60, 15); + connections, + new ConnectionPickArgument(shiftStartTime, 30, 60, 15), + loggerMock.Object); // Assert result.ShouldBeNull(); @@ -95,30 +105,33 @@ public class TransportConnectionCalculatorTests // 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 + new(arrivalTime: "2023-12-15T07:20:00"), // 40 min before shift - acceptable + new(arrivalTime: "2023-12-15T08:05:00") // 5 min after shift }; - var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); + var shiftStartTime = T("2023-12-15T08:00:00"); // Act var result = TransportConnectionCalculator.FindBestConnection( - connections, shiftStartTime, 30, 60, 15); + connections, + new ConnectionPickArgument(shiftStartTime, 30, 60, 15), + loggerMock.Object); // Assert result.ShouldNotBeNull(); - result.ArrivalTime.ShouldBe("2023-12-15T07:20:00"); + result.ArrivalTime.ShouldBe(T("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); + var shiftStartTime = T("2023-12-15T08:00:00"); // Act var result = TransportConnectionCalculator.FindBestConnection( - connections, shiftStartTime, 30, 60, 15); + new List(), + new ConnectionPickArgument(shiftStartTime, 30, 60, 15), + loggerMock.Object); // Assert result.ShouldBeNull(); @@ -130,19 +143,48 @@ public class TransportConnectionCalculatorTests // 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 + new(arrivalTime: "2023-12-15T06:30:00"), // Too early (90 min before shift) + new(arrivalTime: "2023-12-15T07:25:00"), // Valid but would be considered too early + new(arrivalTime: "2023-12-15T08:05:00"), // Late but acceptable + new(arrivalTime: "2023-12-15T08:20:00") // Too late }; - var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); + var shiftStartTime = T("2023-12-15T08:00:00"); // Act - MaxEarlyArrivalMinutes = 30, so earliest acceptable is 7:30 var result = TransportConnectionCalculator.FindBestConnection( - connections, shiftStartTime, 30, 30, 15); + connections, + new ConnectionPickArgument(shiftStartTime, 30, 30, 15), + loggerMock.Object); // 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"); + result.ArrivalTime.ShouldBe(T("2023-12-15T08:05:00")); + } + + [Fact] + public void FindBestConnectionEnhanced_WithMyCase_ShouldChooseBasedOnTiming() + { + var connections = new List + { + new(arrivalTime: "2025-09-01T11:41:00"), + new(arrivalTime: "2025-09-01T12:41:00"), + new(arrivalTime: "2025-09-01T13:41:00"), + new(arrivalTime: "2025-09-01T14:41:00"), + new(arrivalTime: "2025-09-01T15:41:00") + }; + var shiftStartTime = T("2025-09-01T15:30:00"); + + var result = TransportConnectionCalculator.FindBestConnection( + connections, + new ConnectionPickArgument(shiftStartTime, 10, 60, 15), + loggerMock.Object); + + result.ShouldNotBeNull(); + result.ArrivalTime.ShouldBe(T("2025-09-01T14:41:00")); + } + + private DateTimeOffset T(string dateTimeString) + { + return DateTimeOffset.Parse(dateTimeString); } } \ No newline at end of file diff --git a/ShiftScheduler.Services.Tests/TransportServiceTests.cs b/ShiftScheduler.Services.Tests/TransportServiceTests.cs index 516b1bc..4d47771 100644 --- a/ShiftScheduler.Services.Tests/TransportServiceTests.cs +++ b/ShiftScheduler.Services.Tests/TransportServiceTests.cs @@ -46,9 +46,9 @@ public class TransportServiceTests var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); var connection = new TransportConnection { - DepartureTime = "2023-12-15T06:45:00", - ArrivalTime = "2023-12-15T07:30:00", - Duration = "00:45:00", + DepartureTime = T("2023-12-15T06:45:00"), + ArrivalTime = T("2023-12-15T07:30:00"), + Duration = TimeSpan.Parse("00:45:00"), Platform = "5" }; @@ -100,11 +100,11 @@ public class TransportServiceTests public async Task GetConnectionAsync_WithDifferentDates_ShouldCreateSeparateCacheEntries() { // Arrange - var shiftStartTime1 = new DateTime(2023, 12, 15, 8, 0, 0); - var shiftStartTime2 = new DateTime(2023, 12, 16, 8, 0, 0); + var shiftStartTime1 = T("2023-12-15T08:00:00"); + var shiftStartTime2 = T("2023-12-16T08:00:00"); - var connection1 = new TransportConnection { ArrivalTime = "2023-12-15T07:30:00" }; - var connection2 = new TransportConnection { ArrivalTime = "2023-12-16T07:30:00" }; + var connection1 = new TransportConnection { ArrivalTime = T("2023-12-15T07:30:00") }; + var connection2 = new TransportConnection { ArrivalTime = T("2023-12-16T07:30:00") }; _transportServiceMock .Setup(x => x.GetConnectionAsync(shiftStartTime1)) @@ -121,10 +121,15 @@ public class TransportServiceTests // Assert result1.ShouldNotBeNull(); result2.ShouldNotBeNull(); - result1.ArrivalTime.ShouldBe("2023-12-15T07:30:00"); - result2.ArrivalTime.ShouldBe("2023-12-16T07:30:00"); + result1.ArrivalTime.ShouldBe(T("2023-12-15T07:30:00")); + result2.ArrivalTime.ShouldBe(T("2023-12-16T07:30:00")); // Verify that transport service was called twice (different cache keys) - _transportServiceMock.Verify(x => x.GetConnectionAsync(It.IsAny()), Times.Exactly(2)); + _transportServiceMock.Verify(x => x.GetConnectionAsync(It.IsAny()), Times.Exactly(2)); + } + + private DateTimeOffset T(string dateTimeString) + { + return DateTimeOffset.Parse(dateTimeString); } } \ No newline at end of file