В некотором отчёте нужно отобразить три "кучки" покупателей (условно назовём их "золотыми", "серебряными" и "бронзовыми"):
У меня есть некоторый 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);
Такое возможно?
Используя только 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<Customer>
, придется сделать чуть сложнее:
var report = new CustomersDto
{
Gold = new List<Customer>(),
Silver = new List<Customer>(),
Bronze = new List<Customer>()
};
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();
Можно исхитриться и все же получить из одного IEnumerable<Customer>
три таковых)
Моя идея состоит в том, чтобы использовать стандартный 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<Customer> Bronze { get; set; }
public IEnumerable<Customer> Silver { get; set; }
public IEnumerable<Customer> Gold { get; set; }
public CustomersDto()
{
Bronze = new Customer[0];
Silver = new Customer[0];
Gold = new Customer[0];
}
public CustomersDto(IEnumerable<Customer> 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<Customer> 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<IEnumarable<Customer>>
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<IEnumerable<Customer>>
при текущем наборе данных
Но потом я вспомнил, что какой-то из групп может и вовсе не быть, так что доступ по индексу - плохая идея)
Можно, конечно, извратиться с Union
и пустыми IGrouping
, но это уже какой-то мазохизм...)
Если Вам будет интересно - допишу и этот вариант. Тогда решение будет полностью соответствовать задаче: получить 3 коллекции из одной)
Вот также через группировку.
var groups = new List<(Func<int,bool>,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
Можно попробовать Aggregate
:
var report = allCustomers.Aggregate<Customer, CustomersDto>(new CustomersDto
{
Gold = new List<Customer>(),
Silver = new List<Customer>(),
Bronze = new List<Customer>()
},
(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<Customer> Gold { get; set; }
public List<Customer> Silver { get; set; }
public List<Customer> Bronze { get; set; }
}
Кофе для программистов: как напиток влияет на продуктивность кодеров?
Рекламные вывески: как привлечь внимание и увеличить продажи
Стратегії та тренди в SMM - Технології, що формують майбутнє сьогодні
Выделенный сервер, что это, для чего нужен и какие характеристики важны?
Современные решения для бизнеса: как облачные и виртуальные технологии меняют рынок
Подскажите сайты, где четко расписана работа с Word через C#Т
Все таки не могу понять, почему не работает прогрессбарИспользую MVVM WPF