Alexander
Alexander

Reputation: 20234

IEnumerable: Get all before the last that matches a predicate

I have an IEnumerable<int> like this, only longer:

5, 0, 0, 0, 0, 4, 0, 0, 0, 2, 0, 6, 0, 0, 0, 0, 0

and now I want to return all elements before the last non-zero value:

5, 0, 0, 0, 0, 4, 0, 0, 0, 2, 0

It seems sequence.Last() doesn't help here, because it returns the last occurrence, not the index of the last occurrence.

I had thought to use

var lastIndex = sequence.LastIndex(x=>x!=0);
subsequence = sequence.Take(lastIndex);

which would work in the general case, but LastIndex doesn't exist, or

var last = sequence.Last(y=>y!=0);
subsequence = sequence.TakeWhile(x=>x!=last)

which would work on the example, but not in the general case, where there may be duplicated non-zero values.

Any ideas?

Upvotes: 2

Views: 1308

Answers (5)

Dmitry
Dmitry

Reputation: 344

IEnumerable<int> source = new List<int> {5,0,0, 4,0,0,3, 0, 0};
List<int> result = new List<int>();
List<int> buffer = new List<int>();
foreach (var i in source)
{
    buffer.Add(i);
    if (i != 0)
    {
        result.AddRange(buffer);
        buffer.Clear();
    }
}

Upvotes: 1

Jon Hanna
Jon Hanna

Reputation: 113292

would work in the general case, but LastIndex doesn't exist

No, but you can find it with:

var lastIndex = sequence
  .Select((x, i) => new {El = x, Idx = i})
  .Where(x => x.El != 0)
  .Select(x => x.Idx).Last();

If you need to work with IQueryable<T> that's about as good as you can get.

It has a few problems. For one thing it scans through the sequence twice, and who is to say the sequence even allow that. We can do better but we'll have to buffer, though not necessarily buffer the whole thing:

public static IEnumerable<T> BeforeLastMatch<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
  if (source == null) throw new ArgumentNullException(nameof(source));
  if (predicate == null) throw new ArgumentNullException(nameof(predicate));
  return BeforeLastMatchImpl(source, predicate);
}

public static IEnumerable<T> BeforeLastMatchImpl<T>(IEnumerable<T> source, Func<T, bool> predicate)
{
  var buffer = new List<T>();
  foreach(T item in source)
  {
    if (predicate(item) && buffer.Count != 0)
    {
      foreach(T allowed in buffer)
      {
          yield return allowed;
      }
      buffer.Clear();
    }
    buffer.Add(item);
  }
}

Call sequence.BeforeLastMatch(x => x != 0) and you get 5, 0, 0, 0, 0, 4, 0, 0, 0, 2, 0

If you really need it to work with both IEnumerable and IQueryable that can be handled too, but it's a bit more complicated. Don't bother if you know you'll only ever have in-memory IEnumerable. (Also some providers have different support for different features so you might be forced to do the in-memory version above anyway):

private class ElementAndIndex<T>
{
  public T Element { get; set; }
  public int Index { get; set; }
}

public static IQueryable<T> BeforeLastMatch<T>(this IQueryable<T> source, Expression<Func<T, bool>> predicate)
{
  if (source == null) throw new ArgumentNullException(nameof(source));
  if (predicate == null) throw new ArgumentNullException(nameof(predicate));
  // If source is actually an in-memory enumerable, the other method will be more efficient,
  // so use it instead.
  var asEnum = source as EnumerableQuery<T>;
  if (asEnum != null && asEnum.Expression.NodeType == ExpressionType.Constant)
  {
    // On any other IQueryable calling `AsEnumerable()` will force it
    // to be loaded into memory, but on an EnumerableQuery it just
    // unwraps the wrapped enumerable this will chain back to the
    // contained GetEnumerator.
    return BeforeLastMatchImpl(source.AsEnumerable(), predicate.Compile()).AsQueryable();
  }

  // We have a lambda from (T x) => bool, and we need one from
  // (ElementAndIndex<T> x) => bool, so build it here.

  var param = Expression.Parameter(typeof(ElementAndIndex<T>));
  var indexingPredicate = Expression.Lambda<Func<ElementAndIndex<T>, bool>>(
    Expression.Invoke(predicate, Expression.Property(param, "Element")),
    param
  );

  return source.Take( // We're going to Take based on the last index this finds.
    source
      // Elements and indices together
      .Select((x, i) => new ElementAndIndex<T>{ Element = x, Index = i}) 
      // The new predicate we created from that passed to us.
      .Where(indexingPredicate)
      // The last matching element.
      .Select(x => x.Index).Last());
}

Upvotes: 5

Tim Schmelter
Tim Schmelter

Reputation: 460158

Maybe there are more efficent approaches but this one is readable, isn't it?

var allBeforeLastNonZero = sequence
    .Reverse()                // look from the end
    .SkipWhile(i => i == 0)   // skip the zeros
    .Skip(1)                  // skip last non-zero
    .Reverse();               // get original order

Upvotes: 4

jayanta
jayanta

Reputation: 163

You can try this

var allDataBeforeLastNonZero= sequence.GetRange(0,sequence.FindLastIndex(x=>x!=0));

Upvotes: 5

MakePeaceGreatAgain
MakePeaceGreatAgain

Reputation: 37000

You could convert your list into a string and use String.Trim:

var str = String.Join(",", myInputArray);
var result = str.TrimEnd(',', '0').Split(',').Select(x => Convert.ToInt32(x)).ToList();
result.RemoveAt(result.Count - 1);

Have to admit seems a bit ugly, but it should work.

Upvotes: 1

Related Questions