如何在DateTime值数组中找到平均日期/时间

22

如果我有一个包含 DateTime 值的数组:

List<DateTime> arrayDateTimes;

如何找到它们之间的平均DateTime?

例如,如果我有:

2003-May-21 15:00:00
2003-May-21 19:00:00
2003-May-21 20:00:00

平均值应为:

2003-May-21 18:00:00

2
+1 不错的问题。请查看此链接http://42zone.blogspot.com/2011/09/c-how-to-calculate-multiple-datetime.html,我刚测试了一下,可以处理超过38,000个日期。 - Habib
1
请注意,某些答案保留了时区信息,而其他答案则没有。 - Cel
8个回答

16

如果您有大型列表,可以使用以下方法

var count = dates.Count;
double temp = 0D;
for (int i = 0; i < count; i++)
{
    temp += dates[i].Ticks / (double)count;
}
var average = new DateTime((long)temp);

9
对于大型列表,它将抛出溢出异常。 - x2.
如果count > Ticks,那么dates[i].Ticks / count将返回0。 - Uzzy
Console.WriteLine(ticks / (ticks + 1)); Console.WriteLine(ticks / long.MaxValue);会打印什么? - Uzzy
@Uzzy:Ticks是以自1601年1月1日起经过的100纳秒间隔数来衡量的。我不知道这种数字的称呼,但它可能看起来像这样“635,047,830,427,420,548”,因此我认为“count”不会比“Ticks”更大。 - c00000fd
@Damith:System.Int64 是一个整数。 - c00000fd

9

这个不应该溢出,但是它假设日期时间是有序的:

var first = dates.First().Ticks;
var average = new DateTime(first + (long) dates.Average(d => d.Ticks - first));

实际上,以上内容在处理更大的列表和间隔时可能会溢出。我认为您可以使用秒来获得更好的范围(再次排序)。此外,这可能不是最高效的方法,但对于我来说仍然可以快速地完成1000万个日期。不确定它是否更易于阅读, YYMV。

var first = dates.First();
var average = first.AddSeconds(dates.Average(d => (d - first).TotalSeconds));

我不确定我理解了。Ticks的类型是long。未来的tick减去过去的tick将会得到一个相对较小的数字,并且不应该有溢出的可能性。 - neouser99
@c00000fd 没问题,很高兴能帮助你找到解决方案。 - neouser99
这将再次导致溢出,如果列表中有大量日期,请使用4704个日期进行测试。 - Habib
1
已测试,结果为“算术运算导致溢出”。 - Damith
1
它确实会在相当大的列表中溢出,这些列表有相当大的间隔。我已经更新了一个使用秒的答案。 - neouser99

1

代码:

var count = dates.Count;
double temp = 0D;
for (int i = 0; i < count; i++)
{
    temp += dates[i].Ticks / (double)count;
}
var average = new DateTime((long)temp);

这是错误的。平均值应该是 (x1 + x2 + ... + xN) / N,而不是 (x1/N + x2/N + ... + xN/N)。

尝试:

var avg=new DateTime((long)dates.Select(d => d.Ticks).Average());

如果对大量数据进行平均值计算,那么在除以N之前的总和可能会超过long.MaxValue,从而导致算术溢出。另一种方法更加健壮。 - Brendan Hill
1
实际上,(x1 + x2 + ... xN) / N(x1/N + x2/N + ... xN/N) 是相等的。只是不同的写法而已。(但正如@BrendanHill所写,第二种方法更加健壮) - Mathias

1

来源:取自这里并稍作修改。

List<DateTime> dates = new List<DateTime>();
//Add dates
for (int i = 1; i <= 28; i++) //days
    for (int j = 1; j <= 12; j++) //month
        for (int k = 1900; k <= 2013; k++) //year
            dates.Add(new DateTime(k, j, i, 1, 2, 3)); //over 38000 dates

然后你可以这样做:
var averageDateTime = DateTime
                            .MinValue
                            .AddSeconds
                            ((dates
                                 .Sum(r => (r - DateTime.MinValue).TotalSeconds))
                                     / dates.Count);
Console.WriteLine(averageDateTime.ToString("yyyy-MMM-dd HH:mm:ss"));

输出结果为:1956年12月29日06时09分25秒

原始代码如下:

double totalSec = 0;
for (int i = 0; i < dates.Count; i++)
{
    TimeSpan ts = dates[i].Subtract(DateTime.MinValue);
    totalSec += ts.TotalSeconds;
}
double averageSec = totalSec / dates.Count;
DateTime averageDateTime = DateTime.MinValue.AddSeconds(averageSec);

0
class Program
{
    static void Main(string[] args)
    {
        List<DateTime> dates = new List<DateTime>(){
        new DateTime(2003, 5, 21, 16, 0, 0), new DateTime(2003, 5, 21, 17, 0, 0),
        new DateTime(2003, 5, 21, 18, 0, 0), new DateTime(2003, 5, 21, 19, 0, 0),
        new DateTime(2003, 5, 21, 20, 0, 0), new DateTime(2003, 5, 21, 16, 0, 0),
        new DateTime(2003, 5, 21, 17, 0, 0), new DateTime(2003, 5, 21, 18, 0, 0),
        new DateTime(2003, 5, 21, 19, 0, 0), new DateTime(2003, 5, 21, 20, 0, 0),
        new DateTime(2003, 5, 21, 16, 0, 0), new DateTime(2003, 5, 21, 17, 0, 0),
        new DateTime(2003, 5, 21, 18, 0, 0), new DateTime(2003, 5, 21, 19, 0, 0),
        new DateTime(2003, 5, 21, 20, 0, 0), new DateTime(2003, 5, 21, 16, 0, 0),
        new DateTime(2003, 5, 21, 18, 0, 0), new DateTime(2003, 5, 21, 19, 0, 0),
        new DateTime(2003, 5, 21, 20, 0, 0), new DateTime(2003, 5, 21, 16, 0, 0),
        new DateTime(2003, 5, 21, 18, 0, 0), new DateTime(2003, 5, 21, 19, 0, 0),
        new DateTime(2003, 5, 21, 20, 0, 0), new DateTime(2003, 5, 21, 16, 0, 0),
        new DateTime(2003, 5, 21, 18, 0, 0), new DateTime(2003, 5, 21, 19, 0, 0),
        new DateTime(2003, 5, 21, 20, 0, 0), new DateTime(2003, 5, 21, 16, 0, 0),
        new DateTime(2003, 5, 21, 18, 0, 0), new DateTime(2003, 5, 21, 19, 0, 0),
        new DateTime(2003, 5, 21, 20, 0, 0), new DateTime(2003, 5, 21, 16, 0, 0),
        new DateTime(2003, 5, 21, 18, 0, 0), new DateTime(2003, 5, 21, 19, 0, 0),
        new DateTime(2003, 5, 21, 20, 0, 0), new DateTime(2003, 5, 21, 16, 0, 0),
        new DateTime(2003, 5, 21, 18, 0, 0), new DateTime(2003, 5, 21, 19, 0, 0),
        new DateTime(2003, 5, 21, 20, 0, 0), new DateTime(2003, 5, 21, 16, 0, 0),
    };

        var averageDate = dates.Average();

        Console.WriteLine(averageDate);

        Console.ReadKey();
    }

}

public static class Extensions
{
    public static long Average(this IEnumerable<long> longs)
    {
        long count = longs.Count();

        long mean = 0;

        foreach (var val in longs)
        {
            mean += val / count;
        }

        return mean;
    }

    public static DateTime Average(this IEnumerable<DateTime> dates)
    {
        return new DateTime(dates.Select(x => x.Ticks).Average());
    }
}

我显然不会用3个值来做这件事 :) 你的方法将在大约20个日期时溢出。 - c00000fd
@c00000fd:40个日期时间和一个扩展方法之后,再也没有溢出了。 - Leniel Maccaferri
1
是的,谢谢。虽然@Damith已经提出了这个建议。 - c00000fd

0
使用双倍秒替代长整型刻度,将避免在任何真实世界的输入上发生溢出 - 扩展方法在此。
    public static DateTime Average(this IEnumerable<DateTime> elements)
    {
        if (elements == null)
        {
            throw new ArgumentNullException(nameof(elements));
        }
        var enumerated = elements.ToArray(); //so we don't iterate a potentially one-use stream multiple times.
        if (!enumerated.Any())
        {
            throw new ArgumentException("Average of 0 elements is undefined", nameof(elements));
        }

        var epoch = enumerated.Min();
        var secondsSinceEpoch = enumerated.Select(d => (d - epoch).TotalSeconds).ToArray();
        var n = secondsSinceEpoch.LongLength;
        double totalSecondsSinceEpoch = secondsSinceEpoch.Sum();
        return epoch.AddSeconds(totalSecondsSinceEpoch / n);
    }

    [TestMethod]
    public void HugeDateAverage_DoesntThrow()
    {
        var epoch = new DateTime(1900,1,1);
        try
        {
            var dates = Enumerable.Range(1, 1_000_000_000)
             .Select(i => epoch.AddSeconds(i));
            var result = dates.Average();
        }
        catch (Exception ex)
        {
            Assert.Fail();
        }
    }

如果你真的想要变得卑微,你可以检测溢出并对一半的元素进行递归,注意奇数N的情况。这还没有经过测试,但这是个想法:

    //NOT FOR ACTUAL USE - JUST FOR FUN
    public static DateTime AverageHuge(this IEnumerable<DateTime> elements)
    {
        if (elements == null)
        {
            throw new ArgumentNullException(nameof(elements));
        }
        var enumerated = elements.ToArray(); //so we don't iterate a potentially one-use stream multiple times.
        if (!enumerated.Any())
        {
            throw new ArgumentException("Average of 0 elements is undefined", nameof(elements));
        }

        var epoch = enumerated.Min();
        var secondsSinceEpoch = enumerated.Select(d => (d - epoch).TotalSeconds).ToArray();
        var n = secondsSinceEpoch.LongLength;
        if (n > int.MaxValue)
        {
            //we could actually support more by coding Take+Skip with long arguments.
            throw new NotSupportedException($"only int.MaxValue elements supported");
        }

        try
        {
            double totalSecondsSinceEpoch = secondsSinceEpoch.Sum(); //if this throws, we'll have to break the problem up
            //otherwise we're done.
            return epoch.AddSeconds(totalSecondsSinceEpoch / n);
        }
        catch (OverflowException) { } //fall out of this catch first so we don't throw from a catch block

        //Overengineering to support large lists whose totals would be too big for a double.
        //recursively get the average of each half of values.
        int pivot = (int)n / 2;
        var avgOfAvgs = (new []
        {
            enumerated.Take(pivot).AverageHuge(),
            enumerated.Skip(pivot).Take(pivot).AverageHuge()
        }).AverageHuge();
        if (pivot * 2 == n)
        {   // we had an even number of elements so we're done.
            return avgOfAvgs;
        }
        else
        {   //we had an odd number of elements and omitted the last one.
            //it affects the average by 1/Nth its difference from the average (could be negative)
            var adjust = ((enumerated.Last() - avgOfAvgs).TotalSeconds) / n;
            return avgOfAvgs.AddSeconds(adjust);
        }
        
    }

0

所有与long相关的解决方案都导致了溢出问题。

我建议:

public static DateTime DateTimeAverage(IEnumerable<DateTime> dates) => DateTime.FromOADate(dates.Select(d => d.ToOADate()).Average());

-1

neouser99的回答是正确的。它通过进行递增平均值来防止溢出。

然而,David Jiménez的回答是错误的,因为它没有处理溢出并且对公式的误解。

平均值=(x1 + x2 + ... xN) / N 而不是 (x1/N + x2/N + ... xN/N)

这些是相同的公式。这是使用分配律的简单数学:

2(x + y) = 2x + 2y

平均数的公式与将总和乘以1/N相同。或者将每个X乘以1/N并将它们相加。

1/n (x1 + x2 + ... xn)

根据分配律可得:

x1/n + x2/n + ... xn/n

这是分配率相关信息

他的答案也不好,因为它不能像被接受的答案那样防止溢出。

我本来想评论他的回复,但是我声望不够。


这并没有回答问题。一旦您拥有足够的声望,您将能够评论任何帖子;相反,提供不需要询问者澄清的答案。- 来自审核 - lucascaro
我添加了一个编辑:我澄清了为什么被接受的答案是正确的,这并没有解释任何东西(因为它可以防止溢出),以及为什么另一个得分较高的答案是错误的。因此,我相信这确实回答了原始问题。 - swiftest

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接