File: System\Xml\Schema\XsdDateTime.cs
Web Access
Project: src\src\libraries\System.Private.Xml\src\System.Private.Xml.csproj (System.Private.Xml)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Diagnostics;
using System.Numerics;
using System.Text;
using System.Xml;
 
namespace System.Xml.Schema
{
    /// <summary>
    /// This enum specifies what format should be used when converting string to XsdDateTime
    /// </summary>
    [Flags]
    internal enum XsdDateTimeFlags
    {
        DateTime = 0x01,
        Time = 0x02,
        Date = 0x04,
        GYearMonth = 0x08,
        GYear = 0x10,
        GMonthDay = 0x20,
        GDay = 0x40,
        GMonth = 0x80,
        XdrDateTimeNoTz = 0x100,
        XdrDateTime = 0x200,
        XdrTimeNoTz = 0x400,  //XDRTime with tz is the same as xsd:time
        AllXsd = 0xFF //All still does not include the XDR formats
    }
 
    /// <summary>
    /// This structure extends System.DateTime to support timeInTicks zone and Gregorian types components of an Xsd Duration.  It is used internally to support Xsd durations without loss
    /// of fidelity.  XsdDuration structures are immutable once they've been created.
    /// </summary>
    internal struct XsdDateTime
    {
        // DateTime is being used as an internal representation only
        // Casting XsdDateTime to DateTime might return a different value
        private DateTime _dt;
 
        // Additional information that DateTime is not preserving
        // Information is stored in the following format:
        // Bits     Info
        // 31-24    DateTimeTypeCode
        // 23-16    XsdDateTimeKind
        // 15-8     Zone Hours
        // 7-0      Zone Minutes
        private uint _extra;
 
 
        // Subset of XML Schema types XsdDateTime represents
        private enum DateTimeTypeCode
        {
            DateTime,
            Time,
            Date,
            GYearMonth,
            GYear,
            GMonthDay,
            GDay,
            GMonth,
            XdrDateTime,
        }
 
        // Internal representation of DateTimeKind
        private enum XsdDateTimeKind
        {
            Unspecified,
            Zulu,
            LocalWestOfZulu,    // GMT-1..14, N..Y
            LocalEastOfZulu     // GMT+1..14, A..M
        }
 
        // Masks and shifts used for packing and unpacking extra
        private const uint TypeMask = 0xFF000000;
        private const uint KindMask = 0x00FF0000;
        private const uint ZoneHourMask = 0x0000FF00;
        private const uint ZoneMinuteMask = 0x000000FF;
        private const int TypeShift = 24;
        private const int KindShift = 16;
        private const int ZoneHourShift = 8;
 
        // Maximum number of fraction digits;
        private const short MaxFractionDigits = 7;
        private const int TicksToFractionDivisor = 10000000;
 
        private static readonly int s_lzyyyy = "yyyy".Length;
        private static readonly int s_lzyyyy_ = "yyyy-".Length;
        private static readonly int s_lzyyyy_MM = "yyyy-MM".Length;
        private static readonly int s_lzyyyy_MM_ = "yyyy-MM-".Length;
        private static readonly int s_lzyyyy_MM_dd = "yyyy-MM-dd".Length;
        private static readonly int s_lzyyyy_MM_ddT = "yyyy-MM-ddT".Length;
        private static readonly int s_lzHH = "HH".Length;
        private static readonly int s_lzHH_ = "HH:".Length;
        private static readonly int s_lzHH_mm = "HH:mm".Length;
        private static readonly int s_lzHH_mm_ = "HH:mm:".Length;
        private static readonly int s_lzHH_mm_ss = "HH:mm:ss".Length;
        private static readonly int s_Lz_ = "-".Length;
        private static readonly int s_lz_zz = "-zz".Length;
        private static readonly int s_lz_zz_ = "-zz:".Length;
        private static readonly int s_lz_zz_zz = "-zz:zz".Length;
        private static readonly int s_Lz__ = "--".Length;
        private static readonly int s_lz__mm = "--MM".Length;
        private static readonly int s_lz__mm_ = "--MM-".Length;
        private static readonly int s_lz__mm__ = "--MM--".Length;
        private static readonly int s_lz__mm_dd = "--MM-dd".Length;
        private static readonly int s_Lz___ = "---".Length;
        private static readonly int s_lz___dd = "---dd".Length;
 
        // Number of days in a non-leap year
        private const int DaysPerYear = 365;
        // Number of days in 4 years
        private const int DaysPer4Years = DaysPerYear * 4 + 1;       // 1461
        // Number of days in 100 years
        private const int DaysPer100Years = DaysPer4Years * 25 - 1;  // 36524
        // Number of days in 400 years
        private const int DaysPer400Years = DaysPer100Years * 4 + 1; // 146097
 
