Finding previous member in sequence using LINQ

D

DamienS

G'day LINQ junkies,

I have an array of objects. I need to sort them via a date property
and then, given a particular object, find the object in sequence
before it (or null if it's the first in sequence).

I think that I can use the elementAt method... but don't know how to
find the element that I've selected.

Can someone please fill in the gap? Below is a simple console app that
I'm using to play with it.

Thanks very much in advance,


Damien


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LINQ_walkthrough_order
{
class Program
{
static void Main(string[] args)
{
WalkLinq();
}


public static void WalkLinq()
{
List<clsDate> dates = new List<clsDate>
{
new clsDate(new DateTime(2008,11,1)),
new clsDate(new DateTime(2008,10,1)),
new clsDate(new DateTime(2008,9,1)),
new clsDate(new DateTime(2008,8,1))
};
clsDate d1 = dates[2];
Console.WriteLine(d1.d.ToShortDateString());

//dates.OrderByDescending(x=>x.d).
// .... WHAT NOW?


Console.ReadLine();
}
}

public class clsDate
{
public DateTime d;
public clsDate(DateTime _d)
{
d = _d;
}
}

}
 
D

DamienS

.... I found a solution. It's posted below to save someone the hassle
of replying. If there's a 'much' better way to do it, please let me
know.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LINQ_walkthrough_order
{
class Program
{
static void Main(string[] args)
{
WalkLinq();
}


public static void WalkLinq()
{
List<clsDate> dates = new List<clsDate>
{
new clsDate(new DateTime(2008,11,1)),
new clsDate(new DateTime(2005,10,1)),
new clsDate(new DateTime(2006,10,1)),
new clsDate(new DateTime(2007,10,1)),
new clsDate(new DateTime(2008,10,1)),
new clsDate(new DateTime(2008,9,1)),
new clsDate(new DateTime(2005,10,15)),
new clsDate(new DateTime(2008,8,1))


};
clsDate d1 = dates[2]; // Pick a date.

Console.WriteLine(d1.d.ToShortDateString());

clsDate cc = dates.OrderBy(x => x.d)
.Where(y => y == d1)
.First();

int ii = dates.OrderBy(x=>x.d).ToArray().ToList().IndexOf
(cc);

Console.WriteLine(
dates.OrderBy(x => x.d)
.ElementAt(ii>0?ii-1 : 0)
.d.ToShortDateString()
);

Console.ReadLine();
}
}

public class clsDate
{
public DateTime d;
public clsDate(DateTime _d)
{
d = _d;
}

public override string ToString()
{
return d.ToShortDateString();
}
}

}
 
M

Michael C

DamienS said:
clsDate cc = dates.OrderBy(x => x.d)
.Where(y => y == d1)
.First();

Why are you ordering here? If the list contains unique dates then it does
nothing, if it contains duplicates dates then the ordering is undefined.
Also, by using the where clause you are searching the entire list even after
you've found your item, you may as well put your where condition into First,
eg

clsDate cc = dates.First(y => y == d1);

then to get the previous item you could do something like this:

dates.OrderByDescending(y => y.d).First(d => d.d < d1)

This isn't as efficient as it could be as you need to sort the entire list
to get one item, you could search the list looking for the date which is
closest but efore the date you have but I don't think this would be as
simple as you're hoping.

Michael
 
M

Michael C

DamienS said:
... I found a solution. It's posted below to save someone the hassle
of replying. If there's a 'much' better way to do it, please let me
know.

Here's an alternate solution if you want to be smart. You can define your
own extension method that works similar to Min except that instead of
returning the minimum value found, it returns the item out of the collection
that contains the minimum value. We can then do the query like this:

1) Find the item we are after (same method as before)
2) Get all the dates below this date using a where clause
3) Find the date that is closest to the date you're after using the new min
function

clsDate cc = dates.First(y => y == d1);
clsDate res = dates
.Where(d => d.d < d1)
.MinItem(d => (d1- d.d.Value).Days);


Here's the MinItem function that you will need to put into a static class
somewhere:

public static TSource MinItem<TSource, TResult>(this
IEnumerable<TSource> Items, Func<TSource, TResult> func) where TResult :
IComparable<TResult>
{
TResult min = default(TResult);
TSource minItem = default(TSource);
bool minFound = false;
foreach (TSource item in Items)
{
TResult val = func(item);
if (minFound)
{
int comp = val.CompareTo(min);
if (comp == -1)
{
min = val;
minItem = item;
}
}
else
{
min = val;
minItem = item;
minFound = true;
}
}
return minItem;
}
 
F

Frans Bouma [C# MVP]

DamienS said:
... I found a solution. It's posted below to save someone the hassle
of replying. If there's a 'much' better way to do it, please let me
know.
Console.WriteLine(d1.d.ToShortDateString());

clsDate cc = dates.OrderBy(x => x.d)
.Where(y => y == d1)
.First();

int ii = dates.OrderBy(x=>x.d).ToArray().ToList().IndexOf
(cc);

Console.WriteLine(
dates.OrderBy(x => x.d)
.ElementAt(ii>0?ii-1 : 0)
.d.ToShortDateString()
);

Console.ReadLine();
}
}

Don't you think this is a bit inefficient? You're several times
traversing the whole sequence (worse case).

So you have to find d1 back and then grab the object on the index right
before it. So
- first define a sort.
var sortedDates = dates.OrderBy(x=>x.d);
- then traverse the sorted sequence and return the object last seen if
you run into the object you need to look for.

clsDate toReturn = null;
clsDate previousDate = null;
foreach(clsDate currentDate in sortedDates)
{
if(currentDate==d1)
{
toReturn = previousDate;
break;
}
previousDate = currentDate;
}
return toReturn;

so here, you sort once, and traverse the list once (the foreach, which
also does the sorting).

the loop doesn't use linq, but that's not necessary. Linq can be very
helpful, but don't overdo it: it should reduce complexity and increase
performance and flexibility. If you can write a simple loop which does
what you need, write that simple loop: it's easy to read, understand and
very efficient. :)

oh, and drop the MFC class naming style... you shouldn't prefix classes
with 'cls'. Please read the naming conventions section in the .NET
reference manual in the MSDN documentation.

FB
 
M

Michael C

Frans Bouma said:
Don't you think this is a bit inefficient? You're several times traversing
the whole sequence (worse case).

So you have to find d1 back and then grab the object on the index right
before it. So
- first define a sort.

Sorting the entire list is a little inefficient too. :)

Michael
 
F

Frans Bouma [C# MVP]

Michael said:
Sorting the entire list is a little inefficient too. :)

Agreed, a min-compare would be even more efficient, didn't think about
that. So a linq-free solution is the most advanced one ;) (a single
foreach loop which traverses the set once. It can determine the item
that's closest to the sought item. So the 'Where' call you have can be
placed inside the MinItem routine (or simply inside a foreach) and it
should be a simple check on date, if the item is > d1, the loop simply
continues.

Anyway, I hadn't looked at your code before posting ;)

FB
 
D

DamienS

Thanks Micahel and Frans, thanks great. Sorry for the delayed
response; the project went into full production live this week and, as
you'd imagine, it's been busy.

I'll have a relook and rewrite this afternoon.

Frans, I take your points about the overuse of LINQ. I have to admit
that I do get into the bit of the headspace of "this is cool, use it
everywhere". I'll also have a read up on the MFC naming conventions.
It's a little late for this project, but it's a lesson worth
learning.



Kind regards and thank you again,


Damien
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top