5

I'd like to split a sequence in C# to a sequence of sequences using LINQ. I've done some investigation, and the closest SO article I've found that is slightly related is this.

However, this question only asks how to partition the original sequence based upon a constant value. I would like to partition my sequence based on an operation.

Specifically, I have a list of objects which contain a decimal property.

public class ExampleClass
{
    public decimal TheValue { get; set; }
}

Let's say I have a sequence of ExampleClass, and the corresponding sequence of values of TheValue is:

{0,1,2,3,1,1,4,6,7,0,1,0,2,3,5,7,6,5,4,3,2,1}

I'd like to partition the original sequence into an IEnumerable<IEnumerable<ExampleClass>> with values of TheValue resembling:

{{0,1,2,3}, {1,1,4,6,7}, {0,1}, {0,2,3,5,7}, {6,5,4,3,2,1}}

I'm just lost on how this would be implemented. SO, can you help?

I have a seriously ugly solution right now, but have a "feeling" that LINQ will increase the elegance of my code.

Community
  • 1
  • 1
a developer
  • 423
  • 1
  • 5
  • 12
  • So you need to split it in few monotonic subsequences? – Dyppl Jul 11 '11 at 16:48
  • 1
    What would make it partition the values that way? There doesn't appear to be any obvious reasoning, other than they're all ascending sequences until the last one. Are you after monotonic sequences? – Jon Skeet Jul 11 '11 at 16:48
  • does monotonous mean boring in this context, or does it have a specific technical meaning? – a developer Jul 11 '11 at 16:49
  • @generalt: I believe Dyppl means monotonic: either increasing or decreasing, never going up then down or down then up. But it would really help if you could clarify the question around this. – Jon Skeet Jul 11 '11 at 16:49
  • 1
    @jon- the problem i'm trying to solve is to partition the original sequence based upon TheValue changing its direction. for example, the sub-sequences can only increase or stay the same in value, or decrease or stay the same in value. the set {0,1,0,4,5} is not a legal sub-sequence in this case. does that make sense? – a developer Jul 11 '11 at 16:50
  • 1
    yes, that is exactly what i'm trying to do- create a monotonic sequence. also, jon, thanks for commenting on this thread. your book is great. :D – a developer Jul 11 '11 at 16:51
  • @Jon: yes, that's what I meant, thanks – Dyppl Jul 11 '11 at 16:53
  • @generalt: Right, knowing that the change of direction is the signal for change is the important bit :) Will answer... – Jon Skeet Jul 11 '11 at 16:53
  • I think that the solution with loops will be more clear in this particular case – Dyppl Jul 11 '11 at 16:53
  • @Dyppl- here is what i have currently...which, IMO, is very confusing, esp. for a new developer rolling onto the project: [link](http://pastebin.com/B52G84nX). – a developer Jul 11 '11 at 17:00
  • `0 1 2 7 7 6 4 2` goes `{ 0 1 2 7 7 }` and `{6 4 2}`? – user7116 Jul 11 '11 at 17:05
  • @sixlettervariables- yes. a change in direction in the set signals a new set must be created. – a developer Jul 11 '11 at 17:09

3 Answers3

6

Okay, I think we can do this...

public static IEnumerable<IEnumerable<TElement>>
    PartitionMontonically<TElement, TKey>
    (this IEnumerable<TElement> source,
     Func<TElement, TKey> selector)
{
    // TODO: Argument validation and custom comparisons
    Comparer<TKey> keyComparer = Comparer<TKey>.Default;

    using (var iterator = source.GetEnumerator())
    {
        if (!iterator.MoveNext())
        {
            yield break;
        }
        TKey currentKey = selector(iterator.Current);
        List<TElement> currentList = new List<TElement> { iterator.Current };
        int sign = 0;
        while (iterator.MoveNext())
        {
            TElement element = iterator.Current;
            TKey key = selector(element);
            int nextSign = Math.Sign(keyComparer.Compare(currentKey, key));

            // Haven't decided a direction yet
            if (sign == 0)
            {
                sign = nextSign;
                currentList.Add(element);
            }
            // Same direction or no change
            else if (sign == nextSign || nextSign == 0)
            {
                currentList.Add(element);
            }
            else // Change in direction: yield current list and start a new one
            {
                yield return currentList;
                currentList = new List<TElement> { element };
                sign = 0;
            }
            currentKey = key;
        }
        yield return currentList;
    }
}

Completely untested, but I think it might work...

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • works beautifully. thank you jon! also, TIL about Comparer.Default. awesome. – a developer Jul 11 '11 at 17:15
  • Line 21 needs to change `comparer.Compare` to `keyComparer.Compare` to compile, otherwise works fabulously. Learnt something new today. – Kilanash Jul 11 '11 at 17:20
  • @Kilanash: Thanks, fixed. That's the problem with writing code in a hurry, on a train about to arrive, and not bothering to try to compile and run :) – Jon Skeet Jul 11 '11 at 17:28
1

Here's a custom LINQ operator which splits a sequence according to just about any criteria. Its parameters are:

  1. xs: the input element sequence.
  2. func: a function which accepts the "current" input element and a state object, and returns as a tuple:
    • a bool stating whether the input sequence should be split before the "current" element; and
    • a state object which will be passed to the next invocation of func.
  3. initialState: the state object that gets passed to func on its first invocation.

Here it is, along with a helper class (required because yield return apparently cannot be nested):

public static IEnumerable<IEnumerable<T>> Split<T, TState>(
                  this IEnumerable<T> xs,
                  Func<T, TState, Tuple<bool, TState>> func, 
                  TState initialState)
{
    using (var splitter = new Splitter<T, TState>(xs, func, initialState))
    {
        while (splitter.HasNext)
        {
            yield return splitter.GetNext();
        }
    }
}
internal sealed class Splitter<T, TState> : IDisposable
{
    public Splitter(IEnumerable<T> xs, 
                    Func<T, TState, Tuple<bool, TState>> func, 
                    TState initialState)
    {
        this.xs = xs.GetEnumerator();
        this.func = func;
        this.state = initialState;
        this.hasNext = this.xs.MoveNext();
    }

    private readonly IEnumerator<T> xs;
    private readonly Func<T, TState, Tuple<bool, TState>> func;
    private bool hasNext;
    private TState state;

    public bool HasNext { get { return hasNext; } }

    public IEnumerable<T> GetNext()
    {
        while (hasNext)
        {
            Tuple<bool, TState> decision = func(xs.Current, state);
            state = decision.Item2;
            if (decision.Item1) yield break;
            yield return xs.Current;
            hasNext = xs.MoveNext();
        }
    }

    public void Dispose() { xs.Dispose(); }
}

Note: Here are some of the design decisions that went into the Split method:

  • It should make only a single pass over the sequence.
  • State is made explicit so that it's possible to keep side effects out of func.
stakx - no longer contributing
  • 83,039
  • 20
  • 168
  • 268
1

alternatively with linq operators and some abuse of .net closures by reference.

public static IEnumerable<IEnumerable<T>> Monotonic<T>(this IEnumerable<T> enumerable)
{
  var comparator = Comparer<T>.Default;
  int i = 0;
  T last = default(T);
  return enumerable.GroupBy((value) => { i = comparator.Compare(value, last) > 0 ? i : i+1; last = value; return i; }).Select((group) => group.Select((_) => _));
}

Taken from some random utility code for partitioning IEnumerable's into a makeshift table for logging. If I recall properly, the odd ending Select is to prevent ambiguity when the input is an enumeration of strings.

Krypes
  • 561
  • 2
  • 10