Страницы

Поиск по вопросам

воскресенье, 8 декабря 2019 г.

Получить из одного IEnumerable три за один обход

#c_sharp #linq


В некотором отчёте нужно отобразить три "кучки" покупателей (условно назовём их "золотыми",
"серебряными" и "бронзовыми"):


кто сделал покупки на сумму свыше 100 000 рублей, 
свыше 50 000 рублей (но не добрал до 100 тыс.),
свыше 10 тыс рублей (и не набрал 50 тыс.).


У меня есть некоторый IEnumerable, где в CustomerDto лежат Id клиента и его имя,
а также есть поле хранящее сумму покупок - Amount.

В принципе, я могу три раза вырезать нужные мне данные через Where:

var report = new CustomersDto
{
    Gold   = allCustomers.Where(x => x.Amount > 100000),
    Silver = allCustomers.Where(x => x.Amount >  50000 && x.Amount < 100000),
    Bronze = allCustomers.Where(x => x.Amount >  10000 && x.Amount <  50000),
};


Но мне стало любопытно: а можно ли сразу за один обход allCustomers получить нужные
мне данные?

Т.е. что-то вроде:

CustomersDto report = allCustomers.Something(
    x => x.Amount > 100000,
    x => x.Amount >  50000 && x.Amount < 100000,
    x => x.Amount >  10000 && x.Amount <  50000);


Такое возможно?
    


Ответы

Ответ 1



Используя только IEnumerable такого трюка сделать не получится. Но этого можно достичь используя IObservable и Rx.NET (оно же System.Reactive). Сначала нужно превратить allCustomers в IObservable: var source = allCustomers.ToObservable().Publish(); Метод Publish используется для того, чтобы избежать многократного обхода исходной последовательности. Теперь, если свойства класса CustomersDto имеют подходящий тип, можно поступить вот так: var report = new CustomersDto { Gold = source.Where(x => x.Amount > 100000).ToListObservable(), Silver = source.Where(x => x.Amount > 50000 && x.Amount < 100000).ToListObservable(), Bronze = source.Where(x => x.Amount > 10000 && x.Amount < 50000).ToListObservable(), }; source.Connect(); Если же их тип - конкретный класс вроде List, придется сделать чуть сложнее: var report = new CustomersDto { Gold = new List(), Silver = new List(), Bronze = new List() }; source.Where(x => x.Amount > 100000).Subscribe(report.Gold.Add); source.Where(x => x.Amount > 50000 && x.Amount < 100000).Subscribe(report.Silver.Add); source.Where(x => x.Amount > 10000 && x.Amount < 50000).Subscribe(report.Bronze.Add); source.Connect();

Ответ 2



