Add (Try)GetDateTime(Offset) to JsonElement
authorLayomi Akinrinade <layomia@gmail.com>
Sun, 17 Mar 2019 20:20:45 +0000 (16:20 -0400)
committerJeremy Barton <jbarton@microsoft.com>
Sun, 17 Mar 2019 20:20:45 +0000 (13:20 -0700)
Commit migrated from https://github.com/dotnet/corefx/commit/a5d58ccf75741365c9a0f82c5387a9ae2ec99e16

14 files changed:
src/libraries/System.Text.Json/ref/System.Text.Json.cs
src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs
src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs
src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.Date.cs [moved from src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.Date.cs with 99% similarity]
src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs
src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs
src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.MultiSegment.cs
src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs
src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.cs
src/libraries/System.Text.Json/tests/JsonDateTimeTestData.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/JsonDocumentTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests.csproj
src/libraries/System.Text.Json/tests/Utf8JsonReaderTests.TryGet.cs

index c342170..358bce6 100644 (file)
@@ -35,6 +35,8 @@ namespace System.Text.Json
         public System.Text.Json.JsonElement.ObjectEnumerator EnumerateObject() { throw null; }
         public int GetArrayLength() { throw null; }
         public bool GetBoolean() { throw null; }
+        public DateTime GetDateTime() { throw null; }
+        public DateTimeOffset GetDateTimeOffset() { throw null; }
         public decimal GetDecimal() { throw null; }
         public double GetDouble() { throw null; }
         public int GetInt32() { throw null; }
@@ -50,6 +52,8 @@ namespace System.Text.Json
         [System.CLSCompliantAttribute(false)]
         public ulong GetUInt64() { throw null; }
         public override string ToString() { throw null; }
+        public bool TryGetDateTime(out DateTime value) { throw null; }
+        public bool TryGetDateTimeOffset(out DateTimeOffset value) { throw null; }
         public bool TryGetDecimal(out decimal value) { throw null; }
         public bool TryGetDouble(out double value) { throw null; }
         public bool TryGetInt32(out int value) { throw null; }
index 4d8b67c..fdb1f43 100644 (file)
@@ -15,6 +15,7 @@
     <Compile Include="System\Text\Json\JsonCommentHandling.cs" />
     <Compile Include="System\Text\Json\JsonConstants.cs" />
     <Compile Include="System\Text\Json\JsonHelpers.cs" />
+    <Compile Include="System\Text\Json\JsonHelpers.Date.cs" />
     <Compile Include="System\Text\Json\JsonTokenType.cs" />
     <Compile Include="System\Text\Json\ThrowHelper.cs" />
     <Compile Include="System\Text\Json\ThrowHelper.Serialization.cs" />
@@ -34,7 +35,6 @@
     <Compile Include="System\Text\Json\Reader\ConsumeTokenResult.cs" />
     <Compile Include="System\Text\Json\Reader\JsonReaderException.cs" />
     <Compile Include="System\Text\Json\Reader\JsonReaderHelper.cs" />
-    <Compile Include="System\Text\Json\Reader\JsonReaderHelper.Date.cs" />
     <Compile Include="System\Text\Json\Reader\JsonReaderHelper.Unescaping.cs" />
     <Compile Include="System\Text\Json\Reader\JsonReaderOptions.cs" />
     <Compile Include="System\Text\Json\Reader\JsonReaderState.cs" />
index ffcd37f..6f893ce 100644 (file)
@@ -454,6 +454,56 @@ namespace System.Text.Json
         /// <summary>
         ///   This is an implementation detail and MUST NOT be called by source-package consumers.
         /// </summary>
+        internal bool TryGetValue(int index, out DateTime value)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            CheckExpectedType(JsonTokenType.String, row.TokenType);
+
+            ReadOnlySpan<byte> data = _utf8Json.Span;
+            ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);
+
+            if (JsonHelpers.TryParseAsISO(segment, out DateTime tmp, out int bytesConsumed) &&
+                segment.Length == bytesConsumed)
+            {
+                value = tmp;
+                return true;
+            }
+
+            value = default;
+            return false;
+        }
+
+        /// <summary>
+        ///   This is an implementation detail and MUST NOT be called by source-package consumers.
+        /// </summary>
+        internal bool TryGetValue(int index, out DateTimeOffset value)
+        {
+            CheckNotDisposed();
+
+            DbRow row = _parsedData.Get(index);
+
+            CheckExpectedType(JsonTokenType.String, row.TokenType);
+
+            ReadOnlySpan<byte> data = _utf8Json.Span;
+            ReadOnlySpan<byte> segment = data.Slice(row.Location, row.SizeOrLength);
+
+            if (JsonHelpers.TryParseAsISO(segment, out DateTimeOffset tmp, out int bytesConsumed) &&
+                segment.Length == bytesConsumed)
+            {
+                value = tmp;
+                return true;
+            }
+
+            value = default;
+            return false;
+        }
+
+        /// <summary>
+        ///   This is an implementation detail and MUST NOT be called by source-package consumers.
+        /// </summary>
         internal string GetRawValueAsString(int index)
         {
             ReadOnlyMemory<byte> segment = GetRawValue(index, includeQuotes: true);
index d5ed812..266a1ee 100644 (file)
@@ -741,6 +741,102 @@ namespace System.Text.Json
         }
 
         /// <summary>
