簡體   English   中英

從重疊的日期范圍列表中查找日期范圍

[英]Find a Date range from a list of Date ranges where they overlap

我在嘗試處理具有簡單FromTo屬性的對象列表時遇到了一些麻煩,這些對象都是DateTimes ,我希望結果是顯示重疊范圍的相同類型對象的列表, tbh,我想我現在有點代碼/邏輯盲了!

例如(請注意,日期采用 ddMMyyyy 格式):

TS1: 01/01/2020 to 10/01/2020 
TS2: 08/01/2020 to 20/01/2020 

所以在這種情況下,我希望得到 2 個對象,它們都包含相同的數據:

TSA: 08/01/2020 to 10/01/2020
TSB: 08/01/2020 to 10/01/2020

一個更復雜的例子:

TS1: 01/01/2020 to 10/01/2020 
TS2: 08/01/2020 to 20/01/2020 
TS3: 18/01/2020 to 22/01/2020 

所以在這種情況下,我希望得到 4 個對象,兩組包含相同數據的兩組:

TSA: 08/01/2020 to 10/01/2020
TSB: 08/01/2020 to 10/01/2020
TSC: 18/01/2020 to 20/01/2020
TSD: 18/01/2020 to 20/01/2020

再舉一個例子:

TS1: 01/01/2020 to 01/10/2020 
TS2: 01/02/2020 to 01/09/2020 
TS3: 01/03/2020 to 01/04/2020 

所以在這種情況下,我希望得到 3 個對象,所有對象都包含相同的數據:

TSA: 01/03/2020 to 01/04/2020
TSB: 01/03/2020 to 01/04/2020
TSC: 01/03/2020 to 01/04/2020

我試過在線研究算法,但沒有任何運氣得到我想要的,或者它們是基於 SQl 的答案。

任何建議將非常受歡迎。

編輯:只是為了解釋這將用於什么,所以它可能會讓下面的一些評論者更清楚一些。 這些日期范圍中的每一個都表示正在使用的房間。 該系統旨在在根本沒有可用房間時報告回溯日期范圍。 因為我已經知道房間的數量,所以我可以從這些結果中確定是否有空房,並返回沒有空房的日期范圍。

在嘗試了以下一些答案后,我還編輯了預期結果

下面的算法在普通情況下以 O(n log(n)) 計算結果,盡管在最壞情況下它仍然是 O(n^2)。

首先,記錄類。

public class DateRange
{
    public DateRange(DateTime from, DateTime to)
    {
        From = from;
        To = to;
    }

    public DateTime From { get; set; }
    public DateTime To { get; set; }
}

我的算法如下。 我對算法添加了一些注釋,所以我希望它是可以理解的。 原則上,它利用了大多數范圍(希望)不會與多個其他范圍重疊的事實,通過按排序順序處理輸入,一旦當前輸入超過其結束時間,就將較舊的輸入條目從考慮中排除。

public static IEnumerable<DateRange> FindOverlaps(IList<DateRange> dateRanges)
{
    if (dateRanges.Count < 2)
    {
        return Enumerable.Empty<DateRange>();
    }

    // Sort the input ranges by start time, in ascending order, to process them in that order.
    var orderedRanges = dateRanges.OrderBy(x => x.From).ToList();
    // Keep a list of previously processed values.
    var previousRanges = new List<DateRange>
    {
        orderedRanges.First(),
    };

    var result = new List<DateRange>();
    foreach (var value in orderedRanges.Skip(1))
    {
        var toDelete = new List<DateRange>();
        // Go through all ranges that start before the current one, and pick those among
        // them that end after the current one starts as result values, and also, delete all
        // those that end before the current one starts from the list -- given that the input
        // is sorted, they will never overlap with future input values.
        foreach (var dateRange in previousRanges)
        {
            if (value.From >= dateRange.To)
            {
                toDelete.Add(dateRange);
            }
            else
            {
                result.Add(new DateRange(value.From, value.To < dateRange.To ? value.To : dateRange.To));
            }
        }
        foreach (var candidate in toDelete)
        {
            previousRanges.Remove(candidate);
        }
        previousRanges.Add(value);
    }

    return result;
}

