47 votes

L'utilisation de Linq pour la somme d'un nombre (et passer le reste)

Si nous avons une classe qui contient un certain nombre comme ceci:

class Person 
{
  public string Name {get; set;}
  public int Amount {get; set;}
}

et puis une collection de personnes:

IList<Person> people;

Qui contient, disons que 10 personnes de noms aléatoires et les montants est-il une expression Linq qui va me rendre un sous-groupe d'objets de la Personne dont la somme remplit une condition?

Par exemple je veux que le premier x personnes dont la somme de la Quantité est inférieure à 1000. Je peux le faire traditionnellement par

 var subgroup = new List<Person>();

 people.OrderByDescending(x => x.Amount);

 var count = 0;
 foreach (var person in people)
 {
    count += person.Amount;
    if (count < requestedAmount)
    {
        subgroup.Add(person);
    }
    else  
    {
        break;
    }
 }

Mais je me demandais si il y a un élégant Linq moyen de faire quelque chose comme ceci en utilisant la Somme, puis une autre fonction comme Prendre?

Mise à JOUR

C'est fantastique:

var count = 0;
var subgroup = people
                  .OrderByDescending(x => x.Amount)
                  .TakeWhile(x => (count += x.Amount) < requestedAmount)
                  .ToList();

Mais je me demande si je peux changer quelque chose à cela dans le but de saisir la personne suivante dans la liste et ajouter le reste à la somme de sorte que le montant total est égal montant demandé.

48voto

Giorgos Betsos Points 61197

Vous pouvez utiliser TakeWhile:

int s = 0;
var subgroup  = people.OrderBy(x => x.Amount)
                      .TakeWhile(x => (s += x.Amount) < 1000)
                      .ToList();

Remarque: Vous mentionnez dans votre message le premier x personnes. On pourrait interpréter cela comme ceux ayant la plus petite quantité qui ajoute jusqu' 1000 est atteint. Donc, j'ai utilisé OrderBy. Mais vous pouvez la remplacer avec OrderByDescending si vous voulez commencer l'extraction de la personne ayant la plus grande quantité.


Edit:

Pour le faire, sélectionnez l'un plus l'élément de la liste, vous pouvez utiliser:

.TakeWhile(x => {
                   bool bExceeds = s > 1000;
                   s += x.Amount;                                 
                   return !bExceeds;
                })

L' TakeWhile ici examine l' s de la valeur de la précédente itération, donc il va prendre un de plus, juste pour être sûr, 1000 a été dépassé.

24voto

NiklasJ Points 466

Je n'aime pas ces approches de la mutation de l'état à l'intérieur de requêtes linq.

EDIT: Je n'ai pas que mon code précédent a pas été testés et qui a un peu de pseudo-y. J'ai aussi raté le point qui regroupent en fait les mange en toute chose à la fois - comme souligné à juste titre qu'il ne fonctionne pas. L'idée était bonne, mais nous avons besoin d'une alternative à Aggreage.

C'est une honte que LINQ n'ont pas d'agrégation en cours d'exécution. Je suggère le code de user2088029 dans ce post: Comment faire pour calculer une somme en cours d'exécution d'une série d'entiers dans une requête Linq?.

Et puis l'utiliser (ce qui est testé et c'est ce que j'ai prévu):

var y = people.Scanl(new { item = (Person) null, Amount = 0 },
    (sofar, next) => new { 
        item = next, 
        Amount = sofar.Amount + next.Amount 
    } 
);       

Vol de code ici pour la longévité:

public static IEnumerable<TResult> Scanl<T, TResult>(
    this IEnumerable<T> source,
    TResult first,
    Func<TResult, T, TResult> combine)
    {
        using (IEnumerator<T> data = source.GetEnumerator())
        {
            yield return first;

            while (data.MoveNext())
            {
                first = combine(first, data.Current);
                yield return first;
            }
        }
    }

Précédente, mauvais code:

J'ai une autre suggestion, commencer avec une liste

people

[{"a", 100}, 
 {"b", 200}, 
 ... ]

Calculer les totaux en cours d'exécution:

people.Aggregate((sofar, next) => new {item = next, total = sofar.total + next.value})


[{item: {"a", 100}, total: 100}, 
 {item: {"b", 200}, total: 300},
 ... ]

Ensuite, utilisez TakeWhile et Sélectionnez pour revenir simplement des éléments;

people
 .Aggregate((sofar, next) => new {item = next, total = sofar.total + next.value})
 .TakeWhile(x=>x.total<1000)
 .Select(x=>x.Item)

16voto

Eric Lippert Points 300275

Je n'aime pas toutes les réponses à cette question. Ils muter une variable dans une requête -- une mauvaise pratique qui conduit à des résultats inattendus, ou dans le cas de Niklas l' (sinon bonne) solution, renvoie une séquence qui est de type incorrect, ou, dans le cas de Jeroen réponse, le code est correct mais pourrait être fait pour résoudre un problème plus général.

Je voudrais améliorer les efforts de Niklas et Jeroen en faisant en réalité une solution générique qui renvoie le type de droit:

public static IEnumerable<T> AggregatingTakeWhile<T, U>(
  this IEnumerable<T> items, 
  U first,
  Func<T, U, U> aggregator,
  Func<T, U, bool> predicate)
{
  U aggregate = first;
  foreach (var item in items)
  {
    aggregate = aggregator(item, aggregate);
    if (!predicate(item, aggregate))
      yield break;
    yield return item; 
  }
}

Que nous pouvons utiliser pour mettre en œuvre une solution pour le problème spécifique:

var subgroup = people
  .OrderByDescending(x => x.Amount)
  .AggregatingTakeWhile(
    0, 
    (item, count) => count + item.Amount, 
    (item, count) => count < requestedAmount)
  .ToList();

7voto

Jeroen van Langen Points 4444

Essayez:

int sumCount = 0;

var subgroup = people
    .OrderByDescending(item => item.Amount)           // <-- you wanted to sort them?
    .Where(item => (sumCount += item.Amount) < requestedAmount)
    .ToList();

Mais ce n'est pas de charme... Il sera moins lisible.

3voto

Jeroen van Langen Points 4444

J'ai pris le commentaire de l' Eric Lippert et est venu avec ce une meilleure solution. Je pense que la meilleure façon est de créer une fonction (dans mon cas, j'ai écrit une Méthode d'Extension)

public static IEnumerable<T> TakeWhileAdding<T>(
    this IEnumerable<T> source, 
    Func<T, int> selector, 
    Func<int, bool> comparer)
{
    int total = 0;

    foreach (var item in source)
    {
        total += selector(item);

        if (!comparer(total))
            yield break;

        yield return item;
    }
}

Utilisation:

var values = new Person[]
{
    new Person { Name = "Name1", Amount = 300 },
    new Person { Name = "Name2", Amount = 500 },
    new Person { Name = "Name3", Amount = 300 },
    new Person { Name = "Name4", Amount = 300 }
};

var subgroup = values.TakeWhileAdding(
    person => person.Amount, 
    total => total < requestedAmount);

foreach (var v in subgroup)
    Trace.WriteLine(v);

Cela pourrait également être créé pour double, float, ou quelque chose comme un TimeSpan.

De cette façon, chaque fois que l' subgroup est itérée, un nouveau compteur.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X