+        ///   Attempts to represent the current JSON string as a <see cref="DateTime"/>.
+        /// </summary>
+        /// <param name="value">Receives the value.</param>
+        /// <remarks>
+        ///   This method does not create a DateTime representation of values other than JSON strings.
+        /// </remarks>
+        /// <returns>
+        ///   <see langword="true"/> if the string can be represented as a <see cref="DateTime"/>,
+        ///   <see langword="false"/> otherwise.
+        /// </returns>
+        /// <exception cref="InvalidOperationException">
+        ///   This value's <see cref="Type"/> is not <see cref="JsonValueType.String"/>.
+        /// </exception>
+        /// <exception cref="ObjectDisposedException">
+        ///   The parent <see cref="JsonDocument"/> has been disposed.
+        /// </exception>
+        public bool TryGetDateTime(out DateTime value)
+        {
+            CheckValidInstance();
+
+            return _parent.TryGetValue(_idx, out value);
+        }
+
+        /// <summary>
+        ///   Gets the value of the element as a <see cref="DateTime"/>.
+        /// </summary>
+        /// <remarks>
+        ///   This method does not create a DateTime representation of values other than JSON strings.
+        /// </remarks>
+        /// <returns>The value of the element as a <see cref="DateTime"/>.</returns>
+        /// <exception cref="InvalidOperationException">
+        ///   This value's <see cref="Type"/> is not <see cref="JsonValueType.String"/>.
+        /// </exception>
+        /// <exception cref="ObjectDisposedException">
+        ///   The parent <see cref="JsonDocument"/> has been disposed.
+        /// </exception>
+        /// <seealso cref="ToString"/>
+        public DateTime GetDateTime()
+        {
+            if (TryGetDateTime(out DateTime value))
+            {
+                return value;
+            }
+
+            throw new FormatException();
+        }
+
+        /// <summary>
+        ///   Attempts to represent the current JSON string as a <see cref="DateTimeOffset"/>.
+        /// </summary>
+        /// <param name="value">Receives the value.</param>
+        /// <remarks>
+        ///   This method does not create a DateTimeOffset representation of values other than JSON strings.
+        /// </remarks>
+        /// <returns>
+        ///   <see langword="true"/> if the string can be represented as a <see cref="DateTimeOffset"/>,
+        ///   <see langword="false"/> otherwise.
+        /// </returns>
+        /// <exception cref="InvalidOperationException">
+        ///   This value's <see cref="Type"/> is not <see cref="JsonValueType.String"/>.
+        /// </exception>
+        /// <exception cref="ObjectDisposedException">
+        ///   The parent <see cref="JsonDocument"/> has been disposed.
+        /// </exception>
+        public bool TryGetDateTimeOffset(out DateTimeOffset value)
+        {
+            CheckValidInstance();
+
+            return _parent.TryGetValue(_idx, out value);
+        }
+
+        /// <summary>
+        ///   Gets the value of the element as a <see cref="DateTimeOffset"/>.
+        /// </summary>
+        /// <remarks>
+        ///   This method does not create a DateTimeOffset representation of values other than JSON strings.
+        /// </remarks>
+        /// <returns>The value of the element as a <see cref="DateTimeOffset"/>.</returns>
+        /// <exception cref="InvalidOperationException">
+        ///   This value's <see cref="Type"/> is not <see cref="JsonValueType.String"/>.
+        /// </exception>
+        /// <exception cref="ObjectDisposedException">
+        ///   The parent <see cref="JsonDocument"/> has been disposed.
+        /// </exception>
+        /// <seealso cref="ToString"/>
+        public DateTimeOffset GetDateTimeOffset()
+        {
+            if (TryGetDateTimeOffset(out DateTimeOffset value))
+            {
+                return value;
+            }
+
+            throw new FormatException();
+        }
+
+        /// <summary>
         ///   This is an implementation detail and MUST NOT be called by source-package consumers.
         /// </summary>
         internal string GetPropertyName()
@@ -7,7 +7,7 @@ using System.Runtime.CompilerServices;
 
 namespace System.Text.Json
 {
-    internal static partial class JsonReaderHelper
+    internal static partial class JsonHelpers
     {
         public static bool TryParseAsISO(ReadOnlySpan<byte> source, out DateTime value, out int bytesConsumed)
         {
index 6184b0e..6d99ab5 100644 (file)
@@ -63,5 +63,11 @@ namespace System.Text.Json
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static bool IsInRangeInclusive(JsonTokenType value, JsonTokenType lowerBound, JsonTokenType upperBound)
             => (value - lowerBound) <= (upperBound - lowerBound);
+
+        /// <summary>
+        /// Returns <see langword="true"/> iff <paramref name="value"/> is in the range [0..9].
+        /// Otherwise, returns <see langword="false"/>.
+        /// </summary>
+        public static bool IsDigit(byte value) => (uint)(value - '0') <= '9' - '0';
     }
 }
index 1620655..5af1352 100644 (file)
@@ -48,10 +48,6 @@ namespace System.Text.Json
             }
         }
 
-        // A digit is valid if it is in the range: [0..9]
-        // Otherwise, return false.
-        public static bool IsDigit(byte nextByte) => (uint)(nextByte - '0') <= '9' - '0';
-
         // Returns true if the TokenType is a primitive "value", i.e. String, Number, True, False, and Null
         // Otherwise, return false.
         public static bool IsTokenTypePrimitive(JsonTokenType tokenType) =>
index 1f849c3..2a0a2d5 100644 (file)
@@ -323,7 +323,7 @@ namespace System.Text.Json
             }
             else
             {
-                if (JsonReaderHelper.IsDigit(first) || first == '-')
+                if (JsonHelpers.IsDigit(first) || first == '-')
                 {
                     if (!TryGetNumberMultiSegment(_buffer.Slice(_consumed), out int numberOfBytes))
                     {
@@ -422,7 +422,7 @@ namespace System.Text.Json
                 {
                     StartArray();
                 }
-                else if (JsonReaderHelper.IsDigit(marker) || marker == '-')
+                else if (JsonHelpers.IsDigit(marker) || marker == '-')
                 {
                     return ConsumeNumberMultiSegment();
                 }
@@ -1333,7 +1333,7 @@ namespace System.Text.Json
                 }
 
                 nextByte = data[i];
-                if (!JsonReaderHelper.IsDigit(nextByte))
+                if (!JsonHelpers.IsDigit(nextByte))
                 {
                     ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.RequiredDigitNotFoundAfterSign, nextByte);
                 }
@@ -1400,7 +1400,7 @@ namespace System.Text.Json
             for (; i < data.Length; i++)
             {
                 nextByte = data[i];
-                if (!JsonReaderHelper.IsDigit(nextByte))
+                if (!JsonHelpers.IsDigit(nextByte))
                 {
                     break;
                 }
@@ -1438,7 +1438,7 @@ namespace System.Text.Json
                     for (; i < data.Length; i++)
                     {
                         nextByte = data[i];
-                        if (!JsonReaderHelper.IsDigit(nextByte))
+                        if (!JsonHelpers.IsDigit(nextByte))
                         {
                             break;
                         }
@@ -1493,7 +1493,7 @@ namespace System.Text.Json
                 data = _buffer;
             }
             byte nextByte = data[i];
-            if (!JsonReaderHelper.IsDigit(nextByte))
+            if (!JsonHelpers.IsDigit(nextByte))
             {
                 ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.RequiredDigitNotFoundAfterDecimal, nextByte);
             }
@@ -1552,7 +1552,7 @@ namespace System.Text.Json
                 nextByte = data[i];
             }
 
