Add more DateTime{Offset}.ParseExact "o"/"r" tests (dotnet/corefx#30860)
authorStephen Toub <stoub@microsoft.com>
Fri, 6 Jul 2018 16:28:31 +0000 (12:28 -0400)
committerGitHub <noreply@github.com>
Fri, 6 Jul 2018 16:28:31 +0000 (12:28 -0400)
Commit migrated from https://github.com/dotnet/corefx/commit/0956f3e2122802776ebe1ec614ead2dd4267def9

src/libraries/System.Runtime/tests/System/DateTimeOffsetTests.cs
src/libraries/System.Runtime/tests/System/DateTimeTests.cs

index 6a20af5..eb44bc1 100644 (file)
@@ -4,6 +4,7 @@
 
 using System.Collections.Generic;
 using System.Globalization;
+using System.Linq;
 using System.Threading;
 using Xunit;
 
@@ -855,6 +856,44 @@ namespace System.Tests
             Assert.Equal(0, result.Second);
         }
 
+        public static IEnumerable<object[]> StandardFormatSpecifiers() =>
+            DateTimeTests.StandardFormatSpecifiers()
+            .Where(a => !a[0].Equals("U")); // "U" isn't supported by DateTimeOffset
+
+        [Theory]
+        [MemberData(nameof(StandardFormatSpecifiers))]
+        public static void ParseExact_ToStringThenParseExactRoundtrip_Success(string standardFormat)
+        {
+            var r = new Random(42);
+            for (int i = 0; i < 200; i++) // test with a bunch of random dates
+            {
+                DateTimeOffset dt = new DateTimeOffset(
+                    DateTimeOffset.MinValue.Ticks + (long)(r.NextDouble() * (DateTimeOffset.MaxValue.Ticks - DateTimeOffset.MinValue.Ticks)),
+                    new TimeSpan(r.Next(-13, 13), r.Next(0, 60), 0));
+                try
+                {
+                    string expected = dt.ToString(standardFormat);
+
+                    Assert.Equal(expected, DateTimeOffset.ParseExact(expected, standardFormat, null).ToString(standardFormat));
+                    Assert.Equal(expected, DateTimeOffset.ParseExact(expected, standardFormat, null, DateTimeStyles.None).ToString(standardFormat));
+                    Assert.Equal(expected, DateTimeOffset.ParseExact(expected, new[] { standardFormat }, null, DateTimeStyles.None).ToString(standardFormat));
+                    Assert.Equal(expected, DateTimeOffset.ParseExact(expected, new[] { standardFormat }, null, DateTimeStyles.AllowWhiteSpaces).ToString(standardFormat));
+
+                    Assert.True(DateTimeOffset.TryParseExact(expected, standardFormat, null, DateTimeStyles.None, out DateTimeOffset actual));
+                    Assert.Equal(expected, actual.ToString(standardFormat));
+                    Assert.True(DateTimeOffset.TryParseExact(expected, new[] { standardFormat }, null, DateTimeStyles.None, out actual));
+                    Assert.Equal(expected, actual.ToString(standardFormat));
+
+                    // Should also parse with Parse, though may not round trip exactly
+                    DateTimeOffset.Parse(expected);
+                }
+                catch (Exception e)
+                {
+                    throw new Exception(dt.DateTime.Ticks + ":" + dt.Offset.Ticks, e);
+                }
+            }
+        }
+
         [Fact]
         public static void ParseExact_String_String_FormatProvider()
         {
@@ -885,51 +924,103 @@ namespace System.Tests
             Assert.Equal(expectedString, result.ToString("g"));
         }
 
