Skip to content

Commit

Permalink
feat(intervals): add new methods for intervals
Browse files Browse the repository at this point in the history
Add methods to get intersection and union of intervals.
Add methods to merge intervals in minimum intervals count.

BREAKING CHANGE: Intersect method renamed in IntersectWith
  • Loading branch information
sandre58 committed Jul 23, 2024
1 parent adbe018 commit 73d10fe
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 16 deletions.
2 changes: 2 additions & 0 deletions src/MyNet.Utilities/DateTimes/ObservablePeriod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@ public override void SetInterval(DateTime start, DateTime end)
if (oldEnd != End)
RaisePropertyChanged(nameof(End));
}

protected override Period CreateInstance(DateTime start, DateTime end) => new ObservablePeriod(start, end);
}
}
15 changes: 14 additions & 1 deletion src/MyNet.Utilities/DateTimes/Period.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MyNet.Utilities.Helpers;
using MyNet.Utilities.Sequences;

namespace MyNet.Utilities.DateTimes
{
public class Period : Interval<DateTime>
public class Period : Interval<DateTime, Period>
{
public Period(DateTime start, DateTime end) : base(start, end) { }

Expand All @@ -18,6 +19,11 @@ public IEnumerable<DateTime> ToDates() =>
Enumerable.Range(0, End.Date.Subtract(Start.Date).Days + 1)
.Select(offset => Start.Date.AddDays(offset));

public IEnumerable<Period> ByDays() =>
Enumerable.Range(0, End.Date.Subtract(Start.Date).Days + 1)
.Select(offset => Start.Date.AddDays(offset))
.Select(x => new Period(DateTimeHelper.Max(x.BeginningOfDay(), Start), DateTimeHelper.Min(x.EndOfDay(), End)));

public bool IsCurrent() => Contains(DateTime.Today);

public Period ToUniversalTime() => new(Start.ToUniversalTime(), End.ToUniversalTime());
Expand All @@ -36,14 +42,21 @@ public IEnumerable<DateTime> ToDates() =>

public Period ShiftEarlier(TimeSpan offset) => new(Start.SubtractFluentTimeSpan(offset), End.SubtractFluentTimeSpan(offset));

public IEnumerable<Period> Intersect(TimePeriod interval)
=> ByDays().Select(x => x.ToUniversalTime().Intersect(new Period(x.Start.ToUtcDateTime(interval.Start), x.Start.ToUtcDateTime(interval.End)))).NotNull();

public ImmutablePeriod AsImmutable() => new(Start, End);

protected override Period CreateInstance(DateTime start, DateTime end) => new(start, end);
}

public class ImmutablePeriod : Period
{
public ImmutablePeriod(DateTime start, DateTime end) : base(start, end) { }

public override void SetInterval(DateTime start, DateTime end) => throw new InvalidOperationException("This period is immutable.");

protected override Period CreateInstance(DateTime start, DateTime end) => new ImmutablePeriod(start, end);
}

public class PeriodWithOptionalEnd : IntervalWithOptionalEnd<DateTime>
Expand Down
6 changes: 4 additions & 2 deletions src/MyNet.Utilities/DateTimes/TimePeriod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

namespace MyNet.Utilities.DateTimes
{
public class TimePeriod : Interval<TimeSpan>
public class TimePeriod : Interval<TimeSpan, TimePeriod>
{
public TimePeriod(TimeSpan start, TimeSpan end, DateTimeKind kind) : base(start, end) => Kind = kind;
public TimePeriod(TimeSpan start, TimeSpan end, DateTimeKind kind = DateTimeKind.Local) : base(start, end) => Kind = kind;

public DateTimeKind Kind { get; }

Expand Down Expand Up @@ -39,5 +39,7 @@ public TimePeriod ToLocalTime(DateTime? targetDate = null)
public TimePeriod ShiftLater(TimeSpan offset) => new(Start.AddFluentTimeSpan(offset), End.AddFluentTimeSpan(offset), Kind);

public TimePeriod ShiftEarlier(TimeSpan offset) => new(Start.SubtractFluentTimeSpan(offset), End.SubtractFluentTimeSpan(offset), Kind);

protected override TimePeriod CreateInstance(TimeSpan start, TimeSpan end) => new(start, end);
}
}
2 changes: 2 additions & 0 deletions src/MyNet.Utilities/Extensions/DateTimeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public static class DateTimeExtensions

public static Period ToPeriod(this DateTime dateTime, FluentTimeSpan timeSpan) => new(dateTime, dateTime.AddFluentTimeSpan(timeSpan));

public static Period ToPeriod(this DateTime dateTime, DateTime otherDateTime) => new(DateTimeHelper.Min(dateTime, otherDateTime), DateTimeHelper.Max(dateTime, otherDateTime));

public static DateTime Add(this DateTime date, int value, TimeUnit timeUnitToGet) => date.AddFluentTimeSpan(value.ToTimeSpan(timeUnitToGet));

/// <summary>
Expand Down
44 changes: 44 additions & 0 deletions src/MyNet.Utilities/Extensions/IntervalExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Stéphane ANDRE. All Right Reserved.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Linq;
using MyNet.Utilities.Sequences;

namespace MyNet.Utilities
{
public static class IntervalExtensions
{
public static IEnumerable<TClass> Merge<T, TClass>(this IEnumerable<TClass> intervals)
where T : struct, IComparable
where TClass : Interval<T, TClass>
{
if (intervals.Count() <= 1) return intervals;

var result = new List<TClass>();

TClass? previousInterval = null;
foreach (var item in intervals.OrderBy(x => x.Start).ToList())
{
if (previousInterval is not null)
{
if (previousInterval.Union(item) is TClass interval)
{
result.Add(interval);
previousInterval = interval;
}
else
{
result.Add(previousInterval);
previousInterval = item;
}
}
else
previousInterval = item;
}

return result;
}
}
}
12 changes: 11 additions & 1 deletion src/MyNet.Utilities/Helpers/DateTimeHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,17 @@ public static DateTime Min(DateTime date1, DateTime date2) =>
? date2
: date1;

public static IEnumerable<DateTime> Range(DateTime min, DateTime max, int step, TimeUnit unit)
public static TimeSpan Max(TimeSpan time1, TimeSpan time2) =>
time1 > time2
? time1
: time2;

public static TimeSpan Min(TimeSpan time1, TimeSpan time2) =>
time1 > time2
? time2
: time1;

public static IEnumerable<DateTime> Range(DateTime min, DateTime max, int step = 1, TimeUnit unit = TimeUnit.Day)
{
Func<DateTime, DateTime> increment = null!;

Expand Down
70 changes: 58 additions & 12 deletions src/MyNet.Utilities/Sequences/Interval.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,23 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Linq;
using MyNet.Utilities.Extensions;

namespace MyNet.Utilities.Sequences
{
public class Interval<T> : ValueObject, IComparable where T : struct, IComparable
public class Interval<T> : Interval<T, Interval<T>>
where T : struct, IComparable
{
public Interval(T start, T end) : base(start, end) { }

protected override Interval<T> CreateInstance(T start, T end) => new(start, end);
}

public abstract class Interval<T, TClass> : ValueObject, IComparable, ICloneable<TClass>
where T : struct, IComparable
where TClass : Interval<T, TClass>
{
public Interval(T start, T end) => SetIntervalInternal(start, end);

Expand All @@ -22,33 +34,65 @@ private void SetIntervalInternal(T start, T end)

public virtual void SetInterval(T start, T end) => SetIntervalInternal(start, end);

public bool Contains(T value) => value.CompareTo(Start) >= 0 && value.CompareTo(End) <= 0;
public virtual bool Contains(T value) => value.CompareTo(Start) >= 0 && value.CompareTo(End) <= 0;

public virtual bool Contains(TClass interval) => interval.Start.CompareTo(Start) >= 0 && interval.End.CompareTo(End) <= 0;

public bool Contains(Interval<T> interval) => interval.Start.CompareTo(Start) >= 0 && interval.End.CompareTo(End) <= 0;
public virtual bool IntersectWith(TClass interval)
=> Start.CompareTo(interval.End) < 0 && interval.Start.CompareTo(End) < 0;

public bool Intersect(Interval<T> interval) => Start.CompareTo(interval.End) < 0 && interval.Start.CompareTo(End) < 0;
public virtual TClass? Intersect(TClass interval)
=> Contains(interval)
? interval.Clone()
: interval.Contains(this.CastIn<TClass>())
? Clone()
: interval.Start.CompareTo(Start) >= 0 && interval.Start.CompareTo(End) < 0
? CreateInstance(interval.Start, End)
: interval.End.CompareTo(Start) > 0 && interval.End.CompareTo(End) <= 0
? CreateInstance(Start, interval.End)
: null;

public override bool Equals(object? obj) => obj is Interval<T> vm && Start.Equals(vm.Start) && End.Equals(vm.End);
public virtual TClass? Union(TClass interval)
=> IntersectWith(interval)
? CreateInstance(new List<T> { Start, interval.Start }.Min(), new List<T> { End, interval.End }.Max())
: null;

public virtual IEnumerable<TClass> Exclude(TClass interval)
=> Contains(interval)
? [CreateInstance(Start, interval.Start), CreateInstance(interval.End, End)]
: interval.Contains(this.CastIn<TClass>())
? []
: interval.Start.CompareTo(Start) >= 0 && interval.Start.CompareTo(End) < 0
? [CreateInstance(Start, interval.Start)]
: interval.End.CompareTo(Start) > 0 && interval.End.CompareTo(End) <= 0
? [CreateInstance(interval.End, End)]
: [Clone()];

protected abstract TClass CreateInstance(T start, T end);

public TClass Clone() => CreateInstance(Start, End);

public override bool Equals(object? obj) => obj is TClass vm && Start.Equals(vm.Start) && End.Equals(vm.End);

public override int GetHashCode() => Start.GetHashCode();

public override string ToString() => $"{Start} - {End}";

#region IComparable

public virtual int CompareTo(object? obj) => obj is Interval<T> interval ? !Equals(Start, interval.Start) ? Start.CompareTo(interval.Start) : End.CompareTo(interval.End) : 1;
public virtual int CompareTo(object? obj) => obj is TClass interval ? !Equals(Start, interval.Start) ? Start.CompareTo(interval.Start) : End.CompareTo(interval.End) : 1;

public static bool operator ==(Interval<T> left, Interval<T> right) => left?.Equals(right) ?? right is null;
public static bool operator ==(Interval<T, TClass> left, Interval<T, TClass> right) => left?.Equals(right) ?? right is null;

public static bool operator >(Interval<T> left, Interval<T> right) => left.CompareTo(right) > 0;
public static bool operator >(Interval<T, TClass> left, Interval<T, TClass> right) => left.CompareTo(right) > 0;

public static bool operator <(Interval<T> left, Interval<T> right) => left.CompareTo(right) < 0;
public static bool operator <(Interval<T, TClass> left, Interval<T, TClass> right) => left.CompareTo(right) < 0;

public static bool operator >=(Interval<T> left, Interval<T> right) => left.CompareTo(right) >= 0;
public static bool operator >=(Interval<T, TClass> left, Interval<T, TClass> right) => left.CompareTo(right) >= 0;

public static bool operator <=(Interval<T> left, Interval<T> right) => left.CompareTo(right) <= 0;
public static bool operator <=(Interval<T, TClass> left, Interval<T, TClass> right) => left.CompareTo(right) <= 0;

public static bool operator !=(Interval<T> left, Interval<T> right) => !(left == right);
public static bool operator !=(Interval<T, TClass> left, Interval<T, TClass> right) => !(left == right);

#endregion
}
Expand All @@ -58,6 +102,8 @@ public class ImmutableInterval<T> : Interval<T> where T : struct, IComparable
public ImmutableInterval(T start, T end) : base(start, end) { }

public override void SetInterval(T start, T end) => throw new InvalidOperationException("This interval is immutable.");

protected override Interval<T> CreateInstance(T start, T end) => new ImmutableInterval<T>(start, end);
}

public class IntervalWithOptionalEnd<T> : ValueObject where T : struct, IComparable
Expand Down

0 comments on commit 73d10fe

Please sign in to comment.