Example #1
0
            static bool ParseOffset(ref DateTimeParseData parseData, ReadOnlySpan <byte> offsetData)
            {
                // Parse the hours for the offset
                if (offsetData.Length < 2 ||
                    !TryGetNextTwoDigits(offsetData.Slice(0, 2), ref parseData.OffsetHours))
                {
                    return(false);
                }

                // We now have YYYY-MM-DDThh:mm:ss.s+|-hh

                if (offsetData.Length == 2)
                {
                    // Just hours offset specified
                    return(true);
                }

                // Ensure we have enough for ":mm"
                if (offsetData.Length != 5 ||
                    offsetData[2] != JsonConstants.Colon ||
                    !TryGetNextTwoDigits(offsetData.Slice(3), ref parseData.OffsetMinutes))
                {
                    return(false);
                }

                return(true);
            }
Example #2
0
        private bool TryParseCore(DateTimeParseData data)
        {
            var npData = new NumParseData
            {
                ArgStr   = data.ArgStr,
                StartIdx = data.Idx,
                Idx      = data.Idx,
                Char     = data.ArgStr[data.Idx],
                Values   = new List <int>()
            };

            while (data.Idx < data.Len && !char.IsLetter(npData.Char) && data.IsValid)
            {
                npData.Char = data.ArgStr[data.Idx];

                data.IsValid = TryParseCore(npData);
                npData.Idx++;
            }

            data.Idx  = npData.Idx;
            data.Nums = npData.Values.ToArray();

            data.IsValid = data.IsValid && data.Nums.Any();
            return(data.IsValid);
        }
Example #3
0
        private bool TryParseTime(DateTimeParseData data)
        {
            TryParseDatePart(data,
                             dtParts => !data.DateTimeParts.HasDate,
                             (dtParts, nums) =>
                             nums.ForEach(
                                 (val, idx) => data.DateTimeParts.Hour   = val,
                                 (val, idx) => data.DateTimeParts.Minute = val,
                                 (val, idx) => data.DateTimeParts.Second = val) <= 0);

            return(data.IsValid);
        }
Example #4
0
        private bool TryParseDate(DateTimeParseData data)
        {
            TryParseDatePart(data,
                             dtParts => !data.DateTimeParts.HasDate,
                             (dtParts, nums) =>
                             nums.Reverse().ToArray().ForEach(
                                 (val, idx) => data.DateTimeParts.Day   = val,
                                 (val, idx) => data.DateTimeParts.Month = val,
                                 (val, idx) => data.DateTimeParts.Year  = val) <= 0);

            return(data.IsValid);
        }
Example #5
0
        private bool TryDigestLetter(DateTimeParseData data)
        {
            data.Letter = default;

            while (data.Idx < data.Len && char.IsWhiteSpace(data.Letter = data.ArgStr[data.Idx]))
            {
                data.Idx++;
            }

            data.Letter  = char.ToUpper(data.Letter);
            data.IsValid = data.Idx < data.Len && prefixes.Contains(data.Letter);

            return(data.IsValid);
        }
Example #6
0
        private bool TryParseDatePart(
            DateTimeParseData data,
            Func <DateTimeParts, bool> condition,
            Func <DateTimeParts, int[], bool> parseAction)
        {
            if (data.IsValid = data.IsValid = condition(data.DateTimeParts))
            {
                TryParseCore(data);
            }

            if (data.IsValid)
            {
                data.IsValid = parseAction(data.DateTimeParts, data.Nums);
            }

            return(data.IsValid);
        }
Example #7
0
        private bool TryParseDateTimeParts(DateTimeParseData data)
        {
            if (TryDigestLetter(data))
            {
                switch (data.Letter)
                {
                case DATE_PREFIX:
                    TryParseDate(data);
                    break;

                case TIME_PREFIX:
                    TryParseTime(data);
                    break;

                default:
                    throw new ArgumentException(nameof(data.Letter));
                }
            }

            return(data.IsValid);
        }
Example #8
0
        public override bool TryParse(string argStr, out object parsedValue)
        {
            argStr = argStr?.Trim() ?? string.Empty;
            bool retVal = false;

            parsedValue = null;

            if (!string.IsNullOrEmpty(argStr))
            {
                var data = new DateTimeParseData
                {
                    ArgStr        = argStr,
                    Len           = argStr.Length,
                    DateTimeParts = new DateTimeParts(),
                };

                while (TryParseDateTimeParts(data))
                {
                }

                if (data.IsValid)
                {
                    DateTime now   = DateTime.Now;
                    var      parts = data.DateTimeParts;

                    DateTime value = new DateTime(
                        parts.Year ?? now.Year,
                        parts.Month ?? now.Month,
                        parts.Day ?? now.Day,
                        parts.Hour ?? now.Hour,
                        parts.Minute ?? now.Minute,
                        parts.Second ?? now.Second);

                    parsedValue = value;
                    retVal      = true;
                }
            }

            return(retVal);
        }
Example #9
0
            static bool FinishParsing(ref DateTimeParseData parseData, out DateTimeOffset dateTimeOffset, out DateTimeKind dateTimeKind)
            {
                dateTimeKind = default;

                switch (parseData.OffsetToken)
                {
                case JsonConstants.UtcOffsetToken:
                    // Same as specifying an offset of "+00:00", except that DateTime's Kind gets set to UTC rather than Local
                    if (!TryCreateDateTimeOffset(ref parseData, out dateTimeOffset))
                    {
                        return(false);
                    }

                    dateTimeKind = DateTimeKind.Utc;
                    break;

                case JsonConstants.Plus:
                case JsonConstants.Hyphen:
                    if (!TryCreateDateTimeOffset(ref parseData, out dateTimeOffset))
                    {
                        return(false);
                    }

                    dateTimeKind = DateTimeKind.Local;
                    break;

                default:
                    // No offset, attempt to read as local time.
                    if (!TryCreateDateTimeOffsetInterpretingDataAsLocalTime(ref parseData, out dateTimeOffset))
                    {
                        return(false);
                    }

                    dateTimeKind = DateTimeKind.Unspecified;
                    break;
                }

                return(true);
            }
