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

172
02 февраля 2019, 14:10

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

  • кто сделал покупки на сумму свыше 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);

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

Answer 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<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();
Answer 2

Можно исхитриться и все же получить из одного 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 коллекции из одной)

Answer 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

Answer 4

Можно попробовать 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; }
}
READ ALSO
Работа с MS Word с помощью C# [закрыт]

Работа с MS Word с помощью C# [закрыт]

Подскажите сайты, где четко расписана работа с Word через C#Т

172
Почему не работает Progressbar в Wpf

Почему не работает Progressbar в Wpf

Все таки не могу понять, почему не работает прогрессбарИспользую MVVM WPF

149
ListView извлечение данных

ListView извлечение данных

Застопорился на элементе ListViewИмеется код:

213
Почему свойство = null?

Почему свойство = null?

Я сделал свой UserConrol типа Button ModernBtnxaml

203