請注意,輸入中的所有n值都可能重疊。 在這種情況下,有n*(n-1)重疊,因此算法必然會在 O(n^2) 中運行。 然而,在格式良好的情況下,每個日期范圍與其他日期范圍的重疊次數很少,復雜性將大致為 O(n log(n)),代價高昂的操作是 .OrderBy() 調用輸入。

還有一個考慮。 考慮您有一個輸入值列表,如下所示:

var example = new[]
{
    new DateRange(new DateTime(2000, 1, 1), new DateTime(2010, 1, 10)),
    new DateRange(new DateTime(2000, 2, 1), new DateTime(2000, 10, 10)),
    new DateRange(new DateTime(2000, 3, 11), new DateTime(2000, 9, 12)),
    new DateRange(new DateTime(2000, 4, 11), new DateTime(2000, 8, 12)),
};

在這種情況下,不僅所有的值都重疊,而且它們還相互包含。 我上面發布的算法將多次報告此類區域(例如,它將返回 2000-04-11 到 2000-08-12 的范圍三次,因為它與其他三個日期范圍重疊)。 如果您不希望像這樣多次報告重疊區域,您可以將上述函數的輸出提供給以下函數以過濾它們:

public static IEnumerable<DateRange> MergeRanges(IList<DateRange> dateRanges)
{
    var currentOverlap = dateRanges.First();
    var r = new List<DateRange>();
    foreach (var dateRange in dateRanges.Skip(1))
    {
        if (dateRange.From > currentOverlap.To)
        {
            r.Add(currentOverlap);
            currentOverlap = dateRange;
        }
        else
        {
            currentOverlap.To = currentOverlap.To > dateRange.To ? currentOverlap.To : dateRange.To;
        }
    }
    r.Add(currentOverlap);
    return r;
}

這不會影響整體算法的復雜性,因為它顯然是 O(n)-ish。

假設您定義了一個類型來存儲這樣的日期范圍:

public class DataObject
{
    public DateTime From { get; set; }
    public DateTime To { get; set; }
}

然后您可以將列表中的項目相互比較以確定它們是否重疊,如果是,則返回重疊的時間段(只是為了指出正確的方向,我沒有徹底測試此算法)

public DataObject[] GetOverlaps(DataObject[] objects)
{
    var result = new List<DataObject>();

    if (objects.Length > 1)
    {
        for (var i = 0; i < objects.Length - 1; i++)
        {
            var pivot = objects[i];

            for (var j = i + 1; j < objects.Length; j++)
            {
                var other = objects[j];

                // Do both ranges overlap?
                if (pivot.From > other.To || pivot.To < other.From)
                {
                    // No
                    continue;
                }

                result.Add(new DataObject
                {
                    From = pivot.From >= other.From ? pivot.From : other.From,
                    To = pivot.To <= other.To ? pivot.To : other.To,
                });
            }
        }
    }

    return result.ToArray();
}

嵌套范圍和其他極端情況的一些要求並不完全清楚。 但這是另一種算法,似乎可以滿足您的要求。 有限測試的通常警告當然適用 - 對於我沒有測試的極端情況根本無法使用。

該算法首先依賴於對數據進行排序。 如果你不能這樣做,那么這將不起作用。 算法本身如下:

    static IEnumerable<DateRange> ReduceToOverlapping(IEnumerable<DateRange> source)
    {
        if (!source.Any())
            yield break;

        Stack<DateRange> stack = new Stack<DateRange>();

        foreach (var r in source)
        {
            while (stack.Count > 0 && r.Start > stack.Peek().End)
                stack.Pop();

            foreach (var left in stack)
            {
                if (left.GetOverlap(r) is DateRange overlap)
                    yield return overlap;
            }

            stack.Push(r);
        }
    }

