diff --git a/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs b/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs index 7d3bf2299..52035e4b0 100644 --- a/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs +++ b/src/Orchard.Tests/Localization/DefaultDateFormatterTests.cs @@ -1,5 +1,10 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; +using System.Linq; +using System.Threading.Tasks; using Autofac; using Moq; using NUnit.Framework; @@ -13,93 +18,142 @@ namespace Orchard.Framework.Tests.Localization { public class DefaultDateFormatterTests { [Test] - [Description("Correct en-US date is parsed correctly.")] - public void ParseTest01() { - var container = InitializeContainer("en-US"); - var culture = CultureInfo.GetCultureInfo("en-US"); - var formats = container.Resolve(); - var target = container.Resolve(); + [Description("Date and time parsing works correctly for all combinations of months, hours, format strings and cultures.")] + public void ParseDateTimeTest01() { + var allCases = new ConcurrentBag(); + var failedCases = new ConcurrentDictionary(); + var maxFailedCases = 0; - var value = new DateTime(2014, 5, 31, 10, 0, 0).ToString(formats.ShortDateTimeFormat, culture); - var result = target.ParseDateTime(value); - var expected = new DateTimeParts(2014, 5, 31, 10, 0, 0, 0); - - Assert.AreEqual(expected, result); + var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures); + Parallel.ForEach(allCultures, culture => { // All cultures on the machine. + var container = InitializeContainer(culture.Name, "GregorianCalendar"); + var formats = container.Resolve(); + var target = container.Resolve(); + foreach (var dateTimeFormat in formats.AllDateTimeFormats) { // All date and time formats supported by the culture. + var caseKey = String.Format("{0}:{1}", culture.Name, dateTimeFormat); + 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. + for (var hour = 0; hour <= 23; hour++) { // All hours in the day. + DateTime dateTime = new DateTime(1998, month, 1, hour, 30, 30); + // Print string using Gregorian calendar to avoid calendar conversion. + var cultureGregorian = (CultureInfo)culture.Clone(); + cultureGregorian.DateTimeFormat.Calendar = cultureGregorian.OptionalCalendars.OfType().First(); + var dateTimeString = dateTime.ToString(dateTimeFormat, cultureGregorian); + var result = target.ParseDateTime(dateTimeString); + 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); + } + } + } + catch (Exception ex) { + failedCases.TryAdd(caseKey, ex); + } + } + }); + + if (failedCases.Count > maxFailedCases) { + throw new AggregateException(String.Format("Parse tests failed for {0} of {1} cases. Expected {2} failed cases or less.", failedCases.Count, allCases.Count, maxFailedCases), failedCases.Values); + } } [Test] - [Description("Incorrect en-US date yields an exception.")] - [ExpectedException(typeof(FormatException))] - public void ParseTest02() { - var container = InitializeContainer("en-US"); - var target = container.Resolve(); + [Description("Date parsing works correctly for all combinations of months, format strings and cultures.")] + public void ParseDateTest01() { + var allCases = new ConcurrentBag(); + var failedCases = new ConcurrentDictionary(); + var maxFailedCases = 0; - var value = "BlaBlaBla"; - var result = target.ParseDateTime(value); + var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures); + Parallel.ForEach(allCultures, 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); + var result = target.ParseDate(dateString); + var expected = new DateParts(date.Year, date.Month, date.Day); + Assert.AreEqual(expected, result); + } + } + catch (Exception ex) { + failedCases.TryAdd(caseKey, ex); + } + } + }); + + if (failedCases.Count > maxFailedCases) { + throw new AggregateException(String.Format("Parse tests failed for {0} of {1} cases. Expected {2} failed cases or less.", failedCases.Count, allCases.Count, maxFailedCases), failedCases.Values); + } } [Test] - [Description("Correct sv-SE date is parsed correctly.")] - public void ParseTest03() { - var container = InitializeContainer("sv-SE"); - var culture = CultureInfo.GetCultureInfo("sv-SE"); - var formats = container.Resolve(); - var target = container.Resolve(); + [Description("Time parsing works correctly for all combinations of hours, format strings and cultures.")] + public void ParseTimeTest01() { + var allCases = new ConcurrentBag(); + var failedCases = new ConcurrentDictionary(); + var maxFailedCases = 0; - var value = new DateTime(2014, 5, 31, 10, 0, 0).ToString(formats.ShortDateTimeFormat, culture); - var result = target.ParseDateTime(value); - var expected = new DateTimeParts(2014, 5, 31, 10, 0, 0, 0); + var allCultures = CultureInfo.GetCultures(CultureTypes.AllCultures); + Parallel.ForEach(allCultures, 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); + var result = target.ParseTime(timeString); + var reference = DateTime.ParseExact(timeString, timeFormat, culture); + var expected = new TimeParts(reference.Hour, reference.Minute, reference.Second, reference.Millisecond); + Assert.AreEqual(expected, result); + } + } + catch (Exception ex) { + failedCases.TryAdd(caseKey, ex); + } + } + }); - Assert.AreEqual(expected, result); + if (failedCases.Count > maxFailedCases) { + throw new AggregateException(String.Format("Parse tests failed for {0} of {1} cases. Expected {2} failed cases or less.", failedCases.Count, allCases.Count, maxFailedCases), failedCases.Values); + } } - [Test] - [Description("Incorrect sv-SE date yields an exception.")] - [ExpectedException(typeof(FormatException))] - public void ParseTest04() { - var container = InitializeContainer("sv-SE"); - var target = container.Resolve(); - - var value = "BlaBlaBla"; - var result = target.ParseDateTime(value); - } - - //[Test] - //[Description("Loop through all cultures. Test Parse method by all possible DateTimeFormats.")] - //public void ParseTest04() { - // IDateFormatter target = new DefaultDateFormatter(); - // var cultures = CultureInfo.GetCultures(CultureTypes.AllCultures); - // foreach (CultureInfo cultureInfo in cultures) { - // // Due to a bug in .NET 4.5 in combination with updated Upper Sorbian culture in Windows 8. - // if (System.Environment.OSVersion.Version.ToString().CompareTo("6.2.0.0") >= 0 && cultureInfo.Name.StartsWith("hsb")) { - // continue; - // } - // DateTime dateTime = new DateTime(2014, 12, 31, 10, 20, 40, 567); - // cultureInfo.DateTimeFormat.Calendar = new GregorianCalendar(); - // var dateString = dateTime.ToString("G", cultureInfo); - // var result = target.ParseDateTime(dateString, cultureInfo); - // var millisecond = DateTime.Parse(dateString, cultureInfo.DateTimeFormat).Millisecond; - // var expected = new DateTimeParts(2014, 12, 31, 10, 20, 40, millisecond); - // Assert.AreEqual(expected, result); - // } - //} - - private IContainer InitializeContainer(string cultureName) { + private IContainer InitializeContainer(string cultureName, string calendarName) { var builder = new ContainerBuilder(); - builder.RegisterInstance(new StubWorkContext(cultureName)); + builder.RegisterInstance(new StubWorkContext(cultureName, calendarName)); builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); - builder.RegisterInstance(new Mock().Object); + builder.RegisterInstance(new Mock().Object); + builder.RegisterType().As(); return builder.Build(); } private class StubWorkContext : WorkContext { private string _cultureName; + private string _calendarName; - public StubWorkContext(string cultureName) { + public StubWorkContext(string cultureName, string calendarName) { _cultureName = cultureName; + _calendarName = calendarName; } public override T Resolve() { @@ -112,7 +166,7 @@ namespace Orchard.Framework.Tests.Localization { public override T GetState(string name) { if (name == "CurrentCulture") return (T)((object)_cultureName); - if (name == "CurrentCalendar") return (T)default(object); + if (name == "CurrentCalendar") return (T)((object)_calendarName); throw new NotImplementedException(String.Format("Property '{0}' is not implemented.", name)); } diff --git a/src/Orchard/Localization/Models/DateParts.cs b/src/Orchard/Localization/Models/DateParts.cs index 197049a2e..60346a17a 100644 --- a/src/Orchard/Localization/Models/DateParts.cs +++ b/src/Orchard/Localization/Models/DateParts.cs @@ -30,5 +30,9 @@ namespace Orchard.Framework.Localization.Models { return _day; } } + + public override string ToString() { + return String.Format("{0}-{1}-{2}", _year, _month, _day); + } } } diff --git a/src/Orchard/Localization/Models/DateTimeParts.cs b/src/Orchard/Localization/Models/DateTimeParts.cs index 08e7c84d2..fcca1ff9a 100644 --- a/src/Orchard/Localization/Models/DateTimeParts.cs +++ b/src/Orchard/Localization/Models/DateTimeParts.cs @@ -23,10 +23,15 @@ namespace Orchard.Framework.Localization.Models { return _date; } } + public TimeParts Time { get { return _time; } } + + public override string ToString() { + return String.Format("{0} {1}", _date, _time); + } } } diff --git a/src/Orchard/Localization/Models/TimeParts.cs b/src/Orchard/Localization/Models/TimeParts.cs index 3c9f4c9d4..b61d3662f 100644 --- a/src/Orchard/Localization/Models/TimeParts.cs +++ b/src/Orchard/Localization/Models/TimeParts.cs @@ -40,5 +40,9 @@ namespace Orchard.Framework.Localization.Models { return _millisecond; } } + + public override string ToString() { + return String.Format("{0}:{1}:{2}.{3}", _hour, _minute, _second, _millisecond); + } } } diff --git a/src/Orchard/Localization/Services/CultureDateTimeFormatProvider.cs b/src/Orchard/Localization/Services/CultureDateTimeFormatProvider.cs index 17753a083..883bd0693 100644 --- a/src/Orchard/Localization/Services/CultureDateTimeFormatProvider.cs +++ b/src/Orchard/Localization/Services/CultureDateTimeFormatProvider.cs @@ -16,50 +16,54 @@ namespace Orchard.Localization.Services { // 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; - public CultureDateTimeFormatProvider(IWorkContextAccessor workContextAccessor) { + public CultureDateTimeFormatProvider( + IWorkContextAccessor workContextAccessor, + ICalendarManager calendarManager) { _workContextAccessor = workContextAccessor; + _calendarManager = calendarManager; } public virtual IEnumerable MonthNames { get { - return CurrentCulture.DateTimeFormat.MonthNames; + return DateTimeFormat.MonthNames; } } public virtual IEnumerable MonthNamesShort { get { - return CurrentCulture.DateTimeFormat.AbbreviatedMonthNames; + return DateTimeFormat.AbbreviatedMonthNames; } } public virtual IEnumerable DayNames { get { - return CurrentCulture.DateTimeFormat.DayNames; + return DateTimeFormat.DayNames; } } public virtual IEnumerable DayNamesShort { get { - return CurrentCulture.DateTimeFormat.AbbreviatedDayNames; + return DateTimeFormat.AbbreviatedDayNames; } } public virtual IEnumerable DayNamesMin { get { - return CurrentCulture.DateTimeFormat.ShortestDayNames; + return DateTimeFormat.ShortestDayNames; } } public virtual string ShortDateFormat { get { - return CurrentCulture.DateTimeFormat.ShortDatePattern; + return DateTimeFormat.ShortDatePattern; } } public virtual string ShortTimeFormat { get { - return CurrentCulture.DateTimeFormat.ShortTimePattern; + return DateTimeFormat.ShortTimePattern; } } @@ -68,33 +72,33 @@ namespace Orchard.Localization.Services { // From empirical testing I am fairly certain this 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 CurrentCulture.DateTimeFormat.GetAllDateTimePatterns('g').First(); + return DateTimeFormat.GetAllDateTimePatterns('g').First(); } } public virtual string LongDateFormat { get { - return CurrentCulture.DateTimeFormat.LongDatePattern; + return DateTimeFormat.LongDatePattern; } } public virtual string LongTimeFormat { get { - return CurrentCulture.DateTimeFormat.LongTimePattern; + return DateTimeFormat.LongTimePattern; } } public virtual string LongDateTimeFormat { get { - return CurrentCulture.DateTimeFormat.FullDateTimePattern; + return DateTimeFormat.FullDateTimePattern; } } public virtual IEnumerable AllDateFormats { get { var patterns = new List(); - patterns.AddRange(CurrentCulture.DateTimeFormat.GetAllDateTimePatterns('d')); - patterns.AddRange(CurrentCulture.DateTimeFormat.GetAllDateTimePatterns('D')); + patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('d')); + patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('D')); // 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. @@ -105,8 +109,8 @@ namespace Orchard.Localization.Services { public virtual IEnumerable AllTimeFormats { get { var patterns = new List(); - patterns.AddRange(CurrentCulture.DateTimeFormat.GetAllDateTimePatterns('t')); - patterns.AddRange(CurrentCulture.DateTimeFormat.GetAllDateTimePatterns('T')); + patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('t')); + patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('T')); return patterns.Distinct(); } } @@ -114,22 +118,22 @@ namespace Orchard.Localization.Services { public virtual IEnumerable AllDateTimeFormats { get { var patterns = new List(); - patterns.AddRange(CurrentCulture.DateTimeFormat.GetAllDateTimePatterns('f')); - patterns.AddRange(CurrentCulture.DateTimeFormat.GetAllDateTimePatterns('F')); - patterns.AddRange(CurrentCulture.DateTimeFormat.GetAllDateTimePatterns('g')); - patterns.AddRange(CurrentCulture.DateTimeFormat.GetAllDateTimePatterns('G')); - patterns.AddRange(CurrentCulture.DateTimeFormat.GetAllDateTimePatterns('o')); - patterns.AddRange(CurrentCulture.DateTimeFormat.GetAllDateTimePatterns('r')); - patterns.AddRange(CurrentCulture.DateTimeFormat.GetAllDateTimePatterns('s')); - patterns.AddRange(CurrentCulture.DateTimeFormat.GetAllDateTimePatterns('u')); - patterns.AddRange(CurrentCulture.DateTimeFormat.GetAllDateTimePatterns('U')); + patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('f')); + patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('F')); + patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('g')); + patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('G')); + patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('o')); + patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('r')); + patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('s')); + patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('u')); + patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('U')); return patterns.Distinct(); } } public virtual int FirstDay { get { - return Convert.ToInt32(CurrentCulture.DateTimeFormat.FirstDayOfWeek); + return Convert.ToInt32(DateTimeFormat.FirstDayOfWeek); } } @@ -143,13 +147,13 @@ namespace Orchard.Localization.Services { public virtual string DateSeparator { get { - return CurrentCulture.DateTimeFormat.DateSeparator; + return DateTimeFormat.DateSeparator; } } public virtual string TimeSeparator { get { - return CurrentCulture.DateTimeFormat.TimeSeparator; + return DateTimeFormat.TimeSeparator; } } @@ -161,7 +165,31 @@ namespace Orchard.Localization.Services { public virtual IEnumerable AmPmDesignators { get { - return new string[] { CurrentCulture.DateTimeFormat.AMDesignator, CurrentCulture.DateTimeFormat.PMDesignator }; + return new string[] { DateTimeFormat.AMDesignator, DateTimeFormat.PMDesignator }; + } + } + + protected virtual DateTimeFormatInfo DateTimeFormat { + get { + var culture = CurrentCulture; + var calendar = CurrentCalendar; + + // The configured Calendar affects the format strings provided by the DateTimeFormatInfo + // class. Therefore, if the site is configured to use a calendar that is supported as an + // optional calendar of the configured culture, use a customized DateTimeFormatInfo instance + // configured with that calendar to get the correct formats. + var usingCultureCalendar = culture.DateTimeFormat.Calendar.GetType().IsInstanceOfType(calendar); + if (!usingCultureCalendar) { + foreach (var optionalCalendar in culture.OptionalCalendars) { + if (optionalCalendar.GetType().IsInstanceOfType(calendar)) { + var calendarSpecificDateTimeFormat = (DateTimeFormatInfo)culture.DateTimeFormat.Clone(); + calendarSpecificDateTimeFormat.Calendar = optionalCalendar; + return calendarSpecificDateTimeFormat; + } + } + } + + return culture.DateTimeFormat; } } @@ -171,5 +199,14 @@ namespace Orchard.Localization.Services { return CultureInfo.GetCultureInfo(workContext.CurrentCulture); } } + + protected virtual Calendar CurrentCalendar { + get { + var workContext = _workContextAccessor.GetContext(); + if (!String.IsNullOrEmpty(workContext.CurrentCalendar)) + return _calendarManager.GetCalendarByName(workContext.CurrentCalendar); + return CurrentCulture.Calendar; + } + } } } \ No newline at end of file diff --git a/src/Orchard/Localization/Services/DefaultDateFormatter.cs b/src/Orchard/Localization/Services/DefaultDateFormatter.cs index 76ff46c64..917a91df8 100644 --- a/src/Orchard/Localization/Services/DefaultDateFormatter.cs +++ b/src/Orchard/Localization/Services/DefaultDateFormatter.cs @@ -25,39 +25,45 @@ namespace Orchard.Framework.Localization.Services { } public virtual DateTimeParts ParseDateTime(string dateTimeString) { - var replacements = GetDateParseReplacements().Union(GetTimeParseReplacements()).ToDictionary(item => item.Key, item => item.Value); - var dateTimePattern = ConvertFormatStringToRegExPattern(_dateTimeFormatProvider.ShortDateTimeFormat, replacements); + var replacements = GetDateParseReplacements().Concat(GetTimeParseReplacements()).ToDictionary(item => item.Key, item => item.Value); - Match m = Regex.Match(dateTimeString, dateTimePattern, RegexOptions.IgnoreCase); - if (!m.Success) { - throw new FormatException("The string was not recognized as a valid date and time."); + 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)); + } } - return new DateTimeParts(ExtractDateParts(m), ExtractTimeParts(m)); + throw new FormatException("The string was not recognized as a valid date and time."); } public virtual DateParts ParseDate(string dateString) { var replacements = GetDateParseReplacements(); - var datePattern = ConvertFormatStringToRegExPattern(_dateTimeFormatProvider.ShortDateFormat, replacements); - Match m = Regex.Match(dateString, datePattern, RegexOptions.IgnoreCase); - if (!m.Success) { - throw new FormatException("The string was not recognized as a valid date."); + 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); + } } - return ExtractDateParts(m); + throw new FormatException("The string was not recognized as a valid date."); } public virtual TimeParts ParseTime(string timeString) { var replacements = GetTimeParseReplacements(); - var timePattern = ConvertFormatStringToRegExPattern(_dateTimeFormatProvider.LongTimeFormat, replacements); - Match m = Regex.Match(timeString, timePattern, RegexOptions.IgnoreCase); - if (!m.Success) { - throw new FormatException("The string was not recognized as a valid time."); + 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); + } } - return ExtractTimeParts(m); + throw new FormatException("The string was not recognized as a valid time."); } public virtual string FormatDateTime(DateTimeParts parts) { @@ -96,10 +102,19 @@ namespace Orchard.Framework.Localization.Services { day = 0; year = CurrentCalendar.ToFourDigitYear(Int32.Parse(m.Groups["year"].Value)); - month = Int32.Parse(m.Groups["month"].Value); - day = Int32.Parse(m.Groups["day"].Value); - // TODO: Also extract month names, not just numbers. + // For the month we can either use the month number, the abbreviated month name or the full month name. + if (m.Groups["month"].Success) { + 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; + } + else if (m.Groups["monthName"].Success) { + month = _dateTimeFormatProvider.MonthNames.Select(x => x.ToLowerInvariant()).ToList().IndexOf(m.Groups["monthName"].Value.ToLowerInvariant()) + 1; + } + + day = Int32.Parse(m.Groups["day"].Value); return new DateParts(year, month, day); } @@ -110,28 +125,41 @@ namespace Orchard.Framework.Localization.Services { second = 0, millisecond = 0; - hour = Int32.Parse(m.Groups["hour"].Value); - minute = Int32.Parse(m.Groups["minute"].Value); + // For the hour we can either use 24-hour notation or 12-hour notation in combination with AM/PM designator. + if (m.Groups["hour24"].Success) { + hour = Int32.Parse(m.Groups["hour24"].Value); + } + else if (m.Groups["hour12"].Success) { + 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); + } + + if (m.Groups["minute"].Success) { + minute = Int32.Parse(m.Groups["minute"].Value); + } + if (m.Groups["second"].Success) { second = Int32.Parse(m.Groups["second"].Value); } + if (m.Groups["millisecond"].Success) { second = Int32.Parse(m.Groups["millisecond"].Value); } - // TODO: We must also handle 12-hour time with AM/PM designator. - return new TimeParts(hour, minute, second, millisecond); } 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))}, + {"ddd", String.Format("(?{0})", String.Join("|", _dateTimeFormatProvider.DayNamesShort))}, {"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.Where(x => !String.IsNullOrEmpty(x))))}, + {"MMM", String.Format("(?{0})", String.Join("|", _dateTimeFormatProvider.MonthNamesShort.Where(x => !String.IsNullOrEmpty(x))))}, {"MM", "(?[0-9]{2})"}, {"M", "(?[0-9]{1,2})"}, {"yyyyy", "(?[0-9]{5})"}, @@ -144,10 +172,10 @@ namespace Orchard.Framework.Localization.Services { protected virtual Dictionary GetTimeParseReplacements() { return new Dictionary() { - {"HH", "(?[0-9]{2})"}, - {"H", "(?[0-9]{1,2})"}, - {"hh", "(?[0-9]{2})"}, - {"h", "(?[0-9]{1,2})"}, + {"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})"}, @@ -158,10 +186,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*", _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])} }; } @@ -206,14 +234,22 @@ namespace Orchard.Framework.Localization.Services { // }; //} - protected virtual string ConvertFormatStringToRegExPattern(string format, IDictionary replacements) { + protected virtual string ConvertFormatStringToRegexPattern(string format, IDictionary replacements) { string result = null; result = Regex.Replace(format, @"\.|\$|\^|\{|\[|\(|\||\)|\*|\+|\?|\\", m => String.Format(@"\{0}", m.Value)); result = Regex.Replace(result, @"(? String.Format("(.{{{0}}})", m.Value.Replace("\\", "").Length - 2)); result = result.ReplaceAll(replacements); + result = String.Format(@"^{0}$", result); // Make sure string is anchored to beginning and end. return result; } + protected virtual int ConvertHour12ToHour24(int hour12, bool isPm) { + if (isPm) { + return hour12 == 12 ? 12 : hour12 + 12; + } + return hour12 == 12 ? 0 : hour12; + } + 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 d4819b4e7..6d30e47ee 100644 --- a/src/Orchard/Localization/Services/IDateLocalizationServices.txt +++ b/src/Orchard/Localization/Services/IDateLocalizationServices.txt @@ -41,11 +41,8 @@ struct DateLocalizationOptions { } TODO: - * Consider current calendar in CultureDateTimeFormatProvider - * Handle multiple allowed formats when parsing in DefaultDateFormatter - * Handle month names as well as numbers in DefaultDateFormatter - * Fix TODOs in DefaultDateFormatter * Rewrite DefaultDateLocalizationServices * Write unit tests for DefaultDateLocalizationServices * Add warning message when saving unsupported combination in settings - * Add support for the different Gregorian calendar types \ No newline at end of file + * Add support for the different Gregorian calendar types + * User ICultureManager and ICultureRepository to get the current CultureInfo in other classes \ No newline at end of file