mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-15 19:54:57 +08:00
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:
@@ -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() {
|
||||
|
@@ -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) {
|
||||
|
@@ -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) {
|
||||
|
Reference in New Issue
Block a user