Extended CultureDateTimeFormatProvider to take the configured calendar into consideration if it is one of the optional calendars for the configured culture, to get the correct formats.

Added ToString() overrides to the model classes.
Extended DefaultDateFormatter to try parsing with all supported formats provided by IDateTimeFormatProvider.
Extended DefaultDateFormatter with support for month names and abbreviated month names, in addition to month numbers.
Exterded DefaultDateFormatter with support for 12-hour time notation with AM/PM designator.
Fixed various parsing bugs in DefaultDateFormatter.
Added more extensive unit tests for DefaultDateFormatter.
This commit is contained in:
Daniel Stolt
2014-07-27 17:29:43 +02:00
parent d4dc61038f
commit b6cdebb58d
7 changed files with 271 additions and 134 deletions

View File

@@ -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<IDateTimeFormatProvider>();
var target = container.Resolve<IDateFormatter>();
[Description("Date and time parsing works correctly for all combinations of months, hours, format strings and cultures.")]
public void ParseDateTimeTest01() {
var allCases = new ConcurrentBag<string>();
var failedCases = new ConcurrentDictionary<string, Exception>();
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<IDateTimeFormatProvider>();
var target = container.Resolve<IDateFormatter>();
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<GregorianCalendar>().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<IDateFormatter>();
[Description("Date parsing works correctly for all combinations of months, format strings and cultures.")]
public void ParseDateTest01() {
var allCases = new ConcurrentBag<string>();
var failedCases = new ConcurrentDictionary<string, Exception>();
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<IDateTimeFormatProvider>();
var target = container.Resolve<IDateFormatter>();
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<GregorianCalendar>().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<IDateTimeFormatProvider>();
var target = container.Resolve<IDateFormatter>();
[Description("Time parsing works correctly for all combinations of hours, format strings and cultures.")]
public void ParseTimeTest01() {
var allCases = new ConcurrentBag<string>();
var failedCases = new ConcurrentDictionary<string, Exception>();
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<IDateTimeFormatProvider>();
var target = container.Resolve<IDateFormatter>();
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<IDateFormatter>();
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<WorkContext>(new StubWorkContext(cultureName));
builder.RegisterInstance<WorkContext>(new StubWorkContext(cultureName, calendarName));
builder.RegisterType<StubWorkContextAccessor>().As<IWorkContextAccessor>();
builder.RegisterType<CultureDateTimeFormatProvider>().As<IDateTimeFormatProvider>();
builder.RegisterType<DefaultDateFormatter>().As<IDateFormatter>();
builder.RegisterInstance(new Mock<ICalendarManager>().Object);
builder.RegisterInstance(new Mock<ICalendarSelector>().Object);
builder.RegisterType<DefaultCalendarManager>().As<ICalendarManager>();
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<T>() {
@@ -112,7 +166,7 @@ namespace Orchard.Framework.Tests.Localization {
public override T GetState<T>(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));
}

View File

@@ -30,5 +30,9 @@ namespace Orchard.Framework.Localization.Models {
return _day;
}
}
public override string ToString() {
return String.Format("{0}-{1}-{2}", _year, _month, _day);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<string> MonthNames {
get {
return CurrentCulture.DateTimeFormat.MonthNames;
return DateTimeFormat.MonthNames;
}
}
public virtual IEnumerable<string> MonthNamesShort {
get {
return CurrentCulture.DateTimeFormat.AbbreviatedMonthNames;
return DateTimeFormat.AbbreviatedMonthNames;
}
}
public virtual IEnumerable<string> DayNames {
get {
return CurrentCulture.DateTimeFormat.DayNames;
return DateTimeFormat.DayNames;
}
}
public virtual IEnumerable<string> DayNamesShort {
get {
return CurrentCulture.DateTimeFormat.AbbreviatedDayNames;
return DateTimeFormat.AbbreviatedDayNames;
}
}
public virtual IEnumerable<string> 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<string> AllDateFormats {
get {
var patterns = new List<string>();
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<string> AllTimeFormats {
get {
var patterns = new List<string>();
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<string> AllDateTimeFormats {
get {
var patterns = new List<string>();
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<string> 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;
}
}
}
}

View File

@@ -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<string, string> GetDateParseReplacements() {
return new Dictionary<string, string>() {
{"dddd", String.Format("(?<day>{0})", String.Join("|", _dateTimeFormatProvider.DayNames))},
{"ddd", String.Format("(?<day>{0})", String.Join("|", _dateTimeFormatProvider.DayNamesShort))},
{"dddd", String.Format("(?<dayName>{0})", String.Join("|", _dateTimeFormatProvider.DayNames))},
{"ddd", String.Format("(?<dayNameShort>{0})", String.Join("|", _dateTimeFormatProvider.DayNamesShort))},
{"dd", "(?<day>[0-9]{2})"},
{"d", "(?<day>[0-9]{1,2})"},
{"MMMM", String.Format("(?<month>{0})", String.Join("|", _dateTimeFormatProvider.MonthNames.Where(x => !String.IsNullOrEmpty(x))))},
{"MMM", String.Format("(?<month>{0})", String.Join("|", _dateTimeFormatProvider.MonthNamesShort.Where(x => !String.IsNullOrEmpty(x))))},
{"MMMM", String.Format("(?<monthName>{0})", String.Join("|", _dateTimeFormatProvider.MonthNames.Where(x => !String.IsNullOrEmpty(x))))},
{"MMM", String.Format("(?<monthNameShort>{0})", String.Join("|", _dateTimeFormatProvider.MonthNamesShort.Where(x => !String.IsNullOrEmpty(x))))},
{"MM", "(?<month>[0-9]{2})"},
{"M", "(?<month>[0-9]{1,2})"},
{"yyyyy", "(?<year>[0-9]{5})"},
@@ -144,10 +172,10 @@ namespace Orchard.Framework.Localization.Services {
protected virtual Dictionary<string, string> GetTimeParseReplacements() {
return new Dictionary<string, string>() {
{"HH", "(?<hour>[0-9]{2})"},
{"H", "(?<hour>[0-9]{1,2})"},
{"hh", "(?<hour>[0-9]{2})"},
{"h", "(?<hour>[0-9]{1,2})"},
{"HH", "(?<hour24>[0-9]{2})"},
{"H", "(?<hour24>[0-9]{1,2})"},
{"hh", "(?<hour12>[0-9]{2})"},
{"h", "(?<hour12>[0-9]{1,2})"},
{"mm", "(?<minute>[0-9]{2})"},
{"m", "(?<minute>[0-9]{1,2})"},
{"ss", "(?<second>[0-9]{2})"},
@@ -158,10 +186,10 @@ namespace Orchard.Framework.Localization.Services {
{"ffff", "(?<millisecond>[0-9]{4})"},
{"fffff", "(?<millisecond>[0-9]{5})"},
{"ffffff", "(?<millisecond>[0-9]{6})"},
{"tt", String.Format("\\s*(?<AMPM>{0}|{1})\\s*", _dateTimeFormatProvider.AmPmDesignators.ToArray()[0], _dateTimeFormatProvider.AmPmDesignators.ToArray()[1])},
{"t", String.Format("\\s*(?<AMPM>{0}|{1})\\s*", _dateTimeFormatProvider.AmPmDesignators.ToArray()[0], _dateTimeFormatProvider.AmPmDesignators.ToArray()[1])},
{" tt", String.Format("\\s*(?<AMPM>{0}|{1})\\s*", _dateTimeFormatProvider.AmPmDesignators.ToArray()[0], _dateTimeFormatProvider.AmPmDesignators.ToArray()[1])},
{" t", String.Format("\\s*(?<AMPM>{0}|{1})\\s*", _dateTimeFormatProvider.AmPmDesignators.ToArray()[0], _dateTimeFormatProvider.AmPmDesignators.ToArray()[1])}
{"tt", String.Format("\\s*(?<amPm>{0}|{1})\\s*", _dateTimeFormatProvider.AmPmDesignators.ToArray()[0], _dateTimeFormatProvider.AmPmDesignators.ToArray()[1])},
{"t", String.Format("\\s*(?<amPm>{0}|{1})\\s*", _dateTimeFormatProvider.AmPmDesignators.ToArray()[0], _dateTimeFormatProvider.AmPmDesignators.ToArray()[1])},
{" tt", String.Format("\\s*(?<amPm>{0}|{1})\\s*", _dateTimeFormatProvider.AmPmDesignators.ToArray()[0], _dateTimeFormatProvider.AmPmDesignators.ToArray()[1])},
{" t", String.Format("\\s*(?<amPm>{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<string, string> replacements) {
protected virtual string ConvertFormatStringToRegexPattern(string format, IDictionary<string, string> replacements) {
string result = null;
result = Regex.Replace(format, @"\.|\$|\^|\{|\[|\(|\||\)|\*|\+|\?|\\", m => String.Format(@"\{0}", m.Value));
result = Regex.Replace(result, @"(?<!\\)'(.*?)((?<!\\)')", m => 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();

View File

@@ -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
* Add support for the different Gregorian calendar types
* User ICultureManager and ICultureRepository to get the current CultureInfo in other classes