【问题标题】:Finding lowest price for overlapping date ranges - C# algorithm查找重叠日期范围的最低价格 - C# 算法
【发布时间】:2017-05-08 12:04:51
【问题描述】:

为特定时间段设置了价格...我无法想出一种算法来确定特定时间段的最低价格。

我正在使用对象列表执行此操作,其中对象具有属性DateTime StartDate, DateTime EndDate, decimal Price

例如,两个价格集及其有效日期范围:

A. 09/26/16 - 12/31/17 at $20.00
B. 12/01/16 - 12/31/16 at $18.00

您可以看到 B 在 A 时间段内并且更低。

我需要把它转换成这个:

A. 09/26/16 - 11/30/16 at $20.00
B. 12/01/16 - 12/31/16 at $18.00
C. 01/01/17 - 12/31/17 at $20.00

它必须适用于任意数量的日期范围和组合。有没有人遇到过任何我可以操纵以获得我需要的结果的东西?或者有什么建议?

编辑:我的数据结构:

public class PromoResult
{
    public int ItemId { get; set; }
    public decimal PromoPrice { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public int PromoType { get; set; } // can ignore this...
}

【问题讨论】:

  • Stack Overflow 更专注于帮助解决您提出的实现中的问题,而不是提供新的实现。
  • 哦,老天,老天……我一直在努力解决这个问题。我有一个解决方案,但我不确定你会喜欢它。
  • @Chris Berger 我会接受的。我整天都在处理合并日期范围并修复它们,这是我需要解开这个谜题的最后一部分……而且到目前为止还不是很好。
  • 所以,我正在浏览我的代码,试图找到我的解决方案,我意识到我的实现完全是在 SQL 中,而不是在 C# 中......编辑:而且,它非常复杂,非常复杂案例,并且还专门针对一次一个日期...
  • SQL 可能会更好。我正在获取一个独特的 ItemId, StartDate, EndDate, Price, PriceType 并在 C# 中对其进行操作。如果可能的话,在 SQL 中可能会更好......但我已经走了一条复杂的道路来解决我的问题。基本上,如果日期范围重叠或接触并且价格相同,我必须合并它们。我也许可以将其转换为 Linq。

标签: c# algorithm date-range


【解决方案1】:

我将使用两个函数DateRangeGroupSequenceWhile

List<PromoResult> promoResult = new List<PromoResult>()
{
    new PromoResult() {  PromoPrice=20, StartDate = new DateTime(2016, 9, 26),EndDate=new DateTime(2017, 12, 31)},
    new PromoResult() {  PromoPrice=18, StartDate = new DateTime(2016, 12, 1),EndDate=new DateTime(2016, 12, 31)}
};

var result = promoResult.SelectMany(x => DateRange(x.StartDate, x.EndDate, TimeSpan.FromDays(1))
                                         .Select(y => new { promo = x, date = y }))
            .GroupBy(x => x.date).Select(x => x.OrderBy(y => y.promo.PromoPrice).First())
            .OrderBy(x=>x.date)
            .ToList();

var final = result.GroupSequenceWhile((x, y) => x.promo.PromoPrice == y.promo.PromoPrice)
            .Select(g => new { start = g.First().date, end = g.Last().date, price = g.First().promo.PromoPrice })
            .ToList();

foreach (var r in final)
{
    Console.WriteLine(r.price + "$ " + r.start.ToString("MM/dd/yy", CultureInfo.InvariantCulture) + " " + r.end.ToString("MM/dd/yy", CultureInfo.InvariantCulture));
}

输出:

20$ 09/26/16 11/30/16
18$ 12/01/16 12/31/16
20$ 01/01/17 12/31/17

算法:

1- 为promoResult 列表中的每个项目创建一个&lt;day,price&gt; 元组

2- 按天分组此元组并选择最低价格

3- 按日期排序此元组

4-选择连续几天价格发生变化的起始日和结束日


IEnumerable<DateTime> DateRange(DateTime start, DateTime end, TimeSpan period)
{
    for (var dt = start; dt <= end; dt = dt.Add(period))
    {
        yield return dt;
    }
}

public static IEnumerable<IEnumerable<T>> GroupSequenceWhile<T>(this IEnumerable<T> seq, Func<T, T, bool> condition) 
{
    List<T> list = new List<T>();
    using (var en = seq.GetEnumerator())
    {
        if (en.MoveNext())
        {
            var prev = en.Current;
            list.Add(en.Current);

            while (en.MoveNext())
            {
                if (condition(prev, en.Current))
                {
                    list.Add(en.Current);
                }
                else
                {
                    yield return list;
                    list = new List<T>();
                    list.Add(en.Current);
                }
                prev = en.Current;
            }

            if (list.Any())
                yield return list;
        }
    }
}

【讨论】:

  • 我正在修改它,我真的认为这超出了我的理解。这适用于无限数量的日期范围/价格还是仅适用于两个?
  • @justiceorjustus 只是连接输入范围。见range1.Concat(range2....)所以没有限制...
  • 我还是不明白。如何使用 List 而不是单个变量来做到这一点?还是我自己的日期范围/价格列表?
  • @justiceorjustus 这就是我能做的。如果您发布数据结构,显示您如何表示日期范围和相关价格,我可以给出一个可能更清楚的答案......(我在您的帖子中没有看到任何代码,只是一个请求)
  • 非常感谢您的帮助。我进行了编辑以包含我的数据结构。实际上,每个 ItemId 有 3 种 PromoType,但可以忽略该属性。我一直在为每种类型的 PromoType 单独运行它,所以它不会让人感到困惑。
【解决方案2】:

这是使用 Linq 的绝佳案例。假设您的价格范围对象称为PriceRecord...

您需要创建所有日期的列表,然后过滤到两个连续日期之间的价格记录。实现可能如下所示:

    public static IEnumerable<PriceRecord> ReduceOverlaps(IEnumerable<PriceRecord> source)
    {
        // Get a list of all edges of date ranges
        // edit, added OrderBy (!)
        var edges = source.SelectMany(record => new[] { record.StartDate, record.EndDate }).OrderBy(d => d).ToArray();
        // iterate over pairs of edges (i and i-1)
        for (int i = 1; i < edges.Length; i++)
        {
            // select min price for range i-1, i
            var price = source.Where(r => r.StartDate <= edges[i - 1] && r.EndDate >= edges[i]).Select(r => r.Price).Min();
            // return a new record from i-1, i with price
            yield return new PriceRecord() { StartDate = edges[i - 1], EndDate = edges[i], Price = price };
        }
    }

我没有对此进行测试,您可能需要修改比较运算符,但这可能是一个很好的起点。 我现在已经测试了代码,这里的示例适用于问题中的数据。

请随时提出修改建议以改进此示例。

【讨论】:

  • 我想我喜欢这个解决方案。有一定的潜力。
  • 这看起来很有希望...试一试,但必须先进行一些更改。我会告诉你的。
  • 我一直在用我的例子来玩它,它与我在原始帖子中寻找的结果略有不同。 StartDates 和 EndDates 之间需要有 1 天的间隔。这将返回 A: 9/26/2016 - 12/1/2016, B: 12/1/2016 - 12/31/2016, C: 12/31/2016-12/31/2017,其中 A 和 C 不正确。不知道我是否应该创建另一种方法来修复它,或者这个方法中是否有可以改变的东西......我把它搞砸了。
  • 您可以使用EndDate = edges[i].AddDays(-1) 将范围的最后一天向后推。
  • 我确实尝试过,它会在每个 EndDate 中减少一天,而实际上只有内部范围需要关闭这一天。返回:A: 9/26/2016 - 11/30/2016, B: 12/1/2016 - 12/30/2016, C: 12/31/2016-12/30/2017
【解决方案3】:

没有直接回答您的问题,但这里有一些 SQL 用于解决我遇到的类似问题(简化了一点,因为我还处理多个位置和不同的价格类型):

SELECT RI.ItemNmbr, RI.UnitPrice, RI.CasePrice
    , RP.ProgramID
    , Row_Number() OVER (PARTITION BY RI.ItemNmbr,
                         ORDER BY CASE WHEN RI.UnitPrice > 0 
                                       THEN RI.UnitPrice
                                       ELSE 1000000 END ASC
                                  , CASE WHEN RI.CasePrice > 0
                                         THEN RI.CasePrice
                                         ELSE 1000000 END ASC
                                  , RP.EndDate DESC
                                  , RP.BeginDate ASC
                                  , RP.ProgramID ASC) AS RowNumBtl
    , Row_Number() OVER (PARTITION BY RI.UnitPrice, 
                         ORDER BY CASE WHEN RI.CasePrice > 0 
                                       THEN RI.CasePrice
                                       ELSE 1000000 END ASC
                                  , CASE WHEN RI.UnitPrice > 0
                                         THEN RI.UnitPrice
                                         ELSE 1000000 END ASC
                                  , RP.EndDate DESC
                                  , RP.BeginDate ASC
                                  , RP.ProgramID ASC) AS RowNumCase
  FROM RetailPriceProgramItem AS RI
    INNER JOIN RetailPriceMaster AS RP
        ON RP.ProgramType = RI.ProgramType AND RP.ProgramID = RI.ProgramID
  WHERE RP.ProgramType='S'
        AND RP.BeginDate <= @date AND RP.EndDate >= @date
                    AND RI.Active=1

我从 RowNumBtl=1 的 UnitPrice 和 RowNumCase=1 的 CasePrice 中选择。如果您随后创建一个日期表(您可以使用 CTE 执行此操作),您可以在每个日期交叉应用。这有点低效,因为您只需要在日期范围之间的边界条件下进行测试,所以......祝你好运。

【讨论】:

    【解决方案4】:

    试试这个

    假设我们有:

    public class DatePrice
    {
        public DateTime StartDate { get; set; }
        public DateTime EndDate { get; set; }
        public decimal Price { get; set; }
    }
    

    IList<DatePrice> list = new List<DatePrice>(); // populate your data from the source..
    var lowestPriceItem = list.OrderBy(item => item.Price).First();
    

    应该给你最低价的商品。

    【讨论】:

    • 答案取决于日期,各种范围可以重叠。
    【解决方案5】:

    我会从基于开始日期的日期顺序的范围开始,将第一个条目作为一个完整的范围添加:

    09/26/16 - 12/31/17 at $20.00
    TBD:
    12/01/16 - 12/31/16 at $18.00
    

    接下来抓取你拥有的下一个范围,如果它与前一个重叠,则分割重叠(重叠的种类很少,确保全部处理)取重叠区域的最小值:

    09/26/16 - 11/30/16 at $20.00
    12/01/16 - 12/31/16 at $18.00
    TBD:
    01/01/17 - 12/31/17 at $20.00
    

    请注意,您还没有最后一个,因为您会进行任何拆分,然后将它们放回您的“尚未比较”项目的排序列表中。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-05-27
      • 1970-01-01
      相关资源
      最近更新 更多