-            if (!JsonReaderHelper.IsDigit(nextByte))
+            if (!JsonHelpers.IsDigit(nextByte))
             {
                 ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.RequiredDigitNotFoundAfterSign, nextByte);
             }
index c98bdf9..861fc1e 100644 (file)
@@ -447,7 +447,7 @@ namespace System.Text.Json
             }
 
             ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
-            return JsonReaderHelper.TryParseAsISO(span, out value, out int bytesConsumed) && span.Length == bytesConsumed;
+            return JsonHelpers.TryParseAsISO(span, out value, out int bytesConsumed) && span.Length == bytesConsumed;
         }
 
         /// <summary>
@@ -468,7 +468,7 @@ namespace System.Text.Json
             }
 
             ReadOnlySpan<byte> span = HasValueSequence ? ValueSequence.ToArray() : ValueSpan;
-            return JsonReaderHelper.TryParseAsISO(span, out value, out int bytesConsumed) && span.Length == bytesConsumed;
+            return JsonHelpers.TryParseAsISO(span, out value, out int bytesConsumed) && span.Length == bytesConsumed;
         }
     }
 }
index aec49d9..91c8377 100644 (file)
@@ -720,7 +720,7 @@ namespace System.Text.Json
                 // Create local copy to avoid bounds checks.
                 ReadOnlySpan<byte> localBuffer = _buffer;
 
-                if (JsonReaderHelper.IsDigit(first) || first == '-')
+                if (JsonHelpers.IsDigit(first) || first == '-')
                 {
                     if (!TryGetNumber(localBuffer.Slice(_consumed), out int numberOfBytes))
                     {
@@ -835,7 +835,7 @@ namespace System.Text.Json
                 {
                     StartArray();
                 }
-                else if (JsonReaderHelper.IsDigit(marker) || marker == '-')
+                else if (JsonHelpers.IsDigit(marker) || marker == '-')
                 {
                     return ConsumeNumber();
                 }
@@ -1330,7 +1330,7 @@ namespace System.Text.Json
                 }
 
                 nextByte = data[i];
-                if (!JsonReaderHelper.IsDigit(nextByte))
+                if (!JsonHelpers.IsDigit(nextByte))
                 {
                     _bytePositionInLine += i;
                     ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.RequiredDigitNotFoundAfterSign, nextByte);
@@ -1382,7 +1382,7 @@ namespace System.Text.Json
             for (; i < data.Length; i++)
             {
                 nextByte = data[i];
-                if (!JsonReaderHelper.IsDigit(nextByte))
+                if (!JsonHelpers.IsDigit(nextByte))
                 {
                     break;
                 }
@@ -1421,7 +1421,7 @@ namespace System.Text.Json
                 return ConsumeNumberResult.NeedMoreData;
             }
             byte nextByte = data[i];
-            if (!JsonReaderHelper.IsDigit(nextByte))
+            if (!JsonHelpers.IsDigit(nextByte))
             {
                 _bytePositionInLine += i;
                 ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.RequiredDigitNotFoundAfterDecimal, nextByte);
@@ -1459,7 +1459,7 @@ namespace System.Text.Json
                 nextByte = data[i];
             }
 
