Search code examples
c#.netfor-loopcasting

C# function that generates timeline items when threshold is crossed


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.

  • OkCount is 1
  • WarningCount is 2
  • ErrorCount is 3

Everytime a threshold is crossed a new timeline item should be added. The result from the following reservations:

  • reservation from 9:00 till 12:00
  • reservation from 10:00 till 12:00
  • reservation from 11:00 till 12:00
  • reservation from 13:00 till 14:00

Should give these timeline items as a result:

  • timeline item from 8:00 till 10:00 with OK
  • timeline item from 10:00 till 11:00 with Warning (almost full capacity)
  • timeline item from 11:00 till 12:00 with Error (full)
  • timeline itsm from 12:00 till 18:00 with OK (between 0 and 1 reservations in this time)

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.

Convert function that is WIP

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.

TimeLineItem class

public class TimeLineItem
    {
        public Color ItemColor { get; set; }
        public string ItemTimeText { get; set; }

        public TimeLineItem(Color itemColor, string itemTimeText)
        {
            ItemColor = itemColor;
            ItemTimeText = itemTimeText;
        }
    }

Enum class Color

public enum Color 
{
    Success,
    Warning,
    Error
}

Reservation class

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;
    }
}

Solution

  • Function improvement and fully working

    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.

    Main 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;
    }
    
    

    Helper function to get correct color from availability

    private Color GetAvailabilityColor(int availability)
    {
        if (availability >= _okAvailabilityAmount)
        {
            return Color.Success;
        }
        if (availability >= _warningAvailabilityAmount)
        {
            return Color.Warning;
        }
        return Color.Error;
    }
    

    Helper classes and records

    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.