        private static ReadOnlySpan<int> DaysToMonth365 => [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365];
        private static ReadOnlySpan<int> DaysToMonth366 => [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366];
 
        private const int CharStackBufferSize = 64;
 
        /// <summary>
        /// Constructs an XsdDateTime from a string using specific format.
        /// </summary>
        public XsdDateTime(string text, XsdDateTimeFlags kinds) : this()
        {
            Parser parser = default;
            if (!parser.Parse(text, kinds))
            {
                throw new FormatException(SR.Format(SR.XmlConvert_BadFormat, text, kinds));
            }
            InitiateXsdDateTime(parser);
        }
 
        private XsdDateTime(Parser parser) : this()
        {
            InitiateXsdDateTime(parser);
        }
 
        private void InitiateXsdDateTime(Parser parser)
        {
            _dt = new DateTime(parser.year, parser.month, parser.day, parser.hour, parser.minute, parser.second);
            if (parser.fraction != 0)
            {
                _dt = _dt.AddTicks(parser.fraction);
            }
            _extra = (uint)(((int)parser.typeCode << TypeShift) | ((int)parser.kind << KindShift) | (parser.zoneHour << ZoneHourShift) | parser.zoneMinute);
        }
 
        internal static bool TryParse(string text, XsdDateTimeFlags kinds, out XsdDateTime result)
        {
            Parser parser = default;
            if (!parser.Parse(text, kinds))
            {
                result = default;
                return false;
            }
            result = new XsdDateTime(parser);
            return true;
        }
 
        /// <summary>
        /// Constructs an XsdDateTime from a DateTime.
        /// </summary>
        public XsdDateTime(DateTime dateTime, XsdDateTimeFlags kinds)
        {
            Debug.Assert(BitOperations.IsPow2((uint)kinds), "One and only one DateTime type code can be set.");
            _dt = dateTime;
 
            DateTimeTypeCode code = (DateTimeTypeCode)BitOperations.TrailingZeroCount((uint)kinds);
            int zoneHour = 0;
            int zoneMinute = 0;
            XsdDateTimeKind kind;
 
            switch (dateTime.Kind)
            {
                case DateTimeKind.Unspecified: kind = XsdDateTimeKind.Unspecified; break;
                case DateTimeKind.Utc: kind = XsdDateTimeKind.Zulu; break;
 
                default:
                    {
                        Debug.Assert(dateTime.Kind == DateTimeKind.Local, $"Unknown DateTimeKind: {dateTime.Kind}");
                        TimeSpan utcOffset = TimeZoneInfo.Local.GetUtcOffset(dateTime);
 
                        if (utcOffset.Ticks < 0)
                        {
                            kind = XsdDateTimeKind.LocalWestOfZulu;
                            zoneHour = -utcOffset.Hours;
                            zoneMinute = -utcOffset.Minutes;
                        }
                        else
                        {
                            kind = XsdDateTimeKind.LocalEastOfZulu;
                            zoneHour = utcOffset.Hours;
                            zoneMinute = utcOffset.Minutes;
                        }
                        break;
                    }
            }
 
            _extra = (uint)(((int)code << TypeShift) | ((int)kind << KindShift) | (zoneHour << ZoneHourShift) | zoneMinute);
        }
 
        // Constructs an XsdDateTime from a DateTimeOffset
        public XsdDateTime(DateTimeOffset dateTimeOffset) : this(dateTimeOffset, XsdDateTimeFlags.DateTime)
        {
        }
 
        public XsdDateTime(DateTimeOffset dateTimeOffset, XsdDateTimeFlags kinds)
        {
            Debug.Assert(BitOperations.IsPow2((uint)kinds), "Only one DateTime type code can be set.");
 
            _dt = dateTimeOffset.DateTime;
 
            TimeSpan zoneOffset = dateTimeOffset.Offset;
            DateTimeTypeCode code = (DateTimeTypeCode)BitOperations.TrailingZeroCount((uint)kinds);
            XsdDateTimeKind kind;
            if (zoneOffset.TotalMinutes < 0)
            {
                zoneOffset = zoneOffset.Negate();
                kind = XsdDateTimeKind.LocalWestOfZulu;
            }
            else if (zoneOffset.TotalMinutes > 0)
            {
                kind = XsdDateTimeKind.LocalEastOfZulu;
            }
            else
            {
                kind = XsdDateTimeKind.Zulu;
            }
 
            _extra = (uint)(((int)code << TypeShift) | ((int)kind << KindShift) | (zoneOffset.Hours << ZoneHourShift) | zoneOffset.Minutes);
        }
 
        /// <summary>
        /// Returns auxiliary enumeration of XSD date type
        /// </summary>
        private DateTimeTypeCode InternalTypeCode
        {
            get { return (DateTimeTypeCode)((_extra & TypeMask) >> TypeShift); }
        }
 
