private/runtime/Iso/IsoDate.cs

using System;
using System.Text;

namespace Carbon.Data
{
    public struct IsoDate
    {
        public int Year { get; set; } // 0-3000

        public int Month { get; set; } // 1-12

        public int Day { get; set; } // 1-31

        public int Hour { get; set; } // 0-24

        public int Minute { get; set; } // 0-60 (60 is a special case)

        public int Second { get; set; } // 0-60 (60 is used for leap seconds)

        public double Millisecond { get; set; } // 0-999.9...

        public TimeSpan Offset { get; set; }

        public DateTimeKind Kind { get; set; }

        public TimeSpan TimeOfDay => new TimeSpan(Hour, Minute, Second);

        public DateTime ToDateTime()
        {
            if (Kind == DateTimeKind.Utc || Offset == TimeSpan.Zero)
            {
                return new DateTime(Year, Month, Day, Hour, Minute, Second, (int)Millisecond, DateTimeKind.Utc);
            }

            return ToDateTimeOffset().DateTime;
        }

        public DateTimeOffset ToDateTimeOffset()
        {
            return new DateTimeOffset(
                Year,
                Month,
                Day,
                Hour,
                Minute,
                Second,
                (int)Millisecond,
                Offset
            );
        }

        public DateTime ToUtcDateTime()
        {
            return ToDateTimeOffset().UtcDateTime;
        }

        public override string ToString()
        {
            var sb = new StringBuilder();

            // yyyy-MM-dd
            sb.Append($"{Year}-{Month:00}-{Day:00}");

            if (TimeOfDay > new TimeSpan(0))
            {
                sb.Append($"T{Hour:00}:{Minute:00}");

                if (TimeOfDay.Seconds > 0)
                {
                    sb.Append($":{Second:00}");
                }
            }

            if (Offset.Ticks == 0)
            {
                sb.Append('Z'); // UTC
            }
            else
            {
                if (Offset.Ticks >= 0)
                {
                    sb.Append('+');
                }

                sb.Append($"{Offset.Hours:00}:{Offset.Minutes:00}");
            }

            return sb.ToString();
        }

        public static IsoDate FromDateTimeOffset(DateTimeOffset date)
        {
            return new IsoDate {
                Year = date.Year,
                Month = date.Month,
                Day = date.Day,
                Hour = date.Hour,
                Minute = date.Minute,
                Second = date.Second,
                Offset = date.Offset,
                Kind = date.Offset == TimeSpan.Zero ? DateTimeKind.Utc : DateTimeKind.Unspecified
            };
        }

        private static readonly char[] timeSeperators = { ':', '.' };

        public static IsoDate Parse(string text)
        {
            var tzIndex = -1;
            var timeIndex = text.IndexOf('T');

            var builder = new IsoDate { Day = 1, Month = 1 };

            // TODO: strip the time zone offset off the end
            string dateTime = text;
            string timeZone = null;

            if (dateTime.IndexOf('Z') > -1)
            {
                tzIndex = dateTime.LastIndexOf('Z');

                builder.Kind = DateTimeKind.Utc;
            }
            else if (dateTime.LastIndexOf('+') > 10)
            {
                tzIndex = dateTime.LastIndexOf('+');
            }
            else if (dateTime.LastIndexOf('-') > 10)
            {
                tzIndex = dateTime.LastIndexOf('-');
            }

            if (tzIndex > -1)
            {
                timeZone = dateTime.Substring(tzIndex);
                dateTime = dateTime.Substring(0, tzIndex);
            }

            string date = (timeIndex == -1) ? dateTime : dateTime.Substring(0, timeIndex);

            var dateParts = date.Split(Seperator.Dash); // '-'

            for (int i = 0; i < dateParts.Length; i++)
            {
                var part = dateParts[i];

                switch (i)
                {
                    case 0: builder.Year = int.Parse(part); break;
                    case 1: builder.Month = int.Parse(part); break;
                    case 2: builder.Day = int.Parse(part); break;
                }
            }

            if (timeIndex > -1)
            {
                string[] timeParts = dateTime.Substring(timeIndex + 1).Split(timeSeperators);

                for (int i = 0; i < timeParts.Length; i++)
                {
                    var part = timeParts[i];

                    switch (i)
                    {
                        case 0: builder.Hour = int.Parse(part); break;
                        case 1: builder.Minute = int.Parse(part); break;
                        case 2: builder.Second = int.Parse(part); break;
                        case 3: builder.Millisecond = double.Parse("0." + part) * 1000; break;
                    }
                }
            }

            if (timeZone != null && timeZone != "Z")
            {
                var hours = int.Parse(timeZone.Substring(1, 2));
                var minutes = int.Parse(timeZone.Substring(4, 2));

                if (timeZone[0] == '-')
                {
                    hours = -hours;
                    minutes = -minutes;
                }

                builder.Offset = new TimeSpan(hours, minutes, 0);
            }

            return builder;
        }
    }

    /*
    YYYY # eg 1997
    YYYY-MM # eg 1997-07
    YYYY-MM-DD # eg 1997-07-16
    YYYY-MM-DDThh:mmTZD # eg 1997-07-16T19:20+01:00
    YYYY-MM-DDThh:mm:ssTZD # eg 1997-07-16T19:20:30+01:00
    YYYY-MM-DDThh:mm:ss.sTZD # eg 1997-07-16T19:20:30.45+01:00

    where:

    YYYY = four-digit year
    MM = two-digit month (01=January, etc.)
    DD = two-digit day of month (01 through 31)
    hh = two digits of hour (00 through 23) (am/pm NOT allowed)
    mm = two digits of minute (00 through 59)
    ss = two digits of second (00 through 59)
    s = one or more digits representing a decimal fraction of a second
    TZD = time zone designator (Z or +hh:mm or -hh:mm)
    */
}