Fixed a DST-related time zone conversion bug and added 3 unit tests to verify.

This commit is contained in:
Daniel Stolt
2014-08-08 02:43:10 +02:00
parent 6b70c086b0
commit 2e334baa37
4 changed files with 117 additions and 42 deletions

View File

@@ -261,42 +261,99 @@ namespace Orchard.Tests.Localization {
Assert.AreEqual(expected, result);
}
[Test]
[Description("Non-DST date and time are properly round-tripped.")]
public void ConvertToLocalizedTimeStringTest01() {
var timeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
var container = TestHelpers.InitializeContainer("en-US", null, timeZone);
var target = container.Resolve<IDateLocalizationServices>();
var dateString = "3/11/2012";
var timeString = "12:00:00 PM";
var dateTimeUtc = target.ConvertFromLocalizedString(dateString, timeString);
var dateString2 = target.ConvertToLocalizedDateString(dateTimeUtc);
var timeString2 = target.ConvertToLocalizedTimeString(dateTimeUtc);
Assert.AreEqual(dateString, dateString2);
Assert.AreEqual(timeString, timeString2);
}
[Test]
[Description("DST date and time are properly round-tripped.")]
public void ConvertToLocalizedTimeStringTest02() {
var timeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
var container = TestHelpers.InitializeContainer("en-US", null, timeZone);
var target = container.Resolve<IDateLocalizationServices>();
var dateString = "3/10/2012";
var timeString = "12:00:00 PM";
var dateTimeUtc = target.ConvertFromLocalizedString(dateString, timeString);
var dateString2 = target.ConvertToLocalizedDateString(dateTimeUtc);
var timeString2 = target.ConvertToLocalizedTimeString(dateTimeUtc);
Assert.AreEqual(dateString, dateString2);
Assert.AreEqual(timeString, timeString2);
}
[Test]
[Description("DST date and time are not properly round-tripped when date is ignored.")]
public void ConvertToLocalizedTimeStringTest03() {
var timeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
var container = TestHelpers.InitializeContainer("en-US", null, timeZone);
var target = container.Resolve<IDateLocalizationServices>();
var dateString = "3/10/2012";
var timeString = "12:00:00 PM";
var dateTimeUtc = target.ConvertFromLocalizedString(dateString, timeString);
var dateString2 = target.ConvertToLocalizedDateString(dateTimeUtc);
var timeString2 = target.ConvertToLocalizedTimeString(dateTimeUtc, new DateLocalizationOptions() { IgnoreDate = true });
Assert.AreEqual(dateString, dateString2);
Assert.AreNotEqual(timeString, timeString2);
}
/*
ConvertToLocalizedTimeStringTest
Time zone conversion works properly (even though there is no date component).
Time zone conversion works properly (even though there is no date component).
ConvertToLocalizedStringTest
Non-nullable DateTime.MinValue is converted to NullText.
Nullable DateTime.MinValue is converted to date/time string.
Nullable null is converted to NullText.
Time zone conversion is performed when EnableTimeZoneConversion is true.
Time zone conversion is not performed when EnableTimeZoneConversion is false.
Calendar conversion is performed when EnableCalendarConversion is true.
Calendar conversion is not performed when EnableCalendarConversion is false.
Full conversion is performed with default options.
Non-nullable DateTime.MinValue is converted to NullText.
Nullable DateTime.MinValue is converted to date/time string.
Nullable null is converted to NullText.
Time zone conversion is performed when EnableTimeZoneConversion is true.
Time zone conversion is not performed when EnableTimeZoneConversion is false.
Calendar conversion is performed when EnableCalendarConversion is true.
Calendar conversion is not performed when EnableCalendarConversion is false.
Full conversion is performed with default options.
ConvertFromLocalizedStringCombinedTest
Null date/time string is converted to null.
Empty date/time string is converted to null.
Custom NullText date/time string is converted to null.
Time zone conversion is performed when EnableTimeZoneConversion is true.
Time zone conversion is not performed when EnableTimeZoneConversion is false.
Calendar conversion is performed when EnableCalendarConversion is true.
Calendar conversion is not performed when EnableCalendarConversion is false.
Full conversion is performed with default options.
Null date/time string is converted to null.
Empty date/time string is converted to null.
Custom NullText date/time string is converted to null.
Time zone conversion is performed when EnableTimeZoneConversion is true.
Time zone conversion is not performed when EnableTimeZoneConversion is false.
Calendar conversion is performed when EnableCalendarConversion is true.
Calendar conversion is not performed when EnableCalendarConversion is false.
Full conversion is performed with default options.
ConvertFromLocalizedStringSeparateTest
Null date string and time string is converted to null.
Empty date string and time string is converted to null.
Custom NullText date string and time string is converted to null.
Time zone conversion works properly when date component is omitted.
Time zone conversion is never performed when time component is omitted.
Time zone conversion is performed when EnableTimeZoneConversion is true.
Time zone conversion is not performed when EnableTimeZoneConversion is false.
Calendar conversion is never performed when date component is omitted.
Calendar conversion is performed when EnableCalendarConversion is true.
Calendar conversion is not performed when EnableCalendarConversion is false.
Full conversion is performed with default options.
*/
Null date string and time string is converted to null.
Empty date string and time string is converted to null.
Custom NullText date string and time string is converted to null.
Time zone conversion works properly when date component is omitted.
Time zone conversion is never performed when time component is omitted.
Time zone conversion is performed when EnableTimeZoneConversion is true.
Time zone conversion is not performed when EnableTimeZoneConversion is false.
Calendar conversion is never performed when date component is omitted.
Calendar conversion is performed when EnableCalendarConversion is true.
Calendar conversion is not performed when EnableCalendarConversion is false.
Full conversion is performed with default options.
*/
}
}

