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

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.