Reputation: 20234
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
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
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
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
Reputation: 163
You can try this
var allDataBeforeLastNonZero= sequence.GetRange(0,sequence.FindLastIndex(x=>x!=0));
Upvotes: 5
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