web-dev-qa-db-fra.com

Groupement complexe de Linq

Je suis nouveau sur Stack Overflow, mais j'ai essayé de mettre autant d'informations

J'ai la structure de classe suivante

public class ItemEntity
{
    public int ItemId { get; set; }
    public int GroupId { get; set; }
    public string GroupName { get; set; }
    public DateTime ItemDate { get; set; }
    public string Field1 { get; set; }
    public string Filed2 { get; set; }
    public string Field3 { get; set; }
    public string Field4 { get; set; }
    public int Duration { get; set; }        
}

public class MasterEntity
{
    public ItemEntity Item { get; set; }
    public List<int> ItemList { get; set; }
    public List<int> GroupList { get; set; }
}

J'essaie de regrouper la liste de ItemEntity dans MasterEntity. Les champs de regroupement sont Field1, Field2 et Field3.

J'ai fait le regroupement jusqu'à présent comme ci-dessous

var items = new List<ItemEntity>
            {
                new ItemEntity
                {
                    ItemId = 100,
                    GroupId = 1,
                    GroupName= "Group 1",
                    ItemDate = new DateTime(2018,10,17),
                    Duration = 7,
                    Field1 = "Item Name 1",
                    Filed2 = "aaa",
                    Field3= "bbb",
                    Field4= "abc"
                },
                new ItemEntity
                {
                    ItemId = 150,
                    GroupId = 2,
                    GroupName= "Group 2",
                    ItemDate = new DateTime(2018,10,17),
                    Duration = 5,
                    Field1 = "Item Name 1",
                    Filed2 = "aaa",
                    Field3= "bbb",
                    Field4= "efg"
                },
                new ItemEntity
                {
                    ItemId = 250,
                    GroupId = 3,
                    GroupName= "Group 3",
                    ItemDate = new DateTime(2018,10,15),
                    Duration = 7,
                    Field1 = "Item Name 1",
                    Filed2 = "aaa",
                    Field3= "bbb",
                    Field4= "xyz"
                }
            };


            var group = items.GroupBy(g => new
            {
                g.Field1,
                g.Filed2,
                g.Field3
            }).Select(s => new MasterEntity
            {
                Item = new ItemEntity
                {
                    Field1 = s.Key.Field1,
                    Filed2 = s.Key.Filed2,
                    Field3 = s.Key.Field3
                },
                ItemList = s.Select(g => g.ItemId).ToList(),
                GroupList = s.Select(g => g.GroupId).ToList()
            }).ToList();

Avec dans ce groupe, je veux diviser davantage ceci par ItemDate et Duration réels afin que cela ressemble à ci-dessous 

 Expected Output

Fondamentalement, je veux diviser ce groupe en trois dans ce cas.

Comme seul le groupe 3 a la date du 15 au 17, ce sera un groupe. Du 17 au 22, Group1, Group2 et Group3 sont identiques. de sorte que cela deviendra un autre groupe . Et enfin, seul le groupe 1 aura du 22 au 24, alors

Les données groupées finales ressemblent à

G1
{
 ItemEntity :{
 ItemDate : 15/10/2018,
 Duration : 2,
 Field1 : "Item Name 1",
 Filed2 : "aaa",
 Field3 : "bbb",
    },
ItemList: {250},
GroupList:{3}
}

,
G2
{
 ItemEntity :{
 ItemDate : 17/10/2018,
 Duration : 5,
 Field1 : "Item Name 1",
 Filed2 : "aaa",
 Field3 : "bbb",
},
ItemList: {100,150,250},
GroupList:{1,2,3}
}
,
G3
{
 ItemEntity :{
 ItemDate : 22/10/2018,
 Duration : 2,
 Field1 : "Item Name 1",
 Filed2 : "aaa",
 Field3 : "bbb",
},
ItemList: {100},
GroupList:{1}
}
14
unknown

C'était assez difficile. J'ai utilisé certaines méthodes d'extension pratiques que je devais déjà faciliter, et j'ai créé une sous-classe HashSet qui utilise par défaut SetEqual (.Net a vraiment besoin de certaines classes de collection égales membres intégrées).

Tout d'abord, la classe HashSetEq qui implémente l'égalité lorsque ses membres correspondent:

public class HashSetEq<T> : HashSet<T>, IEquatable<HashSetEq<T>> {
    private static readonly IEqualityComparer<HashSet<T>> SetEq = HashSet<T>.CreateSetComparer();

