Fixed another rounding bug with fraction formatting of millisecond, and added unit tests to cover this once and for all.

This commit is contained in:
Daniel Stolt
2014-08-07 04:44:47 +02:00
parent 58efe9c5b2
commit 56141a7549
3 changed files with 122 additions and 7 deletions

View File

@@ -187,10 +187,59 @@ namespace Orchard.Tests.Localization {
}
}
[Test]
[Description("Date/time parsing works correctly for all combinations of milliseconds, format strings and cultures..")]
public void ParseDateTimeTest04() {
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.
foreach (var millisecond in new[] { 0, 10, 500, 990, 999 }) { // Enough values to cover all fraction rounding cases.
var container = TestHelpers.InitializeContainer(culture.Name, "GregorianCalendar", TimeZoneInfo.Utc);
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.
DateTime dateTime = new DateTime(1998, 1, 1, 10, 30, 30, millisecond, DateTimeKind.Utc);
// 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, TimeZoneInfo.Utc);
Assert.AreEqual(expected, result);
}
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 throws a FormatException for unparsable date/time strings.")]
[ExpectedException(typeof(FormatException))]
public void ParseDateTimeTest04() {
public void ParseDateTimeTest05() {
var container = TestHelpers.InitializeContainer("en-US", null, TimeZoneInfo.Utc);
var target = container.Resolve<IDateFormatter>();
target.ParseDateTime("BlaBlaBla");
@@ -525,6 +574,69 @@ namespace Orchard.Tests.Localization {
}
}
[Test]
[Description("Date/time formatting works correctly for all combinations of milliseconds, format strings and cultures.")]
public void FormatDateTimeTest04() {
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 = TestHelpers.InitializeContainer(culture.Name, "GregorianCalendar", TimeZoneInfo.Utc);
var formats = container.Resolve<IDateTimeFormatProvider>();
var target = container.Resolve<IDateFormatter>();
foreach (var dateTimeFormat in formats.AllDateTimeFormats) { // All date/time formats supported by the culture.
foreach (var millisecond in new[] { 0, 10, 500, 990, 999 }) { // Enough values to cover all fraction rounding cases.
DateTime dateTime = new DateTime(1998, 1, 1, 10, 30, 30, millisecond);
DateTimeParts dateTimeParts = new DateTimeParts(1998, 1, 1, 10, 30, 30, millisecond, DateTimeKind.Unspecified, offset: TimeSpan.Zero);
// Print reference string using Gregorian calendar to avoid calendar conversion.
var cultureGregorian = (CultureInfo)culture.Clone();
cultureGregorian.DateTimeFormat.Calendar = cultureGregorian.OptionalCalendars.OfType<GregorianCalendar>().First();
var caseKey = String.Format("{0}___{1}___{2}", culture.Name, dateTimeFormat, dateTimeParts);
allCases.Add(caseKey);
//Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey));
try {
var result = target.FormatDateTime(dateTimeParts, dateTimeFormat);
var expected = dateTime.ToString(dateTimeFormat, cultureGregorian);
if (result != expected) {
// The .NET date formatting logic contains a bug that causes it to recognize 'd' and 'dd'
// as numerical day specifiers even when they are embedded in literals. Our implementation
// does not contain this bug. If we encounter an unexpected result and the .NET reference
// result contains the genitive month name, replace it with the non-genitive month name
// before asserting.
var numericalDayPattern = @"(\b|[^d])d{1,2}(\b|[^d])";
var containsNumericalDay = Regex.IsMatch(dateTimeFormat, numericalDayPattern);
if (containsNumericalDay) {
var monthName = formats.MonthNames[0];
var monthNameGenitive = formats.MonthNamesGenitive[0];
expected = expected.Replace(monthNameGenitive, monthName);
}
}
Assert.AreEqual(expected, result);
}
catch (Exception ex) {
failedCases.TryAdd(caseKey, ex);
}
}
}
});
if (failedCases.Count > maxFailedCases) {
throw new AggregateException(String.Format("Format tests failed for {0} of {1} cases. Expected {2} failed cases or less.", failedCases.Count, allCases.Count, maxFailedCases), failedCases.Values);
}
}
[Test]
[Description("Date formatting works correctly for all combinations of months, format strings and cultures.")]
public void FormatDateTest01() {

View File

@@ -320,9 +320,12 @@ namespace Orchard.Localization.Services {
fraction1Zero = (((decimal)parts.Millisecond) / 1000).ToString("0.0", CultureInfo.InvariantCulture).Substring(2);
fraction2Zero = (((decimal)parts.Millisecond) / 1000).ToString("0.00", CultureInfo.InvariantCulture).Substring(2);
fraction3Zero = (((decimal)parts.Millisecond) / 1000).ToString("0.000", CultureInfo.InvariantCulture).Substring(2);
fraction1Digit = parts.Millisecond >= 50 ? (((decimal)parts.Millisecond) / 1000).ToString("0.#", CultureInfo.InvariantCulture).Substring(2) : "";
fraction2Digit = parts.Millisecond >= 5 ? (((decimal)parts.Millisecond) / 1000).ToString("0.##", CultureInfo.InvariantCulture).Substring(2) : "";
fraction3Digit = parts.Millisecond > 0 ? (((decimal)parts.Millisecond) / 1000).ToString("0.###", CultureInfo.InvariantCulture).Substring(2) : "";
var fraction1 = (((decimal)parts.Millisecond) / 1000).ToString("0.#", CultureInfo.InvariantCulture);
var fraction2 = (((decimal)parts.Millisecond) / 1000).ToString("0.##", CultureInfo.InvariantCulture);
var fraction3 = (((decimal)parts.Millisecond) / 1000).ToString("0.###", CultureInfo.InvariantCulture);
fraction1Digit = fraction1.Length >= 2 ? fraction1.Substring(2) : "";
fraction2Digit = fraction2.Length >= 2 ? fraction2.Substring(2) : "";
fraction3Digit = fraction3.Length >= 2 ? fraction3.Substring(2) : "";
}
protected virtual bool GetUseGenitiveMonthName(string format) {

View File

@@ -39,12 +39,12 @@ namespace Orchard.Localization.Services {
}
if (CurrentTimeZone == TimeZoneInfo.Utc) {
if (dateUtc.Kind == DateTimeKind.Unspecified) {
return new DateTime(dateUtc.Ticks, DateTimeKind.Utc);
return DateTime.SpecifyKind(dateUtc, DateTimeKind.Utc);
}
return dateUtc;
}
var dateLocal = TimeZoneInfo.ConvertTimeFromUtc(dateUtc, CurrentTimeZone);
return new DateTime(dateLocal.Ticks, DateTimeKind.Local);
return DateTime.SpecifyKind(dateLocal, DateTimeKind.Local);
}
public virtual DateTime ConvertFromSiteTimeZone(DateTime dateLocal) {
@@ -58,7 +58,7 @@ namespace Orchard.Localization.Services {
}
var dateUnspecified = new DateTime(dateLocal.Ticks, DateTimeKind.Unspecified);
var dateUtc = TimeZoneInfo.ConvertTimeToUtc(dateUnspecified, CurrentTimeZone);
return new DateTime(dateUtc.Ticks, DateTimeKind.Utc);
return DateTime.SpecifyKind(dateUtc, DateTimeKind.Utc);
}
public virtual DateTimeParts ConvertToSiteCalendar(DateTime date, TimeSpan offset) {