I have this class representing a period of time:
public class Period
{
public Period(DateTime dateFrom)
{
DateFrom = dateFrom;
}
public Period(DateTime dateFrom, DateTime? dateTo)
{
DateFrom = dateFrom;
DateTo = dateTo;
}
public DateTime DateFrom { get; set; }
public DateTime? DateTo { get; set; }
public bool IsOverlapping(Period other)
{
if (!DateTo.HasValue)
{
return DateFrom <= other.DateTo.Value;
}
if (!other.DateTo.HasValue)
{
return other.DateFrom <= DateTo.Value;
}
return DateFrom <= other.DateTo.Value && other.DateFrom <= DateTo.Value;
}
public bool IsFinite => DateTo.HasValue;
public bool IsInfinite => !IsFinite;
protected bool Equals(Period other)
{
return DateFrom.Equals(other.DateFrom) && Nullable.Equals(DateTo, other.DateTo);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((Period) obj);
}
public override int GetHashCode()
{
return HashCode.Combine(DateFrom, DateTo);
}
}
Now a have a list of periods and for each of them I have to perform an network call so to minimize them I decide to merge all overlapping periods.
So a list like that :
- 2020-01-01 -> 2020-01-10
- 2020-02-05 -> 2020-02-10
- 2020-02-07 -> 2020-02-15
- 2020-02-13 -> 2020-02-20
- 2020-03-01 -> 2020-03-10
- 2020-03-25 -> 2020-03-31
- 2020-03-30 ->
Should become :
- 2020-01-01 -> 2020-01-10
- 2020-02-05 -> 2020-02-20
- 2020-03-01 -> 2020-03-10
- 2020-03-25 ->
I tried this code
public static IEnumerable<Period> Implementation(IEnumerable<Period> periods)
{
return periods.OrderBy(p => p.DateFrom)
.Aggregate(new List<Period>(), (ps, p) =>
{
if (!ps.Any())
{
ps.Add(p);
return ps;
}
var last = ps.Last();
if (last.IsOverlapping(p))
{
if (last.IsInfinite || p.IsInfinite)
{
ps[ps.Count() - 1] = new Period(DateTimeHelpers.Min(last.DateFrom, p.DateFrom), null);
}
else
{
ps[ps.Count() - 1] = new Period(DateTimeHelpers.Min(last.DateFrom, p.DateFrom),
DateTimeHelpers.Max(last.DateTo.Value, p.DateTo.Value));
}
return ps;
}
ps.Add(p);
return ps;
});
}
Here is the DateTimeHelpers I'm using :
public class DateTimeHelpers
{
public static DateTime Max(DateTime first, DateTime second)
{
return first <= second
? second
: first;
}
public static DateTime Min(DateTime first, DateTime second)
{
return first >= second
? second
: first;
}
}
It's working properly but I'm not satisfied with it so I wonder if there is a more performant/elegant/readable way to do it ?
I'm not talking about just refactoring to extract some methods but a fundamentally different solution, it's possible I missed a useful LINQ operator.
Here is my test if you want to reproduce it (MsTest + FluentAssertions). The period are not in the right order to ensure it will be take care of by the method itself:
// Arrange
var periods = new List<Period>()
{
new Period(new DateTime(2020, 2, 13), new DateTime(2020, 2, 20)),
new Period(new DateTime(2020, 3, 1), new DateTime(2020, 3, 10)),
new Period(new DateTime(2020, 3, 25), new DateTime(2020, 3, 31)),
new Period(new DateTime(2020, 3, 30)),
new Period(new DateTime(2020, 1, 1), new DateTime(2020, 1, 10)),
new Period(new DateTime(2020, 2, 5), new DateTime(2020, 2, 10)),
new Period(new DateTime(2020, 2, 7), new DateTime(2020, 2, 15))
};
// Act
var mergedPeriods = Implementation(periods);
// Assert
mergedPeriods.Should().HaveCount(4);
mergedPeriods[0].Should().Be(new Period(new DateTime(2020, 1, 1), new DateTime(2020, 1, 10)));
mergedPeriods[1].Should().Be(new Period(new DateTime(2020, 2, 5), new DateTime(2020, 2, 20)));
mergedPeriods[2].Should().Be(new Period(new DateTime(2020, 3, 1), new DateTime(2020, 3, 10)));
mergedPeriods[3].Should().Be(new Period(new DateTime(2020, 3, 25)));
Period
class, as presented, doesn't compile and therefore can't be reviewed. \$\endgroup\$Aggreate
still does not compile:DateFrom
does not haveValue
. Please also indicate whichDateTimeHelpers
nuget package are you using. \$\endgroup\$