Change strings to date time offset

This commit is contained in:
Claudio Schaad 2025-09-03 21:12:10 +02:00
parent cc81cacda8
commit 3ad9e48fcd
11 changed files with 79 additions and 71 deletions

View file

@ -4,11 +4,11 @@ namespace ShiftScheduler.Services
{ {
public interface ITransportService public interface ITransportService
{ {
Task<TransportConnection?> GetConnectionAsync(DateTime shiftStartTime); Task<TransportConnection?> GetConnectionAsync(DateTimeOffset shiftStartTime);
} }
public interface ITransportApiService public interface ITransportApiService
{ {
Task<TransportConnection?> GetConnectionAsync(DateTime shiftStartTime); Task<TransportConnection?> GetConnectionAsync(DateTimeOffset shiftStartTime);
} }
} }

View file

@ -26,7 +26,7 @@ namespace ShiftScheduler.Services
var summary = $"{shift.Name} (Morning)"; var summary = $"{shift.Name} (Morning)";
var description = ""; var description = "";
if (shiftWithTransport.MorningTransport != null && !string.IsNullOrEmpty(shiftWithTransport.MorningTransport.DepartureTime)) if (shiftWithTransport.MorningTransport != null)
{ {
var transportSummary = FormatTransportInfo(shiftWithTransport.MorningTransport); var transportSummary = FormatTransportInfo(shiftWithTransport.MorningTransport);
description = $"Transport: {transportSummary}"; description = $"Transport: {transportSummary}";
@ -47,7 +47,7 @@ namespace ShiftScheduler.Services
var summary = $"{shift.Name} (Afternoon)"; var summary = $"{shift.Name} (Afternoon)";
var description = ""; var description = "";
if (shiftWithTransport.AfternoonTransport != null && !string.IsNullOrEmpty(shiftWithTransport.AfternoonTransport.DepartureTime)) if (shiftWithTransport.AfternoonTransport != null)
{ {
var transportSummary = FormatTransportInfo(shiftWithTransport.AfternoonTransport); var transportSummary = FormatTransportInfo(shiftWithTransport.AfternoonTransport);
description = $"Transport: {transportSummary}"; description = $"Transport: {transportSummary}";
@ -68,9 +68,8 @@ namespace ShiftScheduler.Services
private string FormatTransportInfo(TransportConnection transport) private string FormatTransportInfo(TransportConnection transport)
{ {
var departure = DateTime.TryParse(transport.DepartureTime, out var dep) ? dep.ToString("HH:mm") : transport.DepartureTime; var departure =transport.DepartureTime.ToString("HH:mm");
var arrival = DateTime.TryParse(transport.ArrivalTime, out var arr) ? arr.ToString("HH:mm") : transport.ArrivalTime; var arrival = transport.ArrivalTime.ToString("HH:mm");
return $"{departure} → {arrival}"; return $"{departure} → {arrival}";
} }
} }

View file

@ -176,13 +176,13 @@ namespace ShiftScheduler.Services
var transportLines = new List<string>(); var transportLines = new List<string>();
if (shiftWithTransport.MorningTransport != null && !string.IsNullOrEmpty(shiftWithTransport.MorningTransport.DepartureTime)) if (shiftWithTransport.MorningTransport != null)
{ {
var morningInfo = FormatTransportConnection(shiftWithTransport.MorningTransport); var morningInfo = FormatTransportConnection(shiftWithTransport.MorningTransport);
transportLines.Add(morningInfo); transportLines.Add(morningInfo);
} }
if (shiftWithTransport.AfternoonTransport != null && !string.IsNullOrEmpty(shiftWithTransport.AfternoonTransport.DepartureTime)) if (shiftWithTransport.AfternoonTransport != null)
{ {
var afternoonInfo = FormatTransportConnection(shiftWithTransport.AfternoonTransport); var afternoonInfo = FormatTransportConnection(shiftWithTransport.AfternoonTransport);
transportLines.Add(afternoonInfo); transportLines.Add(afternoonInfo);
@ -193,9 +193,8 @@ namespace ShiftScheduler.Services
private static string FormatTransportConnection(TransportConnection transport) private static string FormatTransportConnection(TransportConnection transport)
{ {
var departure = DateTime.TryParse(transport.DepartureTime, out var dep) ? dep.ToString("HH:mm") : transport.DepartureTime; var departure = transport.DepartureTime.ToString("HH:mm");
var arrival = DateTime.TryParse(transport.ArrivalTime, out var arr) ? arr.ToString("HH:mm") : transport.ArrivalTime; var arrival = transport.ArrivalTime.ToString("HH:mm");
return $"🚂 {departure}→{arrival}"; return $"🚂 {departure}→{arrival}";
} }

View file

@ -1,3 +1,4 @@
using System.Globalization;
using System.Text.Json; using System.Text.Json;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using ShiftScheduler.Shared; using ShiftScheduler.Shared;
@ -6,7 +7,7 @@ namespace ShiftScheduler.Services
{ {
public class TransportApiService(HttpClient httpClient, IConfigurationService configurationService, ILogger<TransportApiService> logger) : ITransportApiService public class TransportApiService(HttpClient httpClient, IConfigurationService configurationService, ILogger<TransportApiService> logger) : ITransportApiService
{ {
public async Task<TransportConnection?> GetConnectionAsync(DateTime shiftStartTime) public async Task<TransportConnection?> GetConnectionAsync(DateTimeOffset shiftStartTime)
{ {
var config = configurationService.GetTransportConfiguration(); var config = configurationService.GetTransportConfiguration();
@ -45,11 +46,12 @@ namespace ShiftScheduler.Services
if (apiConnection == null) if (apiConnection == null)
return new TransportConnection(); return new TransportConnection();
var formats = new[] { @"dd'd'hh\:mm\:ss", @"d'd'hh\:mm\:ss" };
return new TransportConnection return new TransportConnection
{ {
DepartureTime = apiConnection.From?.Departure ?? string.Empty, DepartureTime = DateTimeOffset.Parse(apiConnection.From!.Departure),
ArrivalTime = apiConnection.To?.Arrival ?? string.Empty, ArrivalTime = DateTimeOffset.Parse(apiConnection.To!.Arrival),
Duration = apiConnection.Duration, Duration = TimeSpan.ParseExact(apiConnection.Duration, formats, CultureInfo.InvariantCulture),
Platform = apiConnection.From?.Platform ?? string.Empty, Platform = apiConnection.From?.Platform ?? string.Empty,
}; };
} }

View file

@ -1,11 +1,10 @@
using System.Globalization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using ShiftScheduler.Shared; using ShiftScheduler.Shared;
namespace ShiftScheduler.Services; namespace ShiftScheduler.Services;
public record ConnectionPickArgument( public record ConnectionPickArgument(
DateTime ShiftStartTime, DateTimeOffset ShiftStartTime,
int SafetyBufferMinutes, int SafetyBufferMinutes,
int MaxEarlyArrivalMinutes, int MaxEarlyArrivalMinutes,
int MaxLateArrivalMinutes) int MaxLateArrivalMinutes)
@ -35,35 +34,31 @@ public static class TransportConnectionCalculator
foreach (var connection in connections) foreach (var connection in connections)
{ {
if (DateTime.TryParse(connection.ArrivalTime, out var arrivalTime)) if (connection.ArrivalTime <= latestArrivalTime)
{
if (arrivalTime <= latestArrivalTime)
{ {
validConnections.Add(connection); validConnections.Add(connection);
} }
else if (arrivalTime <= latestAcceptableTime) else if (connection.ArrivalTime <= latestAcceptableTime)
{ {
lateValidConnections.Add(connection); lateValidConnections.Add(connection);
} }
} }
}
// If we have valid connections (arriving before latest arrival time) // If we have valid connections (arriving before latest arrival time)
if (validConnections.Count > 0) if (validConnections.Count > 0)
{ {
var sortedValid = validConnections var sortedValid = validConnections
.OrderBy(c => DateTime.Parse(c.ArrivalTime ?? "00:00")) .OrderBy(c => c.ArrivalTime)
.ToList(); .ToList();
var bestValidConnection = 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) // Check if the best valid connection arrives too early (more than maxEarlyArrivalMinutes before shift)
if (bestValidArrivalTime < earliestAcceptableTime && lateValidConnections.Count > 0) if (bestValidConnection.ArrivalTime < earliestAcceptableTime && lateValidConnections.Count > 0)
{ {
// Return the earliest connection that arrives after latest arrival time but within acceptable range // Return the earliest connection that arrives after latest arrival time but within acceptable range
var sortedLateValid = lateValidConnections var sortedLateValid = lateValidConnections
.OrderBy(c => DateTime.Parse(c.ArrivalTime ?? "23:59")) .OrderBy(c => c.ArrivalTime)
.ToList(); .ToList();
return sortedLateValid.First(); return sortedLateValid.First();
@ -76,7 +71,7 @@ public static class TransportConnectionCalculator
if (lateValidConnections.Count > 0) if (lateValidConnections.Count > 0)
{ {
var sortedLateValid = lateValidConnections var sortedLateValid = lateValidConnections
.OrderBy(c => DateTime.Parse(c.ArrivalTime ?? "23:59")) .OrderBy(c => c.ArrivalTime)
.ToList(); .ToList();
return sortedLateValid.First(); return sortedLateValid.First();

View file

@ -5,7 +5,7 @@ namespace ShiftScheduler.Services
{ {
public class TransportService(ITransportApiService transportService, IConfigurationService configurationService, IMemoryCache cache) : ITransportService public class TransportService(ITransportApiService transportService, IConfigurationService configurationService, IMemoryCache cache) : ITransportService
{ {
public async Task<TransportConnection?> GetConnectionAsync(DateTime shiftStartTime) public async Task<TransportConnection?> GetConnectionAsync(DateTimeOffset shiftStartTime)
{ {
var config = configurationService.GetTransportConfiguration(); var config = configurationService.GetTransportConfiguration();
var searchDate = shiftStartTime.ToString("yyyy-MM-dd"); var searchDate = shiftStartTime.ToString("yyyy-MM-dd");

View file

@ -19,10 +19,8 @@ namespace ShiftScheduler.Shared
private string FormatTransportSummary(TransportConnection connection) private string FormatTransportSummary(TransportConnection connection)
{ {
if (string.IsNullOrEmpty(connection.DepartureTime)) return string.Empty; var departure = connection.DepartureTime.ToString("HH:mm");
var arrival = connection.ArrivalTime.ToString("HH:mm");
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;
return $"{departure}→{arrival}"; return $"{departure}→{arrival}";
} }
} }

View file

@ -20,12 +20,12 @@ namespace ShiftScheduler.Shared
public TransportConnection(string arrivalTime) public TransportConnection(string arrivalTime)
{ {
ArrivalTime = arrivalTime; ArrivalTime = DateTimeOffset.Parse(arrivalTime);
} }
public string DepartureTime { get; set; } = string.Empty; public DateTimeOffset DepartureTime { get; set; }
public string ArrivalTime { get; set; } = string.Empty; public DateTimeOffset ArrivalTime { get; set; }
public string Duration { get; set; } = string.Empty; public TimeSpan Duration { get; set; }
public string? Platform { get; set; } = string.Empty; public string? Platform { get; set; } = string.Empty;
public override string ToString() public override string ToString()

View file

@ -58,9 +58,9 @@ public class TransportApiServiceTests
// Assert // Assert
result.ShouldNotBeNull(); result.ShouldNotBeNull();
result.DepartureTime.ShouldBe("2023-12-15T06:45:00"); result.DepartureTime.ShouldBe(T("2023-12-15T06:45:00"));
result.ArrivalTime.ShouldBe("2023-12-15T07:30:00"); result.ArrivalTime.ShouldBe(T("2023-12-15T07:30:00"));
result.Duration.ShouldBe("00:45:00"); result.Duration.ShouldBe(TimeSpan.Parse("00:45:00"));
result.Platform.ShouldBe("5"); result.Platform.ShouldBe("5");
} }
@ -115,7 +115,7 @@ public class TransportApiServiceTests
// Latest acceptable arrival: 8:00 - 30 min = 7:30 // Latest acceptable arrival: 8:00 - 30 min = 7:30
// Valid connections: 07:15 and 07:25 (both arrive before 7:30) // Valid connections: 07:15 and 07:25 (both arrive before 7:30)
// Algorithm should return the latest valid: 07:25 // 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) private void SetupHttpMockResponse(HttpStatusCode statusCode, string content)
@ -151,7 +151,7 @@ public class TransportApiServiceTests
Arrival = "2023-12-15T07:30:00", Arrival = "2023-12-15T07:30:00",
Platform = "3" Platform = "3"
}, },
Duration = "00:45:00", Duration = "00d00:45:00",
Sections = new List<TransportApiSection> Sections = new List<TransportApiSection>
{ {
new TransportApiSection new TransportApiSection
@ -202,7 +202,7 @@ public class TransportApiServiceTests
Arrival = "2023-12-15T07:15:00", Arrival = "2023-12-15T07:15:00",
Platform = "3" Platform = "3"
}, },
Duration = "00:45:00" Duration = "00d00:45:00"
}, },
// Later valid connection - should be selected as best // Later valid connection - should be selected as best
new TransportApiConnection new TransportApiConnection
@ -219,7 +219,7 @@ public class TransportApiServiceTests
Arrival = "2023-12-15T07:25:00", Arrival = "2023-12-15T07:25:00",
Platform = "2" 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) // Too late connection - invalid (arrives after 07:30 which is shift start - 30 min buffer)
new TransportApiConnection new TransportApiConnection
@ -236,9 +236,14 @@ public class TransportApiServiceTests
Arrival = "2023-12-15T07:35:00", Arrival = "2023-12-15T07:35:00",
Platform = "1" Platform = "1"
}, },
Duration = "00:45:00" Duration = "00d00:45:00"
} }
} }
}; };
} }
private DateTimeOffset T(string dateTimeString)
{
return DateTimeOffset.Parse(dateTimeString);
}
} }