-        [Theory, MemberData(nameof(Format_String_TestData_O))]
-        public static void ParseExact_String_String_FormatProvider_DateTimeStyles_O(DateTimeOffset dt, string expected)
-        {
-            string actual = dt.ToString("o");
-            Assert.Equal(expected, actual);
+        [Theory]
+        [MemberData(nameof(ParseExact_TestData_O))]
+        public static void ParseExact_String_String_FormatProvider_DateTimeStyles_O(DateTimeOffset dt, string input)
+        {
+            string expectedString = dt.ToString("o");
+
+            Assert.Equal(expectedString, DateTimeOffset.ParseExact(input, "o", null).ToString("o"));
+            Assert.Equal(expectedString, DateTimeOffset.ParseExact(input, "o", null, DateTimeStyles.None).ToString("o"));
+
+            const string Whitespace = " \t\r\n ";
+            Assert.Equal(expectedString, DateTimeOffset.ParseExact(Whitespace + input, "o", null, DateTimeStyles.AllowLeadingWhite).ToString("o"));
+            Assert.Equal(expectedString, DateTimeOffset.ParseExact(input + Whitespace, "o", null, DateTimeStyles.AllowTrailingWhite).ToString("o"));
+            Assert.Equal(expectedString, DateTimeOffset.ParseExact(
+                Whitespace +
+                input +
+                Whitespace, "o", null, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite).ToString("o"));
+            Assert.Equal(expectedString, DateTimeOffset.ParseExact(
+                input.Substring(0, 27) +
+                Whitespace +
+                input.Substring(27), "o", null, DateTimeStyles.AllowInnerWhite).ToString("o"));
+            Assert.Equal(expectedString, DateTimeOffset.ParseExact(
+                Whitespace +
+                input.Substring(0, 27) +
+                Whitespace +
+                input.Substring(27) +
+                Whitespace, "o", null, DateTimeStyles.AllowWhiteSpaces).ToString("o"));
+        }
+
+        public static IEnumerable<object[]> ParseExact_TestData_O()
+        {
+            foreach (TimeSpan offset in new[] { TimeSpan.Zero, new TimeSpan(-1, 23, 0), new TimeSpan(7, 0, 0) })
+            {
+                var dto = new DateTimeOffset(new DateTime(1234567891234567891, DateTimeKind.Unspecified), offset);
+                yield return new object[] { dto, dto.ToString("o") };
 
-            DateTimeOffset result = DateTimeOffset.ParseExact(actual, "o", null, DateTimeStyles.None);
-            Assert.Equal(expected, result.ToString("o"));
+                yield return new object[] { DateTimeOffset.MinValue, DateTimeOffset.MinValue.ToString("o") };
+                yield return new object[] { DateTimeOffset.MaxValue, DateTimeOffset.MaxValue.ToString("o") };
+            }
         }
 
-        public static IEnumerable<object[]> Format_String_TestData_O()
+        [Theory]
+        [MemberData(nameof(ParseExact_TestData_InvalidData_O))]
+        public static void ParseExact_InvalidData_O(string invalidString)
         {
-            yield return new object[] { DateTimeOffset.MaxValue, "9999-12-31T23:59:59.9999999+00:00" };
-            yield return new object[] { DateTimeOffset.MinValue, "0001-01-01T00:00:00.0000000+00:00" };
-            yield return new object[] { new DateTimeOffset(1906, 8, 15, 7, 24, 5, 300, new TimeSpan(0, 0, 0)), "1906-08-15T07:24:05.3000000+00:00" };
-            yield return new object[] { new DateTimeOffset(1906, 8, 15, 7, 24, 5, 300, new TimeSpan(7, 30, 0)), "1906-08-15T07:24:05.3000000+07:30" };
+            Assert.Throws<FormatException>(() => DateTimeOffset.ParseExact(invalidString, "o", null));
+            Assert.Throws<FormatException>(() => DateTimeOffset.ParseExact(invalidString, "o", null, DateTimeStyles.None));
+            Assert.Throws<FormatException>(() => DateTimeOffset.ParseExact(invalidString, new string[] { "o" }, null, DateTimeStyles.None));
         }
 
-        [Theory, MemberData(nameof(Format_String_TestData_R))]
-        public static void ParseExact_String_String_FormatProvider_DateTimeStyles_R(DateTimeOffset dt, string expected)
-        {
-            string actual = dt.ToString("r");
-            Assert.Equal(expected, actual);
+        public static IEnumerable<object[]> ParseExact_TestData_InvalidData_O() =>
+            DateTimeTests.ParseExact_TestData_InvalidData_O();
 
-            DateTimeOffset result = DateTimeOffset.ParseExact(actual, "r", null, DateTimeStyles.None);
-            Assert.Equal(expected, result.ToString("r"));
+        [Theory]
+        [MemberData(nameof(ParseExact_TestData_R))]
+        public static void ParseExact_String_String_FormatProvider_DateTimeStyles_R(DateTimeOffset dt, string input)
+        {
+            Assert.Equal(dt.ToString("r"), DateTimeOffset.ParseExact(input, "r", null).ToString("r"));
+            Assert.Equal(dt.ToString("r"), DateTimeOffset.ParseExact(input, "r", null, DateTimeStyles.None).ToString("r"));
+
+            const string Whitespace = " \t\r\n ";
+            Assert.Equal(dt.ToString("r"), DateTimeOffset.ParseExact(Whitespace + input, "r", null, DateTimeStyles.AllowLeadingWhite).ToString("r"));
+            Assert.Equal(dt.ToString("r"), DateTimeOffset.ParseExact(input + Whitespace, "r", null, DateTimeStyles.AllowTrailingWhite).ToString("r"));
+            Assert.Equal(dt.ToString("r"), DateTimeOffset.ParseExact(
+                Whitespace +
+                input +
+                Whitespace, "r", null, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite).ToString("r"));
+            Assert.Equal(dt.ToString("r"), DateTimeOffset.ParseExact(
+                input.Substring(0, 4) +
+                Whitespace +
+                input.Substring(4), "r", null, DateTimeStyles.AllowInnerWhite).ToString("r"));
+            Assert.Equal(dt.ToString("r"), DateTimeOffset.ParseExact(
+                Whitespace +
+                input.Substring(0, 4) +
+                Whitespace +
+                input.Substring(4) +
+                Whitespace, "r", null, DateTimeStyles.AllowWhiteSpaces).ToString("r"));
+        }
+
+        public static IEnumerable<object[]> ParseExact_TestData_R()
+        {
+            foreach (object[] dateTimeData in DateTimeTests.ParseExact_TestData_R())
+            {
+                yield return new object[] { new DateTimeOffset((DateTime)dateTimeData[0], TimeSpan.Zero), (string)dateTimeData[1] };
+            }
         }
 
-        public static IEnumerable<object[]> Format_String_TestData_R()
+        [Theory]
+        [MemberData(nameof(ParseExact_TestData_InvalidData_R))]
+        public static void ParseExact_InvalidData_R(string invalidString)
         {
-            yield return new object[] { DateTimeOffset.MaxValue, "Fri, 31 Dec 9999 23:59:59 GMT" };
-            yield return new object[] { DateTimeOffset.MinValue, "Mon, 01 Jan 0001 00:00:00 GMT" };
-            yield return new object[] { new DateTimeOffset(1906, 8, 15, 7, 24, 5, 300, new TimeSpan(0, 0, 0)), "Wed, 15 Aug 1906 07:24:05 GMT" };
-            yield return new object[] { new DateTimeOffset(1906, 8, 15, 7, 24, 5, 300, new TimeSpan(7, 30, 0)), "Tue, 14 Aug 1906 23:54:05 GMT" };
+            Assert.Throws<FormatException>(() => DateTimeOffset.ParseExact(invalidString, "r", null));
+            Assert.Throws<FormatException>(() => DateTimeOffset.ParseExact(invalidString, "r", null, DateTimeStyles.None));
+            Assert.Throws<FormatException>(() => DateTimeOffset.ParseExact(invalidString, new string[] { "r" }, null, DateTimeStyles.None));
         }
 
-        [Fact]
-        public static void ParseExact_String_String_FormatProvider_DateTimeStyles_R()
-        {
-            DateTimeOffset expected = DateTimeOffset.MaxValue;
-            string expectedString = expected.ToString("r");
-
-            DateTimeOffset result = DateTimeOffset.ParseExact(expectedString, "r", null, DateTimeStyles.None);
-            Assert.Equal(expectedString, result.ToString("r"));
-        }
+        public static IEnumerable<object[]> ParseExact_TestData_InvalidData_R() =>
+            DateTimeTests.ParseExact_TestData_InvalidData_R();
 
         [Fact]
         public static void ParseExact_String_String_FormatProvider_DateTimeStyles_CustomFormatProvider()
index ba446cc..efac071 100644 (file)
@@ -751,25 +751,32 @@ namespace System.Tests
             Assert.False(DateTime.TryParseExact(expected, new[] { parseFormat }, null, DateTimeStyles.None, out result));
         }
 
-        public static IEnumerable<object[]> Format_String_TestData_O()
-        {
-            yield return new object[] { DateTime.MaxValue, "9999-12-31T23:59:59.9999999" };
-            yield return new object[] { DateTime.MinValue, "0001-01-01T00:00:00.0000000" };
-            yield return new object[] { new DateTime(1906, 8, 15, 7, 24, 5, 300), "1906-08-15T07:24:05.3000000" };
-        }
-
         [Theory]
         [MemberData(nameof(ParseExact_TestData_R))]
         public static void ParseExact_String_String_FormatProvider_DateTimeStyles_R(DateTime dt, string input)
         {
+            Assert.Equal(DateTimeKind.Unspecified, DateTime.ParseExact(input, "r", null).Kind);
+
             Assert.Equal(dt.ToString("r"), DateTime.ParseExact(input, "r", null).ToString("r"));
             Assert.Equal(dt.ToString("r"), DateTime.ParseExact(input, "r", null, DateTimeStyles.None).ToString("r"));
 
             const string Whitespace = " \t\r\n ";
             Assert.Equal(dt.ToString("r"), DateTime.ParseExact(Whitespace + input, "r", null, DateTimeStyles.AllowLeadingWhite).ToString("r"));
             Assert.Equal(dt.ToString("r"), DateTime.ParseExact(input + Whitespace, "r", null, DateTimeStyles.AllowTrailingWhite).ToString("r"));
-            Assert.Equal(dt.ToString("r"), DateTime.ParseExact(Whitespace + input + Whitespace, "r", null, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite).ToString("r"));
-            Assert.Equal(dt.ToString("r"), DateTime.ParseExact(input.Substring(0, 4) + Whitespace + input.Substring(4), "r", null, DateTimeStyles.AllowInnerWhite).ToString("r"));
+            Assert.Equal(dt.ToString("r"), DateTime.ParseExact(
+                Whitespace + 
+                input +
+                Whitespace, "r", null, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite).ToString("r"));
+            Assert.Equal(dt.ToString("r"), DateTime.ParseExact(
+                input.Substring(0, 4) +
+                Whitespace +
+                input.Substring(4), "r", null, DateTimeStyles.AllowInnerWhite).ToString("r"));
+            Assert.Equal(dt.ToString("r"), DateTime.ParseExact(
+                Whitespace +
+                input.Substring(0, 4) +
+                Whitespace +
+                input.Substring(4) +
+                Whitespace, "r", null, DateTimeStyles.AllowWhiteSpaces).ToString("r"));
         }
 
         public static IEnumerable<object[]> ParseExact_TestData_R()
@@ -881,6 +888,140 @@ namespace System.Tests
             yield return new object[] { "Wed, 15 Aug 1906 07:24:0A GMT" }; // invalid digits
         }
 
