Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to use LINQ to select a range of items based on an enclosing condition (i.e. not a simple WHERE clause)?

Tags:

c#

lambda

linq

Consider you have a List<Foo> of objects and Foo has an IsSelected property like so...

public class Foo
{
    public string Name{ get; set; }
    public bool IsSelected{ get; set; }
}

List<Foo> sourceItems = new List<Foo>
{
    new Foo(){ Name="First",   IsSelected=false},
    new Foo(){ Name="Second",  IsSelected=true },
    new Foo(){ Name="Third",   IsSelected=false},
    new Foo(){ Name="Fourth",  IsSelected=true },
    new Foo(){ Name="Fifth",   IsSelected=false},
    new Foo(){ Name="Sixth",   IsSelected=true },
    new Foo(){ Name="Seventh", IsSelected=true },
    new Foo(){ Name="Eighth",  IsSelected=false},
    new Foo(){ Name="Ninth",   IsSelected=false},
    new Foo(){ Name="Tenth",   IsSelected=false}
};

Using a Where clause, I can of course get just the items that are selected, like so...

var results = sourceItems.Where(item => item.IsSelected);

...but what if I want all items between the first and last items where IsSelected is true? (i.e. Second through Seventh)

I know I can use SkipWhile since it skips until the first true statement, then returns everything after...

// Returns from Second on
var results = sourceItems.SkipWhile(item => !item.IsSelected);

...and I know I can reverse and do the same again, but then I'd have to re-reverse at the end and double-reversing just feels like it would be unnecessarily expensive.

My other thought is to use the Select with index and store the last index where IsSelected is true, then use a Where clause at the end, checking if the index is below the last selected one, but that just seems expensive and cludgy.

int lastSelectedIndex = -1;
var results = sourceItems
    .SkipWhile(item => !item.IsSelected)
    .Select( (item, itemIndex) => 
    {
        if(item.IsSelected)
            lastSelectedIndex = index;

        return new {item, index};
    })
    .Where(anonObj => anonObj.index <= lastSelectedIndex)
    .Select(anonObj => anonObj.Item);

Alternately I think I can replace that last Where with a Take clause and just take the correct number of items so I wouldn't have to iterate the entire list, but I'm not sure lastSelectedIndex will have the correct value as I don't think Select returns the entire list, only the next enumeration, but I could be wrong.

.Take(lastSelectedIndex + 1);

So is there another way to do what I want?

like image 657
Mark A. Donohoe Avatar asked Jan 11 '23 06:01

Mark A. Donohoe


1 Answers

Well, let me approach this differently by using indices and adding up the items to a new collection.

List<Foo> sourceItems = new List<Foo>
{
    new Foo(){ Name="First",   IsSelected=false},
    new Foo(){ Name="Second",  IsSelected=true },
    new Foo(){ Name="Third",   IsSelected=false},
    new Foo(){ Name="Fourth",  IsSelected=true },
    new Foo(){ Name="Fifth",   IsSelected=false},
    new Foo(){ Name="Sixth",   IsSelected=true },
    new Foo(){ Name="Seventh", IsSelected=true },
    new Foo(){ Name="Eighth",  IsSelected=false},
    new Foo(){ Name="Ninth",   IsSelected=false},
    new Foo(){ Name="Tenth",   IsSelected=false}
};

int startIndex = sourceItems.FindIndex(x => x.IsSelected);
int endIndex   = sourceItems.FindLastIndex(x => x.IsSelected);

var items = new List<Foo>();

for (int i = startIndex; i <= endIndex; i++)
    items.Add(sourceItems[i]);    

With 1 000 000 entries it only took 13 Milliseconds to get the result out.

like image 142
Dimi Takis Avatar answered Jan 12 '23 18:01

Dimi Takis