DateRange 是一個簡單的類,用於保存您顯示的日期。 它看起來像這樣:

class DateRange 
{
    public DateRange(DateRange other)
    { this.Start = other.Start; this.End = other.End; }

    public DateRange(DateTime start, DateTime end)
    { this.Start = start; this.End = end; }

    public DateRange(string start, string end)
    {
        const string format = "dd/MM/yyyy";

        this.Start = DateTime.ParseExact(start, format, CultureInfo.InvariantCulture);
        this.End = DateTime.ParseExact(end, format, CultureInfo.InvariantCulture);
    }

    public DateTime Start { get; set; }
    public DateTime End { get; set; }

    public DateRange GetOverlap(DateRange next)
    {
        if (this.Start <= next.Start && this.End >= next.Start)
        {
            return new DateRange(next.Start, this.End < next.End ? this.End : next.End);
        }

        return null;
    }
}

正如我提到的,這是通過首先對其進行排序來使用的。 對一些測試數據進行排序和調用方法的示例如下:

    static void Main(string[] _)
    {
        foreach (var (inputdata, expected) in TestData)
        {
            var sorted = inputdata.OrderBy(x => x.Start).ThenBy(x => x.End);
            var reduced = ReduceToOverlapping(sorted).ToArray();

            if (!Enumerable.SequenceEqual(reduced, expected, new CompareDateRange()))
                throw new ArgumentException("failed to produce correct result");
        }

        Console.WriteLine("all results correct");
    }

要進行測試,您需要一個相等比較器和此處的測試數據:

class CompareDateRange : IEqualityComparer<DateRange>
{
    public bool Equals([AllowNull] DateRange x, [AllowNull] DateRange y)
    {
        if (null == x && null == y)
            return true;

        if (null == x || null == y)
            return false;

        return x.Start == y.Start && x.End == y.End;
    }

    public int GetHashCode([DisallowNull] DateRange obj)
    {
        return obj.Start.GetHashCode() ^ obj.End.GetHashCode();
    }
}

    public static (DateRange[], DateRange[])[] TestData = new (DateRange[], DateRange[])[]
    {
        (new DateRange[]
            {
                new DateRange("01/01/2020", "18/01/2020"),
                new DateRange("08/01/2020", "17/01/2020"),
                new DateRange("09/01/2020", "15/01/2020"),
                new DateRange("14/01/2020", "20/01/2020"),
            },
         new DateRange[]
            {
                new DateRange("08/01/2020", "17/01/2020"),
                new DateRange("09/01/2020", "15/01/2020"),
                new DateRange("09/01/2020", "15/01/2020"),
                new DateRange("14/01/2020", "15/01/2020"),
                new DateRange("14/01/2020", "17/01/2020"),
                new DateRange("14/01/2020", "18/01/2020"),
            }),
        (new DateRange[]
            {
                new DateRange("01/01/2020", "10/01/2020"),
                new DateRange("08/01/2020", "20/01/2020"),
            },
         new DateRange[]
            {
                new DateRange("08/01/2020", "10/01/2020"),
            }),
        (new DateRange[]
            {
                new DateRange("01/01/2020", "10/01/2020"),
                new DateRange("08/01/2020", "20/01/2020"),
                new DateRange("18/01/2020", "22/01/2020"),
            },
         new DateRange[]
            {
                new DateRange("08/01/2020", "10/01/2020"),
                new DateRange("18/01/2020", "20/01/2020"),
            }),
        (new DateRange[]
            {
                new DateRange("01/01/2020", "18/01/2020"),
                new DateRange("08/01/2020", "10/01/2020"),
                new DateRange("18/01/2020", "22/01/2020"),
            },
         new DateRange[]
            {
                new DateRange("08/01/2020", "10/01/2020"),
                new DateRange("18/01/2020", "18/01/2020"),
            }),
    };

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM