Fixed remaining bugs in DefaultDateFormatter. Reached 100% verified correctness! Yaaay!

This commit is contained in:
Daniel Stolt
2014-07-28 05:54:09 +02:00
parent 28d02a503b
commit 4a10352ba6
2 changed files with 112 additions and 40 deletions

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Autofac;
using Moq;
@@ -18,7 +19,7 @@ namespace Orchard.Framework.Tests.Localization {
public class DefaultDateFormatterTests {
[Test]
[Description("Date and time parsing works correctly for all combinations of months, hours, format strings and cultures.")]
[Description("Date/time parsing works correctly for all combinations of months, format strings and cultures.")]
public void ParseDateTimeTest01() {
var allCases = new ConcurrentBag<string>();
var failedCases = new ConcurrentDictionary<string, Exception>();
@@ -34,27 +35,78 @@ namespace Orchard.Framework.Tests.Localization {
var container = InitializeContainer(culture.Name, "GregorianCalendar");
var formats = container.Resolve<IDateTimeFormatProvider>();
var target = container.Resolve<IDateFormatter>();
foreach (var dateTimeFormat in formats.AllDateTimeFormats) { // All date and time formats supported by the culture.
var caseKey = String.Format("{0}:{1}", culture.Name, dateTimeFormat);
allCases.Add(caseKey);
Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey));
try {
for (var month = 1; month <= 12; month++) { // All months in the year.
for (var hour = 0; hour <= 23; hour++) { // All hours in the day.
DateTime dateTime = new DateTime(1998, month, 1, hour, 30, 30);
// Print string using Gregorian calendar to avoid calendar conversion.
var cultureGregorian = (CultureInfo)culture.Clone();
cultureGregorian.DateTimeFormat.Calendar = cultureGregorian.OptionalCalendars.OfType<GregorianCalendar>().First();
var dateTimeString = dateTime.ToString(dateTimeFormat, cultureGregorian);
var result = target.ParseDateTime(dateTimeString, dateTimeFormat);
var reference = DateTime.ParseExact(dateTimeString, dateTimeFormat, culture);
var expected = new DateTimeParts(reference.Year, reference.Month, reference.Day, reference.Hour, reference.Minute, reference.Second, reference.Millisecond);
Assert.AreEqual(expected, result);
}
for (var month = 1; month <= 12; month++) { // All months in the year.
DateTime dateTime = new DateTime(1998, month, 1, 10, 30, 30);
// Print string using Gregorian calendar to avoid calendar conversion.
var cultureGregorian = (CultureInfo)culture.Clone();
cultureGregorian.DateTimeFormat.Calendar = cultureGregorian.OptionalCalendars.OfType<GregorianCalendar>().First();
var dateTimeString = dateTime.ToString(dateTimeFormat, cultureGregorian);
var caseKey = String.Format("{0}___{1}___{2}", culture.Name, dateTimeFormat, dateTimeString);
allCases.Add(caseKey);
//Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey));
try {
var result = target.ParseDateTime(dateTimeString, dateTimeFormat);
var expected = GetExpectedDateTimeParts(dateTime, dateTimeFormat);
Assert.AreEqual(expected, result);
}
catch (Exception ex) {
failedCases.TryAdd(caseKey, ex);
}
}
catch (Exception ex) {
failedCases.TryAdd(caseKey, ex);
}
});
if (failedCases.Count > maxFailedCases) {
throw new AggregateException(String.Format("Parse tests failed for {0} of {1} cases. Expected {2} failed cases or less.", failedCases.Count, allCases.Count, maxFailedCases), failedCases.Values);
}
}
[Test]
[Description("Date/time parsing works correctly for all combinations of hours, format strings and cultures.")]
public void ParseDateTimeTest02() {
var allCases = new ConcurrentBag<string>();
var failedCases = new ConcurrentDictionary<string, Exception>();
var maxFailedCases = 0;
var options = new ParallelOptions();
if (Debugger.IsAttached) {
options.MaxDegreeOfParallelism = 1;
}
var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures);
Parallel.ForEach(allCultures, options, culture => { // All cultures on the machine.
var container = InitializeContainer(culture.Name, "GregorianCalendar");
var formats = container.Resolve<IDateTimeFormatProvider>();
var target = container.Resolve<IDateFormatter>();
foreach (var dateTimeFormat in formats.AllDateTimeFormats) { // All date and time formats supported by the culture.
foreach (var hour in new [] {0, 6, 12, 18}) { // Enough hours to cover all code paths (AM/PM, 12<->00, etc).
DateTime dateTime = new DateTime(1998, 1, 1, hour, 30, 30);
// Print string using Gregorian calendar to avoid calendar conversion.
var cultureGregorian = (CultureInfo)culture.Clone();
cultureGregorian.DateTimeFormat.Calendar = cultureGregorian.OptionalCalendars.OfType<GregorianCalendar>().First();
var dateTimeString = dateTime.ToString(dateTimeFormat, cultureGregorian);
var caseKey = String.Format("{0}___{1}___{2}", culture.Name, dateTimeFormat, dateTimeString);
allCases.Add(caseKey);
//Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey));
try {
var result = target.ParseDateTime(dateTimeString, dateTimeFormat);
var expected = GetExpectedDateTimeParts(dateTime, dateTimeFormat);
Assert.AreEqual(expected, result);
}
catch (Exception ex) {
failedCases.TryAdd(caseKey, ex);
}
}
}
});
@@ -81,16 +133,21 @@ namespace Orchard.Framework.Tests.Localization {
var container = InitializeContainer(culture.Name, "GregorianCalendar");
var formats = container.Resolve<IDateTimeFormatProvider>();
var target = container.Resolve<IDateFormatter>();
foreach (var dateFormat in formats.AllDateFormats) { // All date formats supported by the culture.
for (var month = 1; month <= 12; month++) { // All months in the year.
DateTime date = new DateTime(1998, month, 1);
// Print string using Gregorian calendar to avoid calendar conversion.
var cultureGregorian = (CultureInfo)culture.Clone();
cultureGregorian.DateTimeFormat.Calendar = cultureGregorian.OptionalCalendars.OfType<GregorianCalendar>().First();
var dateString = date.ToString(dateFormat, cultureGregorian);
var caseKey = String.Format("{0}___{1}___{2}", culture.Name, dateFormat, dateString);
allCases.Add(caseKey);
//Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey));
try {
var result = target.ParseDate(dateString, dateFormat);
var expected = GetExpectedDateParts(date, dateFormat);
@@ -125,13 +182,17 @@ namespace Orchard.Framework.Tests.Localization {
var container = InitializeContainer(culture.Name, null);
var formats = container.Resolve<IDateTimeFormatProvider>();
var target = container.Resolve<IDateFormatter>();
foreach (var timeFormat in formats.AllTimeFormats) { // All time formats supported by the culture.
for (var hour = 0; hour <= 23; hour++) { // All hours in the day.
DateTime time = new DateTime(1998, 1, 1, hour, 30, 30);
var timeString = time.ToString(timeFormat, culture);
var caseKey = String.Format("{0}___{1}___{2}", culture.Name, timeFormat, timeString);
allCases.Add(caseKey);
//Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey));
try {
var result = target.ParseTime(timeString, timeFormat);
var expected = GetExpectedTimeParts(time, timeFormat);
@@ -149,20 +210,29 @@ namespace Orchard.Framework.Tests.Localization {
}
}
private DateTimeParts GetExpectedDateTimeParts(DateTime dateTime, string format) {
return new DateTimeParts(
GetExpectedDateParts(dateTime, format),
GetExpectedTimeParts(dateTime, format)
);
}
private DateParts GetExpectedDateParts(DateTime date, string format) {
var formatWithoutLiterals = Regex.Replace(format, @"((?<!\\)'(.*?)((?<!\\)')|(?<!\\)""(.*?)((?<!\\)""))", "");
return new DateParts(
format.Contains('y') ? date.Year : 0,
format.Contains('M') ? date.Month : 0,
format.Contains('d') ? date.Day : 0
formatWithoutLiterals.Contains('y') ? date.Year : 0,
formatWithoutLiterals.Contains('M') ? date.Month : 0,
formatWithoutLiterals.Contains('d') ? date.Day : 0
);
}
private TimeParts GetExpectedTimeParts(DateTime time, string format) {
var formatWithoutLiterals = Regex.Replace(format, @"((?<!\\)'(.*?)((?<!\\)')|(?<!\\)""(.*?)((?<!\\)""))", "");
return new TimeParts(
format.Contains('H') || format.Contains('h') ? time.Hour : 0,
format.Contains('m') ? time.Minute : 0,
format.Contains('s') ? time.Second : 0,
format.Contains('f') ? time.Millisecond : 0
formatWithoutLiterals.Contains('H') || format.Contains('h') ? time.Hour : 0,
formatWithoutLiterals.Contains('m') ? time.Minute : 0,
formatWithoutLiterals.Contains('s') ? time.Second : 0,
formatWithoutLiterals.Contains('f') ? time.Millisecond : 0
);
}

View File

@@ -221,9 +221,9 @@ namespace Orchard.Framework.Localization.Services {
if (m.Groups["second"].Success) {
second = Int32.Parse(m.Groups["second"].Value);
}
if (m.Groups["millisecond"].Success) {
second = Int32.Parse(m.Groups["millisecond"].Value);
millisecond = Int32.Parse(m.Groups["millisecond"].Value);
}
return new TimeParts(hour, minute, second, millisecond);
@@ -257,16 +257,18 @@ namespace Orchard.Framework.Localization.Services {
{"m", "(?<minute>[0-9]{1,2})"},
{"ss", "(?<second>[0-9]{2})"},
{"s", "(?<second>[0-9]{1,2})"},
{"f", "(?<millisecond>[0-9]{1})"},
{"ff", "(?<millisecond>[0-9]{2})"},
{"fff", "(?<millisecond>[0-9]{3})"},
{"ffff", "(?<millisecond>[0-9]{4})"},
{"fffff", "(?<millisecond>[0-9]{5})"},
{"fffffff", "(?<millisecond>[0-9]{7})"},
{"ffffff", "(?<millisecond>[0-9]{6})"},
{"tt", String.Format("\\s*(?<amPm>{0}|{1})\\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))},
{"t", String.Format("\\s*(?<amPm>{0}|{1})\\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))},
{" tt", String.Format("\\s*(?<amPm>{0}|{1})\\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))},
{" t", String.Format("\\s*(?<amPm>{0}|{1})\\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))}
{"fffff", "(?<millisecond>[0-9]{5})"},
{"ffff", "(?<millisecond>[0-9]{4})"},
{"fff", "(?<millisecond>[0-9]{3})"},
{"ff", "(?<millisecond>[0-9]{2})"},
{"f", "(?<millisecond>[0-9]{1})"},
{"tt", String.Format(@"\s*(?<amPm>{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))},
{"t", String.Format(@"\s*(?<amPm>{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))},
{" tt", String.Format(@"\s*(?<amPm>{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))},
{" t", String.Format(@"\s*(?<amPm>{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))},
{"K", @"(?<timezone>Z|(\+|-)[0-9]{2}:[0-9]{2})*"},
};
}
@@ -316,13 +318,13 @@ namespace Orchard.Framework.Localization.Services {
// Transform the / and : characters into culture-specific date and time separators.
result = Regex.Replace(result, @"\/|:", m => m.Value == "/" ? _dateTimeFormatProvider.DateSeparator : _dateTimeFormatProvider.TimeSeparator);
// Escape all characters that are intrinsic Regex syntax.
result = EscapeForRegex(result);
// Transform all literals to corresponding wildcard matches.
result = Regex.Replace(result, @"(?<!\\)'(.*?)((?<!\\)')", m => String.Format("(.{{{0}}})", m.Value.Replace("\\", "").Length - 2));
// Transform all DateTime format specifiers into corresponding Regex captures.
result = result.ReplaceAll(replacements);