mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-15 19:54:57 +08:00
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:
@@ -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));
|
||||
|
@@ -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);
|
||||
|
@@ -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();
|
||||
|
@@ -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();
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user