Skip to content

Commit

Permalink
ISO8601 compatibility for DateTime ToString (#158)
Browse files Browse the repository at this point in the history
***NO_CI***
  • Loading branch information
networkfusion authored Oct 11, 2021
1 parent d81e62f commit ebcb832
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 68 deletions.
215 changes: 160 additions & 55 deletions Tests/NFUnitTestSystemLib/UnitTestDateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,58 +157,146 @@ public void DateTime_ToStringTest6()
Assert.Equal(dt.ToString(), str);
}

//[TestMethod]
//public void DateTime_ToStringTest7()
//{
// /// <summary>
// /// 1. Creates a DateTime
// /// 2. Verifies DateTime.ToString (String) returns correct String using a specified format
// /// </summary>
// OutputHelper.WriteLine("Generating random DateTime");
// DateTime dt = GetRandomDateTime();
// OutputHelper.WriteLine("DateTime.ToString(String) using Standard Formats and Verifying");
// string[] standardFmts = { "d", "D", "f", "F", "g", "G", "m", "M", "o", "R", "r", "s", "t", "T", "u", "U", "Y", "y" };
// foreach (string standardFmt in standardFmts)
// {
// try
// {
// if (dt.ToString(standardFmt).Length < 1)
// {
// OutputHelper.WriteLine("Expected a String length greater than '0' but got '" +
// dt.ToString(standardFmt).Length + "'");
// testResult = MFTestResults.Fail;
// }
// }
// catch (Exception ex)
// {
// OutputHelper.WriteLine("This currently fails, DateTime.ToString(String)" +
// " throws ArgumentException for some string formats, see 22837 for details");
// OutputHelper.WriteLine("Caught " + ex.Message + " when Trying DateTime.ToString(" + standardFmt + ")");
// testResult = MFTestResults.KnownFailure;
// }
// }
// OutputHelper.WriteLine("DateTime.ToString(String) using Custom Formats and Verifying");
// string[] customFmts = {"h:mm:ss.ff t", "d MMM yyyy", "HH:mm:ss.f","dd MMM HH:mm:ss",
// @"\Mon\t\h\: M", "MM/dd/yyyy", "dddd, dd MMMM yyyy", "MMMM dd", "ddd, dd MMM yyyy HH':'mm':'ss 'GMT'",
// "yyyy'-'MM'-'dd'T'HH':'mm':'ss", "HH:mm", "yyyy'-'MM'-'dd HH':'mm':'ss'Z'", "yyyy MMMM"};
// foreach (string customFmt in customFmts)
// {
// try
// {
// if (dt.ToString(customFmt).Length < 1)
// {
// OutputHelper.WriteLine("Expected a String length greater than '0' but got '" +
// dt.ToString(customFmt).Length + "'");
// testResult = MFTestResults.Fail;
// }
// }
// catch (Exception ex)
// {
// OutputHelper.WriteLine("Caught " + ex.Message + " when Trying DateTime.ToString(" + customFmt + ")");
// testResult = MFTestResults.KnownFailure;
// }
// }
//}
[TestMethod]
public void DateTime_ToStringTest7()
{
/// <summary>
/// 1. Creates a DateTime
/// 2. Verifies DateTime.ToString (String) returns correct String using a specified format
/// </summary>
OutputHelper.WriteLine("Generating random DateTime");
DateTime dt = GetRandomDateTime();
OutputHelper.WriteLine("DateTime.ToString(String) using Standard Formats and Verifying");
string[] standardFmts = { "d", "D", "f", "F", "g", "G", "m", "M", "o", "O", "R", "r", "s", "t", "T", "u", "U", "Y", "y" };
foreach (string standardFmt in standardFmts)
{
try
{
if (dt.ToString(standardFmt).Length < 1)
{
throw new Exception("Expected a String length greater than '0' but got '" +
dt.ToString(standardFmt).Length + "'");
}
}
catch (Exception ex)
{
throw new Exception("Caught " + ex.Message + " when Trying DateTime.ToString(" + standardFmt + ")");
}
OutputHelper.WriteLine("Successfully verified 'DateTime.ToString(" + standardFmt + ")' format as: " + dt.ToString(standardFmt));
}
OutputHelper.WriteLine("DateTime.ToString(String) using Custom Formats and Verifying");
string[] customFmts = {"h:mm:ss.ff t", "d MMM yyyy", "HH:mm:ss.f","dd MMM HH:mm:ss",
@"\Mon\t\h\: M", "MM/dd/yyyy", "dddd, dd MMMM yyyy", "MMMM dd", "ddd, dd MMM yyyy HH':'mm':'ss 'GMT'",
"yyyy'-'MM'-'dd'T'HH':'mm':'ss", "HH:mm", "yyyy'-'MM'-'dd HH':'mm':'ss'Z'", "yyyy MMMM"};
foreach (string customFmt in customFmts)
{
try
{
if (dt.ToString(customFmt).Length < 1)
{
throw new Exception("Expected a String length greater than '0' but got '" +
dt.ToString(customFmt).Length + "'");
}
}
catch (Exception ex)
{
throw new Exception("Caught " + ex.Message + " when Trying DateTime.ToString(" + customFmt + ")");
}
OutputHelper.WriteLine("Successfully verified 'DateTime.ToString(" + customFmt + ")' format as: " + dt.ToString(customFmt));
}
}

[TestMethod]
public void DateTime_ToStringTest8()
{
/// <summary>
/// 1. Creates a DateTime
/// 2. Verifies DateTime.ToString (String) returns correct String using a specified format
/// </summary>
OutputHelper.WriteLine("Generating random DateTime");

DateTime dt = GetRandomDateTime();

OutputHelper.WriteLine($"Test DateTime is: {dt}");

OutputHelper.WriteLine("DateTime.ToString(String) using specified formats and Verifying");

// "o" and "O"
string specifier1 = "o";
string specifier2 = "O";

OutputHelper.WriteLine($"Testing specified format(s): '{specifier1}' and '{specifier2}'");

try
{
string dtOutput1 = dt.ToString(specifier1);
string dtOutput2 = dt.ToString(specifier2);

OutputHelper.WriteLine($"Output from ToString(\"{specifier1}\") was '{dtOutput1}'");
OutputHelper.WriteLine($"Output from ToString(\"{specifier2}\") was '{dtOutput2}'");

// expected format is yyyy-MM-ddTHH:mm:ss.fffffffZ

int length = 28;

// check length
Assert.True(length == dtOutput1.Length, $"Wrong output1 length: {dtOutput1.Length}, should have been {length}");
Assert.True(length == dtOutput2.Length, $"Wrong output1 length: {dtOutput2.Length}, should have been {length}");

// check 'yyyy'
Assert.Equal(dt.Year, int.Parse(dtOutput1.Substring(0, 4)), "Wrong output1 for 'yyyy'");
Assert.Equal(dt.Year, int.Parse(dtOutput2.Substring(0, 4)), "Wrong output2 for 'yyyy'");
// check 'MM'
Assert.Equal(dt.Month, int.Parse(dtOutput1.Substring(5, 2)), "Wrong output1 in for 'MM'");
Assert.Equal(dt.Month, int.Parse(dtOutput2.Substring(5, 2)), "Wrong output2 in for 'MM'");
// check 'dd'
Assert.Equal(dt.Day, int.Parse(dtOutput1.Substring(8, 2)), "Wrong output1 in for 'dd'");
Assert.Equal(dt.Day, int.Parse(dtOutput2.Substring(8, 2)), "Wrong output2 in for 'dd'");
// check 'HH'
Assert.Equal(dt.Hour, int.Parse(dtOutput1.Substring(11, 2)), "Wrong output1 in for 'HH'");
Assert.Equal(dt.Hour, int.Parse(dtOutput2.Substring(11, 2)), "Wrong output2 in for 'HH'");
// check 'mm'
Assert.Equal(dt.Minute, int.Parse(dtOutput1.Substring(14, 2)), "Wrong output1 in for 'mm'");
Assert.Equal(dt.Minute, int.Parse(dtOutput2.Substring(14, 2)), "Wrong output2 in for 'mm'");
// check 'ss'
Assert.Equal(dt.Second, int.Parse(dtOutput1.Substring(17, 2)), "Wrong output1 in for 'ss'");
Assert.Equal(dt.Second, int.Parse(dtOutput2.Substring(17, 2)), "Wrong output2 in for 'ss'");

// check 'fffffff'
// need to do the math to get the fraction part from ticks
var fraction = dt.Ticks % _TicksPerSecond;
Assert.Equal(fraction, int.Parse(dtOutput1.Substring(20, 7)), "Wrong output1 in for 'fffffff'");
Assert.Equal(fraction, int.Parse(dtOutput2.Substring(20, 7)), "Wrong output2 in for 'fffffff'");

// check '-'
Assert.Equal("-", dtOutput1.Substring(4, 1), "Wrong output1 in for '-'");
Assert.Equal("-", dtOutput2.Substring(4, 1), "Wrong output2 in for '-'");
Assert.Equal("-", dtOutput1.Substring(7, 1), "Wrong output1 in for '-'");
Assert.Equal("-", dtOutput2.Substring(7, 1), "Wrong output2 in for '-'");
// check 'T'
Assert.Equal("T", dtOutput1.Substring(10, 1), "Wrong output1 in for 'T'");
Assert.Equal("T", dtOutput2.Substring(10, 1), "Wrong output2 in for 'T'");
// check ':'
Assert.Equal(":", dtOutput1.Substring(13, 1), "Wrong output1 in for ':'");
Assert.Equal(":", dtOutput2.Substring(13, 1), "Wrong output2 in for ':'");
Assert.Equal(":", dtOutput1.Substring(16, 1), "Wrong output1 in for ':'");
Assert.Equal(":", dtOutput2.Substring(16, 1), "Wrong output2 in for ':'");
// check '.'
Assert.Equal(".", dtOutput1.Substring(19, 1), "Wrong output1 in for '.'");
Assert.Equal(".", dtOutput2.Substring(19, 1), "Wrong output2 in for '.'");
// check 'Z'
Assert.Equal("Z", dtOutput1.Substring(27, 1), "Wrong output1 in for 'Z'");
Assert.Equal("Z", dtOutput2.Substring(27, 1), "Wrong output2 in for 'Z'");
}
catch (Exception ex)
{
throw new Exception($"Caught {ex.Message} when Trying DateTime.ToString(\"{specifier1}\")");
}

OutputHelper.WriteLine("");

}


[TestMethod]
public void DateTime_AddTest8()
Expand Down Expand Up @@ -1047,6 +1135,10 @@ public void DateTime_AboveMaxDatTime_ArgumentOutOfRangeExceptionTest59()
static int[] leapYear = new int[] {2000, 2004, 2008, 2012, 2016, 2020, 2024, 2028, 2032, 2036, 2040, 2044, 2048,
2052, 2056, 2060, 2064, 2068, 2072, 2076, 2080, 2084, 2088, 2092, 2096};

// computing our constants here, as these are not accessible
// equivalent to DateTime.TicksPerSecond
const int _TicksPerSecond = 10000 * 1000;

private DateTime[] Get_ArrayOfRandomDateTimes()
{
OutputHelper.WriteLine(DateTime_btwn_1801_And_2801().ToString());
Expand Down Expand Up @@ -1089,21 +1181,34 @@ private DateTime GetRandomDateTime()
Random random = new Random();
year = random.Next(1399) + 1601;
month = random.Next(12) + 1;

if (month == 2 && IsLeapYear(year))
{
day = random.Next(29) + 1;
}
else if (month == 2 && (!IsLeapYear(year)))
{
day = random.Next(28) + 1;
else if (((month <= 7) && ((month + 1) % 2 == 0)) ||
((month > 7) && ((month % 2) == 0)))
}
else if (((month <= 7) && ((month + 1) % 2 == 0))
|| ((month > 7) && ((month % 2) == 0)))
{
day = random.Next(31) + 1;
}
else
{
day = random.Next(30) + 1;
}

hour = random.Next(24);
minute = random.Next(60);
second = random.Next(60);
millisec = random.Next(1000);

return new DateTime(year, month, day, hour, minute, second, millisec);
DateTime dt = new(year, month, day, hour, minute, second, millisec);

// fill in random ticks value so we can have a fully filled ticks value
return new DateTime(dt.Ticks + random.Next(1000_000));
}

