如何 GroupBy 时间范围(关闭时间分组在一起)

How to GroupBy time-range (closed-times grouped together)

假设我们有这样的记录:

NID    CId    PushedAt
120    796    2015-09-04 18:00:53.6012627 +00:00
120    967    2015-09-04 18:00:51.9891748 +00:00
119    669    2015-09-04 17:45:56.8179094 +00:00
119    955    2015-09-04 17:45:55.2078154 +00:00
119    100    2015-09-04 17:45:53.5867187 +00:00
116    384    2015-09-04 17:01:01.5375630 +00:00
116    155    2015-09-04 17:00:59.9284665 +00:00
116    517    2015-09-04 17:00:58.3193725 +00:00
113    109    2015-09-04 16:00:53.5269438 +00:00
113    111    2015-09-04 16:00:51.9168442 +00:00
107    603    2015-09-04 13:45:59.9994496 +00:00

我想按时间范围(不是特定时间)对它们进行分组。如果我按时间分组:

var grouped = list.GroupBy(t => new {
    t.PushedAt.Year, 
    t.PushedAt.Month, 
    t.PushedAt.Day, 
    t.PushedAt.Hour, 
    t.PushedAt.Minute
});

然后我会想念具有不同 Minute 但实际上在同一组中的组。例如,这些行:

116    384    2015-09-04 17:01:01.5375630 +00:00
116    155    2015-09-04 17:00:59.9284665 +00:00
116    517    2015-09-04 17:00:58.3193725 +00:00

将前往这些群组:

// group 1:
116    384    2015-09-04 17:01:01.5375630 +00:00
// group 2:
116    155    2015-09-04 17:00:59.9284665 +00:00
116    517    2015-09-04 17:00:58.3193725 +00:00

但是,我要找的是这个组:

// group 1:
116    384    2015-09-04 17:01:01.5375630 +00:00
116    155    2015-09-04 17:00:59.9284665 +00:00
116    517    2015-09-04 17:00:58.3193725 +00:00

意味着,这 3 行应该组合在一起。比如说,所有在 5 分钟范围内的行都应该组合在一起。完整的输出应该是这样的:

// group 1:
120    796    2015-09-04 18:00:53.6012627 +00:00
120    967    2015-09-04 18:00:51.9891748 +00:00
// group 2:
119    669    2015-09-04 17:45:56.8179094 +00:00
119    955    2015-09-04 17:45:55.2078154 +00:00
119    100    2015-09-04 17:45:53.5867187 +00:00
// group 3:
116    384    2015-09-04 17:01:01.5375630 +00:00
116    155    2015-09-04 17:00:59.9284665 +00:00
116    517    2015-09-04 17:00:58.3193725 +00:00
// group 4:
113    109    2015-09-04 16:00:53.5269438 +00:00
113    111    2015-09-04 16:00:51.9168442 +00:00
// group 5:
107    603    2015-09-04 13:45:59.9994496 +00:00

你有什么想法吗?

注意: NID 字段不可分组。

更新:

我知道我可以通过迭代项目来解决问题(正如 juharr 在评论中所说)。但是,我正在寻找 LINQ 解决方案(如果有的话)。谢谢。

根据 pasty 的评论,我想出了我可以将 IEqualityComparer<> 传递给 GroupBy 的方法。所以,我这样做了:

var grouped = list.GroupBy(t => t.PushedAt, new MyComparer());

有了这个比较器:

internal class MyComparer : IEqualityComparer<DateTime> {

    private static readonly TimeSpan Span = TimeSpan.FromMinutes(5);

    public bool Equals(DateTime x, DateTime y){
        return (x - y).Duration() <= Span;
    }

    public int GetHashCode(DateTime obj) {
        return obj.Year.GetHashCode() ^ obj.Month.GetHashCode() ^ obj.Day.GetHashCode();
    }

}

这正是我要找的东西。

我认为这样的事情可能会有所帮助:

var list = new List<myClass>();
list.Add(new myClass(120, 796, new DateTime(2015, 09, 04, 18, 00, 53)));
list.Add(new myClass(120, 967, new DateTime(2015, 09, 04, 18, 03, 51)));
list.Add(new myClass(119, 669, new DateTime(2015, 09, 04, 17, 45, 56)));
list.Add(new myClass(119, 955, new DateTime(2015, 09, 04, 17, 42, 55)));
list.Add(new myClass(119, 100, new DateTime(2015, 09, 04, 17, 41, 53)));
list.Add(new myClass(116, 384, new DateTime(2015, 09, 04, 17, 01, 01)));
list.Add(new myClass(116, 155, new DateTime(2015, 09, 04, 17, 00, 59)));
list.Add(new myClass(116, 517, new DateTime(2015, 09, 04, 17, 00, 58)));
list.Add(new myClass(113, 109, new DateTime(2015, 09, 04, 16, 02, 53)));
list.Add(new myClass(113, 111, new DateTime(2015, 09, 04, 16, 00, 51)));
list.Add(new myClass(107, 603, new DateTime(2015, 09, 04, 13, 45, 59)));

var grouped = list.GroupBy(t =>
    t.PushedAt.ToString("yyyyMMddHH") +
    ((int)(t.PushedAt.Minute / 5)).ToString("00")
);

foreach (var g in grouped) {
    Console.WriteLine(g.Key);
    foreach (var itm in g) {
        Console.WriteLine(String.Format("{0}\t{1}\t{2}", itm.CId, itm.NID, itm.PushedAt));
    }
}

控制台结果:

201509041800
796     120     9/4/2015 6:00:53 PM
967     120     9/4/2015 6:03:51 PM
201509041709
669     119     9/4/2015 5:45:56 PM
201509041708
955     119     9/4/2015 5:42:55 PM
100     119     9/4/2015 5:41:53 PM
201509041700
384     116     9/4/2015 5:01:01 PM
155     116     9/4/2015 5:00:59 PM
517     116     9/4/2015 5:00:58 PM
201509041600
109     113     9/4/2015 4:02:53 PM
111     113     9/4/2015 4:00:51 PM
201509041309
603     107     9/4/2015 1:45:59 PM

使用 .Ticks 属性.

这真的很容易做到

如果您从问题的输入开始:

var records = new[]
{
    new { NID = 120, PID = 796, PushedAt = DateTime.Parse("2015-09-04 18:00:53.6012627") },
    new { NID = 120, PID = 967, PushedAt = DateTime.Parse("2015-09-04 18:00:51.9891748") },
    new { NID = 119, PID = 669, PushedAt = DateTime.Parse("2015-09-04 17:45:56.8179094") },
    new { NID = 119, PID = 955, PushedAt = DateTime.Parse("2015-09-04 17:45:55.2078154") },
    new { NID = 119, PID = 100, PushedAt = DateTime.Parse("2015-09-04 17:45:53.5867187") },
    new { NID = 116, PID = 384, PushedAt = DateTime.Parse("2015-09-04 17:01:01.5375630") },
    new { NID = 116, PID = 155, PushedAt = DateTime.Parse("2015-09-04 17:00:59.9284665") },
    new { NID = 116, PID = 517, PushedAt = DateTime.Parse("2015-09-04 17:00:58.3193725") },
    new { NID = 113, PID = 109, PushedAt = DateTime.Parse("2015-09-04 16:00:53.5269438") },
    new { NID = 113, PID = 111, PushedAt = DateTime.Parse("2015-09-04 16:00:51.9168442") },
    new { NID = 107, PID = 603, PushedAt = DateTime.Parse("2015-09-04 13:45:59.9994496") },
};

那么分组的方法如下:

var results =
    records
        .GroupBy(x => x.PushedAt.Ticks / TimeSpan.TicksPerMinute / 5);

我得到这些结果: