Judah Gabriel Himango
Judah Gabriel Himango

Reputation: 60041

Clean way to reduce many TimeSpans into fewer, average TimeSpans?

I have a C# Queue<TimeSpan> containing 500 elements.

I need to reduce those into 50 elements by taking groups of 10 TimeSpans and selecting their average.

Is there a clean way to do this? I'm thinking LINQ will help, but I can't figure out a clean way. Any ideas?

Upvotes: 4

Views: 1603

Answers (7)

Richard
Richard

Reputation: 109100

How is the grouping going to be performed?

Assuming something very simple (take 10 at a time ), you can start with something like:

List<TimeSpan> input = Enumerable.Range(0, 500)
                                 .Select(i => new TimeSpan(0, 0, i))
                                  .ToList();

var res = input.Select((t, i) => new { time=t.Ticks, index=i })
               .GroupBy(v => v.index / 10, v => v.time)
               .Select(g => new TimeSpan((long)g.Average()));

int n = 0;
foreach (var t in res) {
    Console.WriteLine("{0,3}: {1}", ++n, t);
}

Notes:

  • Overload of Select to get the index, then use this and integer division pick up groups of 10. Could use modulus to take every 10th element into one group, every 10th+1 into another, ...
  • The result of the grouping is a sequence of enumerations with a Key property. But just need those separate sequences here.
  • There is no Enumerable.Average overload for IEnumerable<TimeSpan> so use Ticks (a long).

EDIT: Take groups of 10 to fit better with question.
EDIT2: Now with tested code.

Upvotes: 1

Jonathan Allen
Jonathan Allen

Reputation: 70327

I would use the Chunk function and a loop.

foreach(var set in source.ToList().Chunk(10)){
    target.Enqueue(TimeSpan.FromMilliseconds(
                            set.Average(t => t.TotalMilliseconds)));
}

Chunk is part of my standard helper library. http://clrextensions.codeplex.com/

Source for Chunk

Upvotes: 3

MarkusQ
MarkusQ

Reputation: 21950

Zipping it together with the integers (0..n) and grouping by the sequence number div 10?

I'm not a linq user, but I believe it would look something like this:

for (n,item) from Enumerable.Range(0, queue.length).zip(queue) group by n/10

The take(10) solution is probably better.

Upvotes: 1

mqp
mqp

Reputation: 71995

I'd use a loop, but just for fun:

IEnumerable<TimeSpan> AverageClumps(Queue<TimeSpan> lots, int clumpSize)
{
    while (lots.Any())
    {
        var portion = Math.Min(clumpSize, lots.Count);
        yield return Enumerable.Range(1, portion).Aggregate(TimeSpan.Zero,
            (t, x) => t.Add(lots.Dequeue()),
            (t) => new TimeSpan(t.Ticks / portion));
        }
    }
}

That only examines each element once, so the performance is a lot better than the other LINQ offerings. Unfortunately, it mutates the queue, but maybe it's a feature and not a bug?

It does have the nice bonus of being an iterator, so it gives you the averages one at a time.

Upvotes: 1

Daniel LeCheminant
Daniel LeCheminant

Reputation: 51091

You could just use

static public TimeSpan[] Reduce(TimeSpan[] spans, int blockLength)
{
    TimeSpan[] avgSpan = new TimeSpan[original.Count / blockLength];

    int currentIndex = 0;

    for (int outputIndex = 0;
         outputIndex < avgSpan.Length; 
         outputIndex++)
    {
        long totalTicks = 0;

        for (int sampleIndex = 0; sampleIndex < blockLength; sampleIndex++)
        {
            totalTicks += spans[currentIndex].Ticks;
            currentIndex++;
        }

        avgSpan[outputIndex] =
            TimeSpan.FromTicks(totalTicks / blockLength);
    }

    return avgSpan;
}

It's a little more verbose (it doesn't use LINQ), but it's pretty easy to see what it's doing... (you can a Queue to/from an array pretty easily)

Upvotes: 1

Michael Meadows
Michael Meadows

Reputation: 28426

Probably nothing beats a good old procedural execution in a method call in this case. It's not fancy, but it's easy, and it can be maintained by Jr. level devs.

public static Queue<TimeSpan> CompressTimeSpan(Queue<TimeSpan> original, int interval)
{
    Queue<TimeSpan> newQueue = new Queue<TimeSpan>();
    if (original.Count == 0) return newQueue;

    int current = 0;
    TimeSpan runningTotal = TimeSpan.Zero;
    TimeSpan currentTimeSpan = original.Dequeue();

    while (original.Count > 0 && current < interval)
    {
        runningTotal += currentTimeSpan;
        if (++current >= interval)
        {
            newQueue.Enqueue(TimeSpan.FromTicks(runningTotal.Ticks / interval));
            runningTotal = TimeSpan.Zero;
            current = 0;
        }
        currentTimeSpan = original.Dequeue();
    }
    if (current > 0)
        newQueue.Enqueue(TimeSpan.FromTicks(runningTotal.Ticks / current));

    return newQueue;
}

Upvotes: 1

Bob King
Bob King

Reputation: 25866

Take a look at the .Skip() and .Take() extension methods to partition your queue into sets. You can then use .Average(t => t.Ticks) to get the new TimeSpan that represents the average. Just jam each of those 50 averages into a new Queue and you are good to go.

Queue<TimeSpan> allTimeSpans = GetQueueOfTimeSpans();
Queue<TimeSpan> averages = New Queue<TimeSpan>(50);
int partitionSize = 10;
for (int i = 0; i <50; i++) {
    var avg = allTimeSpans.Skip(i * partitionSize).Take(partitionSize).Average(t => t.Ticks)
    averages.Enqueue(new TimeSpan(avg));
}

I'm a VB.NET guy, so there may be some syntax that isn't 100% write in that example. Let me know and I'll fix it!

Upvotes: 2

Related Questions