【问题标题】:How do I find average difference between a sequence of timestamps in C# using LINQ?如何使用 LINQ 在 C# 中找到一系列时间戳之间的平均差异?
【发布时间】:2022-10-23 11:59:33
【问题描述】:

我有一个无序的时间戳序列。我需要能够计算分钟,最大限度平均每个后续时间戳之间的差异。例如给定:

DateTimeOffset now = new DateTimeOffset(new DateTime(2022, 1, 1, 0, 0, 0, 0));
DateTimeOffset[] timestamps = new[] {
    now,
    now.AddSeconds(5),
    now.AddSeconds(10),
    now.AddSeconds(15),
    now.AddSeconds(30),
    now.AddSeconds(31)
};
    
IEnumerable<DateTimeOffset> timestampsSorted = timestamps.OrderByDescending(x => x);

应该产生:

2022-01-01 00:00:31->2022-01-01 00:00:30 | 00:00:01
2022-01-01 00:00:30->2022-01-01 00:00:15 | 00:00:15
2022-01-01 00:00:15->2022-01-01 00:00:10 | 00:00:05
2022-01-01 00:00:10->2022-01-01 00:00:05 | 00:00:05
2022-01-01 00:00:05->2022-01-01 00:00:00 | 00:00:05

Min 00:00:01
Max 00:00:15
Avg 00:00:06.2000000

我想出的程序解决方案如下,如果我可以使用 LINQ 简化它,那就太好了。

TimeSpan min = TimeSpan.MaxValue;
TimeSpan max = TimeSpan.MinValue;
List<TimeSpan> deltas = new();

for (int i = timestampsSorted.Length - 1; i > 0; i--)
{
    DateTimeOffset later = timestamps[i];
    DateTimeOffset prev = timestamps[i - 1];

    TimeSpan delta = later - prev;
    
    if (delta > max) { max = delta; }
    if (delta < min) { min = delta; }

    deltas.Add(delta);
    Console.WriteLine($"{later:yyyy-MM-dd HH:mm:ss}->{prev:yyyy-MM-dd HH:mm:ss} | {delta}");
}

var result = new { 
    Min = min,
    Max = max,
    Avg = TimeSpan.FromMilliseconds(deltas.Average(d => d.TotalMilliseconds))
};

【问题讨论】:

  • 您是否知道deltas.Average() 会抛出异常,而timestamps.Length01minmax 将保持不变?此外,i 应该初始化为timestamps.Length - 1,因为timestampsSorted 是一个IEnumerable&lt;DateTimeOffset&gt;,它没有Length 属性。

标签: c# .net linq


【解决方案1】:

使用LINQ 的内置MinMaxAverage 函数。

var timestampsSorted = timestamps.OrderByDescending(o => o).ToArray();
var data = timestampsSorted
    .Skip(1)
    .Select((o, i) => timestampsSorted[i] - o)
    .ToArray();
var min = data.Min();
var max = data.Max();
var avg = TimeSpan.FromSeconds(data.Average(o => o.TotalSeconds));

请注意,对这些 MinMaxAverage 函数的单独调用会导致对 data 数组中的项目进行 3 次迭代。

【讨论】:

  • 这在功能上是相同的,但如前所述,执行不同;您不妨坚持使用for 循环。此外,如果timestamps 包含少于 2 个元素,Min()Max()Average() 将各自抛出异常。
  • @LanceU.Matthews 关于循环计数是正确的,因此我的笔记。但是,如果timestamps 的元素少于 2 个,则无需进行任何计算,因为每个时间戳都需要与其后续时间戳进行比较;所以不会抛出异常。
  • 但是会抛出异常,因为此代码不处理这种情况,而在原始代码中不会进入循环。你和我可能知道没有必要计算 0 或 1 个元素的最小/最大/平均值,但这段代码不需要。
  • @LanceU.Matthews 我不反对。但是如果没有时间戳,问题中的代码不会以同样的方式在deltas.Average 上崩溃吗? minmax 设置为 TimeSpan.MaxValueTimeSpan.MinValue 也不正确。我倾向于说答案不必提供完整的程序代码,而只是 OP 要求的部分,即匹配的LINQ 设置。 +6k OP 将能够很好地处理其余部分。
  • 谢谢@pfx。是的,为简洁起见,我在这里跳过了一些内容。我设法利用。总计的一次计算所有三个,但感谢您的回答。
【解决方案2】:

您不需要将所有delta 值存储在List&lt;TimeSpan&gt; 中以调用Average();只保留一个运行总和然后将其除以比较的对数 (timestamps.Length - 1) 会更有效。所以这...

// ...
List<TimeSpan> deltas = new();

for (int i = timestamps.Length - 1; i > 0; i--)
{
    // ...
    deltas.Add(delta);
    // ...
}

var result = new {
    // ...
    Avg = TimeSpan.FromMilliseconds(deltas.Average(d => d.TotalMilliseconds))
};

……会改成……

// ...
TimeSpan sum = TimeSpan.Zero;

for (int i = timestamps.Length - 1; i > 0; i--)
{
    // ...
    sum += delta;
    // ...
}

