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, string> SelectedSchedule { get; set; } = new();
|
||||||
private Dictionary<DateTime, ShiftWithTransport> SelectedShiftsWithTransport { 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 _isLoadingTransport = false;
|
||||||
private bool _isLoadingInitial = false;
|
private bool _isLoadingInitial = false;
|
||||||
private bool _showConfigDialog = false;
|
private bool _showConfigDialog = false;
|
||||||
|
|
|
||||||
|
|
@ -46,13 +46,14 @@ namespace ShiftScheduler.Server.Controllers
|
||||||
TransportConnection? morningTransport = null;
|
TransportConnection? morningTransport = null;
|
||||||
TransportConnection? afternoonTransport = null;
|
TransportConnection? afternoonTransport = null;
|
||||||
|
|
||||||
|
var shifTimes = _configurationService.ParseShiftTimes(request.Date, shift);
|
||||||
|
|
||||||
// Get transport for morning shift if it has morning time
|
// Get transport for morning shift if it has morning time
|
||||||
if (!string.IsNullOrEmpty(shift.MorningTime))
|
if (!string.IsNullOrEmpty(shift.MorningTime))
|
||||||
{
|
{
|
||||||
var morningStartTime = ParseShiftTime(request.Date, shift.MorningTime);
|
if (shifTimes.MorningStart.HasValue)
|
||||||
if (morningStartTime.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 both morning and afternoon shifts exist, check break duration
|
||||||
if (!string.IsNullOrEmpty(shift.MorningTime) && !string.IsNullOrEmpty(shift.AfternoonTime))
|
if (!string.IsNullOrEmpty(shift.MorningTime) && !string.IsNullOrEmpty(shift.AfternoonTime))
|
||||||
{
|
{
|
||||||
var morningEndTime = ParseShiftEndTime(request.Date, shift.MorningTime);
|
if (shifTimes.MorningEnd.HasValue && shifTimes.AfternoonStart.HasValue)
|
||||||
var afternoonStartTime = ParseShiftTime(request.Date, shift.AfternoonTime);
|
|
||||||
|
|
||||||
if (morningEndTime.HasValue && afternoonStartTime.HasValue)
|
|
||||||
{
|
{
|
||||||
var breakDurationMinutes = (afternoonStartTime.Value - morningEndTime.Value).TotalMinutes;
|
var breakDurationMinutes = (shifTimes.AfternoonStart.Value - shifTimes.MorningEnd.Value).TotalMinutes;
|
||||||
shouldLoadAfternoonTransport = breakDurationMinutes >= transportConfig.MinBreakMinutes;
|
shouldLoadAfternoonTransport = breakDurationMinutes >= transportConfig.MinBreakMinutes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldLoadAfternoonTransport)
|
if (shouldLoadAfternoonTransport)
|
||||||
{
|
{
|
||||||
var afternoonStartTime = ParseShiftTime(request.Date, shift.AfternoonTime);
|
if (shifTimes.AfternoonStart.HasValue)
|
||||||
if (afternoonStartTime.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);
|
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")]
|
[HttpPost("export_ics")]
|
||||||
public IActionResult ExportIcsWithTransport([FromBody] List<ShiftWithTransport> shiftsWithTransport)
|
public IActionResult ExportIcsWithTransport([FromBody] List<ShiftWithTransport> shiftsWithTransport)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,15 @@ namespace ShiftScheduler.Services
|
||||||
List<Shift> GetShifts();
|
List<Shift> GetShifts();
|
||||||
TransportConfiguration GetTransportConfiguration();
|
TransportConfiguration GetTransportConfiguration();
|
||||||
|
|
||||||
// Schedule persistence methods
|
|
||||||
Task SaveScheduleAsync(int year, int month, Dictionary<DateTime, string> schedule);
|
Task SaveScheduleAsync(int year, int month, Dictionary<DateTime, string> schedule);
|
||||||
Task<Dictionary<DateTime, string>> LoadScheduleAsync(int year, int month);
|
Task<Dictionary<DateTime, string>> LoadScheduleAsync(int year, int month);
|
||||||
Task DeleteScheduleAsync(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
|
public class ConfigurationService : IConfigurationService
|
||||||
{
|
{
|
||||||
private ApplicationConfiguration _configuration;
|
private ApplicationConfiguration _configuration;
|
||||||
|
|
@ -26,6 +29,8 @@ namespace ShiftScheduler.Services
|
||||||
private readonly string _shiftsFilePath;
|
private readonly string _shiftsFilePath;
|
||||||
private readonly string _transportFilePath;
|
private readonly string _transportFilePath;
|
||||||
|
|
||||||
|
readonly TimeZoneInfo localTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Zurich");
|
||||||
|
|
||||||
public ConfigurationService(ApplicationConfiguration initialConfiguration)
|
public ConfigurationService(ApplicationConfiguration initialConfiguration)
|
||||||
{
|
{
|
||||||
_configDirectory = Path.Combine(Directory.GetCurrentDirectory(), "config");
|
_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)
|
private ApplicationConfiguration LoadConfigurationFromFiles(ApplicationConfiguration fallbackConfiguration)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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,15 +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");
|
||||||
|
|
||||||
var mainJourney = transport.Sections?.FirstOrDefault()?.Journey;
|
|
||||||
if (mainJourney != null)
|
|
||||||
{
|
|
||||||
return $"{mainJourney.Category} {mainJourney.Number}: {departure} → {arrival}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"{departure} → {arrival}";
|
return $"{departure} → {arrival}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
@ -28,17 +29,11 @@ namespace ShiftScheduler.Services
|
||||||
|
|
||||||
foreach (var connection in allConnections)
|
foreach (var connection in allConnections)
|
||||||
{
|
{
|
||||||
logger.LogInformation($"Found connection {connection}");
|
logger.LogDebug($"Found connection {connection}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var bestConnection = TransportConnectionCalculator.FindBestConnection(
|
var bestConnection = TransportConnectionCalculator.FindBestConnection(
|
||||||
allConnections,
|
allConnections, new ConnectionPickArgument(shiftStartTime, config.SafetyBufferMinutes, config.MaxEarlyArrivalMinutes, config.MaxLateArrivalMinutes), logger);
|
||||||
shiftStartTime,
|
|
||||||
config.SafetyBufferMinutes,
|
|
||||||
config.MaxEarlyArrivalMinutes,
|
|
||||||
config.MaxLateArrivalMinutes);
|
|
||||||
|
|
||||||
logger.LogInformation($"Found best {bestConnection}");
|
|
||||||
|
|
||||||
return bestConnection;
|
return bestConnection;
|
||||||
}
|
}
|
||||||
|
|
@ -51,43 +46,13 @@ 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,
|
||||||
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;
|
using ShiftScheduler.Shared;
|
||||||
|
|
||||||
namespace ShiftScheduler.Services;
|
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 class TransportConnectionCalculator
|
||||||
{
|
{
|
||||||
public static TransportConnection? FindBestConnection(
|
public static TransportConnection? FindBestConnection(
|
||||||
IReadOnlyList<TransportConnection> connections,
|
IReadOnlyList<TransportConnection> connections, ConnectionPickArgument args, ILogger logger)
|
||||||
DateTime shiftStartTime,
|
|
||||||
int safetyBufferMinutes,
|
|
||||||
int maxEarlyArrivalMinutes,
|
|
||||||
int maxLateArrivalMinutes)
|
|
||||||
{
|
{
|
||||||
var latestArrivalTime = shiftStartTime.AddMinutes(-safetyBufferMinutes);
|
var latestArrivalTime = args.ShiftStartTime.AddMinutes(-args.SafetyBufferMinutes);
|
||||||
var earliestAcceptableTime = shiftStartTime.AddMinutes(-maxEarlyArrivalMinutes);
|
var earliestAcceptableTime = args.ShiftStartTime.AddMinutes(-args.MaxEarlyArrivalMinutes);
|
||||||
var latestAcceptableTime = shiftStartTime.AddMinutes(maxLateArrivalMinutes);
|
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 validConnections = new List<TransportConnection>();
|
||||||
var lateValidConnections = 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);
|
||||||
{
|
}
|
||||||
validConnections.Add(connection);
|
else if (connection.ArrivalTime <= latestAcceptableTime)
|
||||||
}
|
{
|
||||||
else if (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 bestValidConnection = validConnections.Last();
|
||||||
.OrderBy(c => DateTime.Parse(c.ArrivalTime ?? "00:00"))
|
if (bestValidConnection.ArrivalTime > earliestAcceptableTime)
|
||||||
.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)
|
|
||||||
{
|
{
|
||||||
// Return the earliest connection that arrives after latest arrival time but within acceptable range
|
return bestValidConnection;
|
||||||
var sortedLateValid = lateValidConnections
|
|
||||||
.OrderBy(c => DateTime.Parse(c.ArrivalTime ?? "23:59"))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return sortedLateValid.First();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return bestValidConnection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no connections arrive before latest arrival time, check if any arrive within acceptable late range
|
// If no connections arrive before latest arrival time, check if any arrive within acceptable late range
|
||||||
if (lateValidConnections.Count > 0)
|
if (lateValidConnections.Count > 0)
|
||||||
{
|
{
|
||||||
var sortedLateValid = lateValidConnections
|
return lateValidConnections.First();
|
||||||
.OrderBy(c => DateTime.Parse(c.ArrivalTime ?? "23:59"))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return sortedLateValid.First();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No suitable connections found
|
return validConnections.FirstOrDefault();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
namespace ShiftScheduler.Shared
|
namespace ShiftScheduler.Shared
|
||||||
{
|
{
|
||||||
// View model that combines shift information with transport data for a specific date
|
|
||||||
public record ShiftWithTransport
|
public record ShiftWithTransport
|
||||||
{
|
{
|
||||||
public DateTime Date { get; init; }
|
public DateTime Date { get; init; }
|
||||||
|
|
@ -20,15 +19,9 @@ 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;
|
return $"{departure}→{arrival}";
|
||||||
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}";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,43 +14,23 @@ namespace ShiftScheduler.Shared
|
||||||
|
|
||||||
public class TransportConnection
|
public class TransportConnection
|
||||||
{
|
{
|
||||||
public string DepartureTime { get; set; } = string.Empty;
|
public TransportConnection()
|
||||||
public string ArrivalTime { get; set; } = string.Empty;
|
{
|
||||||
public string Duration { get; set; } = string.Empty;
|
}
|
||||||
|
|
||||||
|
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 string? Platform { get; set; } = string.Empty;
|
||||||
public List<TransportSection> Sections { get; set; } = new();
|
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{Platform}: {DepartureTime} - {ArrivalTime} ({Duration})";
|
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
|
// 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");
|
||||||
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]
|
[Fact]
|
||||||
|
|
@ -122,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)
|
||||||
|
|
@ -158,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
|
||||||
|
|
@ -209,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
|
||||||
|
|
@ -226,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
|
||||||
|
|
@ -243,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using ShiftScheduler.Services;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
using ShiftScheduler.Shared;
|
using ShiftScheduler.Shared;
|
||||||
using Shouldly;
|
using Shouldly;
|
||||||
|
|
||||||
|
|
@ -6,26 +7,29 @@ namespace ShiftScheduler.Services.Tests;
|
||||||
|
|
||||||
public class TransportConnectionCalculatorTests
|
public class TransportConnectionCalculatorTests
|
||||||
{
|
{
|
||||||
// Tests for new enhanced logic
|
private readonly Mock<ILogger> loggerMock = new();
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FindBestConnectionEnhanced_WithGoodTimingConnection_ShouldReturnLatestValidConnection()
|
public void FindBestConnectionEnhanced_WithGoodTimingConnection_ShouldReturnLatestValidConnection()
|
||||||
{
|
{
|
||||||
// Arrange - Shift starts at 8:00, safety buffer 30 min, so latest arrival is 7:30
|
// Arrange - Shift starts at 8:00, safety buffer 30 min, so latest arrival is 7:30
|
||||||
var connections = new List<TransportConnection>
|
var connections = new List<TransportConnection>
|
||||||
{
|
{
|
||||||
new TransportConnection { ArrivalTime = "2023-12-15T07:15:00" },
|
new(arrivalTime: "2023-12-15T07:15:00"),
|
||||||
new TransportConnection { ArrivalTime = "2023-12-15T07:25:00" }, // Should be selected (latest valid)
|
new(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-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(
|
||||||
connections, shiftStartTime, 30, 60, 15);
|
connections,
|
||||||
|
new ConnectionPickArgument(shiftStartTime, 30, 60, 15),
|
||||||
|
loggerMock.Object);
|
||||||
|
|
||||||
// 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]
|
||||||
|
|
@ -34,18 +38,20 @@ public class TransportConnectionCalculatorTests
|
||||||
// Arrange - Shift starts at 8:00, max early arrival 60 min, so earliest acceptable is 7:00
|
// Arrange - Shift starts at 8:00, max early arrival 60 min, so earliest acceptable is 7:00
|
||||||
var connections = new List<TransportConnection>
|
var connections = new List<TransportConnection>
|
||||||
{
|
{
|
||||||
new TransportConnection { 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 TransportConnection { 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(
|
||||||
connections, shiftStartTime, 30, 60, 15);
|
connections,
|
||||||
|
new ConnectionPickArgument(shiftStartTime, 30, 60, 15),
|
||||||
|
loggerMock.Object);
|
||||||
|
|
||||||
// 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]
|
||||||
|
|
@ -54,19 +60,21 @@ public class TransportConnectionCalculatorTests
|
||||||
// Arrange - Shift starts at 8:00, no valid early connections
|
// Arrange - Shift starts at 8:00, no valid early connections
|
||||||
var connections = new List<TransportConnection>
|
var connections = new List<TransportConnection>
|
||||||
{
|
{
|
||||||
new TransportConnection { ArrivalTime = "2023-12-15T08:10:00" },
|
new(arrivalTime: "2023-12-15T08:10:00"),
|
||||||
new TransportConnection { ArrivalTime = "2023-12-15T08:05:00" }, // Should be selected (earliest late)
|
new(arrivalTime: "2023-12-15T08:05:00"), // Should be selected (earliest late)
|
||||||
new TransportConnection { 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(
|
||||||
connections, shiftStartTime, 30, 60, 15);
|
connections,
|
||||||
|
new ConnectionPickArgument(shiftStartTime, 30, 60, 15),
|
||||||
|
loggerMock.Object);
|
||||||
|
|
||||||
// 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]
|
||||||
|
|
@ -75,15 +83,17 @@ public class TransportConnectionCalculatorTests
|
||||||
// Arrange - All connections are too late (beyond max late arrival)
|
// Arrange - All connections are too late (beyond max late arrival)
|
||||||
var connections = new List<TransportConnection>
|
var connections = new List<TransportConnection>
|
||||||
{
|
{
|
||||||
new TransportConnection { ArrivalTime = "2023-12-15T08:20:00" },
|
new(arrivalTime: "2023-12-15T08:20:00"),
|
||||||
new TransportConnection { ArrivalTime = "2023-12-15T08:25:00" },
|
new(arrivalTime: "2023-12-15T08:25:00"),
|
||||||
new TransportConnection { 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(
|
||||||
connections, shiftStartTime, 30, 60, 15);
|
connections,
|
||||||
|
new ConnectionPickArgument(shiftStartTime, 30, 60, 15),
|
||||||
|
loggerMock.Object);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.ShouldBeNull();
|
result.ShouldBeNull();
|
||||||
|
|
@ -95,30 +105,33 @@ public class TransportConnectionCalculatorTests
|
||||||
// Arrange - Early connection is acceptable (not too early), should prefer it over late
|
// Arrange - Early connection is acceptable (not too early), should prefer it over late
|
||||||
var connections = new List<TransportConnection>
|
var connections = new List<TransportConnection>
|
||||||
{
|
{
|
||||||
new TransportConnection { ArrivalTime = "2023-12-15T07:20:00" }, // 40 min before shift - acceptable
|
new(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-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(
|
||||||
connections, shiftStartTime, 30, 60, 15);
|
connections,
|
||||||
|
new ConnectionPickArgument(shiftStartTime, 30, 60, 15),
|
||||||
|
loggerMock.Object);
|
||||||
|
|
||||||
// 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 connections = new List<TransportConnection>();
|
var shiftStartTime = T("2023-12-15T08:00:00");
|
||||||
var shiftStartTime = new DateTime(2023, 12, 15, 8, 0, 0);
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = TransportConnectionCalculator.FindBestConnection(
|
var result = TransportConnectionCalculator.FindBestConnection(
|
||||||
connections, shiftStartTime, 30, 60, 15);
|
new List<TransportConnection>(),
|
||||||
|
new ConnectionPickArgument(shiftStartTime, 30, 60, 15),
|
||||||
|
loggerMock.Object);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
result.ShouldBeNull();
|
result.ShouldBeNull();
|
||||||
|
|
@ -130,19 +143,48 @@ public class TransportConnectionCalculatorTests
|
||||||
// Arrange - Test scenario where both valid and late connections exist
|
// Arrange - Test scenario where both valid and late connections exist
|
||||||
var connections = new List<TransportConnection>
|
var connections = new List<TransportConnection>
|
||||||
{
|
{
|
||||||
new TransportConnection { ArrivalTime = "2023-12-15T06:30:00" }, // Too early (90 min before shift)
|
new(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(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(arrivalTime: "2023-12-15T08:05:00"), // Late but acceptable
|
||||||
new TransportConnection { 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(
|
||||||
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
|
// 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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue