Changed from IEnumerable<string> to string[] on some of the properties on IDateTimeFormatProvider because they are naturally accessed by index. Incremental work on formatting implementation in DefaultDateFormatter.

This commit is contained in:
Daniel Stolt
2014-07-28 19:30:41 +02:00
parent e48cdc7bff
commit c02f09d900
7 changed files with 151 additions and 59 deletions

View File

@@ -227,6 +227,55 @@ namespace Orchard.Framework.Tests.Localization {
}
}
[Test]
[Description("Date formatting works correctly for all combinations of months, format strings and cultures.")]
public void FormatDateTest01() {
var allCases = new ConcurrentBag<string>();
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, 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.
for (var month = 1; month <= 12; month++) { // All months in the year.
DateTime date = new DateTime(1998, month, 1);
DateParts dateParts = new DateParts(1998, month, 1);
// Print reference string using Gregorian calendar to avoid calendar conversion.
var cultureGregorian = (CultureInfo)culture.Clone();
cultureGregorian.DateTimeFormat.Calendar = cultureGregorian.OptionalCalendars.OfType<GregorianCalendar>().First();
var caseKey = String.Format("{0}___{1}___{2}", culture.Name, dateFormat, dateParts);
allCases.Add(caseKey);
//Debug.WriteLine(String.Format("{0} cases tested so far. Testing case {1}...", allCases.Count, caseKey));
try {
var result = target.FormatDate(dateParts, dateFormat);
var expected = date.ToString(dateFormat, cultureGregorian);
Assert.AreEqual(expected, result);
}
catch (Exception ex) {
failedCases.TryAdd(caseKey, ex);
}
}
}
});
if (failedCases.Count > maxFailedCases) {
throw new AggregateException(String.Format("Format tests failed for {0} of {1} cases. Expected {2} failed cases or less.", failedCases.Count, allCases.Count, maxFailedCases), failedCases.Values);
}
}
[Test]
[Description("Time parsing throws a FormatException for unparsable time strings.")]
[ExpectedException(typeof(FormatException))]

View File

