C # - Получение плоского списка объектов «многие ко многим» в список различных комбинаций двух списков объектов - PullRequest
2 голосов
/ 08 ноября 2019

Я не могу понять, как это сделать, и не могу найти легкий способ объяснить это ... поэтому я надеюсь, что этот упрощенный пример будет иметь смысл.

Получение списка <> объектов как таковых:

public class FlatManyToMany
{
    public string BookTitle { get; set; }
    public int BookPages { get; set; }
    public string ReaderName { get; set; }
    public int ReaderAge { get; set; }
}

var flatManyToMany = new List<FlatManyToMany>();

flatManyToMany.Add(new FlatManyToMany { BookTitle = "How to Do This Double List", BookPages = 105, ReaderName = "Kyle", ReaderAge = 29 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "How to Do This Double List", BookPages = 105, ReaderName = "Bob", ReaderAge = 34 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Gone With Jon Skeet", BookPages = 192, ReaderName = "Kyle", ReaderAge = 29 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Gone With Jon Skeet", BookPages = 192, ReaderName = "James", ReaderAge = 45 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Gone With Jon Skeet", BookPages = 192, ReaderName = "Brian", ReaderAge = 15 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Why Is This So Hard?", BookPages = 56, ReaderName = "Kyle", ReaderAge = 29 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Why Is This So Hard?", BookPages = 56, ReaderName = "James", ReaderAge = 45 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Why Is This So Hard?", BookPages = 56, ReaderName = "Brian", ReaderAge = 15 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Impostor Syndrome", BookPages = 454, ReaderName = "Kyle", ReaderAge = 29 });
flatManyToMany.Add(new FlatManyToMany { BookTitle = "Self Doubt and You", BookPages = 999, ReaderName = "Kyle", ReaderAge = 29 });

Мне нужен список из двух списков объектов как таковых:

public class ResultDoubleList
{
    public List<Book> Books { get; set; } = new List<Book>();
    public List<Reader> Readers { get; set; } = new List<Reader>();
}

public class Book
{
    public string Title { get; set; }
    public int Pages { get; set; }
}

public class Reader
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Книга должна отображаться только один раз в конечном результате, но Читатель можетпоявляться много раз. Несколько книг можно собрать вместе, если они были прочитаны одним и тем же читателем.

Вот как мне нужен результат:

List<ResultDoubleList> results = new List<ResultDoubleList>();

result(1):
Books
    How to Do This Double List  105
Readers
    Kyle    29
    Bob     34

result(2):
Books
    Gone With Jon Skeet     192
    Why Is This So Hard?    56
Readers
    Kyle    29
    James   45
    Brian   15

result(3):
Books
    Impostor Syndrome   454
    Self Doubt and You  999
Readers
    Kyle    29

Таким образом, различные комбинации Списков книг и Списков читателей - этоконечный результат. Книга появляется только один раз, но Читатели могут появляться более одного раза. Книги с точно таким же списком читателей будут сгруппированы вместе.

Даже если кто-то скажет мне, как называется этот тип конечного результата, я был бы признателен.

Ответы [ 3 ]

5 голосов
/ 08 ноября 2019

Вы можете сделать это с помощью длинного запроса LINQ:

var result = flatManyToMany
    .GroupBy(f1 => (f1.BookTitle, f1.BookPages))
    .Select(g1 => (bookInfo: g1.Key,
                    readers:
                        g1.Select(f2 => new Reader { Name= f2.ReaderName, Age= f2.ReaderAge }),
                    readerKey:
                        String.Join("|", g1.Select(f3 => $"{f3.ReaderName}{f3.ReaderAge}"))))
    .GroupBy(a1 => a1.readerKey)
    .Select(g2 => new ResultDoubleList {
        Books = g2.Select(a2 => new Book {
                    Title = a2.bookInfo.BookTitle,
                    Pages = a2.bookInfo.BookPages
                }
            ).ToList(),
        Readers = g2.First().readers.ToList() // Any will do, since they have the same readers
    })
    .ToList();

Идея состоит в том, чтобы группировать дважды. Один раз за книгу и один раз за группу читателей.

Сначала мы группируем по ValueTuple (f1.BookTitle, f1.BookPages). Преимущество перед созданием объекта Book заключается в том, что ValueTuple автоматически переопределяет Equals и GetHashCode. Это требуется для типов, используемых в качестве ключа в словаре или поиске, как GroupBy. В качестве альтернативы вы можете переопределить эти методы в классе и группе Book объектами Book. Если у вас есть уникальный идентификатор книги, используйте этот.

Затем мы создадим временный результат с Select. Мы снова создаем кортеж с 3 полями. Кортеж, содержащий информацию о книге, перечисление Reader объектов и, наконец, мы создаем строку, содержащую все читатели в качестве ключа, который мы будем использовать позже для группировки по уникальным группам читателей. Если у вас есть уникальный идентификатор читателя, этот вместо имени и возраста.

Пока у нас есть

IEnumerable<(
    (string BookTitle, int BookPages) bookInfo,
    IEnumerable<Reader> readers,
    string readerKey
)>

Теперь мы сгруппируемся по readerKey, а затем создаем списокResultDoubleList objects.

Если у вас возникли трудности с пониманием деталей, разбейте запрос LINQ на несколько запросов. Используя рефакторинг «Сделать явным», вы сможете увидеть, какой тип результата вы получили. (Вот как я получил комплекс IEnumerable<T> сверху.) Это также позволяет вам проверить промежуточные результаты в отладчике.


Этот тест ...

int resultNo = 1;
foreach (ResultDoubleList item in result) {
    Console.WriteLine($"\r\nresult({resultNo++}):");
    Console.WriteLine("Books");
    foreach (var book in item.Books) {
        Console.WriteLine($"    {book.Title,-28} {book.Pages,3}");
    }
    Console.WriteLine("Readers");
    foreach (var reader in item.Readers) {
        Console.WriteLine($"    {reader.Name,-8} {reader.Age,2}");
    }
}
Console.ReadKey();

... дает:

result(1):
Books
    How to Do This Double List   105
Readers
    Kyle     29
    Bob      34

result(2):
Books
    Gone With Jon Skeet          192
    Why Is This So Hard?          56
Readers
    Kyle     29
    James    45
    Brian    15

result(3):
Books
    Impostor Syndrome            454
    Self Doubt and You           999
Readers
    Kyle     29
0 голосов
/ 08 ноября 2019

Предположим, что имя книги и имена читателей являются идентификаторами.

var results = flatManyToMany
.GroupBy(f => new { f.BookTitle, f.BookPages })
.Select(g => new
{
    Book = new Book() { Title = g.Key.BookTitle, Pages = g.Key.BookPages },
    Readers = g.Select(i => new Reader() { Name = i.ReaderName, Age = i.ReaderAge })
})
.GroupBy(i => string.Concat(i.Readers.Select(r => r.Name).Distinct()))
.Select(g => new ResultDoubleList()
{
    Books = g.Select(i => i.Book).ToList(),
    Readers = g.SelectMany(i => i.Readers).GroupBy(r => r.Name).Select(r => r.First()).ToList()
})
;
    foreach(var result in results)
    {
        Console.WriteLine("Result:");
        Console.WriteLine("\tBooks:");
        foreach(var b in result.Books)
        {
            Console.WriteLine($"\t\t{b.Title}");
        }
        Console.WriteLine("\tReaders:");
        foreach (var reader in result.Readers)
        {
            Console.WriteLine($"\t\t{reader.Name}");
        }
    }


0 голосов
/ 08 ноября 2019

A. Со строковым ключом для групп считывателей

var booksReadByGroups = flatManyToMany.GroupBy(a => a.BookTitle)
    .Select(g => new
    {
        Book = new Book { Title = g.Key, Pages = g.Max(a => a.BookPages) },
        Readers = g.Select(a => new Reader { Name = a.ReaderName, Age = a.ReaderAge }).ToList()
    })
    .GroupBy(b => string.Join("+",b.Readers.OrderBy(r=>r.Name).ThenBy(r=>r.Age).Select(r => $"{r.Name}{r.Age}")))
    .Select(g => new
    {
        Books = g.Select(b => b.Book),
        Readers = g.First().Readers
    })
    .ToList();

Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(booksReadByGroups));

Вышеприведенное приводит (с некоторым разрывом строки вручную):

[{
    "Books":[
        {"Title":"How to Do This Double List","Pages":105}
    ],
    "Readers":[
        {"Name":"Kyle","Age":29},
        {"Name":"Bob","Age":34}
    ]
},{
    "Books":[
        {"Title":"Gone With Jon Skeet","Pages":192},
        {"Title":"Why Is This So Hard?","Pages":56}
    ],
    "Readers":[
        {"Name":"Kyle","Age":29},
        {"Name":"James","Age":45},{"Name":"Brian","Age":15}
    ]
},{
    "Books":[
        {"Title":"Impostor Syndrome","Pages":454},
        {"Title":"Self Doubt and You","Pages":999}
    ],
    "Readers":[
        {"Name":"Kyle","Age":29}
    ]
}]

B. Короче, но страшнее

Нам нужно GroupBy дважды, но первая проекция не нужна, и достаточно одного Select.

var readerGroups = flatManyToMany.GroupBy(a => a.BookTitle)
    .GroupBy(g => string.Join("+",g.OrderBy(r=>r.ReaderName).ThenBy(r=>r.ReaderAge).Select(r => $"{r.ReaderName}{r.ReaderAge}")))
    .Select(g => new
    {
        Books = g.Select( g2 => new Book { Title = g2.Key, Pages = g2.Max(a => a.BookPages) }),
        Readers = g.First().Select(a => new Reader { Name = a.ReaderName, Age = a.ReaderAge })
    });

C. С IEquatable

Эта версия самая длинная, но, пожалуй, самая правильная, поскольку она оставляет классу Reader решать, какие читатели считаются равными.

class ReadersComparer : IEqualityComparer<List<Reader>>
{
    public bool Equals(List<Reader> a, List<Reader> b) => Enumerable.SequenceEqual(a, b); // Please note this doesn't order the lists so you either need to order them before, or order them here and implement IComparable on the Reader class
    public int GetHashCode(List<Reader> os)
    {
        int hash = 19;
        foreach (var o in os) { hash = hash * 31 + o.GetHashCode(); }
        return hash;
    }
}

public class Reader : IEquatable<Reader>
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override int GetHashCode() => (Name, Age).GetHashCode();
    public bool Equals(Reader other) => (other is null) ? false : this.Name == other.Name && this.Age == other.Age;
    public override bool Equals(object obj) => Equals(obj as Reader);
}


