Browse Source

Improved DateTime(Offset) deserialization

voice-allocs
RogueException 7 years ago
parent
commit
0bea893076
1 changed files with 168 additions and 4 deletions
  1. +168
    -4
      src/Discord.Net.Serialization/Extensions/BufferExtensions.cs

+ 168
- 4
src/Discord.Net.Serialization/Extensions/BufferExtensions.cs View File

@@ -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;
}
}
}

Loading…
Cancel
Save