@@ -20,43 +20,43 @@ namespace Orchard.Localization.Services {
public Localizer T { get; set; }
public IEnumerable<string> MonthNames {
public string[] MonthNames {
get {
return T("January, February, March, April, May, June, July, August, September, October, November, December").Text.Split(new string[] {", "}, StringSplitOptions.RemoveEmptyEntries);
}
}
public virtual IEnumerable<string> MonthNamesGenitive {
public virtual string[] MonthNamesGenitive {
get {
return MonthNames;
}
}
public IEnumerable<string> MonthNamesShort {
public 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 {
public virtual string[] MonthNamesShortGenitive {
get {
return MonthNamesShort;
}
}
public IEnumerable<string> DayNames {
public string[] DayNames {
get {
return T("Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday").Text.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries);
}
}
public IEnumerable<string> DayNamesShort {
public string[] DayNamesShort {
get {
return T("Sun, Mon, Tue, Wed, Thu, Fri, Sat").Text.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries);
}
}
public IEnumerable<string> DayNamesMin {
public string[] DayNamesMin {
get {
return T("Su, Mo, Tu, We, Th, Fr, Sa").Text.Split(new string[] { ", " }, StringSplitOptions.RemoveEmptyEntries);
}
@@ -160,7 +160,7 @@ namespace Orchard.Localization.Services {
}
}
public IEnumerable<string> AmPmDesignators {
public string[] AmPmDesignators {
get {
var t = T("AM;PM").Text;
var parts = t.Split(';');

View File

@@ -31,6 +31,14 @@ namespace Orchard.Localization.Models {
}
}
public DateTime ToDateTime() {
return new DateTime(
_year > 0 ? _year : DateTime.MinValue.Year,
_month > 0 ? _month : DateTime.MinValue.Month,
_day > 0 ? _day : DateTime.MinValue.Day
);
}
public override string ToString() {
return String.Format("{0}-{1}-{2}", _year, _month, _day);
}

View File

@@ -23,43 +23,43 @@ namespace Orchard.Localization.Services {
_calendarManager = calendarManager;
}
public virtual IEnumerable<string> MonthNames {
public virtual string[] MonthNames {
get {
return DateTimeFormat.MonthNames;
}
}
public virtual IEnumerable<string> MonthNamesGenitive {
public virtual string[] MonthNamesGenitive {
get {
return DateTimeFormat.MonthGenitiveNames;
}
}
public virtual IEnumerable<string> MonthNamesShort {
public virtual string[] MonthNamesShort {
get {
return DateTimeFormat.AbbreviatedMonthNames;
}
}
public virtual IEnumerable<string> MonthNamesShortGenitive {
public virtual string[] MonthNamesShortGenitive {
get {
return DateTimeFormat.AbbreviatedMonthGenitiveNames;
}
}
public virtual IEnumerable<string> DayNames {
public virtual string[] DayNames {
get {
return DateTimeFormat.DayNames;
}
}
public virtual IEnumerable<string> DayNamesShort {
public virtual string[] DayNamesShort {
get {
return DateTimeFormat.AbbreviatedDayNames;
}
}
public virtual IEnumerable<string> DayNamesMin {
public virtual string[] DayNamesMin {
get {
return DateTimeFormat.ShortestDayNames;
}
@@ -111,9 +111,6 @@ namespace Orchard.Localization.Services {
patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('D'));
patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('m'));
patterns.AddRange(DateTimeFormat.GetAllDateTimePatterns('y'));
// 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.
return patterns.Distinct();
}
}
@@ -175,9 +172,9 @@ namespace Orchard.Localization.Services {
}
}
public virtual IEnumerable<string> AmPmDesignators {
public virtual string[] AmPmDesignators {
get {
return new string[] { DateTimeFormat.AMDesignator, DateTimeFormat.PMDesignator };
return new [] { DateTimeFormat.AMDesignator, DateTimeFormat.PMDesignator };
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -112,8 +113,20 @@ namespace Orchard.Localization.Services {
}
public virtual string FormatDate(DateParts parts, string format) {
// TODO: Mahsa should implement!
throw new NotImplementedException();
var replacements = GetDateFormatReplacements();
var formatString = ConvertToFormatString(format, replacements);
var calendar = CurrentCalendar;
var dateTime = parts.ToDateTime();
var yearString = parts.Year.ToString("00", System.Globalization.CultureInfo.InvariantCulture);
var twoDigitYear = Int32.Parse(yearString.Substring(yearString.Length - 2));
var monthName = parts.Month > 0 ? _dateTimeFormatProvider.MonthNamesGenitive[parts.Month - 1] : null;
var monthNameShort = parts.Month > 0 ? _dateTimeFormatProvider.MonthNamesShortGenitive[parts.Month - 1] : null;
var dayName = parts.Day > 0 ? _dateTimeFormatProvider.DayNames[(int)calendar.GetDayOfWeek(dateTime)] : null;
var dayNameShort = parts.Day > 0 ? _dateTimeFormatProvider.DayNamesShort[(int)calendar.GetDayOfWeek(parts.ToDateTime())] : null;
return String.Format(formatString, parts.Year, twoDigitYear, parts.Month, monthName, monthNameShort, parts.Day, dayName, dayNameShort);
}
public virtual string FormatTime(TimeParts parts) {
@@ -127,7 +140,7 @@ namespace Orchard.Localization.Services {
}
protected virtual DateTimeParts? TryParseDateTime(string dateTimeString, string format, IDictionary<string, string> replacements) {
var dateTimePattern = ConvertFormatStringToRegexPattern(format, replacements);
var dateTimePattern = ConvertToRegexPattern(format, replacements);
Match m = Regex.Match(dateTimeString, dateTimePattern, RegexOptions.IgnoreCase);
if (m.Success) {
return new DateTimeParts(ExtractDateParts(m), ExtractTimeParts(m));
@@ -136,7 +149,7 @@ namespace Orchard.Localization.Services {
}
protected virtual DateParts? TryParseDate(string dateString, string format, IDictionary<string, string> replacements) {
var datePattern = ConvertFormatStringToRegexPattern(format, replacements);
var datePattern = ConvertToRegexPattern(format, replacements);
Match m = Regex.Match(dateString, datePattern, RegexOptions.IgnoreCase);
if (m.Success) {
return ExtractDateParts(m);
@@ -145,7 +158,7 @@ namespace Orchard.Localization.Services {
}
protected virtual TimeParts? TryParseTime(string timeString, string format, IDictionary<string, string> replacements) {
var timePattern = ConvertFormatStringToRegexPattern(format, replacements);
var timePattern = ConvertToRegexPattern(format, replacements);
Match m = Regex.Match(timeString, timePattern, RegexOptions.IgnoreCase);
if (m.Success) {
return ExtractTimeParts(m);
@@ -210,8 +223,8 @@ namespace Orchard.Localization.Services {
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);
var isPm = m.Groups["amPm"].Value.Equals(_dateTimeFormatProvider.AmPmDesignators[1], StringComparison.InvariantCultureIgnoreCase);
hour = ConvertToHour24(Int32.Parse(m.Groups["hour12"].Value), isPm);
}
if (m.Groups["minute"].Success) {
@@ -264,31 +277,31 @@ namespace Orchard.Localization.Services {
{"fff", "(?<millisecond>[0-9]{3})"},
{"ff", "(?<millisecond>[0-9]{2})"},
{"f", "(?<millisecond>[0-9]{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]))},
{"tt", String.Format(@"\s*(?<amPm>{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[1]))},
{"t", String.Format(@"\s*(?<amPm>{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[1]))},
{" tt", String.Format(@"\s*(?<amPm>{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[1]))},
{" t", String.Format(@"\s*(?<amPm>{0}|{1})\s*", EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[0]), EscapeForRegex(_dateTimeFormatProvider.AmPmDesignators[1]))},
{"K", @"(?<timezone>Z|(\+|-)[0-9]{2}:[0-9]{2})*"},
};
}
//protected virtual Dictionary<string, string> GetDateFormatReplacements() {
// return new Dictionary<string, string>() {
// {"dddd", "{5:dddd}"},
// {"ddd", "{6:ddd}"},
// {"dd", "{2:00}"},
// {"d", "{2:##}"},
// {"MMMM", "{3:MMMM}"},
// {"MMM", "{4:MMM}"},
// {"MM", "{1:00}"},
// {"M", "{1:##}"},
// {"yyyyy", "{1:00000}"},
// {"yyyy", "{1:0000}"},
// {"yyy", "{1:000}"},
// {"yy", "{1:00}"},
// {"y", "{1:0}"}
// };
//}
protected virtual Dictionary<string, string> GetDateFormatReplacements() {
return new Dictionary<string, string>() {
{"dddd", "{6:dddd}"},
{"ddd", "{7:ddd}"},
{"dd", "{5:00}"},
{"d", "{5:##}"},
{"MMMM", "{3:MMMM}"},
{"MMM", "{4:MMM}"},
{"MM", "{2:00}"},
{"M", "{2:##}"},
{"yyyyy", "{0:00000}"},
{"yyyy", "{0:0000}"},
{"yyy", "{0:000}"},
{"yy", "{1:00}"},
{"y", "{1:0}"}
};
}
//protected virtual Dictionary<string, string> GetTimeFormatReplacements() {
// return new Dictionary<string, string>() {
@@ -313,7 +326,7 @@ namespace Orchard.Localization.Services {
// };
//}
protected virtual string ConvertFormatStringToRegexPattern(string format, IDictionary<string, string> replacements) {
protected virtual string ConvertToRegexPattern(string format, IDictionary<string, string> replacements) {
string result = format;
// Transform the / and : characters into culture-specific date and time separators.
@@ -323,7 +336,6 @@ namespace Orchard.Localization.Services {
result = EscapeForRegex(result);
// Transform all literals to corresponding wildcard matches.
//result = Regex.Replace(result, @"(?<!\\)'(.*?)(?<!\\)'|(?<!\\)""(.*?)(?<!\\)""", m => EscapeForRegex(m.Value.Trim('\'', '"')));
result = Regex.Replace(result, @"(?<!\\)'(.*?)(?<!\\)'|(?<!\\)""(.*?)(?<!\\)""", m => String.Format("(?:.{{{0}}})", m.Value.Replace("\\", "").Length - 2));
// Transform all DateTime format specifiers into corresponding Regex captures.
@@ -335,7 +347,29 @@ namespace Orchard.Localization.Services {
return result;
}
protected virtual int ConvertHour12ToHour24(int hour12, bool isPm) {
protected virtual string ConvertToFormatString(string format, IDictionary<string, string> replacements) {
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);
//// Transform all literals to corresponding text.
//var literals = new List<string>();
//result = Regex.Replace(result, @"(?<!\\)'(.*?)(?<!\\)'|(?<!\\)""(.*?)(?<!\\)""", m => {
// literals.Add(m.Value.Trim('\'', '"'));
// return String.Format("{{{0}}}", literals.Count - 1);
//});
// Transform all literals to corresponding text.
result = Regex.Replace(result, @"(?<!\\)'(.*?)(?<!\\)'|(?<!\\)""(.*?)(?<!\\)""", m => m.Value.Trim('\'', '"'));
// Transform all DateTime format specifiers into corresponding format string placeholders.
result = result.ReplaceAll(replacements);
return result;
}
protected virtual int ConvertToHour24(int hour12, bool isPm) {
if (isPm) {
return hour12 == 12 ? 12 : hour12 + 12;
}

View File

@@ -41,8 +41,12 @@ struct DateLocalizationOptions {
}
TODO:
* Implement formatting in addition to parsing
* Test for proper handling of fraction (f) format specifier - suspect it does not work properly in current state
* Rewrite DefaultDateLocalizationServices
* Write unit tests for DefaultDateLocalizationServices
* Add warning message when saving unsupported combination in settings
* Add support for the different Gregorian calendar types
* Improve DateTimeField:
- Surface the field mode (date, time or both)
- Do not perform time-zone conversion in date-only mode

View File

@@ -12,49 +12,49 @@ namespace Orchard.Localization.Services {
/// <summary>
/// Gets a list of month names.
/// </summary>
IEnumerable<string> MonthNames {
string[] MonthNames {
get;
}
/// <summary>
/// Gets a list of genitive month names (used in contexts when a day is involved).
/// </summary>
IEnumerable<string> MonthNamesGenitive {
string[] MonthNamesGenitive {
get;
}
/// <summary>
/// Gets a list of abbreviated month names.
/// </summary>
IEnumerable<string> MonthNamesShort {
string[] MonthNamesShort {
get;
}
/// <summary>
/// Gets a list of abbreviated genivite month names (used in contexts when a day is involved).
/// </summary>
IEnumerable<string> MonthNamesShortGenitive {
string[] MonthNamesShortGenitive {
get;
}
/// <summary>
/// Gets a list of weekday names.
/// </summary>
IEnumerable<string> DayNames {
string[] DayNames {
get;
}
/// <summary>
/// Gets a list of abbreviated weekday names.
/// </summary>
IEnumerable<string> DayNamesShort {
string[] DayNamesShort {
get;
}
/// <summary>
/// Gets a list of maximally abbreviated weekday names.
/// </summary>
IEnumerable<string> DayNamesMin {
string[] DayNamesMin {
get;
}
@@ -159,7 +159,7 @@ namespace Orchard.Localization.Services {
/// <summary>
/// Gets a list of strings used as display text for the AM and PM designators.
/// </summary>
IEnumerable<string> AmPmDesignators {
string[] AmPmDesignators {
get;
}
}