diff --git a/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs b/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs index b3e591536..0275f6124 100644 --- a/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs +++ b/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs @@ -24,8 +24,13 @@ namespace Orchard.Framework.Tests.Localization { var failedCases = new ConcurrentDictionary(); var maxFailedCases = 0; + var options = new ParallelOptions(); + if (Debugger.IsAttached) { + options.MaxDegreeOfParallelism = 1; + } + var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures); - Parallel.ForEach(allCultures, culture => { // All cultures on the machine. + Parallel.ForEach(allCultures, options, culture => { // All cultures on the machine. var container = InitializeContainer(culture.Name, "GregorianCalendar"); var formats = container.Resolve(); var target = container.Resolve(); @@ -66,29 +71,34 @@ namespace Orchard.Framework.Tests.Localization { var failedCases = new ConcurrentDictionary(); var maxFailedCases = 0; + var options = new ParallelOptions(); + if (Debugger.IsAttached) { + options.MaxDegreeOfParallelism = 1; + } + var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures); - Parallel.ForEach(allCultures, culture => { // All cultures on the machine. + Parallel.ForEach(allCultures, options, culture => { // All cultures on the machine. var container = InitializeContainer(culture.Name, "GregorianCalendar"); var formats = container.Resolve(); var target = container.Resolve(); 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)); - try { - 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().First(); - var dateString = date.ToString(dateFormat, cultureGregorian); + 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().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 = new DateParts(date.Year, date.Month, date.Day); + var expected = GetExpectedDateParts(date, dateFormat); Assert.AreEqual(expected, result); } - } - catch (Exception ex) { - failedCases.TryAdd(caseKey, ex); + catch (Exception ex) { + failedCases.TryAdd(caseKey, ex); + } } } }); @@ -105,27 +115,31 @@ namespace Orchard.Framework.Tests.Localization { var failedCases = new ConcurrentDictionary(); var maxFailedCases = 0; + var options = new ParallelOptions(); + if (Debugger.IsAttached) { + options.MaxDegreeOfParallelism = 1; + } + var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures); - Parallel.ForEach(allCultures, culture => { // All cultures on the machine. + Parallel.ForEach(allCultures, options, culture => { // All cultures on the machine. var container = InitializeContainer(culture.Name, null); var formats = container.Resolve(); var target = container.Resolve(); 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)); - 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); + 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 reference = DateTime.ParseExact(timeString, timeFormat, culture); - var expected = new TimeParts(reference.Hour, reference.Minute, reference.Second, reference.Millisecond); + var expected = GetExpectedTimeParts(time, timeFormat); Assert.AreEqual(expected, result); } - } - catch (Exception ex) { - failedCases.TryAdd(caseKey, ex); + catch (Exception ex) { + failedCases.TryAdd(caseKey, ex); + } } } }); @@ -135,6 +149,23 @@ namespace Orchard.Framework.Tests.Localization { } } + private DateParts GetExpectedDateParts(DateTime date, string format) { + return new DateParts( + format.Contains('y') ? date.Year : 0, + format.Contains('M') ? date.Month : 0, + format.Contains('d') ? date.Day : 0 + ); + } + + private TimeParts GetExpectedTimeParts(DateTime time, string 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 + ); + } + private IContainer InitializeContainer(string cultureName, string calendarName) { var builder = new ContainerBuilder(); builder.RegisterInstance(new StubWorkContext(cultureName, calendarName)); diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationDateTimeFormatProvider.cs b/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationDateTimeFormatProvider.cs index d8c4b6f2e..a104d301f 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationDateTimeFormatProvider.cs +++ b/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationDateTimeFormatProvider.cs @@ -26,12 +26,24 @@ namespace Orchard.Localization.Services { } } + public virtual IEnumerable MonthNamesGenitive { + get { + return MonthNames; + } + } + public IEnumerable MonthNamesShort { get { return T("Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec").Text.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries); } } + public virtual IEnumerable MonthNamesShortGenitive { + get { + return MonthNamesShort; + } + } + public IEnumerable DayNames { get { return T("Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday").Text.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries); diff --git a/src/Orchard/Localization/Services/CultureDateTimeFormatProvider.cs b/src/Orchard/Localization/Services/CultureDateTimeFormatProvider.cs index 883bd0693..7e34fbfa4 100644 --- a/src/Orchard/Localization/Services/CultureDateTimeFormatProvider.cs +++ b/src/Orchard/Localization/Services/CultureDateTimeFormatProvider.cs @@ -13,8 +13,6 @@ namespace Orchard.Localization.Services { /// public class CultureDateTimeFormatProvider : IDateTimeFormatProvider { - // TODO: This implementation should probably also depend on the current calendar, because DateTimeFormatInfo returns different strings depending on the calendar. - private readonly IWorkContextAccessor _workContextAccessor; private readonly ICalendarManager _calendarManager; @@ -31,12 +29,24 @@ namespace Orchard.Localization.Services { } } + public virtual IEnumerable MonthNamesGenitive { + get { + return DateTimeFormat.MonthGenitiveNames; + } + } + public virtual IEnumerable MonthNamesShort { get { return DateTimeFormat.AbbreviatedMonthNames; } } + public virtual IEnumerable MonthNamesShortGenitive { + get { + return DateTimeFormat.AbbreviatedMonthGenitiveNames; + } + } + public virtual IEnumerable DayNames { get { return DateTimeFormat.DayNames; @@ -69,7 +79,7 @@ namespace Orchard.Localization.Services { public virtual string ShortDateTimeFormat { get { - // From empirical testing I am fairly certain this invariably evaluates to + // From empirical testing I am fairly certain First() invariably evaluates to // the pattern actually used when printing using the 'g' (i.e. general date/time // pattern with short time) standard format string. /DS return DateTimeFormat.GetAllDateTimePatterns('g').First(); diff --git a/src/Orchard/Localization/Services/DefaultDateFormatter.cs b/src/Orchard/Localization/Services/DefaultDateFormatter.cs index 6dff29adb..3f01384fd 100644 --- a/src/Orchard/Localization/Services/DefaultDateFormatter.cs +++ b/src/Orchard/Localization/Services/DefaultDateFormatter.cs @@ -167,10 +167,26 @@ namespace Orchard.Framework.Localization.Services { month = Int32.Parse(m.Groups["month"].Value); } else if (m.Groups["monthNameShort"].Success) { - month = _dateTimeFormatProvider.MonthNamesShort.Select(x => x.ToLowerInvariant()).ToList().IndexOf(m.Groups["monthNameShort"].Value.ToLowerInvariant()) + 1; + var shortName = m.Groups["monthNameShort"].Value.ToLowerInvariant(); + var allShortNamesGenitive = _dateTimeFormatProvider.MonthNamesShortGenitive.Select(x => x.ToLowerInvariant()).ToList(); + var allShortNames = _dateTimeFormatProvider.MonthNamesShort.Select(x => x.ToLowerInvariant()).ToList(); + if (allShortNamesGenitive.Contains(shortName)) { + month = allShortNamesGenitive.IndexOf(shortName) + 1; + } + else if (allShortNames.Contains(shortName)) { + month = allShortNames.IndexOf(shortName) + 1; + } } else if (m.Groups["monthName"].Success) { - month = _dateTimeFormatProvider.MonthNames.Select(x => x.ToLowerInvariant()).ToList().IndexOf(m.Groups["monthName"].Value.ToLowerInvariant()) + 1; + var name = m.Groups["monthName"].Value.ToLowerInvariant(); + var allNamesGenitive = _dateTimeFormatProvider.MonthNamesGenitive.Select(x => x.ToLowerInvariant()).ToList(); + var allNames = _dateTimeFormatProvider.MonthNames.Select(x => x.ToLowerInvariant()).ToList(); + if (allNamesGenitive.Contains(name)) { + month = allNamesGenitive.IndexOf(name) + 1; + } + else if (allNames.Contains(name)) { + month = allNames.IndexOf(name) + 1; + } } if (m.Groups["day"].Success) { @@ -215,12 +231,12 @@ namespace Orchard.Framework.Localization.Services { protected virtual Dictionary GetDateParseReplacements() { return new Dictionary() { - {"dddd", String.Format("(?{0})", String.Join("|", _dateTimeFormatProvider.DayNames))}, - {"ddd", String.Format("(?{0})", String.Join("|", _dateTimeFormatProvider.DayNamesShort))}, + {"dddd", String.Format("(?{0})", String.Join("|", _dateTimeFormatProvider.DayNames.Select(x => EscapeForRegex(x))))}, + {"ddd", String.Format("(?{0})", String.Join("|", _dateTimeFormatProvider.DayNamesShort.Select(x => EscapeForRegex(x))))}, {"dd", "(?[0-9]{2})"}, {"d", "(?[0-9]{1,2})"}, - {"MMMM", String.Format("(?{0})", String.Join("|", _dateTimeFormatProvider.MonthNames.Where(x => !String.IsNullOrEmpty(x))))}, - {"MMM", String.Format("(?{0})", String.Join("|", _dateTimeFormatProvider.MonthNamesShort.Where(x => !String.IsNullOrEmpty(x))))}, + {"MMMM", String.Format("(?{0})", String.Join("|", _dateTimeFormatProvider.MonthNames.Union(_dateTimeFormatProvider.MonthNamesGenitive).Where(x => !String.IsNullOrEmpty(x)).Distinct().Select(x => EscapeForRegex(x))))}, + {"MMM", String.Format("(?{0})", String.Join("|", _dateTimeFormatProvider.MonthNamesShort.Union(_dateTimeFormatProvider.MonthNamesShortGenitive).Where(x => !String.IsNullOrEmpty(x)).Distinct().Select(x => EscapeForRegex(x))))}, {"MM", "(?[0-9]{2})"}, {"M", "(?[0-9]{1,2})"}, {"yyyyy", "(?[0-9]{5})"}, @@ -247,10 +263,10 @@ namespace Orchard.Framework.Localization.Services { {"ffff", "(?[0-9]{4})"}, {"fffff", "(?[0-9]{5})"}, {"ffffff", "(?[0-9]{6})"}, - {"tt", String.Format("\\s*(?{0}|{1})\\s*", _dateTimeFormatProvider.AmPmDesignators.ToArray()[0], _dateTimeFormatProvider.AmPmDesignators.ToArray()[1])}, - {"t", String.Format("\\s*(?{0}|{1})\\s*", _dateTimeFormatProvider.AmPmDesignators.ToArray()[0], _dateTimeFormatProvider.AmPmDesignators.ToArray()[1])}, - {" tt", String.Format("\\s*(?{0}|{1})\\s*", _dateTimeFormatProvider.AmPmDesignators.ToArray()[0], _dateTimeFormatProvider.AmPmDesignators.ToArray()[1])}, - {" t", String.Format("\\s*(?{0}|{1})\\s*", _dateTimeFormatProvider.AmPmDesignators.ToArray()[0], _dateTimeFormatProvider.AmPmDesignators.ToArray()[1])} + {"tt", String.Format("\\s*(?{0}|{1})\\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))}, + {"t", String.Format("\\s*(?{0}|{1})\\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))}, + {" tt", String.Format("\\s*(?{0}|{1})\\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))}, + {" t", String.Format("\\s*(?{0}|{1})\\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))} }; } @@ -296,11 +312,23 @@ namespace Orchard.Framework.Localization.Services { //} protected virtual string ConvertFormatStringToRegexPattern(string format, IDictionary replacements) { - string result = null; - result = Regex.Replace(format, @"\.|\$|\^|\{|\[|\(|\||\)|\*|\+|\?|\\", m => String.Format(@"\{0}", m.Value)); + string result = format; + + // 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, @"(? String.Format("(.{{{0}}})", m.Value.Replace("\\", "").Length - 2)); + + // Transform all DateTime format specifiers into corresponding Regex captures. result = result.ReplaceAll(replacements); - result = String.Format(@"^{0}$", result); // Make sure string is anchored to beginning and end. + + // Make sure string is anchored to beginning and end. + result = String.Format(@"^{0}$", result); + return result; } @@ -311,6 +339,10 @@ namespace Orchard.Framework.Localization.Services { return hour12 == 12 ? 0 : hour12; } + protected virtual string EscapeForRegex(string input) { + return Regex.Replace(input, @"\.|\$|\^|\{|\[|\(|\||\)|\*|\+|\?|\\", m => String.Format(@"\{0}", m.Value)); + } + protected virtual CultureInfo CurrentCulture { get { var workContext = _workContextAccessor.GetContext(); diff --git a/src/Orchard/Localization/Services/IDateLocalizationServices.txt b/src/Orchard/Localization/Services/IDateLocalizationServices.txt index b6f9743a6..2413a98f9 100644 --- a/src/Orchard/Localization/Services/IDateLocalizationServices.txt +++ b/src/Orchard/Localization/Services/IDateLocalizationServices.txt @@ -41,10 +41,8 @@ struct DateLocalizationOptions { } TODO: + * Test for proper handling of fraction (f) format specifier - suspect it does not work properly in current state * Literal parts in format strings should be transformed to corresponding literal in Regex, not just into a wildcard, otherwise the resulting wildcard in some cases will match subsequent non-literal parts of the date/time string. - * 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 diff --git a/src/Orchard/Localization/Services/IDateTimeFormatProvider.cs b/src/Orchard/Localization/Services/IDateTimeFormatProvider.cs index 25e5edafe..d31946ad5 100644 --- a/src/Orchard/Localization/Services/IDateTimeFormatProvider.cs +++ b/src/Orchard/Localization/Services/IDateTimeFormatProvider.cs @@ -16,6 +16,13 @@ namespace Orchard.Localization.Services { get; } + /// + /// Gets a list of genitive month names (used in contexts when a day is involved). + /// + IEnumerable MonthNamesGenitive { + get; + } + /// /// Gets a list of abbreviated month names. /// @@ -23,6 +30,13 @@ namespace Orchard.Localization.Services { get; } + /// + /// Gets a list of abbreviated genivite month names (used in contexts when a day is involved). + /// + IEnumerable MonthNamesShortGenitive { + get; + } + /// /// Gets a list of weekday names. ///