    public override int GetHashCode() => SetEq.GetHashCode(this);
    public override bool Equals(object obj) => obj != null && (obj is HashSetEq<T> hs) && this.Equals(hs);
    public bool Equals(HashSetEq<T> other) => SetEq.Equals(this, other);

    public HashSetEq(IEnumerable<T> src) : base(src) {
    }
}

Maintenant, quelques extensions à IEnumerable. Une extension convertit une IEnumerable en une HashSetEq pour faciliter la création d'une collection de clés. L'autre extension est une variante de GroupBy qui regroupe lorsqu'un prédicat est vrai, basé sur une extension ScanPair qui implémente une version paire de l'opérateur de numérisation APL.

public static class IEnumerableExt {
    public static HashSetEq<T> ToHashSetEq<T>(this IEnumerable<T> src) => new HashSetEq<T>(src);


    // TKey combineFn((TKey Key, T Value) PrevKeyItem, T curItem):
    // PrevKeyItem.Key = Previous Key
    // PrevKeyItem.Value = Previous Item
    // curItem = Current Item
    // returns new Key
    public static IEnumerable<(TKey Key, T Value)> ScanPair<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 prevkv = (seedKey, srce.Current);

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

    public static IEnumerable<IGrouping<int, T>> GroupByWhile<T>(this IEnumerable<T> src, Func<T, T, bool> testFn) =>
        src.ScanPair(1, (kvp, cur) => testFn(kvp.Value, cur) ? kvp.Key : kvp.Key + 1)
           .GroupBy(kvp => kvp.Key, kvp => kvp.Value);
}

Afin de regrouper les plages de dates, j'ai développé ma GroupBySequential en fonction de GroupByWhile en ligne afin de pouvoir grouper par séries de dates séquentielles et ensembles correspondants de GroupIds. GroupBySequential dépend d’une séquence entière, j’ai donc besoin d’une date de base pour calculer un numéro de séquence journalière et j’utilise la date la plus ancienne dans tous les éléments:

var baseDate = items.Min(i => i.ItemDate);

Maintenant je peux calculer la réponse.

Pour chaque groupe d'éléments, j'élargis chaque élément à toutes les dates qu'il couvre, en fonction de Duration, et j'associe chaque date à l'élément d'origine:

var group = items.GroupBy(g => new {
    g.Field1,
    g.Filed2,
    g.Field3
})
.Select(g => g.SelectMany(i => Enumerable.Range(0, i.Duration).Select(d => new { ItemDate = i.ItemDate.AddDays(d), i }))

Maintenant que j'ai la date et le poste individuels, je peux les regrouper pour chaque date.

              .GroupBy(di => di.ItemDate)

Et groupez ensuite chaque date + éléments à la date et ensemble de groupes pour cette date et ordre par la date.

              .GroupBy(Dig => new { ItemDate = Dig.Key, Groups = Dig.Select(di => di.i.GroupId).ToHashSetEq() })
              .OrderBy(ig => ig.Key.ItemDate)

Avec eux classés par date, je peux regrouper les dates séquentielles (en utilisant le nombre de jours à partir de baseDate) qui ont le même Groups.

              .GroupByWhile((prevg, curg) => (int)(prevg.Key.ItemDate - baseDate).TotalDays + 1 == (int)(curg.Key.ItemDate - baseDate).TotalDays && prevg.Key.Groups.Equals(curg.Key.Groups))

Enfin, je peux extraire les informations de chaque groupe de dates séquentiel dans un MasterEntity et en faire toute la réponse List.

              .Select(igg => new MasterEntity {
                  Item = new ItemEntity {
                      ItemDate = igg.First().Key.ItemDate,
                      Duration = igg.Count(),
                      Field1 = g.Key.Field1,
                      Filed2 = g.Key.Filed2,
                      Field3 = g.Key.Field3
                  },
                  ItemList = igg.First().First().Select(di => di.i.ItemId).ToList(),
                  GroupList = igg.First().Key.Groups.ToList()
              })
)
.ToList();
2
NetMage

https://dotnetfiddle.net/fFtqgy

L’exemple contient donc 3 parties se rendant à un "hôtel", comme indiqué dans votre explication . Les groupes sont présentés ci-dessous avec les heures d’arrivée et de départ des groupes.

Scénario

Groupe 1) du 15 au 20

Groupe 2) 17ème - 19ème

Groupe 3) du 17 au 22

