按时间间隔对对象进行分组

时间:2019-03-12 16:51:46

标签: c# linq asp.net-core

我有一系列非常复杂的linq查询,除了一件事情外,它可以完成我需要做的所有事情。

首先让我解释一下它的作用。

这一系列查询获取有关游戏会议的数据,并基于相同的EventSession.SessionEventName和相同的gameID连接SessionDate

因此,在视图中,它看起来像这样:

    GameID    | GameName    | Names            | PlayDate     | PlayDuration
    123         Pac-Man       Joe; Jim; Mary     10/1/2018      10:00 AM - 12:30 PM

所以一切正常,除了...我需要将其添加到查询中,但我不知道该放在哪里或如何做:

我需要添加一个条件,如果同一天的两场比赛之间的间隔超过35分钟,则将行分开。

例如,如果乔,吉姆或玛丽的上场时间间隔超过35分钟,则上面的内容看起来像这样。

    EventID | GameID    | GameName    | Names            | PlayDate     | PlayDuration
    -----------------------------------------------------------------------------------------
       1      123         Pac-Man       Joe; Jim           10/1/2018      10:00 AM - 11:00 AM
       2      123         Pac-Man       Mary               10/1/2018      11:40 AM - 12:30 PM

因此,基本上,如果GameID和PlayDate相同,它将玩家分组在一起。但是,如果任何地方有35分钟以上的间隔,它将被分成一个新的行分组。

我不知道该怎么做。

我将不胜感激。

谢谢!

这是我的控制器方法,其中包含以下魔术代码:

    [HttpPost]
    public async Task<IActionResult> SearchByDate(DateTime? start, DateTime? end)
    {

        // create a list of conferenceGames
        var conferenceGames = await _context.PublisherList
                     .Where(m => m.MakerCatalogId != null
                     && !m.IsBetaStatus).ToListAsync();

        // create a list of conferencePlayers
        var conferencePlayers = await _context.PlayerList
            .Where(p => p.PlayerTypeId == 23
            && p.PlayerStartTime != null
            && p.PlayerEndTime != null)
            .OrderBy(p => p.PlayerStartTime).ToListAsync();

        // create a list of conferenceSponsers
        var conferenceSponsers = await _context.SponserList
            .Where(f => f.SponserDateTimeStart >= start 
            && f.SponserDateTimeStart <= end
            && f.PublisherId != 2000111
            ).ToListAsync();

        // create an IEnumerable of EventSession 
        var query = from cp in conferencePlayers
                    join cs in conferenceSponsers on cp.SponserId equals cs.SponserId
                    join cg in conferenceGames on cs.PublisherId equals cg.PublisherId
                    select new EventSession
                    {
                        Id = cp.PlayerId,
                        GameId = cg.PublisherId,
                        SessionGameName = cg.GameDisplayName,
                        SessionEventName = cp.PlayerDisplayName,
                        SessionDate = cs.SponserDateTimeStart,
                        SessionStartTime = cp.PlayerStartTime.Value.TimeOfDay,
                        SessionEndTime = cp.PlayerEndTime.Value.TimeOfDay
                    };

        // order the results
        var orderedResults = query
            .OrderBy(n => n.GameId)
            .ThenBy(d => d.SessionDate)
            .ThenBy(tsa => tsa.SessionStartTime)
            .ToList();

        // group the List by Date and Game
        List<GroupedEvents> playersGroupList = orderedResults.GroupBy(x => new { x.SessionDate, x.GameId }).Select(group => new GroupedEvents
        {
            GameName = group.Select(n => n.SessionGameName).FirstOrDefault(),
            GameId = group.Select(c => c.GameId).FirstOrDefault().ToString(),
            PlayDate = group.Select(d => d.SessionDate).FirstOrDefault(),
            Names = String.Join(" ; ", group.Select(g => g.SessionEventName).ToArray()),
            PlayDuration = group.Select(g => g.SessionStartTime).First() + " - " + group.Select(g => g.SessionEndTime).Last(),
        }).ToList();

        // return the ordered/grouped list back to the view
        return View(playersGroupList);
    }