+        [Theory]
+        [MemberData(nameof(ParseExact_TestData_O))]
+        public static void ParseExact_String_String_FormatProvider_DateTimeStyles_O(DateTime dt, string input)
+        {
+            string expectedString;
+            if (input.Length == 27) // no timezone
+            {
+                Assert.Equal(DateTimeKind.Unspecified, DateTime.ParseExact(input, "o", null).Kind);
+                expectedString = dt.ToString("o");
+            }
+            else // "Z" or +/- offset
+            {
+                Assert.Equal(DateTimeKind.Local, DateTime.ParseExact(input, "o", null).Kind);
+                expectedString = dt.ToLocalTime().ToString("o");
+            }
+
+            Assert.Equal(expectedString, DateTime.ParseExact(input, "o", null).ToString("o"));
+            Assert.Equal(expectedString, DateTime.ParseExact(input, "o", null, DateTimeStyles.None).ToString("o"));
+
+            const string Whitespace = " \t\r\n ";
+            Assert.Equal(expectedString, DateTime.ParseExact(Whitespace + input, "o", null, DateTimeStyles.AllowLeadingWhite).ToString("o"));
+            Assert.Equal(expectedString, DateTime.ParseExact(input + Whitespace, "o", null, DateTimeStyles.AllowTrailingWhite).ToString("o"));
+            Assert.Equal(expectedString, DateTime.ParseExact(
+                Whitespace +
+                input +
+                Whitespace, "o", null, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite).ToString("o"));
+            Assert.Equal(expectedString, DateTime.ParseExact(
+                input.Substring(0, 27) +
+                Whitespace +
+                input.Substring(27), "o", null, DateTimeStyles.AllowInnerWhite).ToString("o"));
+            Assert.Equal(expectedString, DateTime.ParseExact(
+                Whitespace +
+                input.Substring(0, 27) +
+                Whitespace +
+                input.Substring(27) +
+                Whitespace, "o", null, DateTimeStyles.AllowWhiteSpaces).ToString("o"));
+        }
+
+        public static IEnumerable<object[]> ParseExact_TestData_O()
+        {
+            // Arbitrary DateTime in each of Unspecified, Utc, and Local kinds.
+            foreach (DateTimeKind kind in new[] { DateTimeKind.Unspecified, DateTimeKind.Utc, DateTimeKind.Local })
+            {
+                var dt = new DateTime(1234567891234567891, kind);
+                yield return new object[] { dt, dt.ToString("o") };
+            }
+
+            // Min and max in each of Unspecified, Utc, and Local kinds.
+            foreach (DateTime dt in new[] { DateTime.MinValue, DateTime.MaxValue })
+            {
+                yield return new object[] { dt, dt.ToString("o") };
+                yield return new object[] { dt.ToUniversalTime(), dt.ToUniversalTime().ToString("o") };
+                yield return new object[] { dt.ToLocalTime(), dt.ToLocalTime().ToString("o") };
+            }
+
+            // 1-digit offset hour is accepted due to legacy/compat
+            yield return new object[] { new DateTime(636664076235238523, DateTimeKind.Utc), "2018-07-05T18:36:43.5238523+1:23" };
+        }
+
+        [Theory]
+        [MemberData(nameof(ParseExact_TestData_InvalidData_O))]
+        public static void ParseExact_InvalidData_O(string invalidString)
+        {
+            Assert.Throws<FormatException>(() => DateTime.ParseExact(invalidString, "o", null));
+            Assert.Throws<FormatException>(() => DateTime.ParseExact(invalidString, "o", null, DateTimeStyles.None));
+            Assert.Throws<FormatException>(() => DateTime.ParseExact(invalidString, new string[] { "o" }, null, DateTimeStyles.None));
+        }
+
+        public static IEnumerable<object[]> ParseExact_TestData_InvalidData_O()
+        {
+            yield return new object[] { " 2018-07-05T18:36:43.5238523" }; // whitespace before
+            yield return new object[] { " 2018-07-05T18:36:43.5238523Z" }; // whitespace before
+            yield return new object[] { " 2018-07-05T18:36:43.5238523+00:00" }; // whitespace before
+            yield return new object[] { "2018-07-05T18:36:43.5238523 " }; // whitespace after
+            yield return new object[] { "2018-07-05T18:36:43.5238523Z " }; // whitespace after
+            yield return new object[] { "2018-07-05T18:36:43.5238523+00:00 " }; // whitespace after
+            yield return new object[] { "2018-07-05T18:36:43.5238523 Z" }; // whitespace inside
+            yield return new object[] { "2018-07-05T18:36:43.5238523 +00:00" }; // whitespace inside
+
+            yield return new object[] { "201-07-05T18:36:43.5238523" }; // too short year
+            yield return new object[] { "20181-07-05T18:36:43.5238523" }; // too long year
+            yield return new object[] { "2018-7-05T18:36:43.5238523" }; // too short month
+            yield return new object[] { "2018-017-05T18:36:43.5238523" }; // too long month
+            yield return new object[] { "2018-07-5T18:36:43.5238523" }; // too short day
+            yield return new object[] { "2018-07-015T18:36:43.5238523" }; // too long day
+            yield return new object[] { "2018-07-05T018:36:43.5238523" }; // too long hour
+            yield return new object[] { "2018-07-05T8:36:43.5238523" }; // too short hour
+            yield return new object[] { "2018-07-05T18:6:43.5238523" }; // too short minute
+            yield return new object[] { "2018-07-05T18:036:43.5238523" }; // too long minute
+            yield return new object[] { "2018-07-05T18:06:3.5238523" }; // too short second
+            yield return new object[] { "2018-07-05T18:36:043.5238523" }; // too long second
+            yield return new object[] { "2018-07-05T18:06:03.238523" }; // too short fraction
+            yield return new object[] { "2018-07-05T18:36:43.15238523" }; // too long fraction
+            yield return new object[] { "2018-07-05T18:36:43.5238523+001:00" }; // too long offset hour
+            yield return new object[] { "2018-07-05T18:36:43.5238523+01:0" }; // too short offset minute
+            yield return new object[] { "2018-07-05T18:36:43.5238523+01:000" }; // too long offset minute
+
+            yield return new object[] { "2018=07-05T18:36:43.5238523" }; // invalid first hyphen
+            yield return new object[] { "2018-07=05T18:36:43.5238523" }; // invalid second hyphen
+            yield return new object[] { "2018-07-05A18:36:43.5238523" }; // invalid T
+            yield return new object[] { "2018-07-05T18;36:43.5238523" }; // invalid first colon
+            yield return new object[] { "2018-07-05T18:36;43.5238523" }; // invalid second colon
+            yield return new object[] { "2018-07-05T18:36:43,5238523" }; // invalid period
+            yield return new object[] { "2018-07-05T18:36:43.5238523,00:00" }; // invalid +/-/Z
+            yield return new object[] { "2018-07-05T18:36:43.5238523+00;00" }; // invalid third colon
+            yield return new object[] { "2018-07-05T18:36:43.5238523+1;00" }; // invalid colon with 1-digit offset hour
+
+            yield return new object[] { "a018-07-05T18:36:43.5238523" }; // invalid digits
+            yield return new object[] { "2a18-07-05T18:36:43.5238523" }; // invalid digits
+            yield return new object[] { "20a8-07-05T18:36:43.5238523" }; // invalid digits
+            yield return new object[] { "201a-07-05T18:36:43.5238523" }; // invalid digits
+            yield return new object[] { "2018-a7-05T18:36:43.5238523" }; // invalid digits
+            yield return new object[] { "2018-0a-05T18:36:43.5238523" }; // invalid digits
+            yield return new object[] { "2018-07-a5T18:36:43.5238523" }; // invalid digits
+            yield return new object[] { "2018-07-0aT18:36:43.5238523" }; // invalid digits
+            yield return new object[] { "2018-07-05Ta8:36:43.5238523" }; // invalid digits
+            yield return new object[] { "2018-07-05T1a:36:43.5238523" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:a6:43.5238523" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:3a:43.5238523" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:36:a3.5238523" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:36:4a.5238523" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:36:43.a238523" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:36:43.5a38523" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:36:43.52a8523" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:36:43.523a523" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:36:43.5238a23" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:36:43.52385a3" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:36:43.523852a" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:36:43.5238523+a0:00" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:36:43.5238523+0a:00" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:36:43.5238523+00:a0" }; // invalid digits
+            yield return new object[] { "2018-07-05T18:36:43.5238523+00:0a" }; // invalid digits
+        }
+
         [Fact]
         public static void ParseExact_String_String_FormatProvider_DateTimeStyles_CustomFormatProvider()
         {