| @@ -1,4 +1,5 @@ | |||||
| using System; | using System; | ||||
| using System.Runtime.CompilerServices; | |||||
| using System.Text; | using System.Text; | ||||
| using System.Text.Utf8; | using System.Text.Utf8; | ||||
| @@ -95,17 +96,180 @@ namespace Discord.Serialization | |||||
| public static DateTime ParseDateTime(this ReadOnlySpan<byte> text) | 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; | return result; | ||||
| throw new SerializationException("Failed to parse DateTime"); | throw new SerializationException("Failed to parse DateTime"); | ||||
| } | } | ||||
| public static DateTimeOffset ParseDateTimeOffset(this ReadOnlySpan<byte> text) | 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; | return result; | ||||
| throw new SerializationException("Failed to parse DateTimeOffset"); | 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; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||