Search code examples
c#linqfluentmethod-chaining

Chaining fluent LINQ-like queries together


I wanted to build a fluent api to iterate on an array where I filter values and continue processing the remaining (not the filtered ones) values. Something like this pseudo-code:

int[] input = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
from a in Take(3) // a = {5,4,1}
from b in Skip(4) // b = null
from c in TakeWhile(x=> x != 0) // c = {7, 2}
select new Stuff(a, b, c)

I don't know where to start looking, what are the basis for something like this. So I wanted to ask for some help.

The system should not be restricted to int numbers.. another example:

string[] input = { "how", "are", "you", "doing", "?" };
from a in OneOf("how", "what", "where") // a = "how"
from b in Match("are") // b = "are"
from c in TakeWhile(x=> x != "?") // c = { "you", "doing" }
select new Stuff(a, b, c)

Solution

  • The following code will allow you to do input.FirstTake(3).ThenSkip(4).ThenTakeWhile(x => x != 0); to get the sequence 5, 4, 1, 7, 2. The main idea is that you need to keep track of the takes and skips you want to do so they can be applied when you iterate. This is similar to how OrderBy and ThenBy work. Note that you cannot do other Linq operations in between. This build up one enumeration of consecutive skips and takes, then that sequence will be fed through any Linq operations you tack on.

    public interface ITakeAndSkip<out T> : IEnumerable<T>
    {
        ITakeAndSkip<T> ThenSkip(int number);
        ITakeAndSkip<T> ThenTake(int number);
        ITakeAndSkip<T> ThenTakeWhile(Func<T, bool> predicate);
        ITakeAndSkip<T> ThenSkipWhile(Func<T, bool> predicate);
    }
    
    public class TakeAndSkip<T> : ITakeAndSkip<T>
    {
        private readonly IEnumerable<T> _source;
    
        private class TakeOrSkipOperation
        {
            public bool IsSkip { get; private set; }
            public Func<T, bool> Predicate { get; private set; }
            public int Number { get; private set; }
    
            private TakeOrSkipOperation()
            {
            }
    
            public static TakeOrSkipOperation Skip(int number)
            {
                return new TakeOrSkipOperation
                {
                    IsSkip = true,
                    Number = number
                };
            }
    
            public static TakeOrSkipOperation Take(int number)
            {
                return new TakeOrSkipOperation
                {
                    Number = number
                };
            }
    
    
            public static TakeOrSkipOperation SkipWhile(Func<T, bool> predicate)
            {
                return new TakeOrSkipOperation
                {
                    IsSkip = true,
                    Predicate = predicate
                };
            }
    
            public static TakeOrSkipOperation TakeWhile(Func<T, bool> predicate)
            {
                return new TakeOrSkipOperation
                {
                    Predicate = predicate
                };
            }
        }
    
        private readonly List<TakeOrSkipOperation> _operations = new List<TakeOrSkipOperation>();
    
        public TakeAndSkip(IEnumerable<T> source)
        {
            _source = source;
        }
    
        public IEnumerator<T> GetEnumerator()
        {
            using (var enumerator = _source.GetEnumerator())
            {
                // move to the first item and if there are none just return
                if (!enumerator.MoveNext()) yield break;
    
                // Then apply all the skip and take operations
                foreach (var operation in _operations)
                {
                    int n = operation.Number;
                    // If we are not dealing with a while then make the predicate count
                    // down the number to zero.
                    var predicate = operation.Predicate ?? (x => n-- > 0);
    
                    // Iterate the items until there are no more or the predicate is false
                    bool more = true;
                    while (more && predicate(enumerator.Current))
                    {
                        // If this is a Take then yield the current item.
                        if (!operation.IsSkip) yield return enumerator.Current;
                        more = enumerator.MoveNext();
                    }
    
                    // If there are no more items return
                    if (!more) yield break;
                }
    
                // Now we need to decide what to do with the rest of the items. 
                // If there are no operations or the last one was a skip then
                // return the remaining items
                if (_operations.Count == 0 || _operations.Last().IsSkip)
                {
                    do
                    {
                        yield return enumerator.Current;
                    } while (enumerator.MoveNext());
                }
    
                // Otherwise the last operation was a take and we're done.
            }
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    
        public ITakeAndSkip<T> ThenSkip(int number)
        {
            _operations.Add(TakeOrSkipOperation.Skip(number));
            return this;
        }
    
        public ITakeAndSkip<T> ThenSkipWhile(Func<T, bool> predicate)
        {
            _operations.Add(TakeOrSkipOperation.SkipWhile(predicate));
            return this;
        }
    
        public ITakeAndSkip<T> ThenTake(int number)
        {
            _operations.Add(TakeOrSkipOperation.Take(number));
            return this;
        }
    
        public ITakeAndSkip<T> ThenTakeWhile(Func<T, bool> predicate)
        {
            _operations.Add(TakeOrSkipOperation.TakeWhile(predicate));
            return this;
        }
    }
    
    public static class TakeAndSkipExtensions
    {
        public static ITakeAndSkip<T> FirstTake<T>(this IEnumerable<T> source, int number)
        {
            return new TakeAndSkip<T>(source).ThenTake(number);
        }
    
        public static ITakeAndSkip<T> FirstSkip<T>(this IEnumerable<T> source, int number)
        {
            return new TakeAndSkip<T>(source).ThenSkip(number);
        }
    
        public static ITakeAndSkip<T> FirstTakeWhile<T>(this IEnumerable<T> source, Func<T, bool> predicate)
        {
            return new TakeAndSkip<T>(source).ThenTakeWhile(predicate);
        }
    
        public static ITakeAndSkip<T> FirstSkipWhile<T>(this IEnumerable<T> source, Func<T, bool> predicate)
        {
            return new TakeAndSkip<T>(source).ThenSkipWhile(predicate);
        }
    }