views:

483

answers:

7

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?

+1  A: 

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;
}
Michael Meadows
I refuse to revert back to old procedural code! Well, just kidding, but is there a cleaner way? Procedural code here is so damn ugly.
Judah Himango
It's not exciting (sexy), but I'm not sure about ugly. In some cases it's more readable. I'm interested in seeing how the creative minds solve this problem (I like to see all the problems LINQ can solve), but it's often practical to stick with boring when working in a team of mixed skill levels.
Michael Meadows
Compared with some of the LINQ-based answers below, I'd say the procedural is big and ugly.
Judah Himango
It may be big and ugly, but it's not slow. Most of the LINQ answers given have really bad performance characteristics (no big deal for 500 elements though.) Just a thought.
mquander
It's definitely big, and not concise. Beauty is in the eye of the beholder. I certainly don't find it as attractive (I especially like @Grauenwolf's answer). If who inherits your code isn't an issue, definitely go with sexy! :)
Michael Meadows
+2  A: 

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!

Bob King
Yes, I figured Skip and Take might help here. But I think it would still require like 10 different LINQ queries, right?
Judah Himango
It will take 50 LINQ queries if you're doing groups of 10.
Jim Mischel
Yep. This isn't an issue for 50 items, but for longer lists you don't want to do that.
Jonathan Allen
That's pretty good, pretty clean. I've voted you up.
Judah Himango
A: 

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.

MarkusQ
I don't understand what you mean by "grouping by the sequence number div 10". Can you explain?
Judah Himango
Or better yet, show some [pseudo]code?
Judah Himango
That seems like a lot of work.
Jonathan Allen
+3  A: 

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

Jonathan Allen
I like it, but don't like having a dependency on 3rd party library. I guess I could take your Chunk function as-is...
Judah Himango
Of course, I'll add the link.
Jonathan Allen
@Grauenwolf You should have warned me about the VB! I'm not a C# bigot, but it took a few seconds for my eyes to adjust to all of the extra words. :P
Michael Meadows
Grauenwolf, I've took your code and tweaked it for my own purposes, and it turned out to be pretty clean. I'm marking yours as the correct answer, and I'll add a new answer showing my code.
Judah Himango
+1  A: 

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.

Richard
Oh, this is beautiful. Unfortuantely, it doesn't compile. Tweaking it to compile returns only 10 results, rather than 50. If you can update it so it compiles and works right, I'll mark yours as the answer, barring any better answers.
Judah Himango
Also, reworked to use division to group rather than modulus, which is closer to what you are asking for. (Mod 10 would lead to 10 results always.)
Richard
Cool. I'm tempted to mark yours as the answer, but I ended up going with Grauenwolf's LINQ-based method that utilized an extension method. While requiring a "Chunk" extension method, the end resulting code was cleaner. I'll post an answer with the actual code I used, inspired from his answer.
Judah Himango
@Judah: I understand.... the GroupBy isn't really clear unless you can recall its details. It is clear with comprehension syntax, but then there isn't a select that provides the index.
Richard
+1  A: 

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.

mquander
Interesting! Better than the procedural for sure. A little wordy, but not bad. Very interesting.
Judah Himango
+1  A: 

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)

Daniel LeCheminant
Despite being procedural, it's fairly clean. Not bad.
Judah Himango