I'm really stumped on how to achieve this. I have a list of reservations with overlapping start and or end times, from which I have to generate timelime items. These timeline items should only be generated in 3 styles: OK, Warning or Error.
Default start of day is 08:00 and end of the day is 18:00.
Everytime a threshold is crossed a new timeline item should be added. The result from the following reservations:
Should give these timeline items as a result:
I've already tried a few things with for and foreach loops but I really am stumped. Currently, with the code below, I'm having the issue that incorrect timeline items are being generated. This is most likely due to the way I loop through the code. I would like to check the occupancy at any given minute throughout the day and if it crosses one of 3 thresholds create a new timeline item accordingly, without having to loop through every single minute.
The occupancy when looped through currently does not increase or decrease except for the last iteration of the loop. It might be a lot easier to just loop through all minutes of the day and check the occupancy that way but it seems really inefficient.
var startTime = TimeSpan.FromHours(8);
var endTime = TimeSpan.FromHours(20);
var occupancy = 0;
var lastTime = startTime;
var timeLineItems = new List<TimeLineItem>();
var lastThresholdHit = 0;
var totalTime = (endTime - startTime).TotalMinutes;
for (var i = 0; i < totalTime; i++)
{
var currentTime = startTime + TimeSpan.FromMinutes(i);
var newOccupancy = processedReservations.Count(r => r.StartTime <= currentTime && r.EndTime >= currentTime);
if (occupancy != newOccupancy)
{
Debug.WriteLine(currentTime + ": " + newOccupancy);
if (newOccupancy <= _timeLineOkCount && lastThresholdHit != _timeLineOkCount)
{
var itemText = lastTime.ToString(@"hh\:mm") + " - " + currentTime.ToString(@"hh\:mm");
var item = new TimeLineItem(Color.Success, itemText);
timeLineItems.Add(item);
lastThresholdHit = _timeLineOkCount;
lastTime = currentTime;
} else
if (newOccupancy < _timeLineErrorCount && lastThresholdHit != _timeLineWarningCount)
{
var itemText = lastTime.ToString(@"hh\:mm") + " - " + currentTime.ToString(@"hh\:mm");
var item = new TimeLineItem(Color.Warning, itemText);
timeLineItems.Add(item);
lastThresholdHit = _timeLineWarningCount;
lastTime = currentTime;
}
else
if (lastThresholdHit != _timeLineErrorCount)
{
var itemText = lastTime.ToString(@"hh\:mm") + " - " + currentTime.ToString(@"hh\:mm");
var item = new TimeLineItem(Color.Error, itemText);
timeLineItems.Add(item);
lastThresholdHit = _timeLineErrorCount;
lastTime = currentTime;
}
}
occupancy = newOccupancy;
}
if (lastTime < endTime)
{
var itemText = lastTime.ToString(@"hh\:mm") + " - " + endTime.ToString(@"hh\:mm");
var item = new TimeLineItem(Color.Success, itemText);
timeLineItems.Add(item);
}
var orderedTimeLineItems = timeLineItems.OrderBy(i => i.ItemTimeText).ToList();
return orderedTimeLineItems;
I still have some weirdness with the code above. In some cases the debug log returns an occupancy of 0 where it should not and the items generated do not yet show the correct times.
public class TimeLineItem
{
public Color ItemColor { get; set; }
public string ItemTimeText { get; set; }
public TimeLineItem(Color itemColor, string itemTimeText)
{
ItemColor = itemColor;
ItemTimeText = itemTimeText;
}
}
public enum Color
{
Success,
Warning,
Error
}
public class Reservation
{
public int Id { get; set; }
public TimeSpan StartTime { get; set; }
public TimeSpan EndTime { get; set; }
public DateTime Date { get; set; }
public Reservation(int id, TimeSpan startTime, TimeSpan endTime, DateTime date)
{
Id = id;
StartTime = startTime;
EndTime = endTime;
Date = date;
}
}
Instead of using minute granularity and looping through every single minute I decided to take a different more efficient approach by using .Aggregate() and a few other LINQ methods. The final result works really well except for the start item time being 00:00 for some reason. I worked my way around that issue by checking if the item start time is before the allowed start time and if it is then set it to the start time.
The codeblocks below combined give the final result working function.
public async Task<List<TimeLineItem>> GetTimeLineItems()
{
var reservations = await _reservationCollection.GetReservations();
if (reservations.Count < 1)
{
return new List<TimeLineItem>();
}
var processedReservations = reservations.Where(r => r.Date.Date == DateTime.Today)
.OrderBy(r => r.StartTime).ToArray();
var mergedReservations = new List<Reservation>();
if (processedReservations.Length < 1) return new List<TimeLineItem>();
var currentReservation = processedReservations[0];
for (var i = 1; i < processedReservations.Length; i++)
{
var nextReservation = processedReservations[i];
if ((nextReservation.StartTime - currentReservation.EndTime).TotalMinutes <= _timeMarginInMinutes * 2 &&
currentReservation.Charger == nextReservation.Charger)
{
currentReservation.EndTime = nextReservation.EndTime;
}
else
{
mergedReservations.Add(currentReservation);
currentReservation = nextReservation;
}
}
var reservationEvents = processedReservations
.SelectMany(r => new List<ReservationEvent>
{ new(r.StartTime, -1), new(r.EndTime, 1) })
.OrderBy(r => r.Moment).ToArray();
var chargers = await _chargerCollection.GetChargers();
var chargerAmount = chargers.Count;
var result = reservationEvents
.Prepend(new ReservationEvent(_startTime, chargerAmount)) // Add initial state
.GroupBy(r => r.Moment)
.Select(group => new
{
Moment = group.Key,
Action = group.Sum(r => r.Action)
})
.Where(r => r.Action != 0)
.OrderBy(r => r.Moment)
.Aggregate(
new DayAccumulator
{
Availability = 0, // Will be set to 3 by first ReservationEvent
States = new List<TimelineState> { new() { Color = Color.Success }}
},
(acc, r) =>
{
acc.Availability += r.Action;
var lastState = acc.States.Last();
if (lastState.Color != GetAvailabilityColor(acc.Availability))
{
acc.States.Add(new TimelineState
{
Moment = r.Moment,
Color = GetAvailabilityColor(acc.Availability)
});
}
return acc;
}).States;
var lastItem = result.Last();
var timeLineItems = new List<TimeLineItem>();
for (var i = 0; i < result.Count - 1; i++)
{
if (result[i].Moment < _startTime) result[i].Moment = _startTime;
timeLineItems.Add(new TimeLineItem(result[i].Color, $"{result[i].Moment:hh\\:mm} - {result[i + 1].Moment:hh\\:mm}"));
}
if (lastItem.Moment < _endTime)
{
timeLineItems.Add(new TimeLineItem(lastItem.Color, $"{lastItem.Moment:hh\\:mm} - {_endTime:hh\\:mm}"));
}
return timeLineItems;
}
private Color GetAvailabilityColor(int availability)
{
if (availability >= _okAvailabilityAmount)
{
return Color.Success;
}
if (availability >= _warningAvailabilityAmount)
{
return Color.Warning;
}
return Color.Error;
}
internal class DayAccumulator
{
public int Availability { get; set; }
public List<TimelineState> States { get; init; } = new();
}
internal class TimelineState
{
public TimeSpan Moment { get; set; }
public Color Color { get; set; }
}
internal record ReservationEvent(TimeSpan Moment, int Action);
All of this code combined does exactly what I want it to do.