Gilbert Williams
Gilbert Williams

Reputation: 1050

Algorithm to find the closest time

I have a list of DateTime values with dates that contain hours and minutes:

List<DateTime> times = times = new List<DateTime>()
{
    new DateTime(2019, 01, 01, 17, 00, 00),
    new DateTime(2019, 01, 01, 18, 45, 00),
    new DateTime(2019, 01, 01, 19, 00, 00),
    new DateTime(2019, 01, 01, 19, 30, 00),
    new DateTime(2019, 01, 01, 22, 30, 00)
};

DateTime current = DateTime.Now;

I put them all in a ComboBox, and I want to make some sort of algorithm so when I load my form, it will check for the current time and find the closest value to the current time and select the ComboBox item that contains that hour.

How can I achieve this? I tried to loop through them all and check for the least hour, but that doesn't seem to work. Is there a smarter way to do it?

For example: If the current time is 17:32, it will choose 17:00, because that's the closest. But, if the current time is 18:20, it will choose 18:45 and so on.

Upvotes: 3

Views: 1495

Answers (5)

mjwills
mjwills

Reputation: 23864

One option is to use MoreLinq's MinBy:

var actualNow = DateTime.Now;
// set `current` up however you need it
var current = new DateTime(2019, 01, 01, actualNow.Hour, actualNow.Minute, actualNow.Minute, actualNow.Millisecond); // set this up however you need it
var min = times.MinBy(z => Math.Abs((current - z).Ticks)).First();

It avoids the memory pressure of the OrderBy based solutions (since it avoids allocating a list to store the entire set of times).

Note you may want to check whether times is empty before using this (or other) solutions. If you don't wish to do that, consider using:

var min = times.MinBy(z => Math.Abs((current - z).Ticks)).Cast<DateTime?>().FirstOrDefault();

which will return null (rather than default(DateTime), or throw an exception) if times is empty.

Upvotes: 0

Dmitrii Bychenko
Dmitrii Bychenko

Reputation: 186668

You are looking for ArgMax which is not implemented in standard Linq, but can be emulated via Aggreagte

  List<DateTime> times = new List<DateTime>() {
    new DateTime(2019, 01, 01, 17, 00, 00),
    new DateTime(2019, 01, 01, 18, 45, 00),
    new DateTime(2019, 01, 01, 19, 00, 00),
    new DateTime(2019, 01, 01, 19, 30, 00),
    new DateTime(2019, 01, 01, 22, 30, 00),
  };

  DateTime toFind = new DateTime(2019, 5, 8, 18, 20, 0);

  var closestTime = times
    .Aggregate((best, item) => Math.Abs((item.TimeOfDay - toFind.TimeOfDay).Ticks) < 
                               Math.Abs((best.TimeOfDay - toFind.TimeOfDay).Ticks) 
       ? item 
       : best);

Please, note, that if we should find the closest time, we have to get rid of date part - TimeOfDay. If date part should be count, just remove TimeOfDay -

  var closestDateAndTime = times
    .Aggregate((best, item) => Math.Abs((item - toFind).Ticks) < 
                               Math.Abs((best - toFind).Ticks) 
       ? item 
       : best);

Upvotes: 0

Rapha&#235;l Althaus
Rapha&#235;l Althaus

Reputation: 60493

You could take the difference with DateTime.Now for all your datetimes, order by this difference and take the first result.

times.OrderBy(m => Math.Abs((DateTime.Now - m).TotalMilliseconds)).First();

Upvotes: 2

Torben Schramme
Torben Schramme

Reputation: 2140

Compare to the Ticks property of DateTime (MSDN). It can be seen as a linear representation of the whole date and timestamp and is sortable.

Do something like

comboBox.SelectedItem = times.OrderBy(t => Math.Abs(t.Ticks - current.Ticks)).First()

Upvotes: 6

Codor
Codor

Reputation: 17605

You would have to select an instance of DateTime which minimizes the temporal distance to the current time. You could use an extension method for IEnumerable<T> to do that as follows.

public static T ArgMin<T, R>(T t1, T t2, Func<T, R> f)
                where R : IComparable<R>
{
    return f(t1).CompareTo(f(t2)) > 0 ? t2 : t1;
}

public static T ArgMin<T, R>(this IEnumerable<T> Sequence, Func<T, R> f)
                where R : IComparable<R>
{
    return Sequence.Aggregate((t1, t2) => ArgMin<T, R>(t1, t2, f));
}

var iNow = DateTime.Now;
var iResult = times.ArgMin(iTime => Math.Abs((iTime - iNow).Ticks));

Although very generic, this implementation does not involve any sorting.

Upvotes: 0

Related Questions