View file

@ -20,7 +20,7 @@ public class TransportConnectionCalculatorTests
new(arrivalTime: "2023-12-15T07:25:00"), // Should be selected (latest valid) new(arrivalTime: "2023-12-15T07:25:00"), // Should be selected (latest valid)
new(arrivalTime: "2023-12-15T08:05:00") // Too late 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 // Act
var result = TransportConnectionCalculator.FindBestConnection( var result = TransportConnectionCalculator.FindBestConnection(
@ -30,7 +30,7 @@ public class TransportConnectionCalculatorTests
// Assert // Assert
result.ShouldNotBeNull(); result.ShouldNotBeNull();
result.ArrivalTime.ShouldBe("2023-12-15T07:25:00"); result.ArrivalTime.ShouldBe(T("2023-12-15T07:25:00"));
} }
[Fact] [Fact]
@ -42,7 +42,7 @@ public class TransportConnectionCalculatorTests
new(arrivalTime: "2023-12-15T06:45:00"), // Too early (more than 60 min before 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) 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 // Act
var result = TransportConnectionCalculator.FindBestConnection( var result = TransportConnectionCalculator.FindBestConnection(
@ -52,7 +52,7 @@ public class TransportConnectionCalculatorTests
// Assert // Assert
result.ShouldNotBeNull(); result.ShouldNotBeNull();
result.ArrivalTime.ShouldBe("2023-12-15T08:05:00"); result.ArrivalTime.ShouldBe(T("2023-12-15T08:05:00"));
} }
[Fact] [Fact]
@ -65,7 +65,7 @@ public class TransportConnectionCalculatorTests
new(arrivalTime: "2023-12-15T08:05:00"), // Should be selected (earliest late) new(arrivalTime: "2023-12-15T08:05:00"), // Should be selected (earliest late)
new(arrivalTime: "2023-12-15T08:12:00") 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 // Act
var result = TransportConnectionCalculator.FindBestConnection( var result = TransportConnectionCalculator.FindBestConnection(
@ -75,7 +75,7 @@ public class TransportConnectionCalculatorTests
// Assert // Assert
result.ShouldNotBeNull(); result.ShouldNotBeNull();
result.ArrivalTime.ShouldBe("2023-12-15T08:05:00"); result.ArrivalTime.ShouldBe(T("2023-12-15T08:05:00"));
} }
[Fact] [Fact]
@ -88,7 +88,7 @@ public class TransportConnectionCalculatorTests
new(arrivalTime: "2023-12-15T08:25:00"), new(arrivalTime: "2023-12-15T08:25:00"),
new(arrivalTime: "2023-12-15T08:30: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 // Act
var result = TransportConnectionCalculator.FindBestConnection( var result = TransportConnectionCalculator.FindBestConnection(
@ -109,7 +109,7 @@ public class TransportConnectionCalculatorTests
new(arrivalTime: "2023-12-15T07:20:00"), // 40 min before shift - acceptable new(arrivalTime: "2023-12-15T07:20:00"), // 40 min before shift - acceptable
new(arrivalTime: "2023-12-15T08:05:00") // 5 min after shift 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 // Act
var result = TransportConnectionCalculator.FindBestConnection( var result = TransportConnectionCalculator.FindBestConnection(
@ -119,14 +119,14 @@ public class TransportConnectionCalculatorTests
// Assert // Assert
result.ShouldNotBeNull(); result.ShouldNotBeNull();
result.ArrivalTime.ShouldBe("2023-12-15T07:20:00"); result.ArrivalTime.ShouldBe(T("2023-12-15T07:20:00"));
} }
[Fact] [Fact]
public void FindBestConnectionEnhanced_WithEmptyConnections_ShouldReturnNull() public void FindBestConnectionEnhanced_WithEmptyConnections_ShouldReturnNull()
{ {
// Arrange // Arrange
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); var shiftStartTime = T("2023-12-15T08:00:00");
// Act // Act
var result = TransportConnectionCalculator.FindBestConnection( var result = TransportConnectionCalculator.FindBestConnection(
@ -149,7 +149,7 @@ public class TransportConnectionCalculatorTests
new(arrivalTime: "2023-12-15T08:05:00"), // Late but acceptable new(arrivalTime: "2023-12-15T08:05:00"), // Late but acceptable
new(arrivalTime: "2023-12-15T08:20:00") // Too late 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 // Act - MaxEarlyArrivalMinutes = 30, so earliest acceptable is 7:30
var result = TransportConnectionCalculator.FindBestConnection( var result = TransportConnectionCalculator.FindBestConnection(
@ -159,6 +159,11 @@ public class TransportConnectionCalculatorTests
// Assert - 7:25 arrives at 7:25, which is 35 min before shift (more than 30 min), so should prefer late connection // 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.ShouldNotBeNull();
result.ArrivalTime.ShouldBe("2023-12-15T08:05:00"); result.ArrivalTime.ShouldBe(T("2023-12-15T08:05:00"));
}
private DateTimeOffset T(string dateTimeString)
{
return DateTimeOffset.Parse(dateTimeString);
} }
} }

View file

@ -46,9 +46,9 @@ public class TransportServiceTests
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0); var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
var connection = new TransportConnection var connection = new TransportConnection
{ {
DepartureTime = "2023-12-15T06:45:00", DepartureTime = T("2023-12-15T06:45:00"),
ArrivalTime = "2023-12-15T07:30:00", ArrivalTime = T("2023-12-15T07:30:00"),
Duration = "00:45:00", Duration = TimeSpan.Parse("00:45:00"),
Platform = "5" Platform = "5"
}; };
@ -100,11 +100,11 @@ public class TransportServiceTests
public async Task GetConnectionAsync_WithDifferentDates_ShouldCreateSeparateCacheEntries() public async Task GetConnectionAsync_WithDifferentDates_ShouldCreateSeparateCacheEntries()
{ {
// Arrange // Arrange
var shiftStartTime1 = new DateTime(2023, 12, 15, 8, 0, 0); var shiftStartTime1 = T("2023-12-15T08:00:00");
var shiftStartTime2 = new DateTime(2023, 12, 16, 8, 0, 0); var shiftStartTime2 = T("2023-12-16T08:00:00");
var connection1 = new TransportConnection { ArrivalTime = "2023-12-15T07:30:00" }; var connection1 = new TransportConnection { ArrivalTime = T("2023-12-15T07:30:00") };
var connection2 = new TransportConnection { ArrivalTime = "2023-12-16T07:30:00" }; var connection2 = new TransportConnection { ArrivalTime = T("2023-12-16T07:30:00") };
_transportServiceMock _transportServiceMock
.Setup(x => x.GetConnectionAsync(shiftStartTime1)) .Setup(x => x.GetConnectionAsync(shiftStartTime1))
@ -121,10 +121,15 @@ public class TransportServiceTests
// Assert // Assert
result1.ShouldNotBeNull(); result1.ShouldNotBeNull();
result2.ShouldNotBeNull(); result2.ShouldNotBeNull();
result1.ArrivalTime.ShouldBe("2023-12-15T07:30:00"); result1.ArrivalTime.ShouldBe(T("2023-12-15T07:30:00"));
result2.ArrivalTime.ShouldBe("2023-12-16T07:30:00"); result2.ArrivalTime.ShouldBe(T("2023-12-16T07:30:00"));
// Verify that transport service was called twice (different cache keys) // Verify that transport service was called twice (different cache keys)
_transportServiceMock.Verify(x => x.GetConnectionAsync(It.IsAny<DateTime>()), Times.Exactly(2)); _transportServiceMock.Verify(x => x.GetConnectionAsync(It.IsAny<DateTimeOffset>()), Times.Exactly(2));
}
private DateTimeOffset T(string dateTimeString)
{
return DateTimeOffset.Parse(dateTimeString);
} }
} }