        /// <summary>
        /// Returns geographical "position" of the value
        /// </summary>
        private XsdDateTimeKind InternalKind
        {
            get { return (XsdDateTimeKind)((_extra & KindMask) >> KindShift); }
        }
 
        /// <summary>
        /// Returns XmlTypeCode of the value being stored
        /// </summary>
        public XmlTypeCode TypeCode
        {
            get { return s_typeCodes[(int)InternalTypeCode]; }
        }
 
        /// <summary>
        /// Returns the year part of XsdDateTime
        /// The returned value is integer between 1 and 9999
        /// </summary>
        public int Year
        {
            get { return _dt.Year; }
        }
 
        /// <summary>
        /// Returns the month part of XsdDateTime
        /// The returned value is integer between 1 and 12
        /// </summary>
        public int Month
        {
            get { return _dt.Month; }
        }
 
        /// <summary>
        /// Returns the day of the month part of XsdDateTime
        /// The returned value is integer between 1 and 31
        /// </summary>
        public int Day
        {
            get { return _dt.Day; }
        }
 
        /// <summary>
        /// Returns the hour part of XsdDateTime
        /// The returned value is integer between 0 and 23
        /// </summary>
        public int Hour
        {
            get { return _dt.Hour; }
        }
 
        /// <summary>
        /// Returns the minute part of XsdDateTime
        /// The returned value is integer between 0 and 60
        /// </summary>
        public int Minute
        {
            get { return _dt.Minute; }
        }
 
        /// <summary>
        /// Returns the second part of XsdDateTime
        /// The returned value is integer between 0 and 60
        /// </summary>
        public int Second
        {
            get { return _dt.Second; }
        }
 
        /// <summary>
        /// Returns number of ticks in the fraction of the second
        /// The returned value is integer between 0 and 9999999
        /// </summary>
        public int Fraction
        {
            get { return (int)(_dt.Ticks % TicksToFractionDivisor); }
        }
 
        /// <summary>
        /// Returns the hour part of the time zone
        /// The returned value is integer between -13 and 13
        /// </summary>
        public int ZoneHour
        {
            get
            {
                uint result = (_extra & ZoneHourMask) >> ZoneHourShift;
                return (int)result;
            }
        }
 
        /// <summary>
        /// Returns the minute part of the time zone
        /// The returned value is integer between 0 and 60
        /// </summary>
        public int ZoneMinute
        {
            get
            {
                uint result = (_extra & ZoneMinuteMask);
                return (int)result;
            }
        }
 
        public DateTime ToZulu() =>
            InternalKind switch
            {
                // set it to UTC
                XsdDateTimeKind.Zulu => new DateTime(_dt.Ticks, DateTimeKind.Utc),
 
                // Adjust to UTC and then convert to local in the current time zone
                XsdDateTimeKind.LocalEastOfZulu => new DateTime(_dt.Subtract(new TimeSpan(ZoneHour, ZoneMinute, 0)).Ticks, DateTimeKind.Utc),
                XsdDateTimeKind.LocalWestOfZulu => new DateTime(_dt.Add(new TimeSpan(ZoneHour, ZoneMinute, 0)).Ticks, DateTimeKind.Utc),
                _ => _dt,
            };
 
