| @@ -1,4 +1,5 @@ | |||
| using System; | |||
| using System.Runtime.CompilerServices; | |||
| using System.Text; | |||
| using System.Text.Utf8; | |||
| @@ -95,17 +96,180 @@ namespace Discord.Serialization | |||
| public static DateTime ParseDateTime(this ReadOnlySpan<byte> text) | |||
| { | |||
| string str = ParseString(text); | |||
| if (DateTime.TryParse(str, out var result)) //TODO: Improve perf | |||
| if (TryParseDateTime(text, out var result, out int ignored)) | |||
| return result; | |||
| throw new SerializationException("Failed to parse DateTime"); | |||
| } | |||
| public static DateTimeOffset ParseDateTimeOffset(this ReadOnlySpan<byte> text) | |||
| { | |||
| string str = ParseString(text); | |||
| if (DateTimeOffset.TryParse(str, out var result)) //TODO: Improve perf | |||
| if (TryParseDateTimeOffset(text, out var result, out int ignored)) | |||
| return result; | |||
| throw new SerializationException("Failed to parse DateTimeOffset"); | |||
| } | |||
| private static bool TryParseDateTime(ReadOnlySpan<byte> text, out DateTime value, out int bytesConsumed) | |||
| { | |||
| int index = 0; | |||
| bytesConsumed = 0; | |||
| if (!TryParseDateParts(text, ref index, ref bytesConsumed, out int year, out int month, out int day) || | |||
| !TryParseTimeParts(text, ref index, ref bytesConsumed, out int hour, out int min, out int sec, out int milli) || | |||
| !TryParseTimezoneParts(text, ref index, ref bytesConsumed, out var offset)) | |||
| { | |||
| value = default; | |||
| return false; | |||
| } | |||
| value = new DateTime(year, month, day, hour, min, sec, milli, DateTimeKind.Utc); | |||
| if (offset != TimeSpan.Zero) | |||
| value -= offset; | |||
| return true; | |||
| } | |||
| private static bool TryParseDateTimeOffset(ReadOnlySpan<byte> text, out DateTimeOffset value, out int bytesConsumed) | |||
| { | |||
| int index = 0; | |||
| bytesConsumed = 0; | |||
| if (!TryParseDateParts(text, ref index, ref bytesConsumed, out int year, out int month, out int day) || | |||
| !TryParseTimeParts(text, ref index, ref bytesConsumed, out int hour, out int min, out int sec, out int milli) || | |||
| !TryParseTimezoneParts(text, ref index, ref bytesConsumed, out var offset)) | |||
| { | |||
| value = default; | |||
| return false; | |||
| } | |||
| value = new DateTimeOffset(year, month, day, hour, min, sec, milli, offset); | |||
| return true; | |||
| } | |||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |||
| private static bool TryParseDateParts(ReadOnlySpan<byte> text, ref int index, ref int bytesConsumed, | |||
| out int year, out int month, out int day) | |||
| { | |||
| year = 0; | |||
| month = 0; | |||
| day = 0; | |||
| //Format: YYYY-MM-DD | |||
| if (text.Length < 10 || | |||
| !TryParseNumericPart(text, ref index, out year, ref bytesConsumed, 4) || | |||
| text[index++] != (byte)'-' || | |||
| !TryParseNumericPart(text, ref index, out month, ref bytesConsumed, 2) || | |||
| text[index++] != (byte)'-' || | |||
| !TryParseNumericPart(text, ref index, out day, ref bytesConsumed, 2)) | |||
| { | |||
| bytesConsumed = 0; | |||
| return false; | |||
| } | |||
| return true; | |||
| } | |||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |||
| private static bool TryParseTimeParts(ReadOnlySpan<byte> text, ref int index, ref int bytesConsumed, | |||
| out int hour, out int minute, out int second, out int millisecond) | |||
| { | |||
| hour = 0; | |||
| minute = 0; | |||
| second = 0; | |||
| millisecond = 0; | |||
| //Time (hh:mm) | |||
| if (text.Length < 16 || text[index] != (byte)'T') //0001-01-01T01:01 | |||
| return true; | |||
| index++; | |||
| if (!TryParseNumericPart(text, ref index, out hour, ref bytesConsumed, 2) || | |||
| text[index++] != (byte)':' || | |||
| !TryParseNumericPart(text, ref index, out minute, ref bytesConsumed, 2)) | |||
| { | |||
| bytesConsumed = 0; | |||
| return false; | |||
| } | |||
| //Time (hh:mm:ss) | |||
| if (text.Length < 19 || text[index] != (byte)':') //0001-01-01T01:01:01 | |||
| return true; | |||
| index++; | |||
| if (!TryParseNumericPart(text, ref index, out second, ref bytesConsumed, 2)) | |||
| { | |||
| bytesConsumed = 0; | |||
| return false; | |||
| } | |||
| //Time (hh:mm:ss.sss) | |||
| if (text.Length < 21 || text[index] != (byte)'.') //0001-01-01T01:01:01.1 | |||
| return true; | |||
| index++; | |||
| if (!TryParseNumericPart(text, ref index, out millisecond, ref bytesConsumed, 3)) | |||
| { | |||
| bytesConsumed = 0; | |||
| return false; | |||
| } | |||
| return true; | |||
| } | |||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |||
| private static bool TryParseTimezoneParts(ReadOnlySpan<byte> text, ref int index, ref int bytesConsumed, | |||
| out TimeSpan offset) | |||
| { | |||
| offset = default; | |||
| int remaining = text.Length - index; | |||
| if (remaining == 1) //Z | |||
| { | |||
| if (text[index] != 'Z') | |||
| return false; | |||
| return true; | |||
| } | |||
| else if (remaining == 6) //+00:00 | |||
| { | |||
| bool isNegative = text[index] == (byte)'-'; | |||
| if (!isNegative && text[index] != (byte)'+') | |||
| return false; | |||
| index++; | |||
| if (!TryParseNumericPart(text, ref index, out int hours, ref bytesConsumed, 2) || | |||
| text[index++] != (byte)':' || | |||
| !TryParseNumericPart(text, ref index, out int minutes, ref bytesConsumed, 2)) | |||
| { | |||
| bytesConsumed = 0; | |||
| return false; | |||
| } | |||
| offset = new TimeSpan(hours, minutes, 0); | |||
| if (isNegative) offset = -offset; | |||
| return true; | |||
| } | |||
| else | |||
| return false; | |||
| } | |||
| //From https://github.com/dotnet/corefxlab/blob/master/src/System.Text.Primitives/System/Text/Parsing/Unsigned.cs | |||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |||
| private static bool TryParseNumericPart(ReadOnlySpan<byte> text, ref int index, out int value, ref int bytesConsumed, int maxLength) | |||
| { | |||
| // Parse the first digit separately. If invalid here, we need to return false. | |||
| uint firstDigit = text[index++] - 48u; // '0' | |||
| if (firstDigit > 9) | |||
| { | |||
| bytesConsumed = 0; | |||
| value = default; | |||
| return false; | |||
| } | |||
| uint parsedValue = firstDigit; | |||
| for (int i = 1; i < maxLength && index < text.Length; i++, index++) | |||
| { | |||
| uint nextDigit = text[index] - 48u; // '0' | |||
| if (nextDigit > 9) | |||
| { | |||
| bytesConsumed = index; | |||
| value = (int)(parsedValue); | |||
| return true; | |||
| } | |||
| parsedValue = parsedValue * 10 + nextDigit; | |||
| } | |||
| bytesConsumed = text.Length; | |||
| value = (int)(parsedValue); | |||
| return true; | |||
| } | |||
| } | |||
| } | |||