From c02f09d900461364f489cd752987a4d1ebf48f18 Mon Sep 17 00:00:00 2001 From: Daniel Stolt Date: Mon, 28 Jul 2014 19:30:41 +0200 Subject: [PATCH] Changed from IEnumerable to string[] on some of the properties on IDateTimeFormatProvider because they are naturally accessed by index. Incremental work on formatting implementation in DefaultDateFormatter. --- .../Localization/DefaultDateFormatterTests.cs | 49 ++++++++++ .../LocalizationDateTimeFormatProvider.cs | 16 ++-- src/Orchard/Localization/Models/DateParts.cs | 8 ++ .../Services/CultureDateTimeFormatProvider.cs | 21 ++-- .../Services/DefaultDateFormatter.cs | 96 +++++++++++++------ .../Services/IDateLocalizationServices.txt | 4 + .../Services/IDateTimeFormatProvider.cs | 16 ++-- 7 files changed, 151 insertions(+), 59 deletions(-) diff --git a/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs b/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs index cf4f9deb1..2e14cbd2a 100644 --- a/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs +++ b/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs @@ -227,6 +227,55 @@ namespace Orchard.Framework.Tests.Localization { } } + [Test] + [Description("Date formatting works correctly for all combinations of months, format strings and cultures.")] + public void FormatDateTest01() { + var allCases = new ConcurrentBag(); + 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, 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. + for (var month = 1; month <= 12; month++) { // All months in the year. + + DateTime date = new DateTime(1998, month, 1); + DateParts dateParts = new DateParts(1998, month, 1); + + // Print reference string using Gregorian calendar to avoid calendar conversion. + var cultureGregorian = (CultureInfo)culture.Clone(); + cultureGregorian.DateTimeFormat.Calendar = cultureGregorian.OptionalCalendars.OfType().First(); + + var caseKey = String.Format("{0}___{1}___{2}", culture.Name, dateFormat, dateParts); + allCases.Add(caseKey); + //Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey)); + + try { + var result = target.FormatDate(dateParts, dateFormat); + var expected = date.ToString(dateFormat, cultureGregorian); + 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("Time parsing throws a FormatException for unparsable time strings.")] [ExpectedException(typeof(FormatException))] diff --git a/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationDateTimeFormatProvider.cs b/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationDateTimeFormatProvider.cs index a104d301f..f38072588 100644 --- a/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationDateTimeFormatProvider.cs +++ b/src/Orchard.Web/Modules/Orchard.Localization/Services/LocalizationDateTimeFormatProvider.cs @@ -20,43 +20,43 @@ namespace Orchard.Localization.Services { public Localizer T { get; set; } - public IEnumerable MonthNames { + public string[] MonthNames { get { return T("January, February, March, April, May, June, July, August, September, October, November, December").Text.Split(new string[] {", "}, StringSplitOptions.RemoveEmptyEntries); } } - public virtual IEnumerable MonthNamesGenitive { + public virtual string[] MonthNamesGenitive { get { return MonthNames; } } - public IEnumerable MonthNamesShort { + public string[] 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 { + public virtual string[] MonthNamesShortGenitive { get { return MonthNamesShort; } } - public IEnumerable DayNames { + public string[] DayNames { get { return T("Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday").Text.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries); } } - public IEnumerable DayNamesShort { + public string[] DayNamesShort { get { return T("Sun, Mon, Tue, Wed, Thu, Fri, Sat").Text.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries); } } - public IEnumerable DayNamesMin { + public string[] DayNamesMin { get { return T("Su, Mo, Tu, We, Th, Fr, Sa").Text.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries); } @@ -160,7 +160,7 @@ namespace Orchard.Localization.Services { } } - public IEnumerable AmPmDesignators { + public string[] AmPmDesignators { get { var t = T("AM;PM").Text; var parts = t.Split(';'); diff --git a/src/Orchard/Localization/Models/DateParts.cs b/src/Orchard/Localization/Models/DateParts.cs index d06fe4f58..c6e3b3380 100644 --- a/src/Orchard/Localization/Models/DateParts.cs +++ b/src/Orchard/Localization/Models/DateParts.cs @@ -31,6 +31,14 @@ namespace Orchard.Localization.Models { } } + public DateTime ToDateTime() { + return new DateTime( + _year > 0 ? _year : DateTime.MinValue.Year, + _month > 0 ? _month : DateTime.MinValue.Month, + _day > 0 ? _day : DateTime.MinValue.Day + ); + } + public override string ToString() { return String.Format("{0}-{1}-{2}", _year, _month, _day); } diff --git a/src/Orchard/Localization/Services/CultureDateTimeFormatProvider.cs b/src/Orchard/Localization/Services/CultureDateTimeFormatProvider.cs index 3197ba7b8..464151cdb 100644 --- a/src/Orchard/Localization/Services/CultureDateTimeFormatProvider.cs +++ b/src/Orchard/Localization/Services/CultureDateTimeFormatProvider.cs @@ -23,43 +23,43 @@ namespace Orchard.Localization.Services { _calendarManager = calendarManager; } - public virtual IEnumerable MonthNames { + public virtual string[] MonthNames { get { return DateTimeFormat.MonthNames; } } - public virtual IEnumerable MonthNamesGenitive { + public virtual string[] MonthNamesGenitive { get { return DateTimeFormat.MonthGenitiveNames; } } - public virtual IEnumerable MonthNamesShort { + public virtual string[] MonthNamesShort { get { return DateTimeFormat.AbbreviatedMonthNames; } } - public virtual IEnumerable MonthNamesShortGenitive { + public virtual string[] MonthNamesShortGenitive { get { return DateTimeFormat.AbbreviatedMonthGenitiveNames; } } - public virtual IEnumerable DayNames { + public virtual string[] DayNames { get { return DateTimeFormat.DayNames; } } - public virtual IEnumerable DayNamesShort { + public virtual string[] DayNamesShort { get { return DateTimeFormat.AbbreviatedDayNames; } } - public virtual IEnumerable DayNamesMin { + public virtual string[] DayNamesMin { get { return DateTimeFormat.ShortestDayNames; } @@ -111,9 +111,6 @@ namespace Orchard.Localization.Services { patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('D')); patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('m')); patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('y')); - // The standard format strings 'M' (month/day pattern) and 'Y' (year/month - // pattern) are excluded because they can not be round-tripped with full - // date fidelity. return patterns.Distinct(); } } @@ -175,9 +172,9 @@ namespace Orchard.Localization.Services { } } - public virtual IEnumerable AmPmDesignators { + public virtual string[] AmPmDesignators { get { - return new string[] { DateTimeFormat.AMDesignator, DateTimeFormat.PMDesignator }; + return new [] { DateTimeFormat.AMDesignator, DateTimeFormat.PMDesignator }; } } diff --git a/src/Orchard/Localization/Services/DefaultDateFormatter.cs b/src/Orchard/Localization/Services/DefaultDateFormatter.cs index faaa3c822..15ab2c838 100644 --- a/src/Orchard/Localization/Services/DefaultDateFormatter.cs +++ b/src/Orchard/Localization/Services/DefaultDateFormatter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -112,8 +113,20 @@ namespace Orchard.Localization.Services { } public virtual string FormatDate(DateParts parts, string format) { - // TODO: Mahsa should implement! - throw new NotImplementedException(); + var replacements = GetDateFormatReplacements(); + var formatString = ConvertToFormatString(format, replacements); + var calendar = CurrentCalendar; + + var dateTime = parts.ToDateTime(); + + var yearString = parts.Year.ToString("00", System.Globalization.CultureInfo.InvariantCulture); + var twoDigitYear = Int32.Parse(yearString.Substring(yearString.Length - 2)); + var monthName = parts.Month > 0 ? _dateTimeFormatProvider.MonthNamesGenitive[parts.Month - 1] : null; + var monthNameShort = parts.Month > 0 ? _dateTimeFormatProvider.MonthNamesShortGenitive[parts.Month - 1] : null; + var dayName = parts.Day > 0 ? _dateTimeFormatProvider.DayNames[(int)calendar.GetDayOfWeek(dateTime)] : null; + var dayNameShort = parts.Day > 0 ? _dateTimeFormatProvider.DayNamesShort[(int)calendar.GetDayOfWeek(parts.ToDateTime())] : null; + + return String.Format(formatString, parts.Year, twoDigitYear, parts.Month, monthName, monthNameShort, parts.Day, dayName, dayNameShort); } public virtual string FormatTime(TimeParts parts) { @@ -127,7 +140,7 @@ namespace Orchard.Localization.Services { } protected virtual DateTimeParts? TryParseDateTime(string dateTimeString, string format, IDictionary replacements) { - var dateTimePattern = ConvertFormatStringToRegexPattern(format, replacements); + var dateTimePattern = ConvertToRegexPattern(format, replacements); Match m = Regex.Match(dateTimeString, dateTimePattern, RegexOptions.IgnoreCase); if (m.Success) { return new DateTimeParts(ExtractDateParts(m), ExtractTimeParts(m)); @@ -136,7 +149,7 @@ namespace Orchard.Localization.Services { } protected virtual DateParts? TryParseDate(string dateString, string format, IDictionary replacements) { - var datePattern = ConvertFormatStringToRegexPattern(format, replacements); + var datePattern = ConvertToRegexPattern(format, replacements); Match m = Regex.Match(dateString, datePattern, RegexOptions.IgnoreCase); if (m.Success) { return ExtractDateParts(m); @@ -145,7 +158,7 @@ namespace Orchard.Localization.Services { } protected virtual TimeParts? TryParseTime(string timeString, string format, IDictionary replacements) { - var timePattern = ConvertFormatStringToRegexPattern(format, replacements); + var timePattern = ConvertToRegexPattern(format, replacements); Match m = Regex.Match(timeString, timePattern, RegexOptions.IgnoreCase); if (m.Success) { return ExtractTimeParts(m); @@ -210,8 +223,8 @@ namespace Orchard.Localization.Services { if (!m.Groups["amPm"].Success) { throw new FormatException("The string was not recognized as a valid time. The hour is in 12-hour notation but no AM/PM designator was found."); } - var isPm = m.Groups["amPm"].Value.Equals(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1], StringComparison.InvariantCultureIgnoreCase); - hour = ConvertHour12ToHour24(Int32.Parse(m.Groups["hour12"].Value), isPm); + var isPm = m.Groups["amPm"].Value.Equals(_dateTimeFormatProvider.AmPmDesignators[1], StringComparison.InvariantCultureIgnoreCase); + hour = ConvertToHour24(Int32.Parse(m.Groups["hour12"].Value), isPm); } if (m.Groups["minute"].Success) { @@ -264,31 +277,31 @@ namespace Orchard.Localization.Services { {"fff", "(?[0-9]{3})"}, {"ff", "(?[0-9]{2})"}, {"f", "(?[0-9]{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]))}, + {"tt", String.Format(@"\s*(?{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[1]))}, + {"t", String.Format(@"\s*(?{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[1]))}, + {" tt", String.Format(@"\s*(?{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[1]))}, + {" t", String.Format(@"\s*(?{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[1]))}, {"K", @"(?Z|(\+|-)[0-9]{2}:[0-9]{2})*"}, }; } - //protected virtual Dictionary GetDateFormatReplacements() { - // return new Dictionary() { - // {"dddd", "{5:dddd}"}, - // {"ddd", "{6:ddd}"}, - // {"dd", "{2:00}"}, - // {"d", "{2:##}"}, - // {"MMMM", "{3:MMMM}"}, - // {"MMM", "{4:MMM}"}, - // {"MM", "{1:00}"}, - // {"M", "{1:##}"}, - // {"yyyyy", "{1:00000}"}, - // {"yyyy", "{1:0000}"}, - // {"yyy", "{1:000}"}, - // {"yy", "{1:00}"}, - // {"y", "{1:0}"} - // }; - //} + protected virtual Dictionary GetDateFormatReplacements() { + return new Dictionary() { + {"dddd", "{6:dddd}"}, + {"ddd", "{7:ddd}"}, + {"dd", "{5:00}"}, + {"d", "{5:##}"}, + {"MMMM", "{3:MMMM}"}, + {"MMM", "{4:MMM}"}, + {"MM", "{2:00}"}, + {"M", "{2:##}"}, + {"yyyyy", "{0:00000}"}, + {"yyyy", "{0:0000}"}, + {"yyy", "{0:000}"}, + {"yy", "{1:00}"}, + {"y", "{1:0}"} + }; + } //protected virtual Dictionary GetTimeFormatReplacements() { // return new Dictionary() { @@ -313,7 +326,7 @@ namespace Orchard.Localization.Services { // }; //} - protected virtual string ConvertFormatStringToRegexPattern(string format, IDictionary replacements) { + protected virtual string ConvertToRegexPattern(string format, IDictionary replacements) { string result = format; // Transform the / and : characters into culture-specific date and time separators. @@ -323,7 +336,6 @@ namespace Orchard.Localization.Services { result = EscapeForRegex(result); // Transform all literals to corresponding wildcard matches. - //result = Regex.Replace(result, @"(? EscapeForRegex(m.Value.Trim('\'', '"'))); result = Regex.Replace(result, @"(? String.Format("(?:.{{{0}}})", m.Value.Replace("\\", "").Length - 2)); // Transform all DateTime format specifiers into corresponding Regex captures. @@ -335,7 +347,29 @@ namespace Orchard.Localization.Services { return result; } - protected virtual int ConvertHour12ToHour24(int hour12, bool isPm) { + protected virtual string ConvertToFormatString(string format, IDictionary replacements) { + 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); + + //// Transform all literals to corresponding text. + //var literals = new List(); + //result = Regex.Replace(result, @"(? { + // literals.Add(m.Value.Trim('\'', '"')); + // return String.Format("{{{0}}}", literals.Count - 1); + //}); + + // Transform all literals to corresponding text. + result = Regex.Replace(result, @"(? m.Value.Trim('\'', '"')); + + // Transform all DateTime format specifiers into corresponding format string placeholders. + result = result.ReplaceAll(replacements); + + return result; + } + + protected virtual int ConvertToHour24(int hour12, bool isPm) { if (isPm) { return hour12 == 12 ? 12 : hour12 + 12; } diff --git a/src/Orchard/Localization/Services/IDateLocalizationServices.txt b/src/Orchard/Localization/Services/IDateLocalizationServices.txt index f39c2f303..c125ac497 100644 --- a/src/Orchard/Localization/Services/IDateLocalizationServices.txt +++ b/src/Orchard/Localization/Services/IDateLocalizationServices.txt @@ -41,8 +41,12 @@ struct DateLocalizationOptions { } TODO: + * Implement formatting in addition to parsing * Test for proper handling of fraction (f) format specifier - suspect it does not work properly in current state * Rewrite DefaultDateLocalizationServices * Write unit tests for DefaultDateLocalizationServices * Add warning message when saving unsupported combination in settings * Add support for the different Gregorian calendar types + * Improve DateTimeField: + - Surface the field mode (date, time or both) + - Do not perform time-zone conversion in date-only mode diff --git a/src/Orchard/Localization/Services/IDateTimeFormatProvider.cs b/src/Orchard/Localization/Services/IDateTimeFormatProvider.cs index d31946ad5..c023cf974 100644 --- a/src/Orchard/Localization/Services/IDateTimeFormatProvider.cs +++ b/src/Orchard/Localization/Services/IDateTimeFormatProvider.cs @@ -12,49 +12,49 @@ namespace Orchard.Localization.Services { /// /// Gets a list of month names. /// - IEnumerable MonthNames { + string[] MonthNames { get; } /// /// Gets a list of genitive month names (used in contexts when a day is involved). /// - IEnumerable MonthNamesGenitive { + string[] MonthNamesGenitive { get; } /// /// Gets a list of abbreviated month names. /// - IEnumerable MonthNamesShort { + string[] MonthNamesShort { get; } /// /// Gets a list of abbreviated genivite month names (used in contexts when a day is involved). /// - IEnumerable MonthNamesShortGenitive { + string[] MonthNamesShortGenitive { get; } /// /// Gets a list of weekday names. /// - IEnumerable DayNames { + string[] DayNames { get; } /// /// Gets a list of abbreviated weekday names. /// - IEnumerable DayNamesShort { + string[] DayNamesShort { get; } /// /// Gets a list of maximally abbreviated weekday names. /// - IEnumerable DayNamesMin { + string[] DayNamesMin { get; } @@ -159,7 +159,7 @@ namespace Orchard.Localization.Services { /// /// Gets a list of strings used as display text for the AM and PM designators. /// - IEnumerable AmPmDesignators { + string[] AmPmDesignators { get; } }