        /// <summary>
        /// Cast to DateTime
        /// The following table describes the behaviors of getting the default value
        /// when a certain year/month/day values are missing.
        ///
        /// An "X" means that the value exists.  And "--" means that value is missing.
        ///
        /// Year    Month   Day =>  ResultYear  ResultMonth     ResultDay       Note
        ///
        /// X       X       X       Parsed year Parsed month    Parsed day
        /// X       X       --      Parsed Year Parsed month    First day       If we have year and month, assume the first day of that month.
        /// X       --      X       Parsed year First month     Parsed day      If the month is missing, assume first month of that year.
        /// X       --      --      Parsed year First month     First day       If we have only the year, assume the first day of that year.
        ///
        /// --      X       X       CurrentYear Parsed month    Parsed day      If the year is missing, assume the current year.
        /// --      X       --      CurrentYear Parsed month    First day       If we have only a month value, assume the current year and current day.
        /// --      --      X       CurrentYear First month     Parsed day      If we have only a day value, assume current year and first month.
        /// --      --      --      CurrentYear Current month   Current day     So this means that if the date string only contains time, you will get current date.
        /// </summary>
        public static implicit operator DateTime(XsdDateTime xdt)
        {
            DateTime result;
            switch (xdt.InternalTypeCode)
            {
                case DateTimeTypeCode.GMonth:
                case DateTimeTypeCode.GDay:
                    // codeql[cs/leap-year/unsafe-date-construction-from-two-elements] - The XML specification does not explicitly define this behavior for parsing in a non-leap year. We intentionally throw here. Altering this behavior to be more resilient, producing dates like 2/28 or 3/1, could introduce unintended consequences and may not be desirable for user.
                    result = new DateTime(DateTime.Now.Year, xdt.Month, xdt.Day);
                    break;
                case DateTimeTypeCode.Time:
                    //back to DateTime.Now
                    DateTime currentDateTime = DateTime.Now;
                    TimeSpan addDiff = new DateTime(currentDateTime.Year, currentDateTime.Month, currentDateTime.Day) - new DateTime(xdt.Year, xdt.Month, xdt.Day);
                    result = xdt._dt.Add(addDiff);
                    break;
                default:
                    result = xdt._dt;
                    break;
            }
 
            long ticks;
            switch (xdt.InternalKind)
            {
                case XsdDateTimeKind.Zulu:
                    // set it to UTC
                    result = new DateTime(result.Ticks, DateTimeKind.Utc);
                    break;
                case XsdDateTimeKind.LocalEastOfZulu:
                    // Adjust to UTC and then convert to local in the current time zone
                    ticks = result.Ticks - new TimeSpan(xdt.ZoneHour, xdt.ZoneMinute, 0).Ticks;
                    if (ticks < DateTime.MinValue.Ticks)
                    {
                        // Underflow. Return the DateTime as local time directly
                        ticks += TimeZoneInfo.Local.GetUtcOffset(result).Ticks;
                        if (ticks < DateTime.MinValue.Ticks)
                            ticks = DateTime.MinValue.Ticks;
                        return new DateTime(ticks, DateTimeKind.Local);
                    }
                    result = new DateTime(ticks, DateTimeKind.Utc).ToLocalTime();
                    break;
                case XsdDateTimeKind.LocalWestOfZulu:
                    // Adjust to UTC and then convert to local in the current time zone
                    ticks = result.Ticks + new TimeSpan(xdt.ZoneHour, xdt.ZoneMinute, 0).Ticks;
                    if (ticks > DateTime.MaxValue.Ticks)
                    {
                        // Overflow. Return the DateTime as local time directly
                        ticks += TimeZoneInfo.Local.GetUtcOffset(result).Ticks;
                        if (ticks > DateTime.MaxValue.Ticks)
                            ticks = DateTime.MaxValue.Ticks;
                        return new DateTime(ticks, DateTimeKind.Local);
                    }
                    result = new DateTime(ticks, DateTimeKind.Utc).ToLocalTime();
                    break;
                default:
                    break;
            }
            return result;
        }
 
        public static implicit operator DateTimeOffset(XsdDateTime xdt)
        {
            DateTime dt;
 
            switch (xdt.InternalTypeCode)
            {
                case DateTimeTypeCode.GMonth:
                case DateTimeTypeCode.GDay:
                    dt = new DateTime(DateTime.Now.Year, xdt.Month, xdt.Day);
                    break;
                case DateTimeTypeCode.Time:
                    //back to DateTime.Now
                    DateTime currentDateTime = DateTime.Now;
                    TimeSpan addDiff = new DateTime(currentDateTime.Year, currentDateTime.Month, currentDateTime.Day) - new DateTime(xdt.Year, xdt.Month, xdt.Day);
                    dt = xdt._dt.Add(addDiff);
                    break;
                default:
                    dt = xdt._dt;
                    break;
            }
 
            DateTimeOffset result;
            switch (xdt.InternalKind)
            {
                case XsdDateTimeKind.LocalEastOfZulu:
                    result = new DateTimeOffset(dt, new TimeSpan(xdt.ZoneHour, xdt.ZoneMinute, 0));
                    break;
                case XsdDateTimeKind.LocalWestOfZulu:
                    result = new DateTimeOffset(dt, new TimeSpan(-xdt.ZoneHour, -xdt.ZoneMinute, 0));
                    break;
                case XsdDateTimeKind.Zulu:
                    result = new DateTimeOffset(dt, new TimeSpan(0));
                    break;
                case XsdDateTimeKind.Unspecified:
                default:
                    result = new DateTimeOffset(dt, TimeZoneInfo.Local.GetUtcOffset(dt));
                    break;
            }
 
            return result;
        }
 
        /// <summary>
        /// Serialization to a string
        /// </summary>
        public override string ToString()
        {
            Span<char> destination = stackalloc char[CharStackBufferSize];
            bool success = TryFormat(destination, out int charsWritten);
            Debug.Assert(success);
 
            return destination.Slice(0, charsWritten).ToString();
        }
 
