From a09ab71c27267f56eed029452e941c0037f2d66e Mon Sep 17 00:00:00 2001 From: Daniel Stolt Date: Sun, 27 Jul 2014 17:55:03 +0200 Subject: [PATCH] Added format-specific overloads to IDateFormatter and DefaultDateFormatter for performance optimization. --- .../Localization/DefaultDateFormatterTests.cs | 10 +-- .../Services/DefaultDateFormatter.cs | 89 ++++++++++++++++--- .../Localization/Services/IDateFormatter.cs | 3 + .../Services/IDateLocalizationServices.txt | 3 + 4 files changed, 86 insertions(+), 19 deletions(-) diff --git a/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs b/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs index 52035e4b0..b3e591536 100644 --- a/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs +++ b/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs @@ -41,7 +41,7 @@ namespace Orchard.Framework.Tests.Localization { var cultureGregorian = (CultureInfo)culture.Clone(); cultureGregorian.DateTimeFormat.Calendar = cultureGregorian.OptionalCalendars.OfType().First(); var dateTimeString = dateTime.ToString(dateTimeFormat, cultureGregorian); - var result = target.ParseDateTime(dateTimeString); + 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); @@ -74,7 +74,7 @@ namespace Orchard.Framework.Tests.Localization { foreach (var dateFormat in formats.AllDateFormats) { // All date formats supported by the culture. var caseKey = String.Format("{0}:{1}", culture.Name, dateFormat); allCases.Add(caseKey); - Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, 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. DateTime date = new DateTime(1998, month, 1); @@ -82,7 +82,7 @@ namespace Orchard.Framework.Tests.Localization { var cultureGregorian = (CultureInfo)culture.Clone(); cultureGregorian.DateTimeFormat.Calendar = cultureGregorian.OptionalCalendars.OfType().First(); var dateString = date.ToString(dateFormat, cultureGregorian); - var result = target.ParseDate(dateString); + var result = target.ParseDate(dateString, dateFormat); var expected = new DateParts(date.Year, date.Month, date.Day); Assert.AreEqual(expected, result); } @@ -113,12 +113,12 @@ namespace Orchard.Framework.Tests.Localization { foreach (var timeFormat in formats.AllTimeFormats) { // All time formats supported by the culture. var caseKey = String.Format("{0}:{1}", culture.Name, timeFormat); allCases.Add(caseKey); - Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey)); + //Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey)); try { 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 result = target.ParseTime(timeString); + var result = target.ParseTime(timeString, timeFormat); var reference = DateTime.ParseExact(timeString, timeFormat, culture); var expected = new TimeParts(reference.Hour, reference.Minute, reference.Second, reference.Millisecond); Assert.AreEqual(expected, result); diff --git a/src/Orchard/Localization/Services/DefaultDateFormatter.cs b/src/Orchard/Localization/Services/DefaultDateFormatter.cs index 917a91df8..6dff29adb 100644 --- a/src/Orchard/Localization/Services/DefaultDateFormatter.cs +++ b/src/Orchard/Localization/Services/DefaultDateFormatter.cs @@ -28,44 +28,74 @@ namespace Orchard.Framework.Localization.Services { var replacements = GetDateParseReplacements().Concat(GetTimeParseReplacements()).ToDictionary(item => item.Key, item => item.Value); foreach (var dateTimeFormat in _dateTimeFormatProvider.AllDateTimeFormats) { - var dateTimePattern = ConvertFormatStringToRegexPattern(dateTimeFormat, replacements); - Match m = Regex.Match(dateTimeString.Trim(), dateTimePattern, RegexOptions.IgnoreCase); - if (m.Success) { - return new DateTimeParts(ExtractDateParts(m), ExtractTimeParts(m)); + var result = TryParseDateTime(dateTimeString, dateTimeFormat, replacements); + if (result.HasValue) { + return result.Value; } } throw new FormatException("The string was not recognized as a valid date and time."); } + public virtual DateTimeParts ParseDateTime(string dateTimeString, string format) { + var replacements = GetDateParseReplacements().Concat(GetTimeParseReplacements()).ToDictionary(item => item.Key, item => item.Value); + + var result = TryParseDateTime(dateTimeString, format, replacements); + if (result.HasValue) { + return result.Value; + } + + throw new FormatException("The string was not recognized as a valid date and time."); + } + public virtual DateParts ParseDate(string dateString) { var replacements = GetDateParseReplacements(); foreach (var dateFormat in _dateTimeFormatProvider.AllDateFormats) { - var datePattern = ConvertFormatStringToRegexPattern(dateFormat, replacements); - Match m = Regex.Match(dateString.Trim(), datePattern, RegexOptions.IgnoreCase); - if (m.Success) { - return ExtractDateParts(m); + var result = TryParseDate(dateString, dateFormat, replacements); + if (result.HasValue) { + return result.Value; } } throw new FormatException("The string was not recognized as a valid date."); } + public virtual DateParts ParseDate(string dateString, string format) { + var replacements = GetDateParseReplacements(); + + var result = TryParseDate(dateString, format, replacements); + if (result.HasValue) { + return result.Value; + } + + throw new FormatException("The string was not recognized as a valid date."); + } + public virtual TimeParts ParseTime(string timeString) { var replacements = GetTimeParseReplacements(); foreach (var timeFormat in _dateTimeFormatProvider.AllTimeFormats) { - var timePattern = ConvertFormatStringToRegexPattern(timeFormat, replacements); - Match m = Regex.Match(timeString.Trim(), timePattern, RegexOptions.IgnoreCase); - if (m.Success) { - return ExtractTimeParts(m); + var result = TryParseTime(timeString, timeFormat, replacements); + if (result.HasValue) { + return result.Value; } } throw new FormatException("The string was not recognized as a valid time."); } + public virtual TimeParts ParseTime(string timeString, string format) { + var replacements = GetTimeParseReplacements(); + + var result = TryParseTime(timeString, format, replacements); + if (result.HasValue) { + return result.Value; + } + + throw new FormatException("The string was not recognized as a valid time."); + } + public virtual string FormatDateTime(DateTimeParts parts) { // TODO: Mahsa should implement! throw new NotImplementedException(); @@ -96,12 +126,41 @@ namespace Orchard.Framework.Localization.Services { throw new NotImplementedException(); } + protected virtual DateTimeParts? TryParseDateTime(string dateTimeString, string format, IDictionary replacements) { + var dateTimePattern = ConvertFormatStringToRegexPattern(format, replacements); + Match m = Regex.Match(dateTimeString.Trim(), dateTimePattern, RegexOptions.IgnoreCase); + if (m.Success) { + return new DateTimeParts(ExtractDateParts(m), ExtractTimeParts(m)); + } + return null; + } + + protected virtual DateParts? TryParseDate(string dateString, string format, IDictionary replacements) { + var datePattern = ConvertFormatStringToRegexPattern(format, replacements); + Match m = Regex.Match(dateString.Trim(), datePattern, RegexOptions.IgnoreCase); + if (m.Success) { + return ExtractDateParts(m); + } + return null; + } + + protected virtual TimeParts? TryParseTime(string timeString, string format, IDictionary replacements) { + var timePattern = ConvertFormatStringToRegexPattern(format, replacements); + Match m = Regex.Match(timeString.Trim(), timePattern, RegexOptions.IgnoreCase); + if (m.Success) { + return ExtractTimeParts(m); + } + return null; + } + protected virtual DateParts ExtractDateParts(Match m) { int year = 0, month = 0, day = 0; - year = CurrentCalendar.ToFourDigitYear(Int32.Parse(m.Groups["year"].Value)); + if (m.Groups["year"].Success) { + year = CurrentCalendar.ToFourDigitYear(Int32.Parse(m.Groups["year"].Value)); + } // For the month we can either use the month number, the abbreviated month name or the full month name. if (m.Groups["month"].Success) { @@ -114,7 +173,9 @@ namespace Orchard.Framework.Localization.Services { month = _dateTimeFormatProvider.MonthNames.Select(x => x.ToLowerInvariant()).ToList().IndexOf(m.Groups["monthName"].Value.ToLowerInvariant()) + 1; } - day = Int32.Parse(m.Groups["day"].Value); + if (m.Groups["day"].Success) { + day = Int32.Parse(m.Groups["day"].Value); + } return new DateParts(year, month, day); } diff --git a/src/Orchard/Localization/Services/IDateFormatter.cs b/src/Orchard/Localization/Services/IDateFormatter.cs index 1dc182e6f..80c080658 100644 --- a/src/Orchard/Localization/Services/IDateFormatter.cs +++ b/src/Orchard/Localization/Services/IDateFormatter.cs @@ -7,8 +7,11 @@ using Orchard.Framework.Localization.Models; namespace Orchard.Framework.Localization.Services { public interface IDateFormatter : IDependency { DateTimeParts ParseDateTime(string dateTimeString); + DateTimeParts ParseDateTime(string dateTimeString, string format); DateParts ParseDate(string dateString); + DateParts ParseDate(string dateString, string format); TimeParts ParseTime(string timeString); + TimeParts ParseTime(string timeString, string format); string FormatDateTime(DateTimeParts parts); string FormatDateTime(DateTimeParts parts, string format); string FormatDate(DateParts parts); diff --git a/src/Orchard/Localization/Services/IDateLocalizationServices.txt b/src/Orchard/Localization/Services/IDateLocalizationServices.txt index 6d30e47ee..15957e640 100644 --- a/src/Orchard/Localization/Services/IDateLocalizationServices.txt +++ b/src/Orchard/Localization/Services/IDateLocalizationServices.txt @@ -41,6 +41,9 @@ struct DateLocalizationOptions { } TODO: + * Add ability to analyze format string to determine which parts we might reasonably expect back from parsing + - 4-digit year or only 2-digit so we must infer century? + - Which components are present? * Rewrite DefaultDateLocalizationServices * Write unit tests for DefaultDateLocalizationServices * Add warning message when saving unsupported combination in settings