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); }
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); }
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); }
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); }
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); }
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); }
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); }
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); }
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); }
/// <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); }