Example #10
0
        /// <summary>
        /// ISO 8601 date time parser (ISO 8601-1:2019).
        /// </summary>
        /// <param name="source">The date/time to parse in UTF-8 format.</param>
        /// <param name="parseData">The parsed <see cref="DateTimeParseData"/> for the given <paramref name="source"/>.</param>
        /// <remarks>
        /// Supports extended calendar date (5.2.2.1) and complete (5.4.2.1) calendar date/time of day
        /// representations with optional specification of seconds and fractional seconds.
        ///
        /// Times can be explicitly specified as UTC ("Z" - 5.3.3) or offsets from UTC ("+/-hh:mm" 5.3.4.2).
        /// If unspecified they are considered to be local per spec.
        ///
        /// Examples: (TZD is either "Z" or hh:mm offset from UTC)
        ///
        ///  YYYY-MM-DD               (eg 1997-07-16)
        ///  YYYY-MM-DDThh:mm         (eg 1997-07-16T19:20)
        ///  YYYY-MM-DDThh:mm:ss      (eg 1997-07-16T19:20:30)
        ///  YYYY-MM-DDThh:mm:ss.s    (eg 1997-07-16T19:20:30.45)
        ///  YYYY-MM-DDThh:mmTZD      (eg 1997-07-16T19:20+01:00)
        ///  YYYY-MM-DDThh:mm:ssTZD   (eg 1997-07-16T19:20:3001:00)
        ///  YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45Z)
        ///
        /// Generally speaking we always require the "extended" option when one exists (3.1.3.5).
        /// The extended variants have separator characters between components ('-', ':', '.', etc.).
        /// Spaces are not permitted.
        /// </remarks>
        /// <returns>"true" if successfully parsed.</returns>
        private static bool TryParseDateTimeOffset(ReadOnlySpan <byte> source, out DateTimeParseData parseData)
        {
            parseData = default;

            // Source does not have enough characters for YYYY-MM-DD
            if (source.Length < 10)
            {
                return(false);
            }

            // Parse the calendar date
            // -----------------------
            // ISO 8601-1:2019 5.2.2.1b "Calendar date complete extended format"
            //  [dateX] = [year]["-"][month]["-"][day]
            //  [year]  = [YYYY] [0000 - 9999] (4.3.2)
            //  [month] = [MM] [01 - 12] (4.3.3)
            //  [day]   = [DD] [01 - 28, 29, 30, 31] (4.3.4)
            //
            // Note: 5.2.2.2 "Representations with reduced precision" allows for
            // just [year]["-"][month] (a) and just [year] (b), but we currently
            // don't permit it.

            {
                uint digit1 = source[0] - (uint)'0';
                uint digit2 = source[1] - (uint)'0';
                uint digit3 = source[2] - (uint)'0';
                uint digit4 = source[3] - (uint)'0';

                if (digit1 > 9 || digit2 > 9 || digit3 > 9 || digit4 > 9)
                {
                    return(false);
                }

                parseData.Year = (int)(digit1 * 1000 + digit2 * 100 + digit3 * 10 + digit4);
            }

            if (source[4] != JsonConstants.Hyphen ||
                !TryGetNextTwoDigits(source.Slice(start: 5, length: 2), ref parseData.Month) ||
                source[7] != JsonConstants.Hyphen ||
                !TryGetNextTwoDigits(source.Slice(start: 8, length: 2), ref parseData.Day))
            {
                return(false);
            }

            // We now have YYYY-MM-DD [dateX]

            Debug.Assert(source.Length >= 10);
            if (source.Length == 10)
            {
                // Just a calendar date
                return(true);
            }

            // Parse the time of day
            // ---------------------
            //
            // ISO 8601-1:2019 5.3.1.2b "Local time of day complete extended format"
            //  [timeX]   = ["T"][hour][":"][min][":"][sec]
            //  [hour]    = [hh] [00 - 23] (4.3.8a)
            //  [minute]  = [mm] [00 - 59] (4.3.9a)
            //  [sec]     = [ss] [00 - 59, 60 with a leap second] (4.3.10a)
            //
            // ISO 8601-1:2019 5.3.3 "UTC of day"
            //  [timeX]["Z"]
            //
            // ISO 8601-1:2019 5.3.4.2 "Local time of day with the time shift between
            // local time scale and UTC" (Extended format)
            //
            //  [shiftX] = ["+"|"-"][hour][":"][min]
            //
            // Notes:
            //
            // "T" is optional per spec, but _only_ when times are used alone. In our
            // case, we're reading out a complete date & time and as such require "T".
            // (5.4.2.1b).
            //
            // For [timeX] We allow seconds to be omitted per 5.3.1.3a "Representations
            // with reduced precision". 5.3.1.3b allows just specifying the hour, but
            // we currently don't permit this.
            //
            // Decimal fractions are allowed for hours, minutes and seconds (5.3.14).
            // We only allow fractions for seconds currently. Lower order components
            // can't follow, i.e. you can have T23.3, but not T23.3:04. There must be
            // one digit, but the max number of digits is implemenation defined. We
            // currently allow up to 16 digits of fractional seconds only. While we
            // support 16 fractional digits we only parse the first seven, anything
            // past that is considered a zero. This is to stay compatible with the
            // DateTime implementation which is limited to this resolution.

            if (source.Length < 16)
            {
                // Source does not have enough characters for YYYY-MM-DDThh:mm
                return(false);
            }

            // Parse THH:MM (e.g. "T10:32")
            if (source[10] != JsonConstants.TimePrefix || source[13] != JsonConstants.Colon ||
                !TryGetNextTwoDigits(source.Slice(start: 11, length: 2), ref parseData.Hour) ||
                !TryGetNextTwoDigits(source.Slice(start: 14, length: 2), ref parseData.Minute))
            {
                return(false);
            }

            // We now have YYYY-MM-DDThh:mm
            Debug.Assert(source.Length >= 16);
            if (source.Length == 16)
            {
                return(true);
            }

            byte curByte     = source[16];
            int  sourceIndex = 17;

            // Either a TZD ['Z'|'+'|'-'] or a seconds separator [':'] is valid at this point
            switch (curByte)
            {
            case JsonConstants.UtcOffsetToken:
                parseData.OffsetToken = JsonConstants.UtcOffsetToken;
                return(sourceIndex == source.Length);

            case JsonConstants.Plus:
            case JsonConstants.Hyphen:
                parseData.OffsetToken = curByte;
                return(ParseOffset(ref parseData, source.Slice(sourceIndex)));

            case JsonConstants.Colon:
                break;

            default:
                return(false);
            }

            // Try reading the seconds
            if (source.Length < 19 ||
                !TryGetNextTwoDigits(source.Slice(start: 17, length: 2), ref parseData.Second))
            {
                return(false);
            }

            // We now have YYYY-MM-DDThh:mm:ss
            Debug.Assert(source.Length >= 19);
            if (source.Length == 19)
            {
                return(true);
            }

            curByte     = source[19];
            sourceIndex = 20;

            // Either a TZD ['Z'|'+'|'-'] or a seconds decimal fraction separator ['.'] is valid at this point
            switch (curByte)
            {
            case JsonConstants.UtcOffsetToken:
                parseData.OffsetToken = JsonConstants.UtcOffsetToken;
                return(sourceIndex == source.Length);

            case JsonConstants.Plus:
            case JsonConstants.Hyphen:
                parseData.OffsetToken = curByte;
                return(ParseOffset(ref parseData, source.Slice(sourceIndex)));

            case JsonConstants.Period:
                break;

            default:
                return(false);
            }

            // Source does not have enough characters for second fractions (i.e. ".s")
            // YYYY-MM-DDThh:mm:ss.s
            if (source.Length < 21)
            {
                return(false);
            }

            // Parse fraction. This value should never be greater than 9_999_999
            {
                int numDigitsRead = 0;
                int fractionEnd   = Math.Min(sourceIndex + JsonConstants.DateTimeParseNumFractionDigits, source.Length);

                while (sourceIndex < fractionEnd && IsDigit(curByte = source[sourceIndex]))
                {
                    if (numDigitsRead < JsonConstants.DateTimeNumFractionDigits)
                    {
                        parseData.Fraction = (parseData.Fraction * 10) + (int)(curByte - (uint)'0');
                        numDigitsRead++;
                    }

                    sourceIndex++;
                }

                if (parseData.Fraction != 0)
                {
                    while (numDigitsRead < JsonConstants.DateTimeNumFractionDigits)
                    {
                        parseData.Fraction *= 10;
                        numDigitsRead++;
                    }
                }
            }

            // We now have YYYY-MM-DDThh:mm:ss.s
            Debug.Assert(sourceIndex <= source.Length);
            if (sourceIndex == source.Length)
            {
                return(true);
            }

            curByte = source[sourceIndex++];

            // TZD ['Z'|'+'|'-'] is valid at this point
            switch (curByte)
            {
            case JsonConstants.UtcOffsetToken:
                parseData.OffsetToken = JsonConstants.UtcOffsetToken;
                return(sourceIndex == source.Length);

            case JsonConstants.Plus:
            case JsonConstants.Hyphen:
                parseData.OffsetToken = curByte;
                return(ParseOffset(ref parseData, source.Slice(sourceIndex)));

            default:
                return(false);
            }