使用 Linq 在 C# 中进行日期范围插值

本文关键字:日期 范围 插值 Linq 使用 | 更新日期: 2023-09-27 18:30:33

我有一个事件类,其中包含事件的开始和结束日期以及一周中可能发生的特定日期:

public class Event
{
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public DayOfWeek[] DayOfWeekList { get; set; }
    public string Title { get; set; }
}

我可以有一次发生的事件:

事件 1:开始日期 = 9/15/2013,

结束日期 = 9/15/2013,DOW = 星期日

我可以举办多天的活动:

事件 2:开始日期 = 9/15/2013,

结束日期 = 9/16/2013,DOW = 星期日、星期一

我可以在某个日期范围内发生事件,但仅限于一周中特定的、可能不连续的日子,例如,在 9 月份,但仅在周二和周四

:事件 3:开始日期 = 9/1/2013,

结束日期 = 9/30/2013,DOW = 星期二、星期四

给定 9/1 - 9/16 的日期范围,我想在网格中将事件显示为一个事件日记录,如下所示:

活动 3: 9/3/2013 星期二活动 3: 9/5/2013 星期四活动 3: 9/10/2013 星期二活动 3: 9/12/2013 星期四活动 1: 9/15/2013 星期日活动 2: 9/15/2013 星期日活动 2: 9/16/2013 星期一

我只会在数据库中存储一条事件记录来表示事件天数,而不是每天存储一条记录。因此,对于上面的日期范围,我将从数据库中检索 3 个事件记录,并需要使用事件的星期几根据计划生成此日期范围的事件记录序列。

我将使用 Linq to SQL 检索 3 个事件记录,但随后需要扩展每个事件的计划以创建新的事件记录,每天一个。这似乎效率不高,尤其是在网站被广泛使用的情况下。

有没有办法使用 Linq 完成此操作?

另一种方法是在数据库中为事件发生的每一天提供不同的子记录,但如果有人将时间表从每个星期一更改为每个星期二,这将是一个痛苦的维护。此外,我们可以有没有定义结束日期的事件。

使用 Linq 在 C# 中进行日期范围插值

这是一个蛮力解决方案,但这样的事情应该有效:

var startDate = new DateTime(2013, 9, 1);
var endDate = new DateTime(2013, 9, 16);
var totalDays = (int)(endDate - startDate).TotalDays + 1;
var results = 
    from i in Enumerable.Range(0, totalDays)
    let d = startDate.AddDays(i)
    from e in eventList 
    where e.StartDate <= d && 
          e.EndDate >= d &&
          e.DayOfWeekList.Contains(d.DayOfWeek)
    select new { Event = e, Date = d };

当我在内存中测试它时,这会生成正确的结果。遗憾的是,这可能无法很好地转换为纯 Linq 到 SQL 代码。我认为您最好的选择是首先具体化事件的子集,然后使用修改后的查询在内存中生成最终结果集:

var eventList = 
    (from e in db.Events 
     where e.StartDate <= endDate && 
           e.EndDate >= startDate
     select e)
    .AsEnumerable();
var results = 
    from i in Enumerable.Range(0, totalDays)
    let d = startDate.AddDays(i)
    from e in eventList 
    where e.StartDate <= d && 
          e.EndDate >= d &&
          e.DayOfWeekList.Contains(d.DayOfWeek) 
    select new { Event = e, Date = d };

如果您最常阅读事件,并且不介意节省少量额外成本,则可以选择根据规则将事件具体化到新表中。如果需要,可以根据规则清理和重新生成此表,也可以根据需要有选择地重新填充此表。

保存事件 1

时将创建一个条目,事件 ID:1

保存事件 2 时,将为日期范围创建与 DOW 属性匹配的条目,事件 ID:2

事件 3 将执行相同的操作,但日期范围/DOW 为 EventID:3

如果需要,您可以使用一些花哨的 LINQ 来确定要添加哪些行,但最终结果是您希望物理实例行能够真正轻松地进行查询。

更新事件时,只需删除该 EventID 的所有行,然后根据更改重新创建它们。

鉴于上述情况,您的读取操作可以像以下那样简单:

EventInstances.Where(i => i.Date >= startDate && i.Date <= endDate);

相反,如果您想要慢速/复杂读取和快速写入,则需要在每次读取时生成此内存映射。

更新:一些代码来显示我的意思

填充表的逻辑与可用于创建内存中表的逻辑完全相同:

// slightly updated Event class
public class Event
{
    public int ID { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime? EndDate { get; set; }
    public DayOfWeek[] DayOfWeekList { get; set; }
    public string Title { get; set; }
}
var startDate = new DateTime(2013, 9, 1);
var endDate = new DateTime(2013, 9, 16);
var totalDays = (int)endDate.Subtract(startDate).TotalDays + 1;
// sample data, including a 4th event starting a while ago with no end date
var events = new List<Event> {
    new Event { ID = 1, Title = "Event 1", StartDate = new DateTime(2013, 9, 15), EndDate = new DateTime(2013, 9, 15), DayOfWeekList = new[] { DayOfWeek.Sunday } },
    new Event { ID = 2, Title = "Event 2", StartDate = new DateTime(2013, 9, 15), EndDate = new DateTime(2013, 9, 16), DayOfWeekList = new[] { DayOfWeek.Sunday, DayOfWeek.Monday } },
    new Event { ID = 3, Title = "Event 3", StartDate = new DateTime(2013, 9, 1), EndDate = new DateTime(2013, 9, 30), DayOfWeekList = new[] { DayOfWeek.Tuesday, DayOfWeek.Thursday } },
    new Event { ID = 4, Title = "Event 4", StartDate = new DateTime(2013, 1, 1), EndDate = null, DayOfWeekList = new[] { DayOfWeek.Wednesday } },
};
var eventsInRange = events
    .Where(e => e.StartDate <= endDate)
    .Where(e => (e.EndDate == null || e.EndDate.Value >= startDate ))
    // if you are getting from the database, force this data to be 
    // retrieved since the following section would not work with the DB
    .AsEnumerable();
var daysInRange = Enumerable
    .Range(0, totalDays)
    .Select(i => startDate.AddDays(i));
var eventInstances = daysInRange
    .SelectMany(d => eventsInRange
        .Where(e => e.EndDate == null || d <= e.EndDate.Value)
        .Where(e => d >= e.StartDate)
        .Where(e => e.DayOfWeekList.Contains(d.DayOfWeek))
        .Select(e => new { Date = d, Day = d.DayOfWeek, Event = e }));

如果您想使用此数据填充表格以便于查询,只需根据合理的内容设置开始日期和结束日期(例如,1 年前开始,2 年前结束)。

如果要在更新后仅为一个事件重新填充,只需删除该事件 ID 的所有记录,并将eventsInRange限制为仅更新事件。