{
internal static partial class JsonHelpers
{
+ private struct DateTimeParseData
+ {
+ public int Year;
+ public int Month;
+ public int Day;
+ public int Hour;
+ public int Minute;
+ public int Second;
+ public int Fraction; // This value should never be greater than 9_999_999.
+ public int OffsetHours;
+ public int OffsetMinutes;
+ public bool OffsetNegative => OffsetToken == JsonConstants.Hyphen;
+ public byte OffsetToken;
+ }
+
/// <summary>
/// Parse the given UTF-8 <paramref name="source"/> as extended ISO 8601 format.
/// </summary>
/// <returns>"true" if successfully parsed.</returns>
public static bool TryParseAsISO(ReadOnlySpan<byte> source, out DateTime value)
{
- if (!TryParseDateTimeOffset(source, out DateTimeOffset dateTimeOffset, out DateTimeKind kind))
+ DateTimeParseData parseData = new DateTimeParseData();
+
+ if (!TryParseDateTimeOffset(source, ref parseData))
{
value = default;
return false;
}
- switch (kind)
+ if (parseData.OffsetToken == JsonConstants.UtcOffsetToken)
{
- case DateTimeKind.Local:
- value = dateTimeOffset.LocalDateTime;
- break;
- case DateTimeKind.Utc:
- value = dateTimeOffset.UtcDateTime;
- break;
- default:
- Debug.Assert(kind == DateTimeKind.Unspecified);
- value = dateTimeOffset.DateTime;
- break;
+ return TryCreateDateTime(ref parseData, DateTimeKind.Utc, out value);
}
+ else if (parseData.OffsetToken == JsonConstants.Plus || parseData.OffsetToken == JsonConstants.Hyphen)
+ {
+ if (!TryCreateDateTimeOffset(ref parseData, out DateTimeOffset dateTimeOffset))
+ {
+ value = default;
+ return false;
+ }
- return true;
+ value = dateTimeOffset.LocalDateTime;
+ return true;
+ }
+
+ return TryCreateDateTime(ref parseData, DateTimeKind.Unspecified, out value);
}
/// <summary>
/// <returns>"true" if successfully parsed.</returns>
public static bool TryParseAsISO(ReadOnlySpan<byte> source, out DateTimeOffset value)
{
- return TryParseDateTimeOffset(source, out value, out _);
- }
+ DateTimeParseData parseData = new DateTimeParseData();
- private struct DateTimeParseData
- {
- public int Year;
- public int Month;
- public int Day;
- public int Hour;
- public int Minute;
- public int Second;
- public int Fraction; // This value should never be greater than 9_999_999.
- public int OffsetHours;
- public int OffsetMinutes;
- public bool OffsetNegative => OffsetToken == JsonConstants.Hyphen;
- public byte OffsetToken;
+ if (!TryParseDateTimeOffset(source, ref parseData))
+ {
+ value = default;
+ return false;
+ }
+
+ if (parseData.OffsetToken == JsonConstants.UtcOffsetToken || // Same as specifying an offset of "+00:00", except that DateTime's Kind gets set to UTC rather than Local
+ parseData.OffsetToken == JsonConstants.Plus || parseData.OffsetToken == JsonConstants.Hyphen)
+ {
+ return TryCreateDateTimeOffset(ref parseData, out value);
+ }
+
+ // No offset, attempt to read as local time.
+ return TryCreateDateTimeOffsetInterpretingDataAsLocalTime(ref parseData, out value);
}
/// <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="value">The parsed <see cref="DateTimeOffset"/> for the given <paramref name="source"/>.</param>
- /// <param name="kind">
- /// The parsed <see cref="DateTimeKind"/> for extracting the most relevant <see cref="DateTime"/> when
- /// needed.
- /// </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.
/// Spaces are not permitted.
/// </remarks>
/// <returns>"true" if successfully parsed.</returns>
- private static bool TryParseDateTimeOffset(ReadOnlySpan<byte> source, out DateTimeOffset value, out DateTimeKind kind)
+ private static bool TryParseDateTimeOffset(ReadOnlySpan<byte> source, ref DateTimeParseData parseData)
{
- value = default;
- kind = default;
-
// Source does not have enough characters for YYYY-MM-DD
if (source.Length < 10)
{
// just [year][“-”][month] (a) and just [year] (b), but we currently
// don't permit it.
- DateTimeParseData parseData = new DateTimeParseData();
-
{
uint digit1 = source[0] - (uint)'0';
uint digit2 = source[1] - (uint)'0';
if (source.Length == 10)
{
// Just a calendar date
- return FinishParsing(ref parseData, out value, out kind);
+ return true;
}
// Parse the time of day
Debug.Assert(source.Length >= 16);
if (source.Length == 16)
{
- return FinishParsing(ref parseData, out value, out kind);
+ return true;
}
byte curByte = source[16];
{
case JsonConstants.UtcOffsetToken:
parseData.OffsetToken = JsonConstants.UtcOffsetToken;
- return sourceIndex == source.Length
- && FinishParsing(ref parseData, out value, out kind);
+ return sourceIndex == source.Length;
case JsonConstants.Plus:
case JsonConstants.Hyphen:
parseData.OffsetToken = curByte;
- return ParseOffset(ref parseData, source.Slice(sourceIndex))
- && FinishParsing(ref parseData, out value, out kind);
+ return ParseOffset(ref parseData, source.Slice(sourceIndex));
case JsonConstants.Colon:
break;
default:
Debug.Assert(source.Length >= 19);
if (source.Length == 19)
{
- return FinishParsing(ref parseData, out value, out kind);
+ return true;
}
curByte = source[19];
{
case JsonConstants.UtcOffsetToken:
parseData.OffsetToken = JsonConstants.UtcOffsetToken;
- return sourceIndex == source.Length
- && FinishParsing(ref parseData, out value, out kind);
+ return sourceIndex == source.Length;
case JsonConstants.Plus:
case JsonConstants.Hyphen:
parseData.OffsetToken = curByte;
- return ParseOffset(ref parseData, source.Slice(sourceIndex))
- && FinishParsing(ref parseData, out value, out kind);
+ return ParseOffset(ref parseData, source.Slice(sourceIndex));
case JsonConstants.Period:
break;
default:
Debug.Assert(sourceIndex <= source.Length);
if (sourceIndex == source.Length)
{
- return FinishParsing(ref parseData, out value, out kind);
+ return true;
}
curByte = source[sourceIndex++];
{
case JsonConstants.UtcOffsetToken:
parseData.OffsetToken = JsonConstants.UtcOffsetToken;
- return sourceIndex == source.Length
- && FinishParsing(ref parseData, out value, out kind);
+ return sourceIndex == source.Length;
case JsonConstants.Plus:
case JsonConstants.Hyphen:
parseData.OffsetToken = curByte;
return ParseOffset(ref parseData, source.Slice(sourceIndex))
- && FinishParsing(ref parseData, out value, out kind);
+ && true;
default:
return false;
}
return true;
}
-
- 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;
- }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
yield return new object[] { "\"1997-07-16T19:20:30.45\"", "1997-07-16T19:20:30.45" };
yield return new object[] { "\"1997-07-16T19:20:30.4555555\"", "1997-07-16T19:20:30.4555555" };
- // Regression test for https://github.com/dotnet/corefx/issues/39067
- yield return new object[] { "\"0001-01-01T00:00:00\"", "0001-01-01T00:00:00" };
-
// Test fractions.
yield return new object[] { "\"1997-07-16T19:20:30.0\"", "1997-07-16T19:20:30" };
yield return new object[] { "\"1997-07-16T19:20:30.000\"", "1997-07-16T19:20:30" };
<Compile Include="Utf8JsonReaderTests.cs" />
<Compile Include="Utf8JsonReaderTests.MultiSegment.cs" />
<Compile Include="Utf8JsonReaderTests.TryGet.cs" />
+ <Compile Include="Utf8JsonReaderTests.TryGet.Date.cs" />
<Compile Include="Utf8JsonReaderTests.ValueTextEquals.cs" />
<Compile Include="Utf8JsonWriterTests.cs" />
</ItemGroup>
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Globalization;
+using Xunit;
+using Newtonsoft.Json;
+
+namespace System.Text.Json.Tests
+{
+ public static partial class Utf8JsonReaderTests
+ {
+ [Theory]
+ [MemberData(nameof(JsonDateTimeTestData.ValidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))]
+ public static void TestingStringsConversionToDateTime(string jsonString, string expectedString)
+ {
+ byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+ var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
+ while (json.Read())
+ {
+ if (json.TokenType == JsonTokenType.String)
+ {
+ DateTime expected = DateTime.Parse(expectedString);
+
+ Assert.True(json.TryGetDateTime(out DateTime actual));
+ Assert.Equal(expected, actual);
+
+ Assert.Equal(expected, json.GetDateTime());
+ }
+ }
+
+ Assert.Equal(dataUtf8.Length, json.BytesConsumed);
+ }
+
+ [Theory]
+ [MemberData(nameof(JsonDateTimeTestData.ValidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))]
+ public static void TestingStringsConversionToDateTimeOffset(string jsonString, string expectedString)
+ {
+ byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+ var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
+ while (json.Read())
+ {
+ if (json.TokenType == JsonTokenType.String)
+ {
+ DateTimeOffset expected = DateTimeOffset.Parse(expectedString);
+
+ Assert.True(json.TryGetDateTimeOffset(out DateTimeOffset actual));
+ Assert.Equal(expected, actual);
+
+ Assert.Equal(expected, json.GetDateTimeOffset());
+ }
+ }
+
+ Assert.Equal(dataUtf8.Length, json.BytesConsumed);
+ }
+
+ [Theory]
+ [MemberData(nameof(JsonDateTimeTestData.ValidISO8601TestsWithUtcOffset), MemberType = typeof(JsonDateTimeTestData))]
+ public static void TestingStringsWithUTCOffsetToDateTime(string jsonString, string expectedString)
+ {
+ byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+ var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
+ while (json.Read())
+ {
+ if (json.TokenType == JsonTokenType.String)
+ {
+ DateTime expected = DateTime.ParseExact(expectedString, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
+
+ Assert.True(json.TryGetDateTime(out DateTime actual));
+ Assert.Equal(expected, actual);
+
+ Assert.Equal(expected, json.GetDateTime());
+ }
+ }
+
+ Assert.Equal(dataUtf8.Length, json.BytesConsumed);
+ }
+
+ [Theory]
+ [MemberData(nameof(JsonDateTimeTestData.ValidISO8601TestsWithUtcOffset), MemberType = typeof(JsonDateTimeTestData))]
+ public static void TestingStringsWithUTCOffsetToDateTimeOffset(string jsonString, string expectedString)
+ {
+ byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+ var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
+ while (json.Read())
+ {
+ if (json.TokenType == JsonTokenType.String)
+ {
+ DateTimeOffset expected = DateTimeOffset.ParseExact(expectedString, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
+
+ Assert.True(json.TryGetDateTimeOffset(out DateTimeOffset actual));
+ Assert.Equal(expected, actual);
+
+ Assert.Equal(expected, json.GetDateTimeOffset());
+ }
+ }
+
+ Assert.Equal(dataUtf8.Length, json.BytesConsumed);
+ }
+
+ [Theory]
+ [MemberData(nameof(JsonDateTimeTestData.InvalidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))]
+ public static void TestingStringsInvalidConversionToDateTime(string jsonString)
+ {
+ byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+ var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
+ while (json.Read())
+ {
+ Assert.False(json.TryGetDateTime(out DateTime actualDateTime));
+ Assert.Equal(default, actualDateTime);
+
+ try
+ {
+ DateTime value = json.GetDateTime();
+ Assert.True(false, "Expected GetDateTime to throw FormatException due to invalid ISO 8601 input.");
+ }
+ catch (FormatException)
+ { }
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(JsonDateTimeTestData.InvalidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))]
+ public static void TestingStringsInvalidConversionToDateTimeOffset(string jsonString)
+ {
+ byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+ var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
+ while (json.Read())
+ {
+ if (json.TokenType == JsonTokenType.String)
+ {
+ Assert.False(json.TryGetDateTimeOffset(out DateTimeOffset actualDateTime));
+ Assert.Equal(default, actualDateTime);
+
+ try
+ {
+ DateTimeOffset value = json.GetDateTimeOffset();
+ Assert.True(false, "Expected GetDateTimeOffset to throw FormatException due to invalid ISO 8601 input.");
+ }
+ catch (FormatException)
+ { }
+ }
+ }
+ }
+
+ [Fact]
+ public static void Regression39067_TestingDateTimeMinValue()
+ {
+ string jsonString = @"""0001-01-01T00:00:00""";
+ string expectedString = "0001-01-01T00:00:00";
+ byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+ var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
+ while (json.Read())
+ {
+ if (json.TokenType == JsonTokenType.String)
+ {
+ DateTime expected = DateTime.Parse(expectedString);
+
+ Assert.True(json.TryGetDateTime(out DateTime actual));
+ Assert.Equal(expected, actual);
+
+ Assert.Equal(expected, json.GetDateTime());
+ }
+ }
+
+ Assert.Equal(dataUtf8.Length, json.BytesConsumed);
+
+ // Test upstream serializer.
+ Assert.Equal(DateTime.Parse(expectedString), JsonSerializer.Deserialize<DateTime>(jsonString));
+ }
+
+ [Fact]
+ public static void TestingDateTimeMaxValue()
+ {
+ string jsonString = @"""9999-12-31T23:59:59""";
+ string expectedString = "9999-12-31T23:59:59";
+ byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+ var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
+ while (json.Read())
+ {
+ if (json.TokenType == JsonTokenType.String)
+ {
+ DateTime expected = DateTime.Parse(expectedString);
+
+ Assert.True(json.TryGetDateTime(out DateTime actual));
+ Assert.Equal(expected, actual);
+
+ Assert.Equal(expected, json.GetDateTime());
+ }
+ }
+
+ Assert.Equal(dataUtf8.Length, json.BytesConsumed);
+
+ // Test upstream serializer.
+ Assert.Equal(DateTime.Parse(expectedString), JsonSerializer.Deserialize<DateTime>(jsonString));
+ }
+ }
+}
}
[Theory]
- [MemberData(nameof(JsonDateTimeTestData.ValidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))]
- public static void TestingStringsConversionToDateTime(string jsonString, string expectedString)
- {
- byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
-
- var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
- while (json.Read())
- {
- if (json.TokenType == JsonTokenType.String)
- {
- DateTime expected = DateTime.Parse(expectedString);
-
- Assert.True(json.TryGetDateTime(out DateTime actual));
- Assert.Equal(expected, actual);
-
- Assert.Equal(expected, json.GetDateTime());
- }
- }
-
- Assert.Equal(dataUtf8.Length, json.BytesConsumed);
- }
-
- [Theory]
- [MemberData(nameof(JsonDateTimeTestData.ValidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))]
- public static void TestingStringsConversionToDateTimeOffset(string jsonString, string expectedString)
- {
- byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
-
- var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
- while (json.Read())
- {
- if (json.TokenType == JsonTokenType.String)
- {
- DateTimeOffset expected = DateTimeOffset.Parse(expectedString);
-
- Assert.True(json.TryGetDateTimeOffset(out DateTimeOffset actual));
- Assert.Equal(expected, actual);
-
- Assert.Equal(expected, json.GetDateTimeOffset());
- }
- }
-
- Assert.Equal(dataUtf8.Length, json.BytesConsumed);
- }
-
- [Theory]
- [MemberData(nameof(JsonDateTimeTestData.ValidISO8601TestsWithUtcOffset), MemberType = typeof(JsonDateTimeTestData))]
- public static void TestingStringsWithUTCOffsetToDateTime(string jsonString, string expectedString)
- {
- byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
-
- var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
- while (json.Read())
- {
- if (json.TokenType == JsonTokenType.String)
- {
- DateTime expected = DateTime.ParseExact(expectedString, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
-
- Assert.True(json.TryGetDateTime(out DateTime actual));
- Assert.Equal(expected, actual);
-
- Assert.Equal(expected, json.GetDateTime());
- }
- }
-
- Assert.Equal(dataUtf8.Length, json.BytesConsumed);
- }
-
- [Theory]
- [MemberData(nameof(JsonDateTimeTestData.ValidISO8601TestsWithUtcOffset), MemberType = typeof(JsonDateTimeTestData))]
- public static void TestingStringsWithUTCOffsetToDateTimeOffset(string jsonString, string expectedString)
- {
- byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
-
- var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
- while (json.Read())
- {
- if (json.TokenType == JsonTokenType.String)
- {
- DateTimeOffset expected = DateTimeOffset.ParseExact(expectedString, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
-
- Assert.True(json.TryGetDateTimeOffset(out DateTimeOffset actual));
- Assert.Equal(expected, actual);
-
- Assert.Equal(expected, json.GetDateTimeOffset());
- }
- }
-
- Assert.Equal(dataUtf8.Length, json.BytesConsumed);
- }
-
- [Theory]
- [MemberData(nameof(JsonDateTimeTestData.InvalidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))]
- public static void TestingStringsInvalidConversionToDateTime(string jsonString)
- {
- byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
-
- var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
- while (json.Read())
- {
- Assert.False(json.TryGetDateTime(out DateTime actualDateTime));
- Assert.Equal(default, actualDateTime);
-
- try
- {
- DateTime value = json.GetDateTime();
- Assert.True(false, "Expected GetDateTime to throw FormatException due to invalid ISO 8601 input.");
- }
- catch (FormatException)
- { }
- }
- }
-
- [Theory]
- [MemberData(nameof(JsonDateTimeTestData.InvalidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))]
- public static void TestingStringsInvalidConversionToDateTimeOffset(string jsonString)
- {
- byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
-
- var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
- while (json.Read())
- {
- if (json.TokenType == JsonTokenType.String)
- {
- Assert.False(json.TryGetDateTimeOffset(out DateTimeOffset actualDateTime));
- Assert.Equal(default, actualDateTime);
-
- try
- {
- DateTimeOffset value = json.GetDateTimeOffset();
- Assert.True(false, "Expected GetDateTimeOffset to throw FormatException due to invalid ISO 8601 input.");
- }
- catch (FormatException)
- { }
- }
- }
- }
-
- [Theory]
[MemberData(nameof(JsonGuidTestData.ValidGuidTests), MemberType = typeof(JsonGuidTestData))]
[MemberData(nameof(JsonGuidTestData.ValidHexGuidTests), MemberType = typeof(JsonGuidTestData))]
public static void TestingStringsConversionToGuid(string testString, string expectedString)