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
This commit is contained in:
parent
3c01b33070
commit
a1f9f1313e
15 changed files with 280 additions and 258 deletions
|
|
@ -15,7 +15,7 @@ namespace ShiftScheduler.Client.Pages
|
|||
private Dictionary<DateTime, string> SelectedSchedule { get; set; } = new();
|
||||
private Dictionary<DateTime, ShiftWithTransport> SelectedShiftsWithTransport { get; set; } = new();
|
||||
|
||||
private bool _isCurrentMonth = false;
|
||||
private bool _isCurrentMonth = true;
|
||||
private bool _isLoadingTransport = false;
|
||||
private bool _isLoadingInitial = false;
|
||||
private bool _showConfigDialog = false;
|
||||
|
|
|
|||
|
|
@ -46,13 +46,14 @@ namespace ShiftScheduler.Server.Controllers
|
|||
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<ShiftWithTransport> shiftsWithTransport)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,12 +12,15 @@ namespace ShiftScheduler.Services
|
|||
List<Shift> GetShifts();
|
||||
TransportConfiguration GetTransportConfiguration();
|
||||
|
||||
// Schedule persistence methods
|
||||
Task SaveScheduleAsync(int year, int month, Dictionary<DateTime, string> schedule);
|
||||
Task<Dictionary<DateTime, string>> 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");
|
||||
|
|
@ -202,6 +207,46 @@ namespace ShiftScheduler.Services
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ namespace ShiftScheduler.Services
|
|||
{
|
||||
public interface ITransportService
|
||||
{
|
||||
Task<TransportConnection?> GetConnectionAsync(DateTime shiftStartTime);
|
||||
Task<TransportConnection?> GetConnectionAsync(DateTimeOffset shiftStartTime);
|
||||
}
|
||||
|
||||
public interface ITransportApiService
|
||||
{
|
||||
Task<TransportConnection?> GetConnectionAsync(DateTime shiftStartTime);
|
||||
Task<TransportConnection?> GetConnectionAsync(DateTimeOffset shiftStartTime);
|
||||
}
|
||||
}
|
||||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,13 +176,13 @@ namespace ShiftScheduler.Services
|
|||
|
||||
var transportLines = new List<string>();
|
||||
|
||||
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}";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<TransportApiService> logger) : ITransportApiService
|
||||
{
|
||||
public async Task<TransportConnection?> GetConnectionAsync(DateTime shiftStartTime)
|
||||
public async Task<TransportConnection?> 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()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TransportConnection> connections,
|
||||
DateTime shiftStartTime,
|
||||
int safetyBufferMinutes,
|
||||
int maxEarlyArrivalMinutes,
|
||||
int maxLateArrivalMinutes)
|
||||
IReadOnlyList<TransportConnection> 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<TransportConnection>();
|
||||
var lateValidConnections = new List<TransportConnection>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ namespace ShiftScheduler.Services
|
|||
{
|
||||
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 searchDate = shiftStartTime.ToString("yyyy-MM-dd");
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TransportSection> 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;
|
||||
}
|
||||
}
|
||||
47
ShiftScheduler.Services.Tests/ConfigurationServiceTests.cs
Normal file
47
ShiftScheduler.Services.Tests/ConfigurationServiceTests.cs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TransportApiSection>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ILogger> 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<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
|
||||
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<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)
|
||||
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<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" }
|
||||
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<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" }
|
||||
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<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
|
||||
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<TransportConnection>();
|
||||
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<TransportConnection>(),
|
||||
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<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
|
||||
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<TransportConnection>
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DateTime>()), Times.Exactly(2));
|
||||
_transportServiceMock.Verify(x => x.GetConnectionAsync(It.IsAny<DateTimeOffset>()), Times.Exactly(2));
|
||||
}
|
||||
|
||||
private DateTimeOffset T(string dateTimeString)
|
||||
{
|
||||
return DateTimeOffset.Parse(dateTimeString);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue