BCL Refresher: DateTime.ToUniversalTime returns MaxValue/MinValue on overflow [Josh Free]

BCL Refresher: DateTime.ToUniversalTime returns MaxValue/MinValue on overflow [Josh Free]

Rate This
  • Comments 5

DateTime.ToUniversalTime converts the value of the current DateTime object to Coordinated Universal Time (UTC). When the converted value is either too large or too small to fit into a DateTime object then MaxValue or MinValue is returned, respectively. Returning MaxValue and MinValue for these overflow edge cases is fine for the vast majority of applications that do not care about the edge cases near “23:59:59.9999999, December 31, 9999” and “00:00:00.0000000, January 1, 0001”. However, for applications that do not want to work with values that cannot be round-tripped, this presents a problem. Fortunately, this problem is easily solved with a little bit of extra code.

Let’s start by walking through how to use DateTime.ToUniversalTime, how DateTime.ToUniversalTime works under the hood, and finally how to write a little bit of extra code to detect the overflow case yourself:

Sample Program using DateTime.ToUniversalTime:

This code snippet below demonstrates the use of DateTime.ToUniversalTime to convert the local time “8:00PM 12/31/9999” to UTC:

using System;

 

class Program {

    static void Main(string[] args) {

        // 20:00.00 (8PM) on 12/31/9999

        DateTime myTime = new DateTime(

            9999, /* year */

            12, /* month */

            31, /* day */

            20, /* hour */

            0, /* minute */

            0, /* second */

            DateTimeKind.Local);

 

        // Converts the value of myTime to Coordinated Universal Time (UTC).

        // When the conversion results in a value greater than MaxValue or

        // less than MinValue then MaxValue or MinValue will be returned,

        // respectively.

        DateTime myUtcTime = myTime.ToUniversalTime();

 

        Console.WriteLine("myTime            = " + myTime.ToString("o"));

        Console.WriteLine("myUtcTime         = " + myUtcTime.ToString("o"));

        Console.WriteLine("DateTime.MaxValue = " + DateTime.MaxValue.ToString("o"));

        Console.WriteLine("myUtcTime == DateTime.MaxValue : " +

                                        (myUtcTime == DateTime.MaxValue));

    }

}

Program Output in Pacific Time (GMT-08:00):

myTime            = 9999-12-31T20:00:00.0000000-08:00

myUtcTime         = 9999-12-31T23:59:59.9999999Z

DateTime.MaxValue = 9999-12-31T23:59:59.9999999

myUtcTime == DateTime.MaxValue : True

In Pacific Time (GMT-08:00), the returned value is DateTime.MaxValue, because the actual converted value lands in the year 10,000 (“04:00:00.0000000, January 1, 10000”), which is too large to be stored in a DateTime.

How DateTime.ToUniversalTime Works

DateTime.ToUniversalTime is rather simple. It can be effectively implemented like this:

public DateTime ToUniversalTime(DateTime time) {

    if (time.Kind == DateTimeKind.Utc) {

        return time;

    }

    // The UTC time is equal to the local time minus the UTC offset.

    long tickCount = time.Ticks -

                    TimeZone.CurrentTimeZone.GetUtcOffset(time).Ticks;

 

    if (tickCount > DateTime.MaxValue.Ticks) {

        // return MaxValue for values too large to fit in DateTime

        return new DateTime(DateTime.MaxValue.Ticks, DateTimeKind.Utc);

    }

    if (tickCount < DateTime.MinValue.Ticks) {

        // return MinValue for values too small to fit in DateTime

        return new DateTime(DateTime.MinValue.Ticks, DateTimeKind.Utc);

    }

    return new DateTime(tickCount, DateTimeKind.Utc);

}

How to Convert DateTime to UTC and Check for Overflow

Finally the solution to the problem of how to convert DateTime to UTC and detect the overflow case (it should look very similar to the code above :-) ):

public DateTime ToUniversalTime(DateTime time) {

    if (time.Kind == DateTimeKind.Utc) {

        return time;

    }

    // The UTC time is equal to the local time minus the UTC offset.

    long tickCount = time.Ticks -

                    TimeZone.CurrentTimeZone.GetUtcOffset(time).Ticks;

 

    if (tickCount > DateTime.MaxValue.Ticks) {

        // value is too large to fit in DateTime

        // handle the overflow case here...

    }

    if (tickCount < DateTime.MinValue.Ticks) {

        // value is too small to fit in DateTime

        // handle the overflow case here...

    }

    return new DateTime(tickCount, DateTimeKind.Utc);

}

A Note about DateTime.ToLocalTime

DateTime.ToLocalTime works similarly to DateTime.ToUniversalTime. However, instead of subtracting the UTC offset, the offset is added when converting from UTC to Local time.

  • A recent .NET Base Class Library blog post points out that DateTime.ToUniversalTime does not throw an

  • I must protest about the design of this method (as well as most of the type itself, but others have made similar comments in the past). In exceptional causes such as this, I would expect and hope that it would throw an exception. We should not be checking return values for errors in this day (legacy libraries withstanding).

    How about a replacement method (ToUniversalTimeEx)? Or better yet unseal the type and I will do the work myself.

  • Thanks Josh, but... you don't explain *why* this  bug in the DateTime exists. You seem to be saying it is by design. Please expand! Thanks.

  • Sometimes it is easy to build implementations in software that spend so much time striving for consistency

  • Sometimes it is easy to build implementations in software that spend so much time striving for consistency

Page 1 of 1 (5 items)