How do I get the closest DateTime from a List<DateTime>

Say I have the most recent DateTime and a List of all the possible dates. How would I efficiently go about finding the closest date time to last year's date in the list?

Say my list is comprised of the following:

2014-03-07
2014-03-14
2014-03-21
2014-03-28
...
2015-03-06
2015-03-13
2015-03-20

My most recent date is 2015-03-20, but I want to retrieve last year's date, 2014-03-21.

This is what I have currently, but it won't work if last year's date is one day off (eg; my time periods are stored weekly).

public DateTime LastYearDate()
{
    List<DateTime> times = GetAllDates();
    times.Sort();
    times.Reverse();
    DateTime currentDate = times.First();
    return times.Where(dt => dt == currentDate.AddYears(-1)).First();
}

I'm not sure what I would use to recursively calculate the closest date, so if you have any ideas of what direction I should take (reference to any Linq functions to check out), that would be appreciated.

Jon Skeet
people
quotationmark

As it's sorted, you can use a binary search to try to find an exact match. If List<T>.BinarySearch returns a non-negative number, you know you've found an exact match. Otherwise, you can apply the bitwise complement operator to find the index that the value would be inserted at. You then need to check whether the value before or at that index is further from the target. So something like this:

var target = currentDate.AddYears(-1);
List<DateTime> times = GetAllDates();
if (times.Count == 0)
{
    // ??? Work out what you want to do here, e.g. throw an exception
}
times.Sort();
var index = times.BinarySearch(target);
if (index >= 0)
{
    return times[index];
}
int insertIndex = ~index;
// Handle boundary cases
if (insertIndex == 0)
{
    return times[0];
}
if (insertIndex == times.Count)
{
    return times[insertIndex - 1];
}
// Okay, two options - find the closest
var timeBefore = times[insertIndex - 1];
var timeAfter = times[insertIndex];
// TODO: Work out what you want to do if they're equidistant.
return target - timeBefore > timeAfter - target ? timeAfter : timeBefore;

Having said that, spender's comment on Thomas Levesque's answer gives a very simple solution:

var target = currentDate.AddYears(-1);
List<DateTime> times = GetAllDates();
if (times.Count == 0)
{
    // ??? Work out what you want to do here, e.g. throw an exception
}
return times.OrderBy(t => (target - t).Duration).First();

Note that TimeSpan.Duration is always non-negative; it's like Math.Abs but for TimeSpan values.

people

See more on this question at Stackoverflow