-            if (!JsonReaderHelper.IsDigit(nextByte))
+            if (!JsonHelpers.IsDigit(nextByte))
             {
                 _bytePositionInLine += i;
                 ThrowHelper.ThrowJsonReaderException(ref this, ExceptionResource.RequiredDigitNotFoundAfterSign, nextByte);
diff --git a/src/libraries/System.Text.Json/tests/JsonDateTimeTestData.cs b/src/libraries/System.Text.Json/tests/JsonDateTimeTestData.cs
new file mode 100644 (file)
index 0000000..d393fbd
--- /dev/null
@@ -0,0 +1,184 @@
+// 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.Collections.Generic;
+using Xunit;
+
+namespace System.Text.Json.Tests
+{
+    internal class JsonDateTimeTestData
+    {
+        // Test string, Argument to DateTime(Offset).Parse(Exact)
+        public static IEnumerable<object[]> ValidISO8601Tests()
+        {
+            yield return new object[] { "\"0997-07-16\"", "0997-07-16" };
+            yield return new object[] { "\"1997-07-16\"", "1997-07-16" };
+            yield return new object[] { "\"1997-07-16T19:20\"", "1997-07-16T19:20" };
+            yield return new object[] { "\"1997-07-16T19:20:30\"", "1997-07-16T19:20:30" };
+            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" };
+
+            // Skip test T24:00 till #35830 is fixed.
+            // yield return new object[] { "\"1997-07-16T24:00\"", "1997-07-16T24: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" };
+            yield return new object[] { "\"1997-07-16T19:20:30.0000000\"", "1997-07-16T19:20:30" };
+            yield return new object[] { "\"1997-07-16T19:20:30.01\"", "1997-07-16T19:20:30.01" };
+            yield return new object[] { "\"1997-07-16T19:20:30.0001\"", "1997-07-16T19:20:30.0001" };
+            yield return new object[] { "\"1997-07-16T19:20:30.0000001\"", "1997-07-16T19:20:30.0000001" };
+            yield return new object[] { "\"1997-07-16T19:20:30.0000323\"", "1997-07-16T19:20:30.0000323" };
+            yield return new object[] { "\"1997-07-16T19:20:30.1\"", "1997-07-16T19:20:30.1" };
+            yield return new object[] { "\"1997-07-16T19:20:30.22\"", "1997-07-16T19:20:30.22" };
+            yield return new object[] { "\"1997-07-16T19:20:30.333\"", "1997-07-16T19:20:30.333" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4444\"", "1997-07-16T19:20:30.4444" };
+            yield return new object[] { "\"1997-07-16T19:20:30.55555\"", "1997-07-16T19:20:30.55555" };
+            yield return new object[] { "\"1997-07-16T19:20:30.666666\"", "1997-07-16T19:20:30.666666" };
+            yield return new object[] { "\"1997-07-16T19:20:30.7777777\"", "1997-07-16T19:20:30.7777777" };
+            yield return new object[] { "\"1997-07-16T19:20:30.1000000\"", "1997-07-16T19:20:30.1" };
+            yield return new object[] { "\"1997-07-16T19:20:30.2200000\"", "1997-07-16T19:20:30.22" };
+            yield return new object[] { "\"1997-07-16T19:20:30.3330000\"", "1997-07-16T19:20:30.333" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4444000\"", "1997-07-16T19:20:30.4444" };
+            yield return new object[] { "\"1997-07-16T19:20:30.5555500\"", "1997-07-16T19:20:30.55555" };
+            yield return new object[] { "\"1997-07-16T19:20:30.6666660\"", "1997-07-16T19:20:30.666666" };
+
+            // Test fraction truncation.
+            yield return new object[] { "\"1997-07-16T19:20:30.0000000000000\"", "1997-07-16T19:20:30" };
+            yield return new object[] { "\"1997-07-16T19:20:30.00000001\"", "1997-07-16T19:20:30" };
+            yield return new object[] { "\"1997-07-16T19:20:30.00000000000001\"", "1997-07-16T19:20:30" };
+            yield return new object[] { "\"1997-07-16T19:20:30.77777770\"", "1997-07-16T19:20:30.7777777" };
+            yield return new object[] { "\"1997-07-16T19:20:30.777777700\"", "1997-07-16T19:20:30.7777777" };
+            yield return new object[] { "\"1997-07-16T19:20:30.45555554\"", "1997-07-16T19:20:30.45555554" };
+            // We expect the parser to truncate. `DateTime(Offset).Parse` will round up to 7dp in these cases,
+            // so we pass a string representing the Datetime(Offset) we expect to the `Parse` method.
+            yield return new object[] { "\"1997-07-16T19:20:30.45555555\"", "1997-07-16T19:20:30.4555555" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555555555555555\"", "1997-07-16T19:20:30.4555555" };
+
+            // Test Non-UTC timezone designator (TZD).
+            yield return new object[] { "\"1997-07-16T19:20+01:00\"", "1997-07-16T19:20+01:00" };
+            yield return new object[] { "\"1997-07-16T19:20-01:00\"", "1997-07-16T19:20-01:00" };
+            yield return new object[] { "\"1997-07-16T19:20:30+01:00\"", "1997-07-16T19:20:30+01:00" };
+            yield return new object[] { "\"1997-07-16T19:20:30-01:00\"", "1997-07-16T19:20:30-01:00" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+01:00\"", "1997-07-16T19:20:30.4555555+01:00" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-01:00\"", "1997-07-16T19:20:30.4555555-01:00" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+04:30\"", "1997-07-16T19:20:30.4555555+04:30" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-04:30\"", "1997-07-16T19:20:30.4555555-04:30" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+0100\"", "1997-07-16T19:20:30.4555555+01:00" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-0100\"", "1997-07-16T19:20:30.4555555-01:00" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+0430\"", "1997-07-16T19:20:30.4555555+04:30" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-0430\"", "1997-07-16T19:20:30.4555555-04:30" };
+            // Test Non-UTC TZD without minute.
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+01\"", "1997-07-16T19:20:30.4555555+01:00" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-01\"", "1997-07-16T19:20:30.4555555-01:00" };
+            // Test Non-UTC TZD with max UTC offset hour.
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+14:00\"", "1997-07-16T19:20:30.4555555+14:00" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-14:00\"", "1997-07-16T19:20:30.4555555-14:00" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+1400\"", "1997-07-16T19:20:30.4555555+14:00" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-1400\"", "1997-07-16T19:20:30.4555555-14:00" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+14\"", "1997-07-16T19:20:30.4555555+14:00" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-14\"", "1997-07-16T19:20:30.4555555-14:00" };
+            // Test February 29 on a leap year
+            yield return new object[] { "\"2020-02-29T19:20:30.4555555+10:00\"", "2020-02-29T19:20:30.4555555+10:00" };
+        }
+
+        // UTC TZD tests are separate because `DateTime.Parse` for strings with `Z` TZD will return
+        // a `DateTime` with `DateTimeKind.Local` i.e `+00:00` which does not equal our expected result,
+        // a `DateTime` with `DateTimeKind.Utc` i.e `Z`.
+        // Instead, we need to use `DateTime.ParseExact` which returns a DateTime Utc `DateTimeKind`.
+        //
+        // Test string, Argument to DateTime(Offset).Parse(Exact)
+        public static IEnumerable<object[]> ValidISO8601TestsWithUtcOffset()
+        {
+            yield return new object[] { "\"1997-07-16T19:20Z\"", "1997-07-16T19:20:00.0000000Z" };
+            yield return new object[] { "\"1997-07-16T19:20:30Z\"", "1997-07-16T19:20:30.0000000Z" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555Z\"", "1997-07-16T19:20:30.4555555Z" };
+        }
+
+        public static IEnumerable<object[]> InvalidISO8601Tests()
+        {
+            // Invalid YYYY-MM-DD
+            yield return new object[] { "\"0997 07-16\"" };
+            yield return new object[] { "\"0997-0a-16\"" };
+            yield return new object[] { "\"0997-07 16\"" };
+            yield return new object[] { "\"0997-07-160997-07-16\"" };
+            yield return new object[] { "\"0997-07-16abc\"" };
+            yield return new object[] { "\"0997-07-16 \"" };
+            yield return new object[] { "\"0997-07-16,0997-07-16\"" };
+            yield return new object[] { "\"1997-07-16T19:20abc\"" };
+            yield return new object[] { "\"1997-07-16T19:20, 123\"" };
+            yield return new object[] { "\"997-07-16\"" };
+            yield return new object[] { "\"1997-07\"" };
+            yield return new object[] { "\"1997-7-06\"" };
+            yield return new object[] { "\"1997-07-16T\"" };
+            yield return new object[] { "\"1997-07-6\"" };
+            yield return new object[] { "\"1997-07-6T01\"" };
+            yield return new object[] { "\"1997-07-16Z\"" };
+            yield return new object[] { "\"1997-07-16+01:00\"" };
+            yield return new object[] { "\"19970716\"" };
+            yield return new object[] { "\"0997-07-166\"" };
+            yield return new object[] { "\"1997-07-1sdsad\"" };
+            yield return new object[] { "\"1997-07-16T19200\"" };
+
+            // Invalid YYYY-MM-DDThh:mm
+            yield return new object[] { "\"1997-07-16T1\"" };
+            yield return new object[] { "\"1997-07-16Ta0:00\"" };
+            yield return new object[] { "\"1997-07-16T19: 20:30\"" };
+            yield return new object[] { "\"1997-07-16 19:20:30\"" };
+            yield return new object[] { "\"1997-07-16T19:2030\"" };
+
+            // Invalid YYYY-MM-DDThh:mm:ss
+            yield return new object[] { "\"1997-07-16T19:20:3a\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30a\"" };
+            yield return new object[] { "\"1997-07-16T19:20:3045\"" };
+            yield return new object[] { "\"1997-07-16T19:20:304555555\"" };
+
+            // Invalid fractions.
+            yield return new object[] { "\"1997-07-16T19:20:30,45\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30 .\"" };
+            yield return new object[] { "\"abc1997-07-16T19:20:30.000\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+\"" };
+
+            // Invalid TZD.
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+-Z\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555Z \"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+01Z\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+01:\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555 +01:00\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+01:\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555- 01:00\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+04 :30\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-04: 30\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+0100 \"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-010\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+430\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555--0430\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+0\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-01005\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+14:00a\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-14:0\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-14:00 \"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+14 00\"" };
+
+            // Proper format but invalid calendar date, time, or time zone designator fields
+            yield return new object[] { "\"1997-00-16T19:20:30.4555555\"" };
+            yield return new object[] { "\"1997-07-16T25:20:30.4555555\"" };
+            yield return new object[] { "\"1997-00-16T19:20:30.4555555Z\"" };
+            yield return new object[] { "\"1997-07-16T25:20:30.4555555Z\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+14:30\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-14:30\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+15:00\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555-15:00\"" };
+            yield return new object[] { "\"1997-00-16T19:20:30.4555555+10:00\"" };
+            yield return new object[] { "\"1997-07-16T25:20:30.4555555+10:00\"" };
+            yield return new object[] { "\"1997-07-16T19:60:30.4555555+10:00\"" };
+            yield return new object[] { "\"1997-07-16T19:20:60.4555555+10:00\"" };
+            yield return new object[] { "\"1997-07-16T19:20:30.4555555+10:60\"" };
+            yield return new object[] { "\"0000-07-16T19:20:30.4555555+10:00\"" };
+            yield return new object[] { "\"2019-02-29T19:20:30.4555555+10:00\"" };
+            yield return new object[] { "\"9999-12-31T23:59:59.9999999-01:00\"" }; // This date spills over to year 10_000.
+        }
+    }
+}
index dc2cb8b..ae5739c 100644 (file)
@@ -4,6 +4,7 @@
 
 using System.Buffers;
 using System.Collections;