Regroupement des résultats

15 au 17: Groupe 1

17ème - 19ème: Groupes 1, 2, 3

19ème - 20ème: Groupes 1, 3

20ème - 22ème: Groupes 3

Explication

Cela représente les groupes qui seront présents dans l'hôtel à chaque date, un nouveau groupe est créé chaque fois qu'un groupe rejoint ou quitte l'hôtel. C'est pourquoi le code joint toutes les dates de début et de fin de tous les groupes et itère. à travers eux.

Je ne savais pas quoi mettre pour GroupId et ItemID sur la MasterEntity résultante car elle contient une liste d'éléments et de groupes, je l'ai donc définie sur 1 dans l'exemple.

Code pour violon

public static class Utilities
{

    public static bool DatesOverlap(DateTime aStart, DateTime aEnd, DateTime bStart, DateTime bEnd)
    {
        return aStart < bEnd && bStart < aEnd;
    }

    public static IList<MasterEntity> GroupFunky(IList<ItemEntity> list)
    {

        var result = new List<MasterEntity>();
        var ordered = list.OrderBy(x => x.ItemDate).ToArray();

        var startDates = list.Select(x => x.ItemDate);
        var endDates = list.Select(x => x.ItemDate.AddDays(x.Duration));

        var allDates = startDates.Concat(endDates).OrderBy(x => x).ToArray();

        for (var index = 0; index < allDates.Length - 1; index++)
        {
            var group = ordered.Where(x => DatesOverlap(allDates[index], allDates[index + 1], x.ItemDate,
                                                        x.ItemDate.AddDays(x.Duration)));


            var item = new ItemEntity
            {
                Duration = (allDates[index + 1] - allDates[index]).Days,
                ItemDate = allDates[index],
                Field1 = group.First().Field1,
                Field2 = group.First().Field2,
                Field3 = group.First().Field3,
                Field4 = group.First().Field4,
                GroupName = group.First().GroupName,
                ItemId = -1,
                GroupId = -1
            };
            item.ItemDate = allDates[index];
            item.Duration = (allDates[index + 1] - allDates[index]).Days;
            result.Add(new MasterEntity
            {
                Item = item,
                GroupList = group.Select(x => x.GroupId).ToList(),
                ItemList = group.Select(x => x.ItemId).ToList()
            });
        }

        return result.Where(x => x.Item.Duration > 0).ToList();
    }
}

public class ItemEntity
{
    public int ItemId { get; set; }
    public int GroupId { get; set; }
    public string GroupName { get; set; }
    public DateTime ItemDate { get; set; }
    public string Field1 { get; set; }
    public string Field2 { get; set; }
    public string Field3 { get; set; }
    public string Field4 { get; set; }
    public int Duration { get; set; }
}

public class MasterEntity
{
    public ItemEntity Item { get; set; }
    public List<int> ItemList { get; set; }
    public List<int> GroupList { get; set; }
}

public class TestClass
{

    public static void Main()
    {
        var items = new List<ItemEntity>
        {
            new ItemEntity
            {
                ItemId = 100,
                GroupId = 1,
                GroupName = "Group 1",
                ItemDate = new DateTime(2018, 10, 15),
                Duration = 5,
                Field1 = "Item Name 1",
                Field2 = "aaa",
                Field3 = "bbb",
                Field4 = "abc"
            },
            new ItemEntity
            {
                ItemId = 150,
                GroupId = 2,
                GroupName = "Group 2",
                ItemDate = new DateTime(2018, 10, 17),
                Duration = 2,
                Field1 = "Item Name 1",
                Field2 = "aaa",
                Field3 = "bbb",
                Field4 = "efg"
            },
            new ItemEntity
            {
                ItemId = 250,
                GroupId = 3,
                GroupName = "Group 3",
                ItemDate = new DateTime(2018, 10, 17),
                Duration = 5,
                Field1 = "Item Name 1",
                Field2 = "aaa",
                Field3 = "bbb",
                Field4 = "xyz"
            }
        };


        var group = items.GroupBy(g => new
        {
            g.Field1,
            g.Field2,
            g.Field3
        })
            .Select(x => x.AsQueryable().ToList())
            .ToList();

        var result = group.Select(x => Utilities.GroupFunky(x));

        foreach (var item in result)
        {
            Console.WriteLine(JsonConvert.SerializeObject(item, Formatting.Indented));
        }

    }
}
1
Reese De Wind