diff --git a/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs b/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs index fe42a7365..71b2ad780 100644 --- a/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs +++ b/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs @@ -232,6 +232,15 @@ namespace Orchard.Framework.Tests.Localization { } } + [Test] + [Description("Time parsing throws a FormatException for unparsable time strings.")] + [ExpectedException(typeof(FormatException))] + public void ParseTimeTest02() { + var container = InitializeContainer("en-US", null); + var target = container.Resolve(); + target.ParseTime("BlaBlaBla"); + } + [Test] [Description("Date formatting works correctly for all combinations of months, format strings and cultures.")] public void FormatDateTest01() { @@ -296,12 +305,48 @@ namespace Orchard.Framework.Tests.Localization { } [Test] - [Description("Time parsing throws a FormatException for unparsable time strings.")] - [ExpectedException(typeof(FormatException))] - public void ParseTimeTest02() { - var container = InitializeContainer("en-US", null); - var target = container.Resolve(); - target.ParseTime("BlaBlaBla"); + [Description("Time formatting works correctly for all combinations of hours, format strings and cultures.")] + public void FormatTimeTest01() { + 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, null); + var formats = container.Resolve(); + var target = container.Resolve(); + + foreach (var timeFormat in formats.AllTimeFormats) { // All time formats supported by the culture. + for (var hour = 0; hour <= 23; hour++) { // All hours in the day. + + DateTime date = new DateTime(1998, 1, 1, hour, 30, 30); + TimeParts timeParts = new TimeParts(hour, 30, 30, 0); + + var caseKey = String.Format("{0}___{1}___{2}", culture.Name, timeFormat, timeParts); + allCases.Add(caseKey); + //Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey)); + + try { + var result = target.FormatTime(timeParts, timeFormat); + var expected = date.ToString(timeFormat, culture); + 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); + } } private DateTimeParts GetExpectedDateTimeParts(DateTime dateTime, string format) { diff --git a/src/Orchard/Localization/Models/TimeParts.cs b/src/Orchard/Localization/Models/TimeParts.cs index 5393048b7..f57690ee5 100644 --- a/src/Orchard/Localization/Models/TimeParts.cs +++ b/src/Orchard/Localization/Models/TimeParts.cs @@ -41,6 +41,18 @@ namespace Orchard.Localization.Models { } } + public DateTime ToDateTime() { + return new DateTime( + DateTime.MinValue.Year, + DateTime.MinValue.Month, + DateTime.MinValue.Day, + _hour > 0 ? _hour : DateTime.MinValue.Hour, + _minute > 0 ? _minute : DateTime.MinValue.Minute, + _second > 0 ? _second : DateTime.MinValue.Second, + _millisecond > 0 ? _millisecond : DateTime.MinValue.Millisecond + ); + } + public override string ToString() { return String.Format("{0}:{1}:{2}.{3}", _hour, _minute, _second, _millisecond); } diff --git a/src/Orchard/Localization/Services/DefaultDateFormatter.cs b/src/Orchard/Localization/Services/DefaultDateFormatter.cs index eee7ff6e6..a96759710 100644 --- a/src/Orchard/Localization/Services/DefaultDateFormatter.cs +++ b/src/Orchard/Localization/Services/DefaultDateFormatter.cs @@ -108,8 +108,7 @@ namespace Orchard.Localization.Services { } public virtual string FormatDate(DateParts parts) { - // TODO: Mahsa should implement! - throw new NotImplementedException(); + return FormatDate(parts, _dateTimeFormatProvider.ShortDateFormat); } public virtual string FormatDate(DateParts parts, string format) { @@ -134,13 +133,21 @@ namespace Orchard.Localization.Services { } public virtual string FormatTime(TimeParts parts) { - // TODO: Mahsa should implement! - throw new NotImplementedException(); + return FormatTime(parts, _dateTimeFormatProvider.LongTimeFormat); } public virtual string FormatTime(TimeParts parts, string format) { - // TODO: Mahsa should implement! - throw new NotImplementedException(); + var replacements = GetTimeFormatReplacements(); + var formatString = ConvertToFormatString(format, replacements); + + var dateTime = parts.ToDateTime(); + + bool isPm; + var hour12 = ConvertToHour12(parts.Hour, out isPm); + var amPm = _dateTimeFormatProvider.AmPmDesignators[isPm ? 1 : 0]; + var amPmShort = String.IsNullOrEmpty(amPm) ? "" : amPm[0].ToString(); + + return String.Format(formatString, parts.Hour, hour12, parts.Minute, parts.Second, parts.Millisecond, amPm, amPmShort); } protected virtual DateTimeParts? TryParseDateTime(string dateTimeString, string format, IDictionary replacements) { @@ -289,10 +296,8 @@ namespace Orchard.Localization.Services { {"ff", "(?[0-9]{2})"}, {"f", "(?[0-9]{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})*"}, + {"t", String.Format(@"\s*(?{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[0][0].ToString()), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[1][0].ToString()))}, + {"K", @"(?Z|(\+|-)[0-9]{2}:[0-9]{2})*"} }; } @@ -316,28 +321,34 @@ namespace Orchard.Localization.Services { }; } - //protected virtual Dictionary GetTimeFormatReplacements() { - // return new Dictionary() { - // {"HH", "(?[0-9]{2})"}, - // {"H", "(?[0-9]{1,2})"}, - // {"hh", "(?[0-9]{2})"}, - // {"h", "(?[0-9]{1,2})"}, - // {"mm", "(?[0-9]{2})"}, - // {"m", "(?[0-9]{1,2})"}, - // {"ss", "(?[0-9]{2})"}, - // {"s", "(?[0-9]{1,2})"}, - // {"f", "(?[0-9]{1})"}, - // {"ff", "(?[0-9]{2})"}, - // {"fff", "(?[0-9]{3})"}, - // {"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])} - // }; - //} + protected virtual Dictionary GetTimeFormatReplacements() { + return new Dictionary() { + {"HH", "{0:00}"}, + {"H", "{0:#0}"}, + {"hh", "{1:00}"}, + {"h", "{1:#0}"}, + {"mm", "{2:00}"}, + {"m", "{2:#0}"}, + {"ss", "{3:00}"}, + {"s", "{3:#0}"}, + {"fffffff", "{4:0000000}"}, + {"ffffff", "{4:000000}"}, + {"fffff", "{4:00000}"}, + {"ffff", "{4:0000}"}, + {"fff", "{4:000}"}, + {"ff", "{4:00}"}, + {"f", "{4:0}"}, + {"FFFFFFF", "{4:#######}"}, + {"FFFFFF", "{4:######}"}, + {"FFFFF", "{4:#####}"}, + {"FFFF", "{4:####}"}, + {"FFF", "{4:###}"}, + {"FF", "{4:##}"}, + {"F", "{4:#}"}, + {"tt", "{5}"}, + {"t", "{6}"} + }; + } protected virtual string ConvertToRegexPattern(string format, IDictionary replacements) { string result = format; @@ -396,6 +407,15 @@ namespace Orchard.Localization.Services { return hour12 == 12 ? 0 : hour12; } + protected virtual int ConvertToHour12(int hour24, out bool isPm) { + if (hour24 >= 12) { + isPm = true; + return hour24 == 12 ? 12 : hour24 - 12; + } + isPm = false; + return hour24 == 0 ? 12 : hour24; + } + protected virtual string EscapeForRegex(string input) { return Regex.Replace(input, @"\.|\$|\^|\{|\[|\(|\||\)|\*|\+|\?|\\", m => String.Format(@"\{0}", m.Value), RegexOptions.Compiled); }