+using System.Globalization;
 using Newtonsoft.Json;
 using Newtonsoft.Json.Linq;
 using System.Collections.Generic;
@@ -808,6 +809,8 @@ namespace System.Text.Json.Tests
                 }
 
                 Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
+                Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
                 Assert.Throws<InvalidOperationException>(() => root.GetArrayLength());
                 Assert.Throws<InvalidOperationException>(() => root.EnumerateArray());
                 Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
@@ -891,6 +894,8 @@ namespace System.Text.Json.Tests
                 }
 
                 Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
+                Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
                 Assert.Throws<InvalidOperationException>(() => root.GetArrayLength());
                 Assert.Throws<InvalidOperationException>(() => root.EnumerateArray());
                 Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
@@ -944,6 +949,8 @@ namespace System.Text.Json.Tests
                 Assert.Equal(value, root.GetUInt64());
 
                 Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
+                Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
                 Assert.Throws<InvalidOperationException>(() => root.GetArrayLength());
                 Assert.Throws<InvalidOperationException>(() => root.EnumerateArray());
                 Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
@@ -997,6 +1004,8 @@ namespace System.Text.Json.Tests
                 Assert.Throws<FormatException>(() => root.GetUInt64());
 
                 Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
+                Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
                 Assert.Throws<InvalidOperationException>(() => root.GetArrayLength());
                 Assert.Throws<InvalidOperationException>(() => root.EnumerateArray());
                 Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
@@ -1004,6 +1013,87 @@ namespace System.Text.Json.Tests
             }
         }
 