Можно исхитриться и все же получить из одного IEnumerable три таковых) Моя идея состоит в том, чтобы использовать стандартный GroupBy. Приступим: 0) Опишем структуру покупателя, которую будем использовать: public class Customer { public ulong ID { get; private set; } public string Name { get; private set; } public double Amount { get; private set; } public Customer(ulong ID, string Name, double Amount) { this.ID = ID; this.Name = Name; this.Amount = Amount; } public override string ToString() => $"{ID}: {Name} - {Amount}руб."; } 1) Опишу вспомогательный enum: public enum CustomerType { // < 10k None, // >= 10k && < 50k Bronze, // >= 50k && < 100k Silver, // >= 100k Gold } 2) Напишем метод-расширение (я исхожу из того, что Вы не можете по каким-то причинам менять изначального класса. В противном случае Вы можете прямо в нем создать аналогичное свойство)): public static class CustomerHelper { // Получим тип покупателя, исходя из его счета: public static CustomerType GetCustomerType(this Customer Customer) => (CustomerType)(Customer.Amount < 10000 ? 0 : Customer.Amount < 50000 ? 1 : Customer.Amount < 100000 ? 2 : 3); } 3) Опишем структуру класса CustomersDto: public class CustomersDto { public IEnumerable Bronze { get; set; } public IEnumerable Silver { get; set; } public IEnumerable Gold { get; set; } public CustomersDto() { Bronze = new Customer[0]; Silver = new Customer[0]; Gold = new Customer[0]; } public CustomersDto(IEnumerable Customers) : this() { // Сгруппируем покупателей по типу и избавимся от тех, кто недобрал 10к var grouped = Customers.GroupBy(x => x.GetCustomerType()).Where(x => x.Key != CustomerType.None); // Установим нужные поля foreach (var group in grouped) SetCustomers(group.Key, group.Select(x => x)); } public void SetCustomers(CustomerType Type, IEnumerable Customers) { switch (Type) { case CustomerType.Bronze: Bronze = Customers; break; case CustomerType.Silver: Silver = Customers; break; case CustomerType.Gold: Gold = Customers; break; default: break; } } } 4) Протестируем: // Тестовые покупатели Customer[] allCustomers = new[] { new Customer(0, "Vasya", 5000), new Customer(1, "Petya", 11000), new Customer(2, "Vanya", 14500), new Customer(3, "Stepan", 50000), new Customer(4, "Kir", 57000), new Customer(5, "AK", 100000) }; // Инициализируем наш класс CustomersDto dto = new CustomersDto(allCustomers); // Выведем результат: Console.WriteLine("Bronze:"); foreach (Customer bronze in dto.Bronze) Console.WriteLine(bronze); Console.WriteLine("\nSilver:"); foreach (Customer silver in dto.Silver) Console.WriteLine(silver); Console.WriteLine("\nGold:"); foreach (Customer gold in dto.Gold) Console.WriteLine(gold); И получим такой вот вывод: Bronze: 1: Petya - 11000руб.2: Vanya - 14500руб. Silver:3: Stepan - 50000руб.4: Kir - 57000руб. Gold:5: AK - 100000руб. Собственно, все как надо) К слову, сначала я хотел сделать так: // Сгруппируем -> Уберем тех, у кого меньше 10к -> Отсортируем по ключу -> Оставим лишь IEnumerable> var grouped = Customers.GroupBy(x => x.GetCustomerType()).Where(x => x.Key != CustomerType.None).OrderBy(x => x.Key).Select(x => x.Select(y => y)); Bronze = grouped.ElementAt(0); Silver = grouped.ElementAt(1); Gold = grouped.ElementAt(2); Тогда в grouped действительно будет лежать 3 IEnumerable> при текущем наборе данных Но потом я вспомнил, что какой-то из групп может и вовсе не быть, так что доступ по индексу - плохая идея) Можно, конечно, извратиться с Union и пустыми IGrouping, но это уже какой-то мазохизм...) Если Вам будет интересно - допишу и этот вариант. Тогда решение будет полностью соответствовать задаче: получить 3 коллекции из одной)

Ответ 3



Вот также через группировку. var groups = new List<(Func,int)> { ( a => a > 100000, 0), ( a => 50000 < a && a < 100000, 1), ( a => a < 50000, 2) }; var split = customers.GroupBy(c => groups.First(g => g.Item1(c.Amount)).Item2) .OrderBy(g => g.Key) .ToList(); Далее можно достать по индексу или превратить в словарь. ИМХО. Все это все равно немного муторнее, чем шлепнуть 3 раза Where

Ответ 4



Можно попробовать Aggregate: var report = allCustomers.Aggregate(new CustomersDto { Gold = new List(), Silver = new List(), Bronze = new List() }, (dto, cust) => { if (cust.Amount > 100000) dto.Gold.Add(cust); else if (cust.Amount > 50000) dto.Silver.Add(cust); else if (cust.Amount > 10000) dto.Bronze.Add(cust); return dto; }); При условии, что class CustomersDto { public List Gold { get; set; } public List Silver { get; set; } public List Bronze { get; set; } }

Комментариев нет:

Отправить комментарий