        public bool TryFormat(Span<char> destination, out int charsWritten)
        {
            var vsb = new ValueStringBuilder(destination);
 
            switch (InternalTypeCode)
            {
                case DateTimeTypeCode.DateTime:
                    PrintDate(ref vsb);
                    vsb.Append('T');
                    PrintTime(ref vsb);
                    break;
                case DateTimeTypeCode.Time:
                    PrintTime(ref vsb);
                    break;
                case DateTimeTypeCode.Date:
                    PrintDate(ref vsb);
                    break;
                case DateTimeTypeCode.GYearMonth:
                    vsb.AppendSpanFormattable(Year, format: "D4", provider: null);
                    vsb.Append('-');
                    vsb.AppendSpanFormattable(Month, format: "D2", provider: null);
                    break;
                case DateTimeTypeCode.GYear:
                    vsb.AppendSpanFormattable(Year, format: "D4", provider: null);
                    break;
                case DateTimeTypeCode.GMonthDay:
                    vsb.Append("--");
                    vsb.AppendSpanFormattable(Month, format: "D2", provider: null);
                    vsb.Append('-');
                    vsb.AppendSpanFormattable(Day, format: "D2", provider: null);
                    break;
                case DateTimeTypeCode.GDay:
                    vsb.Append("---");
                    vsb.AppendSpanFormattable(Day, format: "D2", provider: null);
                    break;
                case DateTimeTypeCode.GMonth:
                    vsb.Append("--");
                    vsb.AppendSpanFormattable(Month, format: "D2", provider: null);
                    vsb.Append("--");
                    break;
            }
            PrintZone(ref vsb);
 
            charsWritten = vsb.Length;
            return destination.Length >= vsb.Length;
        }
 
        // Serialize year, month and day
        private void PrintDate(ref ValueStringBuilder vsb)
        {
            Span<char> text = vsb.AppendSpan(s_lzyyyy_MM_dd);
            int year, month, day;
            GetYearMonthDay(out year, out month, out day);
            WriteXDigits(text, 0, year, 4);
            text[s_lzyyyy] = '-';
            Write2Digits(text, s_lzyyyy_, month);
            text[s_lzyyyy_MM] = '-';
            Write2Digits(text, s_lzyyyy_MM_, day);
        }
 
        // When printing the date, we need the year, month and the day. When
        // requesting these values from DateTime, it needs to redo the year
        // calculation before it can calculate the month, and it needs to redo
        // the year and month calculation before it can calculate the day. This
        // results in the year being calculated 3 times, the month twice and the
        // day once. As we know that we need all 3 values, by duplicating the
        // logic here we can calculate the number of days and return the intermediate
        // calculations for month and year without the added cost.
        private void GetYearMonthDay(out int year, out int month, out int day)
        {
            long ticks = _dt.Ticks;
            // n = number of days since 1/1/0001
            int n = (int)(ticks / TimeSpan.TicksPerDay);
            // y400 = number of whole 400-year periods since 1/1/0001
            int y400 = n / DaysPer400Years;
            // n = day number within 400-year period
            n -= y400 * DaysPer400Years;
            // y100 = number of whole 100-year periods within 400-year period
            int y100 = n / DaysPer100Years;
            // Last 100-year period has an extra day, so decrement result if 4
            if (y100 == 4)
                y100 = 3;
            // n = day number within 100-year period
            n -= y100 * DaysPer100Years;
            // y4 = number of whole 4-year periods within 100-year period
            int y4 = n / DaysPer4Years;
            // n = day number within 4-year period
            n -= y4 * DaysPer4Years;
            // y1 = number of whole years within 4-year period
            int y1 = n / DaysPerYear;
            // Last year has an extra day, so decrement result if 4
            if (y1 == 4)
                y1 = 3;
 
            year = y400 * 400 + y100 * 100 + y4 * 4 + y1 + 1;
 
            // n = day number within year
            n -= y1 * DaysPerYear;
 
            // Leap year calculation looks different from IsLeapYear since y1, y4,
            // and y100 are relative to year 1, not year 0
            bool leapYear = y1 == 3 && (y4 != 24 || y100 == 3);
            ReadOnlySpan<int> days = leapYear ? DaysToMonth366 : DaysToMonth365;
            // All months have less than 32 days, so n >> 5 is a good conservative
            // estimate for the month
            month = (n >> 5) + 1;
            // m = 1-based month number
            while (n >= days[month])
                month++;
 
            day = n - days[month - 1] + 1;
        }
 
        // Serialize hour, minute, second and fraction
        private void PrintTime(ref ValueStringBuilder vsb)
        {
            Span<char> text = vsb.AppendSpan(s_lzHH_mm_ss);
            Write2Digits(text, 0, Hour);
            text[s_lzHH] = ':';
            Write2Digits(text, s_lzHH_, Minute);
            text[s_lzHH_mm] = ':';
            Write2Digits(text, s_lzHH_mm_, Second);
            int fraction = Fraction;
            if (fraction != 0)
            {
                int fractionDigits = MaxFractionDigits;
                while (fraction % 10 == 0)
                {
                    fractionDigits--;
                    fraction /= 10;
                }
 
                text = vsb.AppendSpan(fractionDigits + 1);
                text[0] = '.';
                WriteXDigits(text, 1, fraction, fractionDigits);
            }
        }
 