1 个答案:

答案 0 :(得分:1)

使用一些扩展方法,可以很容易地做到这一点。

首先,作为Aggregate的变体,它是APL扫描运算符的一个版本,沿着IEnumerable行进,返回中间结果,但是该变体组合了一次,当前和上一项的对:

// TKey combineFn((TKey Key, T Value) CurKeyItem, T nextItem):
// CurKeyItem.Key = Current Key
// CurKeyItem.Value = Current Item
// NextItem = Next Item
// returns (Key, Current Item)
public static IEnumerable<(TKey Key, T Value)> ScanToPairs<T, TKey>(this IEnumerable<T> src, TKey seedKey, Func<(TKey Key, T Value), T, TKey> combineFn) {
    using (var srce = src.GetEnumerator())
        if (srce.MoveNext()) {
            var curkv = (seedKey, srce.Current);

            while (srce.MoveNext()) {
                yield return curkv;
                curkv = (combineFn(curkv, srce.Current), srce.Current);
            }
            yield return curkv;
        }
}

说明:ScanToPairsIEnumerable开始,从第一个和第二个值以及seedKey值开始。它将包含当前Key和当前项目以及(单独)下一个项目的ValueTuple传递到CombineFn,并生成ValueTuple的Key当前项目。因此,第一个结果是(seedKey,FirstItem)。第二个结果将是(combineFn((seedKey,FirstItem),SecondItem),SecondItem)。依此类推。

然后是一个GroupBy运算符,该运算符通过使用布尔测试函数对对进行分组:

// bool testFn(T prevItem, T curItem)
// returns groups by runs of matching bool
public static IEnumerable<IGrouping<int, T>> GroupByPairsWhile<T>(this IEnumerable<T> src, Func<T, T, bool> testFn) =>
    src.ScanToPairs(1, (kvp, cur) => testFn(kvp.Value, cur) ? kvp.Key : kvp.Key + 1)
       .GroupBy(kvp => kvp.Key, kvp => kvp.Value);

说明:使用ScanToPairs方法,此方法将IEnumerable分组为元组,其中键是一个以1开头的整数,表示true的运行次数testFn是通过将前一项与当前项进行比较而得出的。对所有运行编号后,它们将与GroupBy一起分组为属于运行的项目组。

有了这些助手,它就相对简单了。在第一个分组之后添加SelectMany,以根据基于时间的条件将每个分组划分为子分组:

var playersGroupList = orderedResults.GroupBy(x => new { x.SessionDate, x.GameId })
                                     .SelectMany(g => g.GroupByPairsWhile((p, c) => c.SessionStartTime-p.SessionEndTime <= TimeSpan.FromMinutes(35)))
                                     .Select(group => new GroupedEvents {
                                         GameName = group.Select(n => n.SessionGameName).FirstOrDefault(),
                                         GameId = group.Select(c => c.GameId).FirstOrDefault().ToString(),
                                         PlayDate = group.Select(d => d.SessionDate).FirstOrDefault(),
                                         Names = String.Join("; ", group.Select(g => g.SessionEventName).ToArray()),
                                         PlayDuration = group.Select(g => g.SessionStartTime).First() + " - " + group.Select(g => g.SessionEndTime).Last(),
                                     })
                                     .ToList();

因此,SelectMany将每个组放在给定的SessionDate上,然后将它们分组为运行,其中每个成员距下一个成员少于35分钟。由于SelectMany,所有子组都被提升为最终结果的组。因此,现在您有了分组,每个分组包含一组会话,从SessionEndTime到下一个SessionStartTime不到35分钟。请注意,无论何时,跑步都会在一天结束时结束,因此,如果您的跑步时间是午夜,则需要更改分组。

注意:如果同时开始的会话可能具有不同的持续时间(即结束时间),那么您需要在ThenBy(tsa => tsa.SessionEndTime)排序中添加orderedResults