【问题标题】:Clean way to reduce many TimeSpans into fewer, average TimeSpans?将许多 TimeSpan 减少为更少的平均 TimeSpan 的干净方法?
【发布时间】:2009-03-17 17:46:36
【问题描述】:

我有一个包含 500 个元素的 C# Queue

我需要通过将 10 个 TimeSpan 分组并选择它们的平均值,将它们减少为 50 个元素。

有没有一种干净的方法可以做到这一点?我认为 LINQ 会有所帮助,但我想不出一个干净的方法。有什么想法吗?

【问题讨论】:

  • 在我的回答中查看新的代码示例。
  • 有趣的是,有时候给猫剥皮有多少种方法!

标签: c# linq timespan average


【解决方案1】:

我会使用 Chunk 函数和循环。

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

Chunk 是我的标准助手库的一部分。 http://clrextensions.codeplex.com/

Source for Chunk

【讨论】:

  • 我喜欢它,但不喜欢依赖第三方库。我想我可以按原样使用你的 Chunk 函数......
  • @Grauenwolf 你应该警告我关于 VB 的事!我不是 C# 偏执狂,但我的眼睛花了几秒钟才适应所有多余的单词。 :P
  • Grauenwolf,我已将您的代码用于我自己的目的并对其进行了调整,结果证明它非常干净。我将您的答案标记为正确答案,我将添加一个显示我的代码的新答案。
【解决方案2】:

看看.Skip() and .Take() 扩展方法将您的队列划分为集合。然后,您可以使用 .Average(t => t.Ticks) 来获取代表平均值的新 TimeSpan。只需将这 50 个平均值中的每一个都塞入一个新队列,就可以开始了。

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));
}

我是一名 VB.NET 人员,因此在该示例中可能有一些语法不是 100% 编写的。告诉我,我会解决的!

【讨论】:

  • 是的,我认为 Skip and Take 可能会有所帮助。但我认为它仍然需要 10 个不同的 LINQ 查询,对吧?
  • 如果你是 10 人一组,它将需要 50 次 LINQ 查询。
  • 是的。这对于 50 个项目不是问题,但对于较长的列表,您不想这样做。
【解决方案3】:

在这种情况下,在方法调用中可能没有什么能比旧的程序执行更好的了。不花哨,但是很简单,Jr.级别的开发人员都可以维护。

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;
}

【讨论】:

  • 我拒绝恢复到旧的程序代码!好吧,开个玩笑,但是有更清洁的方法吗?这里的程序代码实在是太丑了。
  • 这并不令人兴奋(性感),但我不确定丑陋。在某些情况下,它更具可读性。我有兴趣了解创造性思维如何解决这个问题(我喜欢了解 LINQ 可以解决的所有问题),但在一个技能水平不一的团队中工作时,坚持无聊通常是很实际的。
  • 与下面一些基于 LINQ 的答案相比,我会说程序又大又丑。
  • 它可能又大又丑,但并不慢。给出的大多数 LINQ 答案都具有非常糟糕的性能特征(不过对于 500 个元素来说没什么大不了的。)只是一个想法。
  • 肯定很大,而且不够简洁。美在旁观者的眼中。我当然不觉得它很有吸引力(我特别喜欢@Grauenwolf 的回答)。如果谁继承你的代码不是问题,那么一定要性感! :)
【解决方案4】:

你可以使用

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;
}

它有点冗长(它不使用 LINQ),但很容易看到它在做什么......(你可以很容易地从一个数组中创建一个队列)

【讨论】:

  • 尽管是程序性的,但它相当干净。还不错。
【解决方案5】:

我会使用循环,但只是为了好玩:

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));
        }
    }
}

这只检查每个元素一次,因此性能比其他 LINQ 产品好很多。不幸的是,它改变了队列,但也许这是一个特性而不是一个错误?

它确实具有作为迭代器的好处,所以它一次只给你一个平均值。

【讨论】:

  • 有趣!肯定比程序好。有点啰嗦,但还不错。非常有趣。
【解决方案6】:

将其与整数 (0..n) 一起压缩并按序列号 div 10 分组?

我不是 linq 用户,但我相信它看起来像这样:

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

take(10) 解决方案可能更好。

【讨论】:

  • 我不明白您所说的“按序列号 div 10 分组”是什么意思。你能解释一下吗?
【解决方案7】:

分组将如何进行?

假设一些非常简单的事情(一次取 10 个),您可以从以下内容开始:

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);
}

注意事项:

  • Select 的重载来获取索引,然后使用这个和整数除法拾取 10 组。可以使用模数将每 10 个元素放入一个组,每 10 个 + 1 个放入另一个组,...
  • 分组的结果是具有 Key 属性的枚举序列。但这里只需要那些单独的序列。
  • IEnumerable&lt;TimeSpan&gt; 没有 Enumerable.Average 重载,因此请使用 Ticks(长整数)。

编辑:以 10 人为一组来更好地解决问题。
EDIT2:现在使用经过测试的代码。

【讨论】:

  • 哦,这很漂亮。不幸的是,它没有编译。对其进行调整以编译仅返回 10 个结果,而不是 50 个。如果您可以对其进行更新以使其编译并正常工作,我会将您的结果标记为答案,除非有更好的答案。
  • 另外,重新设计以使用除法分组而不是模数,这更接近您的要求。 (Mod 10 总是会导致 10 个结果。)
  • 酷。我很想将您的答案标记为答案,但我最终选择了 Grauenwolf 的基于 LINQ 的方法,该方法利用了扩展方法。虽然需要“块”扩展方法,但最终生成的代码更干净。受他的回答启发,我将使用我使用的实际代码发布答案。
  • @Judah:我明白...... GroupBy 不是很清楚,除非你能回忆起它的细节。理解语法很清楚,但是没有提供索引的选择。
猜你喜欢
  • 2011-06-23
  • 1970-01-01
  • 2012-02-09
  • 1970-01-01
  • 2014-10-17
  • 2012-09-10
相关资源
最近更新 更多