morelinq / MoreLINQ

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

Default elements from a secondary source #997

Open atifaziz opened 1 year ago

atifaziz commented 1 year ago

I'd like to propose an operator called Default that defaults an element from a secondary sequence when an element from the source sequence is deemed missing or faulty. A function is used to determine if an element of the source sequence is deemed missing or faulty and another function projects a result given the missing/faulty element and a default from secondary sequence for substitution/replacement.

A prototype would be as follows:

public static IEnumerable<TResult>
    Default<TSource, TDefault, TResult>(this IEnumerable<TSource> source,
                                        IEnumerable<TDefault> defaults,
                                        Func<TSource, (bool, TResult)> chooser,
                                        Func<TSource, TDefault, TResult> defaultor)
{
    using var @default = defaults.GetEnumerator();
    foreach (var item in source)
    {
        if (chooser(item) is (true, var v))
        {
            yield return v;
        }
        else
        {
            if (!@default.MoveNext())
                break;
            yield return defaultor(item, @default.Current);
        }
    }
}

The following code demonstrates the operator in action:

var source = new string?[] { "foo", null, "bar", "baz", null };
var nums = Enumerable.Range(1, int.MaxValue);

foreach (var e in source.Default(nums, s => (s is not null, s), (_, r) => $"unnamed{r}"))
    Console.WriteLine(e);

Outputs:

foo
unnamed1
bar
baz
unnamed2
viceroypenguin commented 1 year ago

I would argue that as currently proposed, it is overly specific and complicated. I'd prefer to see a family of methods:

IEnumerable<TSource> DefaultIf<TSource>(this IEnumerable<TSource> source, TSource defaultValue, TSource replacementValue);
IEnumerable<TSource> DefaultIf<TSource>(this IEnumerable<TSource> source, TSource defaultValue, IEnumerable<TSource> replacementValues);
IEnumerable<TSource> DefaultIf<TSource, TDefault>(this IEnumerable<TSource> source, TSource defaultValue, IEnumerable<TDefault> replacementValues, Func<TSource, TDefault, TSource> defaultor);

Each with a matching IComparer<TSource>? comparer overload. Alternatively, Func<TSource, bool> isDefault function instead of TSource defaultValue. I think the Func<TSource, (bool, TResult)> is a pattern that creates complication and awkwardness in the code generally, but that's my opinion.

atifaziz commented 1 year ago

Those family of methods can be added as simpler overloads and which would ultimately be simple wrappers around the more generally applicable workhorse prototype I submitted. The example admittedly is a very simple one and doesn't demonstrate what's fully possible (I was in a hurry and might post a richer one later).

I think the Func<TSource, (bool, TResult)> is a pattern that creates complication and awkwardness in the code generally

The value of that function is that the output doesn't have to be tied to the source or even the default. The three types can vary as far as the algorithm is concerned. Those who don't need that flexibility can use the simpler overloads where the input and output types are the same and the missing value can be identified with an equality check as opposed to a predicate function.

Each with a matching IComparer<TSource>? comparer overload.

You mean IEqualityComparer<TSource>?