【问题标题】:Filling in missing dates using a linq group by date query使用 linq 按日期查询分组填充缺失的日期
【发布时间】:2009-09-23 21:35:14
【问题描述】:

我有一个 Linq 查询,它基本上计算在特定日期创建了多少条目,这是通过按年、月、日分组来完成的。问题是因为有些日子没有任何条目,我需要用 0 计数的条目来回填那些丢失的“日历日”。 我的猜测是,这可能可以通过 Union 或其他东西来完成,或者甚至可以通过一些简单的 for 循环来处理查询后的记录。

这里是查询:

from l in context.LoginToken
 where l.CreatedOn >= start && l.CreatedOn <= finish
 group l by
 new{l.CreatedOn.Year, l.CreatedOn.Month, l.CreatedOn.Day} into groups
 orderby groups.Key.Year , groups.Key.Month , groups.Key.Day
     select new StatsDateWithCount {
                                    Count = groups.Count(),
                                     Year =  groups.Key.Year,
                                    Month = groups.Key.Month,
                                      Day = groups.Key.Day
                                                                  }));

如果我有 2009 年 12 月 1 日至 12 月 4 日的数据(简化):

12/1/2009 20
12/2/2009 15
12/4/2009 16

我想要通过代码添加 12/3/2009 0 的条目。

我知道通常这应该在数据库中使用非规范化表完成,您可以使用数据填充该表或加入日历表,但 我的问题是如何在代码中完成此操作?
可以在 Linq 中完成吗?是否应该在 Linq 中完成?

【问题讨论】:

标签: c# linq datetime group-by


【解决方案1】:

我今天才这样做。我从数据库中收集了完整的数据,然后生成了一个“空样本”表。最后,我将空表与真实数据进行了外连接,并使用 DefaultIfEmpty() 构造来处理知道何时从数据库中丢失了一行,并用默认值填充它。

这是我的代码:

int days = 30;

// Gather the data we have in the database, which will be incomplete for the graph (i.e. missing dates/subsystems).
var dataQuery =
    from tr in SourceDataTable
    where (DateTime.UtcNow - tr.CreatedTime).Days < 30
    group tr by new { tr.CreatedTime.Date, tr.Subsystem } into g
    orderby g.Key.Date ascending, g.Key.SubSystem ascending
    select new MyResults()
    {
        Date = g.Key.Date, 
        SubSystem = g.Key.SubSystem,
        Count = g.Count()
    };

// Generate the list of subsystems we want.
var subsystems = new[] { SubSystem.Foo, SubSystem.Bar }.AsQueryable();

// Generate the list of Dates we want.
var datetimes = new List<DateTime>();
for (int i = 0; i < days; i++)
{
    datetimes.Add(DateTime.UtcNow.AddDays(-i).Date);
}

// Generate the empty table, which is the shape of the output we want but without counts.
var emptyTableQuery =
    from dt in datetimes
    from subsys in subsystems
    select new MyResults()
    {
        Date = dt.Date, 
        SubSystem = subsys,
        Count = 0
    };

// Perform an outer join of the empty table with the real data and use the magic DefaultIfEmpty
// to handle the "there's no data from the database case".
var finalQuery =
    from e in emptyTableQuery
    join realData in dataQuery on 
        new { e.Date, e.SubSystem } equals 
        new { realData.Date, realData.SubSystem } into g
    from realDataJoin in g.DefaultIfEmpty()
    select new MyResults()
    {
        Date = e.Date,
        SubSystem = e.SubSystem,
        Count = realDataJoin == null ? 0 : realDataJoin.Count
    };

return finalQuery.OrderBy(x => x.Date).AsEnumerable();

【讨论】:

  • 这与我最终做的非常相似,但对结果进行了联合而不是执行联接。
【解决方案2】:

我制作了一个辅助函数,旨在与匿名类型一起使用,并以尽可能通用的方式重用。

假设这是您获取每个日期的订单列表的查询。

var orders = db.Orders
             .GroupBy(o => o.OrderDate)
             .Select(o => new 
             {
                OrderDate = o.Key,
                OrderCount = o.Count(),
                Sales = o.Sum(i => i.SubTotal)
             }
             .OrderBy(o => o.OrderDate);

为了使我的功能正常工作,请注意此列表必须按日期排序。如果我们有一天没有销售,那么清单上就会有一个漏洞。

现在是使用默认值(匿名类型的实例)填充空白的函数。

    private static IEnumerable<T> FillInEmptyDates<T>(IEnumerable<DateTime> allDates, IEnumerable<T> sourceData, Func<T, DateTime> dateSelector, Func<DateTime, T> defaultItemFactory)
    {
        // iterate through the source collection
        var iterator = sourceData.GetEnumerator();
        iterator.MoveNext();

        // for each date in the desired list
        foreach (var desiredDate in allDates)
        {
            // check if the current item exists and is the 'desired' date
            if (iterator.Current != null && 
                dateSelector(iterator.Current) == desiredDate)
            {
                // if so then return it and move to the next item
                yield return iterator.Current;
                iterator.MoveNext();

                // if source data is now exhausted then continue
                if (iterator.Current == null)
                {
                    continue;
                }

                // ensure next item is not a duplicate 
                if (dateSelector(iterator.Current) == desiredDate)
                {
                    throw new Exception("More than one item found in source collection with date " + desiredDate);
                }
            }
            else
            {
                // if the current 'desired' item doesn't exist then
                // create a dummy item using the provided factory
                yield return defaultItemFactory(desiredDate);
            }
        }
    }

用法如下:

// first you must determine your desired list of dates which must be in order
// determine this however you want    
var desiredDates = ....; 

// fill in any holes
var ordersByDate = FillInEmptyDates(desiredDates, 

                               // Source list (with holes)
                               orders, 

                               // How do we get a date from an order
                               (order) => order.OrderDate,

                               // How do we create an 'empty' item 
                               (date) => new 
                               {
                                     OrderDate = date,
                                     OrderCount = 0,
                                     Sales = 0
                               });
  • 必须确保所需日期列表中没有重复项
  • desiredDatessourceData 都必须按顺序排列
  • 因为如果您使用匿名类型,该方法是通用的,那么编译器会自动告诉您您的“默认”项目是否与常规项目的“形状”不同。
  • 现在我在sourceData 中包含重复项检查,但在desiredDates 中没有这样的检查
  • 如果您想确保列表按日期排序,您需要添加额外的代码

【讨论】:

  • 我认为这是一个非常具体的“业务”场景,我认为试图将其挤入一个“优雅”的 linq 构造会适得其反——但这是我能想到的第二个最优雅的事情
【解决方案3】:

基本上我在这里最终做的是创建一个相同类型的列表,其中所有日期都在范围内,计数值为 0。然后将我原始查询的结果与此列表合并。主要的障碍只是创建一个自定义的 IEqualityComparer。在此处了解更多详情:click here

【讨论】:

    【解决方案4】:

    您可以生成从“开始”到“结束”的日期列表,然后逐步检查每个日期的计数次数

    【讨论】:

    • 这没问题,但我想看看如何使用一些 linq 结构(如 Union 运算符)来完成。
    猜你喜欢
    • 1970-01-01
    • 2023-03-25
    • 1970-01-01
    • 1970-01-01
    • 2019-07-22
    • 1970-01-01
    • 2018-07-15
    • 1970-01-01
    相关资源
    最近更新 更多