假设我有一个很大的间隔
LargeInterval interval = new LargeInterval
{
StartPeriod = DateTime.Today,
EndPeriod = DateTime.Today.AddYears(1),
};
哪里
public class LargeInterval
{
public DateTime StartPeriod { get; set; }
public DateTime EndPeriod { get; set; }
}
还有一些“较小”的间隔
public class SmallInterval
{
public DateTime From{ get; set; }
public DateTime To { get; set; }
}
例如,
SmallInterval s1 = new SmallInterval
{
From = "2024-05-01"
End = "2024-07-03"
}
和
SmallInterval s2 = new SmallInterval
{
From = "2024-08-01"
End = "2024-10-03"
}
我确实想得到每个
SmallInterval
与 interval
类型的 LargeInterval
之间不相交的部分。比如:
DateTime.Today until "2024-04-30"
和
"2024-07-04" until "2024-07-31"
和
"2024-10-04" until DateTime.Today.AddYears(1).AddDays(-1)
我尝试过这样的事情:
DateTime startPeriod = interval.StartPeriod;
DateTime endPeriod = interval.EndPeriod;
IEnumerable<SmallIntervals> existingPeriodsIntersectingTheNewPeriod = smallIntervals.Where(x => x.From <= endPeriod && x.To >= startPeriod).OrderBy(x => x.From);
foreach (var stsp in existingPeriodsIntersectingTheNewPeriod)
{
/// handling all scenarios
}
我有一个适用于
double
范围的解决方案。您可以将其与 DateTime
值一起使用,方法是使用 DateTime.ToOADate()
和 DateTime.FromOADate()
在 double
和 DateTime
之间进行转换。
用法如下:
var rangeExcluder = new RangeExcluder();
// Include large interval.
rangeExcluder.Include(
DateTime.Today.ToOADate(),
DateTime.Today.AddYears(1).ToOADate());
// Exclude small interval #1.
rangeExcluder.Exclude(
new DateTime(2024, 05, 01).ToOADate(),
new DateTime(2024, 07, 03).AddDays(1).ToOADate());
// Exclude small interval #2.
rangeExcluder.Exclude(
new DateTime(2024, 08, 01).ToOADate(),
new DateTime(2024, 10, 03).AddDays(1).ToOADate());
foreach (var region in rangeExcluder.IncludedRanges())
{
Console.WriteLine($"{DateTime.FromOADate(region.Start):d} - {DateTime.FromOADate(region.End).AddTicks(-1):d}");
}
请注意,传递给
Include()
和 Exclude()
并通过 IncludedRanges()
返回的范围都是半开区间,包括开始但不包括结束。这就是为什么我在设置范围时使用 AddDays(1)
,在输出结果时使用 .AddTicks(-1)
。
上面的代码将输出:
12/03/2024 - 30/04/2024
04/07/2024 - 31/07/2024
04/10/2024 - 11/03/2025
RangeExcluder
的实现如下所示。请注意,它处理多个重叠区域,因此您可以包含与其他包含区域重叠的区域,并排除与其他排除区域重叠的区域。
public sealed class RangeExcluder
{
/// <summary>Adds a region to be included. <paramref name="start"/> must be less than or equal to <paramref name="end"/>.</summary>
/// <param name="start">The start of the region to be included. Must be less than or equal to <paramref name="end"/>.</param>
/// <param name="end">The exclusive end of the region to be included. Must be greater than or equal to <paramref name="start"/>.</param>
public void Include(double start, double end)
{
_boundaries.Add(new Boundary(start, isStart: true, isIncluded: true));
_boundaries.Add(new Boundary(end, isStart: false, isIncluded: true));
_sorted = false;
}
/// <summary>Adds a region to be excluded. <paramref name="start"/> must be less than or equal to <paramref name="end"/>.</summary>
/// <param name="start">The start of the region to be excluded. Must be less than or equal to <paramref name="end"/>.</param>
/// <param name="end">The exclusive end of the region to be excluded. Must be greater than or equal to <paramref name="start"/>.</param>
public void Exclude(double start, double end)
{
_boundaries.Add(new Boundary(start, isStart: true, isIncluded: false));
_boundaries.Add(new Boundary(end, isStart: false, isIncluded: false));
_sorted = false;
}
/// <summary>
/// Iterates all the included ranges after removing all the excluded ranges from them. <br/>
/// The start values are INCLUSIVE and the end values are EXCLUSIVE.
/// This may be an O(N.LogN) operation, so call it sparingly.
/// </summary>
public IEnumerable<(double Start, double End)> IncludedRanges()
{
sortIfNecessary();
return includedRanges();
}
void sortIfNecessary()
{
if (_sorted)
return;
_boundaries.Sort();
_sorted = true;
}
IEnumerable<(double start, double end)> includedRanges()
{
int included = 0;
int excluded = 0;
double start = 0;
for (int i = 0; i < _boundaries.Count; ++i)
{
if (_boundaries[i].IsStart) // Starting a region...
{
if (_boundaries[i].IsIncluded) // Starting an included region...
{
if (++included == 1 && excluded == 0) // Starting a new included region,
start = _boundaries[i].Time; // so remember its start time.
}
else // Starting an excluded region...
{
if (++excluded == 1 && included > 0) // Ending an included region,
yield return (start, _boundaries[i].Time); // so return its range.
}
}
else // Ending a region...
{
if (_boundaries[i].IsIncluded) // Ending an included region...
{
if (--included == 0 && excluded == 0) // Ending an included region,
yield return (start, _boundaries[i].Time); // so return its range.
}
else // Ending an excluded region...
{
if (--excluded == 0 && included > 0) // Starting an included region,
start = _boundaries[i].Time; // so remember its start time.
}
}
}
}
readonly List<Boundary> _boundaries = [];
bool _sorted;
// Struct to hold starts and ends of included and excluded regions.
// (Note: A struct is much more performant than a class for the specific way it is used here.)
readonly struct Boundary : IComparable<Boundary>
{
public Boundary(double time, bool isStart, bool isIncluded)
{
Time = time;
IsStart = isStart;
IsIncluded = isIncluded;
}
// Order by [Time, IsStart] so if two times are the same, a start time is before an end time.
public int CompareTo(Boundary other)
{
if (this.Time < other.Time)
return -1;
if (this.Time > other.Time)
return 1;
if (this.IsStart == other.IsStart)
return 0;
return this.IsStart ? -1 : 1;
}
public readonly double Time;
public readonly bool IsStart;
public readonly bool IsIncluded;
}
}