+        [Theory]
+        [MemberData(nameof(JsonDateTimeTestData.ValidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))]
+        public static void ReadDateTimeAndDateTimeOffset(string jsonString, string expectedString)
+        {
+            byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+            using (JsonDocument doc = JsonDocument.Parse(dataUtf8, default))
+            {
+                JsonElement root = doc.RootElement;
+
+                DateTime expectedDateTime = DateTime.Parse(expectedString);
+                DateTimeOffset expectedDateTimeOffset = DateTimeOffset.Parse(expectedString);
+
+                Assert.Equal(JsonValueType.String, root.Type);
+
+                Assert.True(root.TryGetDateTime(out DateTime DateTimeVal));
+                Assert.Equal(expectedDateTime, DateTimeVal);
+
+                Assert.True(root.TryGetDateTimeOffset(out DateTimeOffset DateTimeOffsetVal));
+                Assert.Equal(expectedDateTimeOffset, DateTimeOffsetVal);
+
+                Assert.Equal(expectedDateTime, root.GetDateTime());
+                Assert.Equal(expectedDateTimeOffset, root.GetDateTimeOffset());
+
+                Assert.Throws<InvalidOperationException>(() => root.GetInt32());
+                Assert.Throws<InvalidOperationException>(() => root.GetUInt32());
+                Assert.Throws<InvalidOperationException>(() => root.GetInt64());
+                Assert.Throws<InvalidOperationException>(() => root.GetUInt64());
+                Assert.Throws<InvalidOperationException>(() => root.GetArrayLength());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateArray());
+                Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
+                Assert.Throws<InvalidOperationException>(() => root.GetBoolean());
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(JsonDateTimeTestData.ValidISO8601TestsWithUtcOffset), MemberType = typeof(JsonDateTimeTestData))]
+        public static void ReadDateTimeAndDateTimeOffset_WithUtcOffset(string jsonString, string expectedString)
+        {
+            byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+            using (JsonDocument doc = JsonDocument.Parse(dataUtf8, default))
+            {
+                JsonElement root = doc.RootElement;
+
+                DateTime expectedDateTime = DateTime.ParseExact(expectedString, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
+                DateTimeOffset expectedDateTimeOffset = DateTimeOffset.ParseExact(expectedString, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
+
+                Assert.Equal(JsonValueType.String, root.Type);
+
+                Assert.True(root.TryGetDateTime(out DateTime DateTimeVal));
+                Assert.Equal(expectedDateTime, DateTimeVal);
+
+                Assert.True(root.TryGetDateTimeOffset(out DateTimeOffset DateTimeOffsetVal));
+                Assert.Equal(expectedDateTimeOffset, DateTimeOffsetVal);
+
+                Assert.Equal(expectedDateTime, root.GetDateTime());
+                Assert.Equal(expectedDateTimeOffset, root.GetDateTimeOffset());
+            }
+        }
+
+        [Theory]
+        [MemberData(nameof(JsonDateTimeTestData.InvalidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))]
+        public static void ReadDateTimeAndDateTimeOffset_InvalidTests(string jsonString)
+        {
+            byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
+
+            using (JsonDocument doc = JsonDocument.Parse(dataUtf8, default))
+            {
+                JsonElement root = doc.RootElement;
+
+                Assert.Equal(JsonValueType.String, root.Type);
+
+                Assert.False(root.TryGetDateTime(out DateTime DateTimeVal));
+                Assert.False(root.TryGetDateTimeOffset(out DateTimeOffset DateTimeOffsetVal));
+
+                Assert.Throws<FormatException>(() => root.GetDateTime());
+                Assert.Throws<FormatException>(() => root.GetDateTimeOffset());
+            }
+        }
+
         public static IEnumerable<object[]> NonIntegerCases
         {
             get
@@ -1052,6 +1142,8 @@ namespace System.Text.Json.Tests
                 Assert.Throws<FormatException>(() => root.GetUInt64());
 
                 Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
+                Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
                 Assert.Throws<InvalidOperationException>(() => root.GetArrayLength());
                 Assert.Throws<InvalidOperationException>(() => root.EnumerateArray());
                 Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
@@ -1116,6 +1208,8 @@ namespace System.Text.Json.Tests
                 Assert.Throws<FormatException>(() => root.GetUInt64());
 
                 Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
+                Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
                 Assert.Throws<InvalidOperationException>(() => root.GetArrayLength());
                 Assert.Throws<InvalidOperationException>(() => root.EnumerateArray());
                 Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
@@ -1164,6 +1258,8 @@ namespace System.Text.Json.Tests
                 Assert.Throws<InvalidOperationException>(() => root.GetUInt64());
                 Assert.Throws<InvalidOperationException>(() => root.TryGetUInt64(out ulong _));
                 Assert.Throws<InvalidOperationException>(() => root.GetString());
+                Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
+                Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
                 Assert.Throws<InvalidOperationException>(() => root.EnumerateObject());
                 Assert.Throws<InvalidOperationException>(() => root.GetBoolean());
             }
@@ -1214,6 +1310,8 @@ namespace System.Text.Json.Tests
             Assert.Throws<InvalidOperationException>(() => root.GetUInt64());
             Assert.Throws<InvalidOperationException>(() => root.TryGetUInt64(out ulong _));
             Assert.Throws<InvalidOperationException>(() => root.GetString());
+            Assert.Throws<InvalidOperationException>(() => root.GetDateTime());
+            Assert.Throws<InvalidOperationException>(() => root.GetDateTimeOffset());
             Assert.Throws<InvalidOperationException>(() => root.GetBoolean());
             Assert.Throws<InvalidOperationException>(() => root.GetRawText());
         }
index 672a8b0..32f2047 100644 (file)
@@ -13,6 +13,7 @@
     <Compile Include="BufferSegment.cs" />
     <Compile Include="FixedSizedBufferWriter.cs" />
     <Compile Include="InvalidBufferWriter.cs" />
+    <Compile Include="JsonDateTimeTestData.cs" />
     <Compile Include="JsonDocumentTests.cs" />
     <Compile Include="JsonNumberTestData.cs" />
     <Compile Include="JsonReaderStateAndOptionsTests.cs" />
index 9506a8b..b8d8e8c 100644 (file)
@@ -12,178 +12,6 @@ namespace System.Text.Json.Tests
 {
     public static partial class Utf8JsonReaderTests
     {
-        // Test string, Argument to DateTime(Offset).Parse(Exact)
-        public static IEnumerable<object[]> ValidISO8601Tests()
-        {
-            yield return new object[] { "\"0997-07-16\"", "0997-07-16" };
-            yield return new object[] { "\"1997-07-16\"", "1997-07-16" };
-            yield return new object[] { "\"1997-07-16T19:20\"", "1997-07-16T19:20" };
-            yield return new object[] { "\"1997-07-16T19:20:30\"", "1997-07-16T19:20:30" };
-            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" };
-
-            // Skip test T24:00 till #35830 is fixed.
-            // yield return new object[] { "\"1997-07-16T24:00\"", "1997-07-16T24: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" };
-            yield return new object[] { "\"1997-07-16T19:20:30.0000000\"", "1997-07-16T19:20:30" };
-            yield return new object[] { "\"1997-07-16T19:20:30.01\"", "1997-07-16T19:20:30.01" };
-            yield return new object[] { "\"1997-07-16T19:20:30.0001\"", "1997-07-16T19:20:30.0001" };
-            yield return new object[] { "\"1997-07-16T19:20:30.0000001\"", "1997-07-16T19:20:30.0000001" };
-            yield return new object[] { "\"1997-07-16T19:20:30.0000323\"", "1997-07-16T19:20:30.0000323" };
-            yield return new object[] { "\"1997-07-16T19:20:30.1\"", "1997-07-16T19:20:30.1" };
-            yield return new object[] { "\"1997-07-16T19:20:30.22\"", "1997-07-16T19:20:30.22" };
-            yield return new object[] { "\"1997-07-16T19:20:30.333\"", "1997-07-16T19:20:30.333" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4444\"", "1997-07-16T19:20:30.4444" };
-            yield return new object[] { "\"1997-07-16T19:20:30.55555\"", "1997-07-16T19:20:30.55555" };
-            yield return new object[] { "\"1997-07-16T19:20:30.666666\"", "1997-07-16T19:20:30.666666" };
-            yield return new object[] { "\"1997-07-16T19:20:30.7777777\"", "1997-07-16T19:20:30.7777777" };
-            yield return new object[] { "\"1997-07-16T19:20:30.1000000\"", "1997-07-16T19:20:30.1" };
-            yield return new object[] { "\"1997-07-16T19:20:30.2200000\"", "1997-07-16T19:20:30.22" };
-            yield return new object[] { "\"1997-07-16T19:20:30.3330000\"", "1997-07-16T19:20:30.333" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4444000\"", "1997-07-16T19:20:30.4444" };
-            yield return new object[] { "\"1997-07-16T19:20:30.5555500\"", "1997-07-16T19:20:30.55555" };
-            yield return new object[] { "\"1997-07-16T19:20:30.6666660\"", "1997-07-16T19:20:30.666666" };
-
-            // Test fraction truncation.
-            yield return new object[] { "\"1997-07-16T19:20:30.0000000000000\"", "1997-07-16T19:20:30" };
-            yield return new object[] { "\"1997-07-16T19:20:30.00000001\"", "1997-07-16T19:20:30" };
-            yield return new object[] { "\"1997-07-16T19:20:30.00000000000001\"", "1997-07-16T19:20:30" };
-            yield return new object[] { "\"1997-07-16T19:20:30.77777770\"", "1997-07-16T19:20:30.7777777" };
-            yield return new object[] { "\"1997-07-16T19:20:30.777777700\"", "1997-07-16T19:20:30.7777777" };
-            yield return new object[] { "\"1997-07-16T19:20:30.45555554\"", "1997-07-16T19:20:30.45555554" };
-            // We expect the parser to truncate. `DateTime(Offset).Parse` will round up to 7dp in these cases,
-            // so we pass a string representing the Datetime(Offset) we expect to the `Parse` method.
-            yield return new object[] { "\"1997-07-16T19:20:30.45555555\"", "1997-07-16T19:20:30.4555555" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555555555555555\"", "1997-07-16T19:20:30.4555555" };
-
-            // Test Non-UTC timezone designator (TZD).
-            yield return new object[] { "\"1997-07-16T19:20+01:00\"", "1997-07-16T19:20+01:00" };
-            yield return new object[] { "\"1997-07-16T19:20-01:00\"", "1997-07-16T19:20-01:00" };
-            yield return new object[] { "\"1997-07-16T19:20:30+01:00\"", "1997-07-16T19:20:30+01:00" };
-            yield return new object[] { "\"1997-07-16T19:20:30-01:00\"", "1997-07-16T19:20:30-01:00" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+01:00\"", "1997-07-16T19:20:30.4555555+01:00" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-01:00\"", "1997-07-16T19:20:30.4555555-01:00" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+04:30\"", "1997-07-16T19:20:30.4555555+04:30" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-04:30\"", "1997-07-16T19:20:30.4555555-04:30" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+0100\"", "1997-07-16T19:20:30.4555555+01:00" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-0100\"", "1997-07-16T19:20:30.4555555-01:00" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+0430\"", "1997-07-16T19:20:30.4555555+04:30" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-0430\"", "1997-07-16T19:20:30.4555555-04:30" };
-            // Test Non-UTC TZD without minute.
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+01\"", "1997-07-16T19:20:30.4555555+01:00" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-01\"", "1997-07-16T19:20:30.4555555-01:00" };
-            // Test Non-UTC TZD with max UTC offset hour.
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+14:00\"", "1997-07-16T19:20:30.4555555+14:00" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-14:00\"", "1997-07-16T19:20:30.4555555-14:00" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+1400\"", "1997-07-16T19:20:30.4555555+14:00" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-1400\"", "1997-07-16T19:20:30.4555555-14:00" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+14\"", "1997-07-16T19:20:30.4555555+14:00" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-14\"", "1997-07-16T19:20:30.4555555-14:00" };
-            // Test February 29 on a leap year
-            yield return new object[] { "\"2020-02-29T19:20:30.4555555+10:00\"", "2020-02-29T19:20:30.4555555+10:00" };
-        }
-
-        // UTC TZD tests are separate because `DateTime.Parse` for strings with `Z` TZD will return
-        // a `DateTime` with `DateTimeKind.Local` i.e `+00:00` which does not equal our expected result,
-        // a `DateTime` with `DateTimeKind.Utc` i.e `Z`.
-        // Instead, we need to use `DateTime.ParseExact` which returns a DateTime Utc `DateTimeKind`.
-        //
-        // Test string, Argument to DateTime(Offset).Parse(Exact)
-        public static IEnumerable<object[]> ValidISO8601TestsWithUtcOffset()
-        {
-            yield return new object[] { "\"1997-07-16T19:20Z\"", "1997-07-16T19:20:00.0000000Z" };
-            yield return new object[] { "\"1997-07-16T19:20:30Z\"", "1997-07-16T19:20:30.0000000Z" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555Z\"", "1997-07-16T19:20:30.4555555Z" };
-        }
-
-        public static IEnumerable<object[]> InvalidISO8601Tests()
-        {
-            // Invalid YYYY-MM-DD
-            yield return new object[] { "\"0997 07-16\"" };
-            yield return new object[] { "\"0997-0a-16\"" };
-            yield return new object[] { "\"0997-07 16\"" };
-            yield return new object[] { "\"0997-07-160997-07-16\"" };
-            yield return new object[] { "\"0997-07-16abc\"" };
-            yield return new object[] { "\"0997-07-16 \"" };
-            yield return new object[] { "\"0997-07-16,0997-07-16\"" };
-            yield return new object[] { "\"1997-07-16T19:20abc\"" };
-            yield return new object[] { "\"1997-07-16T19:20, 123\"" };
-            yield return new object[] { "\"997-07-16\"" };
-            yield return new object[] { "\"1997-07\"" };
-            yield return new object[] { "\"1997-7-06\"" };
-            yield return new object[] { "\"1997-07-16T\"" };
-            yield return new object[] { "\"1997-07-6\"" };
-            yield return new object[] { "\"1997-07-6T01\"" };
-            yield return new object[] { "\"1997-07-16Z\"" };
-            yield return new object[] { "\"1997-07-16+01:00\"" };
-            yield return new object[] { "\"19970716\"" };
-            yield return new object[] { "\"0997-07-166\"" };
-            yield return new object[] { "\"1997-07-1sdsad\"" };
-            yield return new object[] { "\"1997-07-16T19200\"" };
-
-            // Invalid YYYY-MM-DDThh:mm
-            yield return new object[] { "\"1997-07-16T1\"" };
-            yield return new object[] { "\"1997-07-16Ta0:00\"" };
-            yield return new object[] { "\"1997-07-16T19: 20:30\"" };
-            yield return new object[] { "\"1997-07-16 19:20:30\"" };
-            yield return new object[] { "\"1997-07-16T19:2030\"" };
-
-            // Invalid YYYY-MM-DDThh:mm:ss
-            yield return new object[] { "\"1997-07-16T19:20:3a\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30a\"" };
-            yield return new object[] { "\"1997-07-16T19:20:3045\"" };
-            yield return new object[] { "\"1997-07-16T19:20:304555555\"" };
-
-            // Invalid fractions.
-            yield return new object[] { "\"1997-07-16T19:20:30,45\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30 .\"" };
-            yield return new object[] { "\"abc1997-07-16T19:20:30.000\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+\"" };
-
-            // Invalid TZD.
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+-Z\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555Z \"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+01Z\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+01:\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555 +01:00\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+01:\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555- 01:00\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+04 :30\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-04: 30\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+0100 \"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-010\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+430\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555--0430\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+0\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-01005\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+14:00a\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-14:0\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-14:00 \"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+14 00\"" };
-
-            // Proper format but invalid calendar date, time, or time zone designator fields
-            yield return new object[] { "\"1997-00-16T19:20:30.4555555\"" };
-            yield return new object[] { "\"1997-07-16T25:20:30.4555555\"" };
-            yield return new object[] { "\"1997-00-16T19:20:30.4555555Z\"" };
-            yield return new object[] { "\"1997-07-16T25:20:30.4555555Z\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+14:30\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-14:30\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+15:00\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555-15:00\"" };
-            yield return new object[] { "\"1997-00-16T19:20:30.4555555+10:00\"" };
-            yield return new object[] { "\"1997-07-16T25:20:30.4555555+10:00\"" };
-            yield return new object[] { "\"1997-07-16T19:60:30.4555555+10:00\"" };
-            yield return new object[] { "\"1997-07-16T19:20:60.4555555+10:00\"" };
-            yield return new object[] { "\"1997-07-16T19:20:30.4555555+10:60\"" };
-            yield return new object[] { "\"0000-07-16T19:20:30.4555555+10:00\"" };
-            yield return new object[] { "\"2019-02-29T19:20:30.4555555+10:00\"" };
-            yield return new object[] { "\"9999-12-31T23:59:59.9999999-01:00\"" }; // This date spills over to year 10_000.
-        }
-
         [Fact]
         public static void TestingNumbers_TryGetMethods()
         {
@@ -971,7 +799,7 @@ namespace System.Text.Json.Tests
         }
 
         [Theory]
