mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-15 19:54:57 +08:00
Fixed remaining bugs in DefaultDateFormatter. Reached 100% verified correctness! Yaaay!
This commit is contained in:
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
|
||||
|
Reference in New Issue
Block a user