        // Serialize time zone
        private void PrintZone(ref ValueStringBuilder vsb)
        {
            Span<char> text;
            switch (InternalKind)
            {
                case XsdDateTimeKind.Zulu:
                    vsb.Append('Z');
                    break;
                case XsdDateTimeKind.LocalWestOfZulu:
                    text = vsb.AppendSpan(s_lz_zz_zz);
                    text[0] = '-';
                    Write2Digits(text, s_Lz_, ZoneHour);
                    text[s_lz_zz] = ':';
                    Write2Digits(text, s_lz_zz_, ZoneMinute);
                    break;
                case XsdDateTimeKind.LocalEastOfZulu:
                    text = vsb.AppendSpan(s_lz_zz_zz);
                    text[0] = '+';
                    Write2Digits(text, s_Lz_, ZoneHour);
                    text[s_lz_zz] = ':';
                    Write2Digits(text, s_lz_zz_, ZoneMinute);
                    break;
                default:
                    // do nothing
                    break;
            }
        }
 
        // Serialize integer into character Span starting with index [start].
        // Number of digits is set by [digits]
        private static void WriteXDigits(Span<char> text, int start, int value, int digits)
        {
            while (digits-- != 0)
            {
                text[start + digits] = (char)(value % 10 + '0');
                value /= 10;
            }
        }
 
        // Serialize two digit integer into character Span starting with index [start].
        private static void Write2Digits(Span<char> text, int start, int value)
        {
            text[start] = (char)(value / 10 + '0');
            text[start + 1] = (char)(value % 10 + '0');
        }
 
        private static readonly XmlTypeCode[] s_typeCodes = {
            XmlTypeCode.DateTime,
            XmlTypeCode.Time,
            XmlTypeCode.Date,
            XmlTypeCode.GYearMonth,
            XmlTypeCode.GYear,
            XmlTypeCode.GMonthDay,
            XmlTypeCode.GDay,
            XmlTypeCode.GMonth
        };
 
 
        // Parsing string according to XML schema spec
        private struct Parser
        {
            private const int leapYear = 1904;
            private const int firstMonth = 1;
            private const int firstDay = 1;
 
            public DateTimeTypeCode typeCode;
            public int year;
            public int month;
            public int day;
            public int hour;
            public int minute;
            public int second;
            public int fraction;
            public XsdDateTimeKind kind;
            public int zoneHour;
            public int zoneMinute;
 
            private string _text;
            private int _length;
 
