2

I want to concatenate the elements of two sequences, producing a single sequence containing all the elements of the original two, but with their elements interleaved.

The Concat LINQ method can do the concatenation but without the interleaving, so I need something special.

The rules for interleaving are these:

  • For each pair of elements a selector function should be called, and the element selected should be either the first or the second, depending on the boolean result of the function (true: first, false: second)

Here is a practical example of what I want to achieve:

var sequence1 = new int[] { 1, 2, 3 };
var sequence2 = new int[] { 11, 12, 13 };
var result = sequence1.ConcatInterleaved(sequence2, (a, b) => (a + b) % 3 == 0);
Console.WriteLine(String.Join("\r\n", result));

Expected output:

1  // Because (1 + 11) % 3 == 0, the first is selected
11 // Because (2 + 11) % 3 != 0, the second is selected
12 // Because (2 + 12) % 3 != 0, the second is selected
2  // Because (2 + 13) % 3 == 0, the first is selected
13 // Because (3 + 13) % 3 != 0, the second is selected
3  // Because sequence2 has no more elements, the next element of sequence1 is selected

I want a LINQ solution so that the actual concatenation can be deferred, in the spirit of the build-in LINQ methods. This is my current attempt:

public static IEnumerable<TSource> ConcatInterleaved<TSource>(
    this IEnumerable<TSource> source,
    IEnumerable<TSource> other,
    Func<TSource, TSource, bool> selector)
{
    // What to do?
}

Update: I changed the example so it doesn't look like a simple alternating interleaving.

A clarification about the selector function: This function is not applied to preselected pairs of the two sequences, like it happens in the Zip method. The pairs are not predefined. After each selection a new pair is formed, containing the rejected element of the previous selection, and a new element from the previously selected sequence.

For example with the selector (a, b) => true the ConcatInterleaved becomes equivalent to Concat: All elements of sequence1 are returned, followed by all elements of sequence2. Another example: with the selector (a, b) => false all elements of sequence2 are returned, followed by all elements of sequence1.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 1
    Theodor, this is neither interleaving nor zipping. What's the purpose of the selector? The desired output can be produced by simply picking one item from each list. That's already implemented eg in [MoreLinq's Interleave](https://github.com/morelinq/MoreLINQ/blob/master/MoreLinq/Interleave.cs) operator. – Panagiotis Kanavos Apr 22 '19 at 13:24
  • @PanagiotisKanavos yeap, I know how to do it procedurally. I don't know how to do it in LINQ style (with Enumerables). – Theodor Zoulias Apr 22 '19 at 13:26
  • How to do *what*? That's not interleaving. What's the purpose of the selector? As for `LINQ Style` that's what MoreLinq does – Panagiotis Kanavos Apr 22 '19 at 13:27
  • The duplicate shows how to interleave enumerables the lazy and expensive way - use `Zip` to pair values and `SelectMany` to convert the pairs to small enumerabls. MoreLinq does it the fast way - pick one item from each enumerator and yield it. They are both `LINQ`y, both work with Enumerables. `Enumerable.Concat` works the same way. So does `Where` or `Select`, all of them work with enumerators – Panagiotis Kanavos Apr 22 '19 at 13:29
  • @PanagiotisKanavos I looked at MoreLinq's `Interleave`. It doesn't do what I want. There is no control over the interleaving procedure. I want to control the interleaving pair by pair. – Theodor Zoulias Apr 22 '19 at 13:31
  • @IanMercer [`Zip`](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.zip) produces a sequence containing the results of a projection. Instead I want all the original elements unaltered, just ordered in a special way. – Theodor Zoulias Apr 22 '19 at 13:33
  • @Servy the [duplicate question](https://stackoverflow.com/questions/24713558/c-sharp-interweave-two-uneven-list-into-a-new-list) is a related but different question. I can't apply the solutions suggested there to solve my problem. – Theodor Zoulias Apr 22 '19 at 13:39
  • @PanagiotisKanavos `Zip` with `SelectMany` is not a solution to my problem, because in my case the pairs are not predefined. After each selection a new pair is formed, containing the rejected element of the previous selection and a new element from the selected sequence of the previous selection. – Theodor Zoulias Apr 22 '19 at 13:44
  • I'm imagining a solution which relies on a Doubling method: `foreach(var x in source) {yield return x; yield return x;}`. Another solution would involve enumerating both sequences simultaneously by calling GetEnumerator and writing while loops. – Amy B Apr 22 '19 at 13:56
  • @AmyB yeap, probably I need to work with the enumerators. I tried a bit but it is messy, and I couldn't get the expected results. – Theodor Zoulias Apr 22 '19 at 14:10
  • @TheodorZoulias it's still not clear to me how you want the pairs to be created. You have two sequences of 3 elements each, and expected output have 6 elements. A cartesian product would generate 9 pairs, and a comparison by index would return 3 elements. How do you get to 6 elements? What is the logic behind the pairs? – Ortiga Apr 22 '19 at 17:05
  • @Ortiga it is similar to concatenation, with a custom ordering. When you concatenate (with [`Concat`](https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.concat)) two sequences of 3 elements each, you get an output sequence of 6 elements. – Theodor Zoulias Apr 22 '19 at 17:15
  • Might be better to call this a "merge algorithm" as different from interleaving or concatenating. https://en.wikipedia.org/wiki/Merge_algorithm https://stackoverflow.com/questions/9807701/is-there-an-easy-way-to-merge-two-ordered-sequences-using-linq – Amy B Apr 22 '19 at 21:59
  • @AmyB there is already a method in MoreLinq named [`OrderedMerge`](https://morelinq.github.io/1.x/ref/api/html/M_MoreLinq_MoreEnumerable_OrderedMerge__3_1.htm), that does something different (makes a projection, not ordering). – Theodor Zoulias Apr 22 '19 at 22:36
  • Never used MoreLinq. This first impression doesn't impress me. The simplest overload of OrderedMerge returns "A sequence with elements from the two input sequences merged, as in a full outer join." In my opinion, whoever wrote that didn't understand what a full outer join is, and the method is probably misnamed. – Amy B Apr 23 '19 at 03:32
  • @AmyB I haven't used it either. Yesterday I looked at MoreLinq for the first time, searching for a solution to my problem. It seems to be a useful library for anyone who does frequent and heavy use of LINQ. Most [methods](https://github.com/morelinq/MoreLINQ#operators) have logical names. – Theodor Zoulias Apr 23 '19 at 09:16

1 Answers1

2
public static IEnumerable<T> Weave(
  this IEnumerable<T> left,
  IEnumerable<T> right,
  Func<T, T, bool> chooser)
{
  using(var leftEnum = left.GetEnumerator())
  using(var rightEnum = right.GetEnumerator())
  {
    bool moreLeft = leftEnum.MoveNext;
    bool moreRight = rightEnum.MoveNext;
    while(moreLeft && moreRight)
    {
      if (chooser(leftEnum.Current, rightEnum.Current))
      {
        yield return leftEnum.Current;
        moreLeft = leftEnum.MoveNext();
      }
      else
      {
        yield return rightEnum.Current;
        moreRight = rightEnum.MoveNext();
      } 
    }
    // yield the buffered item, if any
    if (moreLeft) yield return leftEnum.Current;
    if (moreRight) yield return rightEnum.Current;

    // yield any leftover elements
    while (leftEnum.MoveNext()) yield return leftEnum.Current;
    while (rightEnum.MoveNext()) yield return rightEnum.Current;
  }
}
Amy B
  • 108,202
  • 21
  • 135
  • 185