7
\$\begingroup\$

I've to do a generic method Classify<T> that, given a sequence of elements of type T and an arbitrary number of predicates, returns an array of n+1 lists (where n is the number of the predicates). More precisely, if the element satisfies the first predicate then it is added to the first list. If not, then if the element satisfies the second predicate it is added to the second list, etc. If none of the predicates can be applied to one element, then that element is added to the last list (the n+1 th).

I managed to do that with the following code. Is there any way to do it better, maybe with Linq?

public List<T>[] Classify<T>(IEnumerable<T> sequence, params Predicate<T>[] predicates)
{
    var result = new List<T>[predicates.Length+1];
    var counter = 0;    
    foreach (var elem in sequence)      
    {
        var valid = false;
        foreach (var predicate in predicates) 
        {
            if(result[counter]==null)
                result[counter] = new List<T>();
            if (predicate(elem) && valid==false)
            {
                var partition = result[counter];
                partition.Add(elem);
                valid = true;
            }
            counter++;
        }
        if (!valid)     
        {
            if (result[counter] == null)
                result[counter] = new List<T>();
            result[counter].Add(elem);
        }
        counter=0;
    }
    return result;
}
\$\endgroup\$
5
\$\begingroup\$
public List<T>[] Classify<T>(IEnumerable<T> sequence, params Predicate<T>[] predicates)
{
    var result = new List<T>[predicates.Length + 1];
    // It's clearer to create elements of the array explicitly
    for (int i = 0; i < result.Length; i++)
        result[i] = new List<T>();

    foreach (var elem in sequence)
    {
        var index = predicates.TakeWhile(p => !p(elem))
                              .Count();

        result[index].Add(elem);
    }
}

There is a few LINQ methods to get an index, f.e. Select((i, e) => ...). If the list of unsatisfied elements was the first (0th), I would use Select. But since it is the last, TakeWhile gives the shorter code.

\$\endgroup\$
  • \$\begingroup\$ Thanks. I have to practice a lot with linq, because it seems to simplify the code so much.. \$\endgroup\$ – Sanci Jan 18 '15 at 15:18
  • 1
    \$\begingroup\$ Also consider this for initializing the lists. var result = Enumerable.Range(0, predicates.Length + 1).Select(i => new List<T>()).ToArray(); \$\endgroup\$ – Rotem Jan 18 '15 at 15:25
  • \$\begingroup\$ @Rotem Thanks. I didn't know this method. \$\endgroup\$ – Mark Shevchenko Jan 18 '15 at 15:29
4
\$\begingroup\$

The idea is to group the elements in the sequence by the predicate they satisfy (line 10) and return an array (line 12) of lists containing the elements in each group (line 11).

This is one way to find the predicate index for the first predicate that is satisfied by an element (line 7): predicates.TakeWhile(p => !p(elem)).Count() ("stolen" from @Mark Shevchenko's answer). This has the advantage that it will return n+1 for elements which do not satisfy any predicate and the ordering is simpler.

After selecting the element and it's coresponding predicate index (lines 3-8), the predicates are ordered by the indexes (line 9).

[1]    public List<T>[] Classify<T>(IEnumerable<T> sequence, params Predicate<T>[] predicates)
[2]    {
[3]        return sequence
[4]            .Select(elem => new
[5]            {
[6]                Element = elem,
[7]                PredicateIndex = predicates.TakeWhile(p => !p(elem)).Count()
[8]            })
[9]            .OrderBy(x => x.PredicateIndex)
[10]           .GroupBy(x => x.PredicateIndex)
[11]           .Select(grp => grp.Select(x => x.Element).ToList())
[12]           .ToArray();
[13]   }
\$\endgroup\$
  • \$\begingroup\$ I think this won't work if there is no item that matches some predicate: there won't be a list for that predicate at all. \$\endgroup\$ – svick Jan 20 '15 at 14:36
4
\$\begingroup\$

There are many ways to strike the balance between LINQ and straight procedural code; the answer by Mark IMO strikes a very good balance between being succinct and easily understandable.

That said you can do this without any procedural code (just LINQ methods) and have the body of the classification method be a single expression. Extra care must be taken so that if there are bins which end up being empty of items the result still includes empty lists in the appropriate indexes (david's answer does not do this and as a result has a bug).

Here's a correct solution with pure LINQ, which I have to admit is more useful as an intellectual exercise rather than as a practical approach:

public List<T>[] Classify<T>(IEnumerable<T> sequence, params Predicate<T>[] predicates)
{
    return Enumerable
               .Range(0, predicates.Length + 1)
               .GroupJoin(
                   sequence.GroupBy(item => predicates.TakeWhile(p => !p(item)).Count()),
                   i => i,
                   g => g.Key,
                   (i, g) => g.SelectMany(g2 => g2).ToList()
               )
               .ToArray();
}
\$\endgroup\$
1
\$\begingroup\$

The Linq answers are readable (well maybe not Jon's :-) ) but they're all slower than the OP's original code. I started with the intention to write a semi-Linq solution that was readable, but faster than the original, however, Linq, and copying arrays,# is slow :-(

In the end, I found this, which is readable, about 10% faster, and more correct*:

public List<T>[] Classify<T>(IEnumerable<T> sequence, params Predicate<T>[] predicates)
{
    var partitions = new List<T>[predicates.Length + 1];
    for (int i = 0; i < partitions.Length; i++)
    {
        partitions[i] = new List<T>();
    }
    foreach (var elem in sequence)
    {
        var notfound = true;
        for (var i = 0; i < predicates.Length; i++)
        {
            if (predicates[i](elem))
            {
                partitions[i].Add(elem);
                notfound = false;
                break;
            }
        }
        if (notfound)
        {
            partitions[predicates.Length].Add(elem);
        }
    }
    return partitions;
}

#I attempted to add a "sentinel" predicate of (e) => true or even just null, but copying the existing predicates array, either to a List or to an array with one more element kills performance.

*The bug fixed is the one addressed by creating the elements of the array explicitly: the OP code fails to return an array of empty lists when supplied an empty sequence.

Note that if the OP's intention was to call all the predicates on each element of sequence as the current code does, dropping the break; and adding && notfound to the if will make it identical in behaviour.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.