morelinq / MoreLINQ

Extensions to LINQ to Objects
https://morelinq.github.io/
Apache License 2.0
3.7k stars 415 forks source link

Find first occurrences in a sequence #322

Open atifaziz opened 7 years ago

atifaziz commented 7 years ago

I propose to add a new operator that takes some sequence of elements and a set of predicates, and returns a sequence of the first matching element (along with its index) for each predicate. The returned matches will be in the order of the predicates. If a predicate has no satisfying element, then its corresponding result will be an index of -1 and undefined element. The search terminates as soon as all predicates have at least one matching element or the source is exhausted. An overload can be supplied on top of this to simply provide values sought (optionally with an equality comparer) instead of predicates.

The source sequence is iterated once. A predicate is evaluated once per element and it is only used as long no matching element has satisfied it.

Prototype

public static IEnumerable<(int Index, T Item)> FindFirst<T>(this IEnumerable<T> source, IEnumerable<T> searches) =>
    source.FindFirst(searches, null);

public static IEnumerable<(int Index, T Item)> FindFirst<T>(this IEnumerable<T> source, IEnumerable<T> searches, IEqualityComparer<T> comparer) =>
    source.FindFirst(searches.Select(v => new Func<T, bool>(e => (comparer ?? EqualityComparer<T>.Default).Equals(e, v)))
                      .ToArray());

public static IEnumerable<(int Index, T Item)> FindFirst<T>(this IEnumerable<T> source, params Func<T, bool>[] searches)
{
    if (searches.Length == 0)
        yield break;

    var count = 0;
    var matches = new (int Position, T Item)[searches.Length];
    var i = 0;
    foreach (var item in source)
    {
        i++;
        for (var pi = 0; pi < searches.Length; pi++)
        {
            if (matches[pi].Position == 0 && searches[pi](item))
            {
                matches[pi] = (i, item);
                count++;
                break;
            }
        }

        if (count == searches.Length)
            break;
    }

    foreach (var m in matches)
        yield return (m.Position - 1, m.Item);
}

Example

var r1 =
    Enumerable.Range(1, 10)
              .FindFirst(x => x >  10,
                         x => x ==  3,
                         x => x >=  5);

// r1 = (-1, 10), (2, 3), (4, 5)

var r2 =
    "cond,anx,age,educ,gender,income,emo,p_harm,tone,eth,treat,english,immigr,anti_info,cong_mesg"
        .Split(',')
        .FindFirst(new[] { "gender", "age", "comment" });

// r2 = (4, "gender"), (2, "age"), (-1, null)
fsateler commented 7 years ago

This sounds a lot like #81, which was rejected.

atifaziz commented 7 years ago

@fsateler #81 was not rejected. It was closed because Craig said “Nevermind then“ when I pointed out that Index can be used to the same effect. If anything, I'd say #236 is identical to #81. This one, however, is about multiple searches and finding the first occurrence of each.