            public bool Parse(string text, XsdDateTimeFlags kinds)
            {
                _text = text;
                _length = text.Length;
 
                // Skip leading whitespace
                int start = 0;
                while (start < _length && char.IsWhiteSpace(text[start]))
                {
                    start++;
                }
                // Choose format starting from the most common and trying not to reparse the same thing too many times
                if (Test(kinds, XsdDateTimeFlags.DateTime | XsdDateTimeFlags.Date | XsdDateTimeFlags.XdrDateTime | XsdDateTimeFlags.XdrDateTimeNoTz))
                {
                    if (ParseDate(start))
                    {
                        if (Test(kinds, XsdDateTimeFlags.DateTime))
                        {
                            if (ParseChar(start + s_lzyyyy_MM_dd, 'T') && ParseTimeAndZoneAndWhitespace(start + s_lzyyyy_MM_ddT))
                            {
                                typeCode = DateTimeTypeCode.DateTime;
                                return true;
                            }
                        }
                        if (Test(kinds, XsdDateTimeFlags.Date))
                        {
                            if (ParseZoneAndWhitespace(start + s_lzyyyy_MM_dd))
                            {
                                typeCode = DateTimeTypeCode.Date;
                                return true;
                            }
                        }
 
                        if (Test(kinds, XsdDateTimeFlags.XdrDateTime))
                        {
                            if (ParseZoneAndWhitespace(start + s_lzyyyy_MM_dd) || (ParseChar(start + s_lzyyyy_MM_dd, 'T') && ParseTimeAndZoneAndWhitespace(start + s_lzyyyy_MM_ddT)))
                            {
                                typeCode = DateTimeTypeCode.XdrDateTime;
                                return true;
                            }
                        }
                        if (Test(kinds, XsdDateTimeFlags.XdrDateTimeNoTz))
                        {
                            if (ParseChar(start + s_lzyyyy_MM_dd, 'T'))
                            {
                                if (ParseTimeAndWhitespace(start + s_lzyyyy_MM_ddT))
                                {
                                    typeCode = DateTimeTypeCode.XdrDateTime;
                                    return true;
                                }
                            }
                            else
                            {
                                typeCode = DateTimeTypeCode.XdrDateTime;
                                return true;
                            }
                        }
                    }
                }
 
                if (Test(kinds, XsdDateTimeFlags.Time))
                {
                    if (ParseTimeAndZoneAndWhitespace(start))
                    { //Equivalent to NoCurrentDateDefault on DateTimeStyles while parsing xs:time
                        year = leapYear;
                        month = firstMonth;
                        day = firstDay;
                        typeCode = DateTimeTypeCode.Time;
                        return true;
                    }
                }
 
                if (Test(kinds, XsdDateTimeFlags.XdrTimeNoTz))
                {
                    if (ParseTimeAndWhitespace(start))
                    { //Equivalent to NoCurrentDateDefault on DateTimeStyles while parsing xs:time
                        year = leapYear;
                        month = firstMonth;
                        day = firstDay;
                        typeCode = DateTimeTypeCode.Time;
                        return true;
                    }
                }
 
                if (Test(kinds, XsdDateTimeFlags.GYearMonth | XsdDateTimeFlags.GYear))
                {
                    if (Parse4Dig(start, ref year) && 1 <= year)
                    {
                        if (Test(kinds, XsdDateTimeFlags.GYearMonth))
                        {
                            if (
                                ParseChar(start + s_lzyyyy, '-') &&
                                Parse2Dig(start + s_lzyyyy_, ref month) && 1 <= month && month <= 12 &&
                                ParseZoneAndWhitespace(start + s_lzyyyy_MM)
                            )
                            {
                                day = firstDay;
                                typeCode = DateTimeTypeCode.GYearMonth;
                                return true;
                            }
                        }
                        if (Test(kinds, XsdDateTimeFlags.GYear))
                        {
                            if (ParseZoneAndWhitespace(start + s_lzyyyy))
                            {
                                month = firstMonth;
                                day = firstDay;
                                typeCode = DateTimeTypeCode.GYear;
                                return true;
                            }
                        }
                    }
                }
                if (Test(kinds, XsdDateTimeFlags.GMonthDay | XsdDateTimeFlags.GMonth))
                {
                    if (
                        ParseChar(start, '-') &&
                        ParseChar(start + s_Lz_, '-') &&
                        Parse2Dig(start + s_Lz__, ref month) && 1 <= month && month <= 12
                    )
                    {
                        if (Test(kinds, XsdDateTimeFlags.GMonthDay) && ParseChar(start + s_lz__mm, '-'))
                        {
                            if (
                                Parse2Dig(start + s_lz__mm_, ref day) && 1 <= day && day <= DateTime.DaysInMonth(leapYear, month) &&
                                ParseZoneAndWhitespace(start + s_lz__mm_dd)
                            )
                            {
                                year = leapYear;
                                typeCode = DateTimeTypeCode.GMonthDay;
                                return true;
                            }
                        }
                        if (Test(kinds, XsdDateTimeFlags.GMonth))
                        {
                            if (ParseZoneAndWhitespace(start + s_lz__mm) || (ParseChar(start + s_lz__mm, '-') && ParseChar(start + s_lz__mm_, '-') && ParseZoneAndWhitespace(start + s_lz__mm__)))
                            {
                                year = leapYear;
                                day = firstDay;
                                typeCode = DateTimeTypeCode.GMonth;
                                return true;
                            }
                        }
                    }
                }
                if (Test(kinds, XsdDateTimeFlags.GDay))
                {
                    if (
                        ParseChar(start, '-') &&
                        ParseChar(start + s_Lz_, '-') &&
                        ParseChar(start + s_Lz__, '-') &&
                        Parse2Dig(start + s_Lz___, ref day) && 1 <= day && day <= DateTime.DaysInMonth(leapYear, firstMonth) &&
                        ParseZoneAndWhitespace(start + s_lz___dd)
 
                    )
                    {
                        year = leapYear;
                        month = firstMonth;
                        typeCode = DateTimeTypeCode.GDay;
                        return true;
                    }
                }
                return false;
            }
 
 
            private bool ParseDate(int start)
            {
                return
                    Parse4Dig(start, ref year) && 1 <= year &&
                    ParseChar(start + s_lzyyyy, '-') &&
                    Parse2Dig(start + s_lzyyyy_, ref month) && 1 <= month && month <= 12 &&
                    ParseChar(start + s_lzyyyy_MM, '-') &&
                    Parse2Dig(start + s_lzyyyy_MM_, ref day) && 1 <= day && day <= DateTime.DaysInMonth(year, month);
            }
 
            private bool ParseTimeAndZoneAndWhitespace(int start)
            {
                if (ParseTime(ref start))
                {
                    if (ParseZoneAndWhitespace(start))
                    {
                        return true;
                    }
                }
                return false;
            }
 
