22

If I have an array of DateTime values:

List<DateTime> arrayDateTimes;

What's the way to find the average DateTime among them?

For instance, if I have:

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

the average should be:

2003-May-21 18:00:00
Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
c00000fd
  • 20,994
  • 29
  • 177
  • 400
  • 2
    +1 nice question. See this http://42zone.blogspot.com/2011/09/c-how-to-calculate-multiple-datetime.html, just tested it and works with over 38,000 dates. – Habib May 22 '13 at 04:56
  • 1
    note that some answers preserve timezone information and others do not .. – Cel Oct 03 '14 at 07:58

8 Answers8

16

If you have large list you can use below method

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);
Damith
  • 62,401
  • 13
  • 102
  • 153
  • 9
    It will throw overflow exception with large list. – x2. May 22 '13 at 04:28
  • **dates[i].Ticks / count** will return 0 if count > Ticks – Uzzy May 22 '13 at 08:19
  • Console.WriteLine(ticks / (ticks + 1)); Console.WriteLine(ticks / long.MaxValue); What would be printed ? – Uzzy May 22 '13 at 08:26
  • @Uzzy: Ticks are measured as the number of 100-nanosecond intervals that elapsed since Jan 1, 1601. I don't know the word for such number but this is how it may look `635,047,830,427,420,548` so I don't think `count` will be greater than `Ticks`. – c00000fd May 22 '13 at 08:32
  • @Damith: `System.Int64` **is** an integer. – c00000fd May 22 '13 at 08:33
9

This shouldn't overflow, it does assume the datetimes are ordered though:

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

The above does in fact overflow with larger lists and larger gaps. I think you could use seconds for better range. (again, sorted first) Also, this might not be the most performant method, but still completed with 10M dates relatively quickly for me. Not sure if it's easier to read or not, YYMV.

var first = dates.First();
var average = first.AddSeconds(dates.Average(d => (d - first).TotalSeconds));
neouser99
  • 1,807
  • 1
  • 10
  • 23
  • I'm not sure I follow. Ticks is of type long. A future tick, minus a past tick, will give a relatively small number, and shouldn't ever have the possibility of overflowing. – neouser99 May 22 '13 at 04:43
  • @c00000fd no problem, glad to help with a solution. – neouser99 May 22 '13 at 04:51
  • This would again result in overflowing, with large list of dates, test it with `4704` dates in the list. – Habib May 22 '13 at 05:18
  • 1
    Tested and this gives `Arithmetic operation resulted in an overflow.` – Damith May 22 '13 at 05:19
  • 1
    It does indeed overflow with fairly large lists, which have fairly large gaps. I have updated with an answer using seconds. – neouser99 May 22 '13 at 06:16
1

Source: Taken from Here and modified a bit.

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

Then you can do:

var averageDateTime = DateTime
                            .MinValue
                            .AddSeconds
                            ((dates
                                 .Sum(r => (r - DateTime.MinValue).TotalSeconds))
                                     / dates.Count);
Console.WriteLine(averageDateTime.ToString("yyyy-MMM-dd HH:mm:ss"));

Output in: 1956-Dec-29 06:09:25

Originally the code from the article was like:

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);
Habib
  • 219,104
  • 29
  • 407
  • 436
1

The code:

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

is Wrong. Average=(x1 + x2 + ... xN) / N not (x1/N + x2/N + ... xN/N)

Try:

var avg=new DateTime((long)dates.Select(d => d.Ticks).Average());
  • This can cause arithmetic overflow if averaging over a large number as the sum (before dividing by N) can exceed long.MaxValue. The alternative method is more robust. – Brendan Hill Apr 09 '19 at 00:54
  • 1
    Actually `(x1 + x2 + ... xN) / N` and `(x1/N + x2/N + ... xN/N)` is equal. It's just a different way of writing. (but as @BrendanHill wrote the second method is more robust) – Mathias Apr 12 '19 at 08:46
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());
    }
}
Leniel Maccaferri
  • 100,159
  • 46
  • 371
  • 480
0

Using double seconds instead of long ticks will avoid overflow on any real-world inputs - extension method here.

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

If you really want to get degenerate, you could detect the overflow and recurse on half the elements, being careful with the odd-N case. This is untested but here's the idea:

    //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);
        }
        
    }
solublefish
  • 1,681
  • 2
  • 18
  • 24
0

All of these solutions involving long were causing an overflow.

I propose:

public static DateTime DateTimeAverage(IEnumerable<DateTime> dates) => DateTime.FromOADate(dates.Select(d => d.ToOADate()).Average());
Jawid Hassim
  • 357
  • 3
  • 9
-1

The answer by neouser99 is correct. It prevents overflow by doing an incremental average.

However, this answer by David Jiménez is wrong because it doesn't handle overflow and his misunderstanding on the formula.

Average=(x1 + x2 + ... xN) / N not (x1/N + x2/N + ... xN/N)

These are the same formula. It's simple math using the distributive property:

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

The average formula is the same as multiplying your sum by 1/N. Or multiplying each individual X by 1/N and adding them together.

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

which by the distributive property becomes:

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

Here's some info on the distributive property

His answer is also bad because it doesn't prevent overflow like the accepted answer.

I would have commented on his reply, but I don't have enough reputation.

swiftest
  • 690
  • 5
  • 7
  • This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/low-quality-posts/21249171) – lucascaro Oct 27 '18 at 02:07
  • I added an edit: I clarified why the accepted answer is correct which doesn't explain anything (because it prevents overflow) and why another answer with a positive score is wrong. So I believe this does answer the original question. – swiftest Oct 29 '18 at 15:21