Add code in DefaultDateFormatterTests to analyze the tested format string to determine which parts we might reasonably expect back from parsing.

Fixed a number of bugs in DefaultDateFormatter.
Improved test code in DefaultDateFormatterTests.
Added genitive month name support to IDateTimeFormatProvider and its implementations.
Added support to DefaultDateFormatter for considering both genitive and non-genitive month names during parsing.
This commit is contained in:
Daniel Stolt
2014-07-28 03:44:00 +02:00
parent 5ab19a0ea8
commit 28d02a503b
6 changed files with 145 additions and 48 deletions

View File

@@ -24,8 +24,13 @@ namespace Orchard.Framework.Tests.Localization {
var failedCases = new ConcurrentDictionary<string, Exception>();
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<IDateTimeFormatProvider>();
var target = container.Resolve<IDateFormatter>();
@@ -66,29 +71,34 @@ namespace Orchard.Framework.Tests.Localization {
var failedCases = new ConcurrentDictionary<string, Exception>();
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<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);
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 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<string, Exception>();
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<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);
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<WorkContext>(new StubWorkContext(cultureName, calendarName));

View File

@@ -26,12 +26,24 @@ namespace Orchard.Localization.Services {
}
}
public virtual IEnumerable<string> MonthNamesGenitive {
get {
return MonthNames;
}
}
public IEnumerable<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<string> MonthNamesShortGenitive {
get {
return MonthNamesShort;
}
}
public IEnumerable<string> DayNames {
get {
return T("Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday").Text.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries);

View File

@@ -13,8 +13,6 @@ namespace Orchard.Localization.Services {
/// </summary>
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<string> MonthNamesGenitive {
get {
return DateTimeFormat.MonthGenitiveNames;
}
}
public virtual IEnumerable<string> MonthNamesShort {
get {
return DateTimeFormat.AbbreviatedMonthNames;
}
}
public virtual IEnumerable<string> MonthNamesShortGenitive {
get {
return DateTimeFormat.AbbreviatedMonthGenitiveNames;
}
}
public virtual IEnumerable<string> 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();

View File

@@ -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<string, string> GetDateParseReplacements() {
return new Dictionary<string, string>() {
{"dddd", String.Format("(?<dayName>{0})", String.Join("|", _dateTimeFormatProvider.DayNames))},
{"ddd", String.Format("(?<dayNameShort>{0})", String.Join("|", _dateTimeFormatProvider.DayNamesShort))},
{"dddd", String.Format("(?<dayName>{0})", String.Join("|", _dateTimeFormatProvider.DayNames.Select(x => EscapeForRegex(x))))},
{"ddd", String.Format("(?<dayNameShort>{0})", String.Join("|", _dateTimeFormatProvider.DayNamesShort.Select(x => EscapeForRegex(x))))},
{"dd", "(?<day>[0-9]{2})"},
{"d", "(?<day>[0-9]{1,2})"},
{"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))))},
{"MMMM", String.Format("(?<monthName>{0})", String.Join("|", _dateTimeFormatProvider.MonthNames.Union(_dateTimeFormatProvider.MonthNamesGenitive).Where(x => !String.IsNullOrEmpty(x)).Distinct().Select(x => EscapeForRegex(x))))},
{"MMM", String.Format("(?<monthNameShort>{0})", String.Join("|", _dateTimeFormatProvider.MonthNamesShort.Union(_dateTimeFormatProvider.MonthNamesShortGenitive).Where(x => !String.IsNullOrEmpty(x)).Distinct().Select(x => EscapeForRegex(x))))},
{"MM", "(?<month>[0-9]{2})"},
{"M", "(?<month>[0-9]{1,2})"},
{"yyyyy", "(?<year>[0-9]{5})"},
@@ -247,10 +263,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*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))},
{"t", String.Format("\\s*(?<amPm>{0}|{1})\\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))},
{" tt", String.Format("\\s*(?<amPm>{0}|{1})\\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators.ToArray()[1]))},
{" t", String.Format("\\s*(?<amPm>{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<string, string> 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, @"(?<!\\)'(.*?)((?<!\\)')", m => 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();

View File

@@ -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

View File

@@ -16,6 +16,13 @@ namespace Orchard.Localization.Services {
get;
}
/// <summary>
/// Gets a list of genitive month names (used in contexts when a day is involved).
/// </summary>
IEnumerable<string> MonthNamesGenitive {
get;
}
/// <summary>
/// Gets a list of abbreviated month names.
/// </summary>
@@ -23,6 +30,13 @@ namespace Orchard.Localization.Services {
get;
}
/// <summary>
/// Gets a list of abbreviated genivite month names (used in contexts when a day is involved).
/// </summary>
IEnumerable<string> MonthNamesShortGenitive {
get;
}
/// <summary>
/// Gets a list of weekday names.
/// </summary>