Using LINQ to do some calculations on the current and the next object
Asked Answered
R

4

11

Is there an elegant solution to walk through an ordered list to do some calculations on the current and the next object? There must be a smarter way with LINQ to do the following:

public static List<double> GetHoursBetweenDates(List<DateTime> aDates)
{
    List<double> lst = new List<double>();
    var olst = aDates.OrderByDescending(d => d).ToList();
    for (int i = 0; i < olst.Count - 1; i++)
    {
        lst.Add(olst[i].Subtract(olst[i+1]).TotalHours);
    }
    return lst;
}
Retribution answered 29/7, 2013 at 19:24 Comment(1)
Be careful about differencing DateTime objects blindly. The .Kind can affect the results. See this answer and this article.Easterly
M
13

The easiest to compare each consecutive element in a list is something like this:

var sorted = aDates.OrderByDescending(d => d);
var results = 
    sorted.Zip(sorted.Skip(1), (a, b) => a.Subtract(b).TotalHours);

Alternatively, you can do this:

var sorted = aDates.OrderByDescending(d => d).ToArray();
var results = 
    from i in Enumerable.Range(0, sorted.Length - 1)
    select sorted[i].Subtract(sorted[i + 1]).TotalHours;

But this second method will only work List<T>, T[] or any type which supports array-style indexers.

Maduro answered 29/7, 2013 at 19:30 Comment(0)
E
9

As an alternative to a solution using LINQ's Zip enumerator, which requires you to iterate over your list twice, here's a custom LINQ operator that iterates over a sequence and returns a "moving pair" of elements:

static IEnumerable<Tuple<T, T>> Pairwise<T>(this IEnumerable<T> xs)
{
    using (IEnumerator<T> enumerator = xs.GetEnumerator())
    {
        if (!enumerator.MoveNext()) yield break;
        T current = enumerator.Current;
        while (enumerator.MoveNext())
        {
            T previous = current;
            current = enumerator.Current;
            yield return Tuple.Create(previous, current);
        }
    }
}

You could then apply it to your DateTime sequence as follows:

dates.Pairwise().Select(_ => _.Item2.Subtract(_.Item1).TotalHours);
Embrasure answered 29/7, 2013 at 19:59 Comment(0)
M
9

Other option is to use the Aggregate function, and returning as aggregate the current element. This has the additional benefit that you iterate the collection only once:

 public static List<double> GetHoursBetweenDates(List<DateTime> aDates)
 {
     List<double> lst = new List<double>();
     aDates.OrderByDescending(d => d).Aggregate((prev, curr) => { lst.Add(prev.Subtract(curr).TotalHours); return curr; });
     return lst;
 }
Manzano answered 29/7, 2013 at 20:12 Comment(0)
N
5

You can use Incremental extension method from moreLINQ library:

public static List<double> GetHoursBetweenDates(List<DateTime> aDates)
{
    return aDates.OrderByDescending(d => d)
                 .Incremental((p,n) => p.Subtract(n).TotalHours)
                 .ToList();
}

It does exactly what you need:

/// <summary>
/// Computes an incremental value between every adjacent element in a sequence: {N,N+1}, {N+1,N+2}, ...
/// </summary>
/// <remarks>
/// The projection function is passed the previous and next element (in that order) and may use
/// either or both in computing the result.<
/// If the sequence has less than two items, the result is always an empty sequence.
/// The number of items in the resulting sequence is always one less than in the source sequence.
/// </remarks>
Nonproductive answered 29/7, 2013 at 19:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.