var result = new { 
    // ...
    //TODO: Avoid division for sequences with less than 2 elements, if expected
    Avg = TimeSpan.FromMilliseconds(sum.TotalMilliseconds / (timestamps.Length - 1))
};

Aggregate() 用于在序列过程中累积一个或多个值。这是一种使用Aggregate() 计算与for 循环相同的值的方法...

static (TimeSpan? Minimum, TimeSpan? Maximum, TimeSpan? Average, int Count) GetDeltaStatistics(IEnumerable<DateTimeOffset> timestamps)
{
    var seed = (
        Previous: (DateTimeOffset?) null,
        Minimum: (TimeSpan?) null,
        Maximum: (TimeSpan?) null,
        Sum: TimeSpan.Zero,
        Count: 0
    );

    return timestamps.Aggregate(
        seed,
        (accumulator, current) => {
            if (accumulator.Previous != null)
            {
                TimeSpan delta = current - accumulator.Previous.Value;

                if (++accumulator.Count > 1)
                {
                    // This is not the first comparison; Minimum and Maximum are non-null
                    accumulator.Minimum = delta < accumulator.Minimum.Value ? delta : accumulator.Minimum.Value;
                    accumulator.Maximum = delta > accumulator.Maximum.Value ? delta : accumulator.Maximum.Value;
                }
                else
                {
                    // No prior comparisons have been performed
                    // Minimum and Maximum must be null so unconditionally overwrite them
                    accumulator.Minimum = accumulator.Maximum = delta;
                }
                accumulator.Sum += delta;

                Console.WriteLine($"{current:yyyy-MM-dd HH:mm:ss}->{accumulator.Previous:yyyy-MM-dd HH:mm:ss} | {delta}");
            }
            accumulator.Previous = current;

            return accumulator;
        },
        accumulator => (
            accumulator.Minimum,
            accumulator.Maximum,
            Average: accumulator.Count > 0
                ? new TimeSpan(accumulator.Sum.Ticks / accumulator.Count)
                : (TimeSpan?) null,
            accumulator.Count
        )
    );
}

Aggregate() 重载的第二个参数是 Func&lt;&gt;,它传递序列中的当前元素 (current) 和从上一次调用 Func&lt;&gt; (accumulator) 返回的状态。第一个参数提供accumulator 的初始值。第三个参数是一个Func&lt;&gt;,它将这个状态的最终值转换为Aggregate()的返回值。状态和返回值都是value tuples

注意GetDeltaStatistics() 只需要IEnumerable&lt;DateTimeOffset&gt; 而不是IList&lt;DateTimeOffset&gt;DateTimeOffset[];但是,由于没有对相邻元素的随机访问,current 的值通过accumulator.Previous 传递到下一次调用。我还让调用者负责提供排序的输入,但您也可以在方法内轻松地执行此操作。

打电话给GetDeltaStatistics()...

static void Main()
{
    DateTimeOffset now = new DateTimeOffset(new DateTime(2022, 1, 1, 0, 0, 0, 0));
    DateTimeOffset[] timestamps = new[] {
        now,
        now.AddSeconds(5),
        now.AddSeconds(10),
        now.AddSeconds(15),
        now.AddSeconds(30),
        now.AddSeconds(31)
    };

    IEnumerable<IEnumerable<DateTimeOffset>> timestampSequences = new IEnumerable<DateTimeOffset>[] {
        timestamps,
        timestamps.Take(2),
        timestamps.Take(1),
        timestamps.Take(0)
    };
    foreach (IEnumerable<DateTimeOffset> sequence in timestampSequences)
    {
        var (minimum, maximum, average, count) = GetDeltaStatistics(sequence.OrderBy(offset => offset));

        Console.WriteLine($"Minimum: {GetDisplayText(minimum)}");
        Console.WriteLine($"Maximum: {GetDisplayText(maximum)}");
        Console.WriteLine($"Average: {GetDisplayText(average)}");
        Console.WriteLine($"  Count: {count}");
        Console.WriteLine();
    }
}

static string GetDisplayText(TimeSpan? delta) => delta == null ? "(null)" : delta.Value.ToString();

...产生这个输出...

2022-01-01 00:00:05->2022-01-01 00:00:00 | 00:00:05
2022-01-01 00:00:10->2022-01-01 00:00:05 | 00:00:05
2022-01-01 00:00:15->2022-01-01 00:00:10 | 00:00:05
2022-01-01 00:00:30->2022-01-01 00:00:15 | 00:00:15
2022-01-01 00:00:31->2022-01-01 00:00:30 | 00:00:01
最小值:00:00:01
最大值:00:00:15
平均:00:00:06.2000000
  计数:5

2022-01-01 00:00:05->2022-01-01 00:00:00 | 00:00:05
最小值:00:00:05
最大值:00:00:05
平均:00:00:05
  计数:1

最小值:(空)
最大值:(空)
平均:(空)
  计数:0

最小值:(空)
最大值:(空)
平均:(空)
  计数:0

虽然原始代码会导致抛出异常,但对于少于两个元素的序列,结果的 Count0,其他字段为 null

【讨论】:

    猜你喜欢
    • 2021-09-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多