Timeline

G
/*
    Author: Lorenzo Lotti
    Name: Timeline (DataStructures.Timeline<TValue>)
    Type: Data structure (class)
    Description: A collection of dates/times and values sorted by dates/times easy to query.
    Usage:
        this data structure can be used to represent an ordered series of dates or times with which to associate values.
        An example is a chronology of events:
            306: Constantine is the new emperor,
            312: Battle of the Milvian Bridge,
            313: Edict of Milan,
            330: Constantine move the capital to Constantinople.
*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace DataStructures;

/// <summary>
///     A collection of <see cref="DateTime" /> and <see cref="TValue" />
///     sorted by <see cref="DateTime" /> field.
/// </summary>
/// <typeparam name="TValue">Value associated with a <see cref="DateTime" />.</typeparam>
public class Timeline<TValue> : ICollection<(DateTime Time, TValue Value)>, IEquatable<Timeline<TValue>>
{
    /// <summary>
    ///     Inner collection storing the timeline events as key-tuples.
    /// </summary>
    private readonly List<(DateTime Time, TValue Value)> timeline = new();

    /// <summary>
    ///     Initializes a new instance of the <see cref="Timeline{TValue}"/> class.
    /// </summary>
    public Timeline()
    {
    }

    /// <summary>
    ///     Initializes a new instance of the <see cref="Timeline{TValue}"/> class populated with an initial event.
    /// </summary>
    /// <param name="time">The time at which the given event occurred.</param>
    /// <param name="value">The event's content.</param>
    public Timeline(DateTime time, TValue value)
        => timeline = new List<(DateTime, TValue)>
        {
            (time, value),
        };

    /// <summary>
    ///     Initializes a new instance of the <see cref="Timeline{TValue}"/> class containing the provided events
    ///     ordered chronologically.
    /// </summary>
    /// <param name="timeline">The timeline to represent.</param>
    public Timeline(params (DateTime, TValue)[] timeline)
        => this.timeline = timeline
            .OrderBy(pair => pair.Item1)
            .ToList();

    /// <summary>
    /// Gets he number of unique times within this timeline.
    /// </summary>
    public int TimesCount
        => GetAllTimes().Length;

    /// <summary>
    ///     Gets all events that has occurred in this timeline.
    /// </summary>
    public int ValuesCount
        => GetAllValues().Length;

    /// <summary>
    ///     Get all values associated with <paramref name="time" />.
    /// </summary>
    /// <param name="time">Time to get values for.</param>
    /// <returns>Values associated with <paramref name="time" />.</returns>
    public TValue[] this[DateTime time]
    {
        get => GetValuesByTime(time);
        set
        {
            var overridenEvents = timeline.Where(@event => @event.Time == time).ToList();
            foreach (var @event in overridenEvents)
            {
                timeline.Remove(@event);
            }

            foreach (var v in value)
            {
                Add(time, v);
            }
        }
    }

    /// <inheritdoc />
    bool ICollection<(DateTime Time, TValue Value)>.IsReadOnly
        => false;

    /// <summary>
    ///     Gets the count of pairs.
    /// </summary>
    public int Count
        => timeline.Count;

    /// <summary>
    ///     Clear the timeline, removing all events.
    /// </summary>
    public void Clear()
        => timeline.Clear();

    /// <summary>
    ///     Copy a value to an array.
    /// </summary>
    /// <param name="array">Destination array.</param>
    /// <param name="arrayIndex">The start index.</param>
    public void CopyTo((DateTime, TValue)[] array, int arrayIndex)
        => timeline.CopyTo(array, arrayIndex);

    /// <summary>
    ///     Add an event at a given time.
    /// </summary>
    /// <param name="item">The tuple containing the event date and value.</param>
    void ICollection<(DateTime Time, TValue Value)>.Add((DateTime Time, TValue Value) item)
        => Add(item.Time, item.Value);

    /// <summary>
    ///     Check whether or not a event exists at a specific date in the timeline.
    /// </summary>
    /// <param name="item">The tuple containing the event date and value.</param>
    /// <returns>True if this event exists at the given date, false otherwise.</returns>
    bool ICollection<(DateTime Time, TValue Value)>.Contains((DateTime Time, TValue Value) item)
        => Contains(item.Time, item.Value);

    /// <summary>
    ///     Remove an event at a specific date.
    /// </summary>
    /// <param name="item">The tuple containing the event date and value.</param>
    /// <returns>True if the event was removed, false otherwise.</returns>
    bool ICollection<(DateTime Time, TValue Value)>.Remove((DateTime Time, TValue Value) item)
        => Remove(item.Time, item.Value);

    /// <inheritdoc />
    IEnumerator IEnumerable.GetEnumerator()
        => timeline.GetEnumerator();

    /// <inheritdoc />
    IEnumerator<(DateTime Time, TValue Value)> IEnumerable<(DateTime Time, TValue Value)>.GetEnumerator()
        => timeline.GetEnumerator();

    /// <inheritdoc />
    public bool Equals(Timeline<TValue>? other)
        => other is not null && this == other;

    /// <summary>
    ///     Checks whether or not two <see cref="Timeline{TValue}"/> are equals.
    /// </summary>
    /// <param name="left">The first timeline.</param>
    /// <param name="right">The other timeline to be checked against the <paramref name="left"/> one.</param>
    /// <returns>True if both timelines are similar, false otherwise.</returns>
    public static bool operator ==(Timeline<TValue> left, Timeline<TValue> right)
    {
        var leftArray = left.ToArray();
        var rightArray = right.ToArray();

        if (left.Count != rightArray.Length)
        {
            return false;
        }

        for (var i = 0; i < leftArray.Length; i++)
        {
            if (leftArray[i].Time != rightArray[i].Time
                && !leftArray[i].Value!.Equals(rightArray[i].Value))
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    ///     Checks whether or not two <see cref="Timeline{TValue}"/> are not equals.
    /// </summary>
    /// <param name="left">The first timeline.</param>
    /// <param name="right">The other timeline to be checked against the <paramref name="left"/> one.</param>
    /// <returns>False if both timelines are similar, true otherwise.</returns>
    public static bool operator !=(Timeline<TValue> left, Timeline<TValue> right)
        => !(left == right);

    /// <summary>
    ///     Get all <see cref="DateTime" /> of the timeline.
    /// </summary>
    public DateTime[] GetAllTimes()
        => timeline.Select(t => t.Time)
            .Distinct()
            .ToArray();

    /// <summary>
    ///     Get <see cref="DateTime" /> values of the timeline that have this <paramref name="value" />.
    /// </summary>
    public DateTime[] GetTimesByValue(TValue value)
        => timeline.Where(pair => pair.Value!.Equals(value))
            .Select(pair => pair.Time)
            .ToArray();

    /// <summary>
    ///     Get all <see cref="DateTime" /> before <paramref name="time" />.
    /// </summary>
    public DateTime[] GetTimesBefore(DateTime time)
        => GetAllTimes()
            .Where(t => t < time)
            .OrderBy(t => t)
            .ToArray();

    /// <summary>
    ///     Get all <see cref="DateTime" /> after <paramref name="time" />.
    /// </summary>
    public DateTime[] GetTimesAfter(DateTime time)
        => GetAllTimes()
            .Where(t => t > time)
            .OrderBy(t => t)
            .ToArray();

    /// <summary>
    ///     Get all <see cref="TValue" /> of the timeline.
    /// </summary>
    public TValue[] GetAllValues()
        => timeline.Select(pair => pair.Value)
            .ToArray();

    /// <summary>
    ///     Get all <see cref="TValue" /> associated with <paramref name="time" />.
    /// </summary>
    public TValue[] GetValuesByTime(DateTime time)
        => timeline.Where(pair => pair.Time == time)
            .Select(pair => pair.Value)
            .ToArray();

    /// <summary>
    ///     Get all <see cref="TValue" /> before <paramref name="time" />.
    /// </summary>
    public Timeline<TValue> GetValuesBefore(DateTime time)
        => new(this.Where(pair => pair.Time < time).ToArray());

    /// <summary>
    ///     Get all <see cref="TValue" /> before <paramref name="time" />.
    /// </summary>
    public Timeline<TValue> GetValuesAfter(DateTime time)
        => new(this.Where(pair => pair.Time > time).ToArray());

    /// <summary>
    ///     Gets all values that happened at specified millisecond.
    /// </summary>
    /// <param name="millisecond">Value to look for.</param>
    /// <returns>Array of values.</returns>
    public Timeline<TValue> GetValuesByMillisecond(int millisecond)
        => new(timeline.Where(pair => pair.Time.Millisecond == millisecond).ToArray());

    /// <summary>
    ///     Gets all values that happened at specified second.
    /// </summary>
    /// <param name="second">Value to look for.</param>
    /// <returns>Array of values.</returns>
    public Timeline<TValue> GetValuesBySecond(int second)
        => new(timeline.Where(pair => pair.Time.Second == second).ToArray());

    /// <summary>
    ///     Gets all values that happened at specified minute.
    /// </summary>
    /// <param name="minute">Value to look for.</param>
    /// <returns>Array of values.</returns>
    public Timeline<TValue> GetValuesByMinute(int minute)
        => new(timeline.Where(pair => pair.Time.Minute == minute).ToArray());

    /// <summary>
    ///     Gets all values that happened at specified hour.
    /// </summary>
    /// <param name="hour">Value to look for.</param>
    /// <returns>Array of values.</returns>
    public Timeline<TValue> GetValuesByHour(int hour)
        => new(timeline.Where(pair => pair.Time.Hour == hour).ToArray());

    /// <summary>
    ///     Gets all values that happened at specified day.
    /// </summary>
    /// <param name="day">Value to look for.</param>
    /// <returns>Array of values.</returns>
    public Timeline<TValue> GetValuesByDay(int day)
        => new(timeline.Where(pair => pair.Time.Day == day).ToArray());

    /// <summary>
    ///     Gets all values that happened at specified time of the day.
    /// </summary>
    /// <param name="timeOfDay">Value to look for.</param>
    /// <returns>Array of values.</returns>
    public Timeline<TValue> GetValuesByTimeOfDay(TimeSpan timeOfDay)
        => new(timeline.Where(pair => pair.Time.TimeOfDay == timeOfDay).ToArray());

    /// <summary>
    ///     Gets all values that happened at specified day of the week.
    /// </summary>
    /// <param name="dayOfWeek">Value to look for.</param>
    /// <returns>Array of values.</returns>
    public Timeline<TValue> GetValuesByDayOfWeek(DayOfWeek dayOfWeek)
        => new(timeline.Where(pair => pair.Time.DayOfWeek == dayOfWeek).ToArray());

    /// <summary>
    ///     Gets all values that happened at specified day of the year.
    /// </summary>
    /// <param name="dayOfYear">Value to look for.</param>
    /// <returns>Array of values.</returns>
    public Timeline<TValue> GetValuesByDayOfYear(int dayOfYear)
        => new(timeline.Where(pair => pair.Time.DayOfYear == dayOfYear).ToArray());

    /// <summary>
    ///     Gets all values that happened at specified month.
    /// </summary>
    /// <param name="month">Value to look for.</param>
    /// <returns>Array of values.</returns>
    public Timeline<TValue> GetValuesByMonth(int month)
        => new(timeline.Where(pair => pair.Time.Month == month).ToArray());

    /// <summary>
    ///     Gets all values that happened at specified year.
    /// </summary>
    /// <param name="year">Value to look for.</param>
    /// <returns>Array of values.</returns>
    public Timeline<TValue> GetValuesByYear(int year)
        => new(timeline.Where(pair => pair.Time.Year == year).ToArray());

    /// <summary>
    ///     Add an event at a given <paramref name="time"/>.
    /// </summary>
    /// <param name="time">The date at which the event occurred.</param>
    /// <param name="value">The event value.</param>
    public void Add(DateTime time, TValue value)
    {
        timeline.Add((time, value));
    }

    /// <summary>
    ///     Add a set of <see cref="DateTime" /> and <see cref="TValue" /> to the timeline.
    /// </summary>
    public void Add(params (DateTime, TValue)[] timeline)
    {
        this.timeline.AddRange(timeline);
    }

    /// <summary>
    ///     Append an existing timeline to this one.
    /// </summary>
    public void Add(Timeline<TValue> timeline)
        => Add(timeline.ToArray());

    /// <summary>
    ///     Add a <paramref name="value" /> associated with <see cref="DateTime.Now" /> to the timeline.
    /// </summary>
    public void AddNow(params TValue[] value)
    {
        var now = DateTime.Now;
        foreach (var v in value)
        {
            Add(now, v);
        }
    }

    /// <summary>
    ///     Check whether or not a event exists at a specific date in the timeline.
    /// </summary>
    /// <param name="time">The date at which the event occurred.</param>
    /// <param name="value">The event value.</param>
    /// <returns>True if this event exists at the given date, false otherwise.</returns>
    public bool Contains(DateTime time, TValue value)
        => timeline.Contains((time, value));

    /// <summary>
    ///     Check if timeline contains this set of value pairs.
    /// </summary>
    /// <param name="timeline">The events to checks.</param>
    /// <returns>True if any of the events has occurred in the timeline.</returns>
    public bool Contains(params (DateTime, TValue)[] timeline)
        => timeline.Any(@event => Contains(@event.Item1, @event.Item2));

    /// <summary>
    ///     Check if timeline contains any of the event of the provided <paramref name="timeline"/>.
    /// </summary>
    /// <param name="timeline">The events to checks.</param>
    /// <returns>True if any of the events has occurred in the timeline.</returns>
    public bool Contains(Timeline<TValue> timeline)
        => Contains(timeline.ToArray());

    /// <summary>
    ///     Check if timeline contains any of the time of the provided <paramref name="times"/>.
    /// </summary>
    /// <param name="times">The times to checks.</param>
    /// <returns>True if any of the times is stored in the timeline.</returns>
    public bool ContainsTime(params DateTime[] times)
    {
        var storedTimes = GetAllTimes();
        return times.Any(value => storedTimes.Contains(value));
    }

    /// <summary>
    ///     Check if timeline contains any of the event of the provided <paramref name="values"/>.
    /// </summary>
    /// <param name="values">The events to checks.</param>
    /// <returns>True if any of the events has occurred in the timeline.</returns>
    public bool ContainsValue(params TValue[] values)
    {
        var storedValues = GetAllValues();
        return values.Any(value => storedValues.Contains(value));
    }

    /// <summary>
    ///     Remove an event at a specific date.
    /// </summary>
    /// <param name="time">The date at which the event occurred.</param>
    /// <param name="value">The event value.</param>
    /// <returns>True if the event was removed, false otherwise.</returns>
    public bool Remove(DateTime time, TValue value)
        => timeline.Remove((time, value));

    /// <summary>
    ///     Remove a set of value pairs from the timeline.
    /// </summary>
    /// <param name="timeline">An collection of all events to remove.</param>
    /// <returns>Returns true if the operation completed successfully.</returns>
    public bool Remove(params (DateTime, TValue)[] timeline)
    {
        var result = false;
        foreach (var (time, value) in timeline)
        {
            result |= this.timeline.Remove((time, value));
        }

        return result;
    }

    /// <summary>
    ///     Remove an existing timeline from this timeline.
    /// </summary>
    /// <param name="timeline">An collection of all events to remove.</param>
    /// <returns>Returns true if the operation completed successfully.</returns>
    public bool Remove(Timeline<TValue> timeline)
        => Remove(timeline.ToArray());

    /// <summary>
    ///     Remove a value pair from the timeline if the time is equal to <paramref name="times" />.
    /// </summary>
    /// <returns>Returns true if the operation completed successfully.</returns>
    public bool RemoveTimes(params DateTime[] times)
    {
        var isTimeContainedInTheTimeline = times.Any(time => GetAllTimes().Contains(time));

        if (!isTimeContainedInTheTimeline)
        {
            return false;
        }

        var eventsToRemove = times.SelectMany(time =>
            timeline.Where(@event => @event.Time == time))
            .ToList();

        foreach (var @event in eventsToRemove)
        {
            timeline.Remove(@event);
        }

        return true;
    }

    /// <summary>
    ///     Remove a value pair from the timeline if the value is equal to <paramref name="values" />.
    /// </summary>
    /// <returns>Returns true if the operation completed successfully.</returns>
    public bool RemoveValues(params TValue[] values)
    {
        var isValueContainedInTheTimeline = values.Any(v => GetAllValues().Contains(v));

        if (!isValueContainedInTheTimeline)
        {
            return false;
        }

        var eventsToRemove = values.SelectMany(value =>
            timeline.Where(@event => EqualityComparer<TValue>.Default.Equals(@event.Value, value)))
            .ToList();

        foreach (var @event in eventsToRemove)
        {
            timeline.Remove(@event);
        }

        return true;
    }

    /// <summary>
    ///     Convert the timeline to an array.
    /// </summary>
    /// <returns>
    /// The timeline as an array of tuples of (<see cref="DateTime"/>, <typeparamref name="TValue"/>).
    /// </returns>
    public (DateTime Time, TValue Value)[] ToArray()
        => timeline.ToArray();

    /// <summary>
    ///     Convert the timeline to a list.
    /// </summary>
    /// <returns>
    /// The timeline as a list of tuples of (<see cref="DateTime"/>, <typeparamref name="TValue"/>).
    /// </returns>
    public IList<(DateTime Time, TValue Value)> ToList()
        => timeline;

    /// <summary>
    ///     Convert the timeline to a dictionary.
    /// </summary>
    /// <returns>
    /// The timeline as an dictionary of <typeparamref name="TValue"/> by <see cref="DateTime"/>.
    /// </returns>
    public IDictionary<DateTime, TValue> ToDictionary()
        => timeline.ToDictionary(@event => @event.Time, @event => @event.Value);

    /// <inheritdoc />
    public override bool Equals(object? obj)
        => obj is Timeline<TValue> otherTimeline
           && this == otherTimeline;

    /// <inheritdoc />
    public override int GetHashCode()
        => timeline.GetHashCode();
}