Zachary Kniebel
Zachary Kniebel

Reputation: 4774

Filtering lambda expression that compares two elements of the same collection

I was wondering if it is possible to write an expression for a Linq extension (or a custom extension) to filter a collection using a lambda expression that compares two elements of the collection.

In other words, if I have a List<DateTime> and some value, var v = DateTime.Today, then I am wondering if it is possible to write create a method that will return the first element of the collection that is less than or equal to the value, current <= v, with the next element of the collection being greater than or equal to the value, next >= v.

Please note that the above is just an example, and may or may not be the final implementation.


The following would be a valid solution, were the .First() method to accept Func<DateTime, DateTime, bool> with the two DateTime parameters being consecutive elements of the sequence:

dateCollection.First((current, next) => current <= v && next >= v);

Please also note that with the example given, a valid workaround could be to use .OrderBy and then find the first index that is greater than d and subtract 1. However, this type of comparison is not the only one that I am looking for. I may have a situation in which I am checking a List<string> for the first situation where the current element starts with the first letter of my value, v, and the next element ends with the last letter of my value, v.


I am looking for something that would be just a few of code. My goal is to find the simplest solution to this possible, so brevity carries a lot of weight.

What I am looking for is something of the form:

public static T First (...)
{
    ...
}

I believe that this will also require two or more lambda expressions as parameters. One thing that may also provide a good solution is to be able to select into all possible, consecutive pairs of elements of the sequence, and call the .First() method on that.

For example:

//value 
var v = 5;

//if my collection is the following
List<int> stuff = { a, b, c, d };

//select into consecutive pairs, giving: 
var pairs = ... // { { a, b }, { b, c }, { c, d } };

//then run comparison
pairs.First(p => p[0] <= v && p[1] >= v).Select(p => p[0]);


Thanks and happy coding! :)

Upvotes: 0

Views: 3135

Answers (5)

Casey Chester
Casey Chester

Reputation: 276

Servy's answer can be handled without an extension method:

var first = items
  .Select((current, index) => index > 0 ? new { Prev = items.ElementAt(index-1), Current = current } : null)
  .First(pair => pair != null && pair.Prev <= v && pair.Current >= v)
  .Prev;      

Upvotes: 0

Arie
Arie

Reputation: 5373

    List<int> list = new List<int>();
    list.Add(3);
    list.Add(2);
    list.Add(8);
    list.Add(1);
    list.Add(4);

    var element = list
         .Where((elem, idx) => idx < list.Count-1 && elem<=list[idx+1])
         .FirstOrDefault();

    Console.WriteLine(element);

rsult: 2

where 'elem' is the current element, and 'idx' is an index of the current element

Upvotes: 3

Servy
Servy

Reputation: 203802

What we can create is a Pairwise method that can map a sequence of values into a sequence of pairs representing each value and the value that comes before it.

public static IEnumerable<Tuple<T, T>> Pairwise<T>(this IEnumerable<T> source)
{
    using (var iterator = source.GetEnumerator())
    {
        if (!iterator.MoveNext())
            yield break;

        T prev = iterator.Current;

        while (iterator.MoveNext())
        {
            yield return Tuple.Create(prev, iterator.Current);
            prev = iterator.Current;
        }
    }
}

Now we can write out:

var item = data.Pairwise()
    .First(pair => pair.Item1 <= v && pair.Item2 >= v)
    .Item1;

If this is something you're going to use a fair bit, it may be worth creating a new custom type to replace Tuple, so that you can have Current and Next properties, instead of Item1 and Item2.

Upvotes: 3

Me.Name
Me.Name

Reputation: 12534

Strictly following the description, you could combine linq and list indexes to find the first index that matches your criterium, and returning its element:

DateTime d= DateTime.Today;
var res = dateCollection[Enumerable.Range(0, dateCollection.Count - 1).First(i => dateCollection[i] <= d && dateCollection[i + 1] >= d)];

Upvotes: 1

MaxOvrdrv
MaxOvrdrv

Reputation: 1916

Not really sure what you want to return here is my main question, but to take your example and put it into a LINQ statement, it would be like this:

DateTime? Firstmatch = dateCollection.DefaultIfEmpty(null).FirstOrDefault(a => a <= d && ((dateCollection.IndexOf(a) + 1) < (dateCollection.Count) && dateCollection[dateCollection.IndexOf(a) + 1] >= d));

Upvotes: 1

Related Questions