-        [MemberData(nameof(ValidISO8601Tests))]
+        [MemberData(nameof(JsonDateTimeTestData.ValidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))]
         public static void TestingStringsConversionToDateTime(string jsonString, string expectedString)
         {
             byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
@@ -995,7 +823,7 @@ namespace System.Text.Json.Tests
         }
 
         [Theory]
-        [MemberData(nameof(ValidISO8601Tests))]
+        [MemberData(nameof(JsonDateTimeTestData.ValidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))]
         public static void TestingStringsConversionToDateTimeOffset(string jsonString, string expectedString)
         {
             byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
@@ -1019,7 +847,7 @@ namespace System.Text.Json.Tests
         }
 
         [Theory]
-        [MemberData(nameof(ValidISO8601TestsWithUtcOffset))]
+        [MemberData(nameof(JsonDateTimeTestData.ValidISO8601TestsWithUtcOffset), MemberType = typeof(JsonDateTimeTestData))]
         public static void TestingStringsWithUTCOffsetToDateTime(string jsonString, string expectedString)
         {
             byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
@@ -1043,7 +871,7 @@ namespace System.Text.Json.Tests
         }
 
         [Theory]
-        [MemberData(nameof(ValidISO8601TestsWithUtcOffset))]
+        [MemberData(nameof(JsonDateTimeTestData.ValidISO8601TestsWithUtcOffset), MemberType = typeof(JsonDateTimeTestData))]
         public static void TestingStringsWithUTCOffsetToDateTimeOffset(string jsonString, string expectedString)
         {
             byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
@@ -1067,7 +895,7 @@ namespace System.Text.Json.Tests
         }
 
         [Theory]
-        [MemberData(nameof(InvalidISO8601Tests))]
+        [MemberData(nameof(JsonDateTimeTestData.InvalidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))]
         public static void TestingStringsInvalidConversionToDateTime(string jsonString)
         {
             byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);
@@ -1088,7 +916,7 @@ namespace System.Text.Json.Tests
         }
 
         [Theory]
-        [MemberData(nameof(InvalidISO8601Tests))]
+        [MemberData(nameof(JsonDateTimeTestData.InvalidISO8601Tests), MemberType = typeof(JsonDateTimeTestData))]
         public static void TestingStringsInvalidConversionToDateTimeOffset(string jsonString)
         {
             byte[] dataUtf8 = Encoding.UTF8.GetBytes(jsonString);