            private bool ParseTimeAndWhitespace(int start)
            {
                if (ParseTime(ref start))
                {
                    while (start < _length)
                    {//&& char.IsWhiteSpace(text[start])) {
                        start++;
                    }
                    return start == _length;
                }
                return false;
            }
 
            private static ReadOnlySpan<int> Power10 => [-1, 10, 100, 1000, 10000, 100000, 1000000];
            private bool ParseTime(ref int start)
            {
                if (
                    Parse2Dig(start, ref hour) && hour < 24 &&
                    ParseChar(start + s_lzHH, ':') &&
                    Parse2Dig(start + s_lzHH_, ref minute) && minute < 60 &&
                    ParseChar(start + s_lzHH_mm, ':') &&
                    Parse2Dig(start + s_lzHH_mm_, ref second) && second < 60
                )
                {
                    start += s_lzHH_mm_ss;
                    if (ParseChar(start, '.'))
                    {
                        // Parse factional part of seconds
                        // We allow any number of digits, but keep only first 7
                        this.fraction = 0;
                        int fractionDigits = 0;
                        int round = 0;
                        while (++start < _length)
                        {
                            int d = _text[start] - '0';
                            if (9u < unchecked((uint)d))
                            { // d < 0 || 9 < d
                                break;
                            }
                            if (fractionDigits < MaxFractionDigits)
                            {
                                this.fraction = (this.fraction * 10) + d;
                            }
                            else if (fractionDigits == MaxFractionDigits)
                            {
                                if (5 < d)
                                {
                                    round = 1;
                                }
                                else if (d == 5)
                                {
                                    round = -1;
                                }
                            }
                            else if (round < 0 && d != 0)
                            {
                                round = 1;
                            }
                            fractionDigits++;
                        }
                        if (fractionDigits < MaxFractionDigits)
                        {
                            if (fractionDigits == 0)
                            {
                                return false; // cannot end with .
                            }
                            fraction *= Power10[MaxFractionDigits - fractionDigits];
                        }
                        else
                        {
                            if (round < 0)
                            {
                                round = fraction & 1;
                            }
                            fraction += round;
                        }
                    }
                    return true;
                }
                // cleanup - conflict with gYear
                hour = 0;
                return false;
            }
 
            private bool ParseZoneAndWhitespace(int start)
            {
                if (start < _length)
                {
                    char ch = _text[start];
                    if (ch == 'Z' || ch == 'z')
                    {
                        kind = XsdDateTimeKind.Zulu;
                        start++;
                    }
                    else if (start + 5 < _length)
                    {
                        if (
                            Parse2Dig(start + s_Lz_, ref zoneHour) && zoneHour <= 99 &&
                            ParseChar(start + s_lz_zz, ':') &&
                            Parse2Dig(start + s_lz_zz_, ref zoneMinute) && zoneMinute <= 99
                        )
                        {
                            if (ch == '-')
                            {
                                kind = XsdDateTimeKind.LocalWestOfZulu;
                                start += s_lz_zz_zz;
                            }
                            else if (ch == '+')
                            {
                                kind = XsdDateTimeKind.LocalEastOfZulu;
                                start += s_lz_zz_zz;
                            }
                        }
                    }
                }
                while (start < _length && char.IsWhiteSpace(_text[start]))
                {
                    start++;
                }
                return start == _length;
            }
 
 
            private bool Parse4Dig(int start, ref int num)
            {
                if (start + 3 < _length)
                {
                    int d4 = _text[start] - '0';
                    int d3 = _text[start + 1] - '0';
                    int d2 = _text[start + 2] - '0';
                    int d1 = _text[start + 3] - '0';
                    if (0 <= d4 && d4 < 10 &&
                        0 <= d3 && d3 < 10 &&
                        0 <= d2 && d2 < 10 &&
                        0 <= d1 && d1 < 10
                    )
                    {
                        num = ((d4 * 10 + d3) * 10 + d2) * 10 + d1;
                        return true;
                    }
                }
                return false;
            }
 
            private bool Parse2Dig(int start, ref int num)
            {
                if (start + 1 < _length)
                {
                    int d2 = _text[start] - '0';
                    int d1 = _text[start + 1] - '0';
                    if (0 <= d2 && d2 < 10 &&
                        0 <= d1 && d1 < 10
                        )
                    {
                        num = d2 * 10 + d1;
                        return true;
                    }
                }
                return false;
            }
 
            private bool ParseChar(int start, char ch)
            {
                return start < _length && _text[start] == ch;
            }
 
            private static bool Test(XsdDateTimeFlags left, XsdDateTimeFlags right)
            {
                return (left & right) != 0;
            }
        }
    }
}