View File

@@ -52,6 +52,7 @@ namespace Orchard.Fields.Drivers {
// Don't do any calendar conversion if field is semantically a time-only field, because the date component might we out of allowed boundaries for the current calendar.
if (settings.Display == DateTimeFieldDisplays.TimeOnly) {
options.EnableCalendarConversion = false;
options.IgnoreDate = true;
}
var showDate = settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.DateOnly;
@@ -88,6 +89,7 @@ namespace Orchard.Fields.Drivers {
// Don't do any calendar conversion if field is semantically a time-only field, because the date component might we out of allowed boundaries for the current calendar.
if (settings.Display == DateTimeFieldDisplays.TimeOnly) {
options.EnableCalendarConversion = false;
options.IgnoreDate = true;
}
var showDate = settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.DateOnly;
@@ -126,6 +128,7 @@ namespace Orchard.Fields.Drivers {
// Don't do any calendar conversion if field is semantically a time-only field, because the date component might we out of allowed boundaries for the current calendar.
if (settings.Display == DateTimeFieldDisplays.TimeOnly) {
options.EnableCalendarConversion = false;
options.IgnoreDate = true;
}
var showDate = settings.Display == DateTimeFieldDisplays.DateAndTime || settings.Display == DateTimeFieldDisplays.DateOnly;

View File

@@ -11,13 +11,14 @@ namespace Orchard.Localization.Models {
NullText = String.Empty;
EnableTimeZoneConversion = true;
EnableCalendarConversion = true;
IgnoreDate = false;
}
/// <summary>
/// Gets on sets the string to use in place of a null date when converting to and from a string. The default is an empty string.
/// </summary>
public string NullText { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether to perform time zone conversion as part of converting a date to and from a string. The default is true.
/// </summary>
@@ -27,5 +28,13 @@ namespace Orchard.Localization.Models {
/// Gets or sets a boolean indicating whether to perform calendar conversion as part of converting a date to and from a string. The default is true.
/// </summary>
public bool EnableCalendarConversion { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether to ignore the date component of the source date and assume today when converting to a time string. The default is false.
/// </summary>
/// <remarks>
/// This affects time zone conversion as the DST offset can be different on different days of the year.
/// </remarks>
public bool IgnoreDate { get; set; }
}
}

View File

@@ -111,15 +111,21 @@ namespace Orchard.Localization.Services {
var offset = TimeSpan.Zero;
if (options.EnableTimeZoneConversion) {
// Since no date component is expected (technically the date component is that of DateTime.MinValue) then
// we must employ some trickery, for two reasons:
// * DST can be active or not dependeng on the time of the year. We want the conversion to always act as if the time represents today, but we don't want that date stored.
// * Time zone conversion cannot wrap DateTime.MinValue around to the previous day, resulting in undefined result.
// Therefore we convert the date to today's date before the conversion, and back to DateTime.MinValue after.
var now = _clock.UtcNow;
dateValue = new DateTime(now.Year, now.Month, now.Day, dateValue.Hour, dateValue.Minute, dateValue.Second, dateValue.Millisecond, dateValue.Kind);
dateValue = ConvertToSiteTimeZone(dateValue);
dateValue = new DateTime(DateTime.MinValue.Year, DateTime.MinValue.Month, DateTime.MinValue.Day, dateValue.Hour, dateValue.Minute, dateValue.Second, dateValue.Millisecond, dateValue.Kind);
if (options.IgnoreDate) {
// The caller has asked us to ignore the date part and assume it is today. This usually because the source
// is a time-only field, in which case the date part is usually DateTime.MinValue which we should not use
// for the following reasons:
// * DST can be active or not dependeng on the time of the year. We want the conversion to always act as if the time represents today, but we don't want that date stored.
// * Time zone conversion cannot wrap DateTime.MinValue around to the previous day, resulting in undefined result.
// Therefore we convert the date to today's date before the conversion, and back to the original date after.
var today = _clock.UtcNow.Date;
var tempDate = new DateTime(today.Year, today.Month, today.Day, dateValue.Hour, dateValue.Minute, dateValue.Second, dateValue.Millisecond, dateValue.Kind);
tempDate = ConvertToSiteTimeZone(tempDate);
dateValue = new DateTime(dateValue.Year, dateValue.Month, dateValue.Day, tempDate.Hour, tempDate.Minute, tempDate.Second, tempDate.Millisecond, tempDate.Kind);
}
else {
dateValue = ConvertToSiteTimeZone(dateValue);
}
offset = CurrentTimeZone.GetUtcOffset(date.Value);
}