Reputation: 4889
Given an IEnumerable<DateTime>
, is there a way to calculate the average of the difference between subsequent elements of the DateTime
objects using LINQ?
EDIT: I realized my original wording was a bit unclear. Just to clarify, I'm looking to average the difference between elements 0 and 1, 1 and 2, 2 and 3, etc.
double GetAvgDiffInMilliseconds(IEnumerable<DateTime> items)
=> items.Aggregate(/* ??? */).Average(); // is this possible with LINQ?
Note: I'm not interested in non-LINQ solutions, as this problem is trivially solved with a loop and an accumulator. I'd like to understand if this is possible with LINQ, for learning purposes.
Upvotes: 1
Views: 615
Reputation: 131591
LINQ doesn't have such an operator yet but LINQ-to-Objects operations use iterators, enumerators and IEnumerable anyway. What you ask is an operator that uses the current and previous item, like MoreLINQ's Pairwise operator. Such an operation would need only a single iteration to process pairs and produce output. Anything else will be a lot more expensive.
Using Pairwise
you can write :
var avg=items.Pairwise((a, b) => (a - b))
.Average(ts=>ts.TotalMilliseconds);
Pairwise
's code is simple and doesn't require multiple iterations :
public static IEnumerable<TResult> Pairwise<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TSource, TResult> resultSelector)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector));
return _(); IEnumerable<TResult> _()
{
using var e = source.GetEnumerator();
if (!e.MoveNext())
yield break;
var previous = e.Current;
while (e.MoveNext())
{
yield return resultSelector(previous, e.Current);
previous = e.Current;
}
}
}
A Pairwise
method may appear in LINQ at some point. It's a very common operation in functional languages, and some MoreLINQ
operators were added in .NET Core 6 Preview 4, like DistinctBy
, MaxBy
and more.
Using Aggregate
It's possible to use Aggregate
to calculate an Average
but it's very ugly and wasteful.
If you had a list of numbers you'd need to carry the item count and sum of items in the accumulator, and calculate the average in the end :
var nums=new[]{1.0,2.0,3.0};
var avg=nums.Aggregate(
(cnt:0.0,sum:0.0),
(acc,c)=>(cnt:acc.cnt+1,sum:acc.sum+c),
acc=>acc.sum/acc.cnt);
Console.WriteLine(avg);
In this case though you want to calculate the difference between the current item and the previous. This means you need to carry the previous value, the sum of differences, and add the difference between the current and previous value.
You also need to handle the first value, when there's no previous value. And since you calculate differences, the actual count is one less than the collection's count. Finally, if there are only two items you can't divide :
var nums=new[]{DateTime.Now,DateTime.Now.AddMinutes(1),DateTime.Now.AddMinutes(2)};
var avg=nums.Aggregate(
(cnt:0.0,sum:0.0,prev:DateTime.MinValue),
(acc,c)=>( cnt:acc.cnt+1,
sum:(acc.prev==DateTime.MinValue)
?0
:acc.sum+(c-acc.prev).TotalMilliseconds,
prev:c),
acc=>acc.cnt==1
?acc.sum
:acc.sum/(acc.cnt-1));
Console.WriteLine(avg);
This prints 60002.95775
. It also took a lot of trial and error and NaN's to get it to work.
Upvotes: 1
Reputation: 142923
You can use Zip
if there is more than one element in collection:
items.Zip(items.Skip(1))
.Select(tuple => (tuple.Second - tuple.First).TotalMilliseconds)
.Average();
Upvotes: 3