private DateTime GetLeapYearDateTime()
Expand Down
2 changes: 1 addition & 1 deletion nanoFramework.CoreLibrary/System/DateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public struct DateTime

// Number of 100ns ticks per time unit
private const long TicksPerMillisecond = 10000;
private const long TicksPerSecond = TicksPerMillisecond * 1000;
internal const long TicksPerSecond = TicksPerMillisecond * 1000;
private const long TicksPerMinute = TicksPerSecond * 60;
private const long TicksPerHour = TicksPerMinute * 60;
private const long TicksPerDay = TicksPerHour * 24;
Expand Down
34 changes: 22 additions & 12 deletions nanoFramework.CoreLibrary/System/Globalization/DateTimeFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ namespace System.Globalization
// "g" general date (short date + short time) culture-specific 10/31/1999 2:00 AM
// "G" general date (short date + long time) culture-specific 10/31/1999 2:00:00 AM
// "m"/"M" Month/Day date culture-specific October 31
//(G) "o"/"O" Round Trip XML "yyyy-MM-ddTHH:mm:ss.fffffffK" 1999-10-31 02:00:00.0000000Z
//(G) "o"/"O" Round Trip ISO 8601 compatible "yyyy-MM-ddTHH:mm:ss.fffffffK" 1999-10-31T02:00:00.0000000Z
//(G) "r"/"R" RFC 1123 date, "ddd, dd MMM yyyy HH':'mm':'ss 'GMT'" Sun, 31 Oct 1999 10:00:00 GMT
//(G) "s" Sortable format, based on ISO 8601. "yyyy-MM-dd'T'HH:mm:ss" 1999-10-31T02:00:00
// ('T' for local time)
Expand All @@ -119,10 +119,11 @@ namespace System.Globalization
internal static
class DateTimeFormat
{
internal const int _maxSecondsFractionDigits = 3;
internal const int MaxSecondsFractionDigits = 7;

////////////////////////////////////////////////////////////////////////////
//
// Format the positive integer value to a string and perfix with assigned
// Format the positive integer value to a string and prefix with assigned
// length of leading zero.
//
// Parameters:
Expand Down Expand Up @@ -327,21 +328,23 @@ private static String FormatCustomized(DateTime dateTime, String format, DateTim
tempResult = FormatDigits(dateTime.Second, tokenLen);
break;
case 'f':
if (tokenLen <= _maxSecondsFractionDigits)
if (tokenLen <= MaxSecondsFractionDigits)
{
var precision = 3;
var fraction = dateTime.Millisecond;
// compute requested precision
var precision = MaxSecondsFractionDigits - (MaxSecondsFractionDigits - tokenLen);

// Note: Need to add special case when tokenLen > precision to begin with
// if we're to change MaxSecondsFractionDigits to be more than 3
// get fraction value from ticks
var fraction = dateTime.Ticks % DateTime.TicksPerSecond;

while (tokenLen < precision)
// compute value with effective digits from requested precision
int effectiveDigits = MaxSecondsFractionDigits - precision;
while (effectiveDigits > 0)
{
fraction /= 10;
precision--;
effectiveDigits--;
}

tempResult = FormatDigits(fraction, tokenLen);
tempResult = FormatDigits((int)fraction, precision);
}
else throw new ArgumentException("Format_InvalidString");
break;
Expand Down Expand Up @@ -440,6 +443,13 @@ internal static String GetRealFormat(String format, DateTimeFormatInfo dtfi)
case 'M': // Month/Day Date
realFormat = dtfi.MonthDayPattern;
break;

case 'o':
case 'O': // Round-trip ISO8601 compatible
// Note: .NET nanoFramework has support for UTC (Z) time, so we're not processing the kind token (K).
realFormat = dtfi.SortableDateTimePattern + ".fffffffZ";
break;

case 'r':
case 'R': // RFC 1123 Standard
realFormat = dtfi.RFC1123Pattern;
Expand Down

0 comments on commit ebcb832

Please sign in to comment.