From 0bea893076dc884c3ec26170579aef9e869b98f9 Mon Sep 17 00:00:00 2001 From: RogueException Date: Mon, 7 Aug 2017 23:49:26 -0300 Subject: [PATCH] Improved DateTime(Offset) deserialization --- .../Extensions/BufferExtensions.cs | 172 ++++++++++++++++++++- 1 file changed, 168 insertions(+), 4 deletions(-) diff --git a/src/Discord.Net.Serialization/Extensions/BufferExtensions.cs b/src/Discord.Net.Serialization/Extensions/BufferExtensions.cs index 66838880e..6db24ceca 100644 --- a/src/Discord.Net.Serialization/Extensions/BufferExtensions.cs +++ b/src/Discord.Net.Serialization/Extensions/BufferExtensions.cs @@ -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 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 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 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 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 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 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 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 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; + } } }