static void Main(string[] args)
{
var actsOfReading = new[]{
    new Reading { BookTitle = "How to Do This Double List", BookPages = 105, ReaderName = "Kyle", ReaderAge = 29},
    new Reading { BookTitle = "How to Do This Double List", BookPages = 105, ReaderName = "Bob", ReaderAge = 34},
    new Reading { BookTitle = "Gone With Jon Skeet", BookPages = 192, ReaderName = "Kyle", ReaderAge = 29},
    new Reading { BookTitle = "Gone With Jon Skeet", BookPages = 192, ReaderName = "James", ReaderAge = 45},
    new Reading { BookTitle = "Gone With Jon Skeet", BookPages = 192, ReaderName = "Brian", ReaderAge = 15},
    new Reading { BookTitle = "Why Is This So Hard?", BookPages = 56, ReaderName = "Kyle", ReaderAge = 29},
    new Reading { BookTitle = "Why Is This So Hard?", BookPages = 56, ReaderName = "James", ReaderAge = 45},
    new Reading { BookTitle = "Why Is This So Hard?", BookPages = 56, ReaderName = "Brian", ReaderAge = 15},
    new Reading { BookTitle = "Impostor Syndrome", BookPages = 454, ReaderName = "Kyle", ReaderAge = 29},
    new Reading { BookTitle = "Self Doubt and You", BookPages = 999, ReaderName = "Kyle", ReaderAge = 29}
};


var booksReadByGroups = actsOfReading.GroupBy(a => a.BookTitle)
    .Select(g => new
    {
        Book = new Book { Title = g.Key, Pages = g.Max(a => a.BookPages) },
        Readers = g.Select(a => new Reader { Name = a.ReaderName, Age = a.ReaderAge }).ToList()
    })
    .GroupBy(b => b.Readers, new ReadersComparer())
    .Select(g => new
    {
        Books = g.Select(b => b.Book),
        Readers = g.First().Readers
    })
    .ToList();

    Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(booksReadByGroups));
}

Вывод (отформатирован вручную)

[{
 "Books": [{
 "Title": "How to Do This Double List", "Pages": 105 }
 ],
 "Readers": [{
 "Name": "Kyle", "Age": 29 }, {
 "Name": "Bob", "Age": 34 }
 ]
}, {
 "Books": [{
 "Title": "Gone With Jon Skeet", "Pages": 192 }, {
 "Title": "Why Is This So Hard?", "Pages": 56 }
 ],
 "Readers": [{
 "Name": "Kyle", "Age": 29 }, {
 "Name": "James", "Age": 45 }, {
 "Name": "Brian", "Age": 15 }
 ]
}, {
 "Books": [{
 "Title": "Impostor Syndrome", "Pages": 454 }, {
 "Title": "Self Doubt and You", "Pages": 999 }
 ],
 "Readers": [{
 "Name": "Kyle", "Age": 29 }
 ]
}
]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...