Группировка результатов "честно" с помощью LINQ - PullRequest
1 голос
/ 14 апреля 2011

У меня есть список системных пользователей , которые ожидают назначения с учетной записью .
Алгоритм назначения очень прост, назначение должно быть максимально справедливым, что означает, что если у меня 40 учетных записей и 20 системных пользователей, мне нужно назначить 2 учетные записи для каждого системного пользователя.
Если у меня 41 учетная запись и 20 системных пользователей, мне нужно назначить 2 учетные записи для каждого системного пользователя и снова разделить оставшиеся учетные записи между системными пользователями (в этом случае одному системному пользователю будет назначена одна дополнительная учетная запись).
Я пытаюсь понять, как это сделать, используя запрос LINQ.
До сих пор я полагал, что группировка должна быть вовлечена, и мой запрос следующий:

from account in accounts
    let accountsPerSystemUser = accounts.Count / systemUsers.Count
    let leftover = accounts.Count % systemUsers.Count
    from systemUser in systemUsers
        group account by systemUser into accountsGroup
select accountsGroup

Однако я не уверен, что делать дальше.
Я уверен, что здесь отсутствует пункт where, который запрещает группирование, если вы достигли максимального количества учетных записей, которые будут назначены системному пользователю. Как правильно реализовать запрос, чтобы группа знала, сколько назначить?

Ответы [ 3 ]

2 голосов
/ 14 апреля 2011

Вот простая реализация, которая работает, если вы можете ограничиться IList<T> для accounts (хотя вы всегда можете использовать ToList).

public static IEnumerable<IGrouping<TBucket, TSource>> DistributeBy<TSource, TBucket>(
    this IEnumerable<TSource> source, IList<TBucket> buckets)
{
    var tagged = source.Select((item,i) => new {item, tag = i % buckets.Count});
    var grouped = from t in tagged
                  group t.item by buckets[t.tag];
    return grouped;
}

// ...
var accountsGrouped = accounts.DistributeBy(systemUsers);

В основном это захватывает индекс каждой учетной записи и «теги», каждая с остатком от целочисленного деления этого индекса на количество пользователей системы. Эти теги являются индексами пользователей системы, к которым они будут принадлежать. Затем он просто группирует их по системному пользователю по этому индексу.

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

0 % 20 = 0
1 % 20 = 1
2 % 20 = 2
...
19 % 20 = 19
20 % 20 = 0
21 % 21 = 1
22 % 22 = 2
...
39 % 20 = 19
40 % 20 = 0
1 голос
/ 14 апреля 2011

Я не тонкий "чистый" LINQ действительно подходит для решения этой проблемы. Тем не менее, вот решение, которое требует только два IEnumerable:

var users = new[] { "A", "B", "C" };
var accounts = new[] { 1, 2, 3, 4, 5, 6, 7, 8 };
var accountsPerUser = accounts.Count()/users.Count();
var leftover = accounts.Count()%users.Count();
var assignments = users
  .Select((u, i) => new {
    User = u,
    AccountsToAssign = accountsPerUser + (i < leftover ? 1 : 0),
    AccountsAlreadyAssigned =
      (accountsPerUser + 1)*(i < leftover ? i : leftover)
      + accountsPerUser*(i < leftover ? 0 : i - leftover)
  })
  .Select(x => new {
    x.User,
    Accounts = accounts
      .Skip(x.AccountsAlreadyAssigned)
      .Take(x.AccountsToAssign)
  });

Чтобы сократить текст, я использую термин User вместо SystemUser.

Идея довольно проста. Первым leftover пользователям назначается accountsPerUser + 1 из accounts. Остальным пользователям назначается только accountsPerUser.

Первый Select использует перегрузку, которая предоставляет индекс для вычисления этих значений:

User | Index | AccountsAlreadyAssigned | AccountsToAssign
-----+-------+-------------------------+-----------------
A    | 0     | 0                       | 3
B    | 1     | 3                       | 3
C    | 1     | 6                       | 2

Второй Select использует эти значения для Skip и Take правильных чисел от accounts.

Если вы хотите, вы можете «объединить» два оператора Select и заменить AccountsAlreadyAssigned и AccountsToAssign на выражения, используемые для их вычисления. Однако это усложнит понимание запроса.

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

IEnumerable<Tuple<T, IList<U>>> AssignEvenly<T, U>(IList<T> targetItems, IList<U> sourceItems) {
  var fraction = sourceItems.Count/targetItems.Count;
  var remainder = sourceItems.Count%targetItems.Count;
  var sourceIndex = 0;
  for (var targetIndex = 0; targetIndex < targetItems.Count; ++targetIndex) {
    var itemsToAssign = fraction + (targetIndex < remainder ? 1 : 0);
    yield return Tuple.Create(
      targetItems[targetIndex],
      (IList<U>) sourceItems.Skip(sourceIndex).Take(itemsToAssign).ToList()
    );
    sourceIndex += itemsToAssign;
  }
}
1 голос
/ 14 апреля 2011

Вы не можете сделать это, используя «чистый LINQ» (т. Е. Используя синтаксис понимания запросов), и, честно говоря, LINQ, вероятно, здесь не лучший подход. Тем не менее, вот пример того, как вы можете это сделать:

var listB = new List<string>() { "a", "b", "c", "d", "e" };
var listA = new List<string>() { "1", "2", "3" };

var groupings = (from b in listB.Select((b, i) => new
                                        {
                                            Index = i,
                                            Element = b
                                        })
                 group b.Element by b.Index % listA.Count).Zip(listA, (bs, a) => new
                                                                      {
                                                                          A = a,
                                                                          Bs = bs
                                                                      });

foreach (var item in groupings)
{
    Console.WriteLine("{0}: {1}", item.A, string.Join(",", item.Bs));
}

Это выводит:

1: a,d
2: b,e
3: c
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...