From d0360e1e396da4cef20d69b08b0879488deb621d Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Fri, 11 Sep 2015 16:02:46 -0500 Subject: [PATCH] Fixing date patterns on Linux. 1. Getting more LongDatePatterns by getting both full and long ICU patterns. 2. Getting more ShortDatePatterns by getting both medium and short ICU patterns. Still keeping the "yMd" pattern, since this closely matches what is used on Windows. 3. Removing any duplicates in the date patterns. 4. "Normalizing" the date patterns from ICU format to .NET format. a. "EEEE", "eeee" and "cccc" is replaced with "dddd" b. "LLLL" is replaced with "MMMM" c. "G" is replaced with "g" d. A single 'y' is replaced with 'yyyy' --- .../calendarData.cpp | 42 +++- .../System/Globalization/CalendarData.Unix.cs | 197 ++++++++++++++++-- 2 files changed, 215 insertions(+), 24 deletions(-) diff --git a/src/corefx/System.Globalization.Native/calendarData.cpp b/src/corefx/System.Globalization.Native/calendarData.cpp index 061fca9172e0..a9c356dbafee 100644 --- a/src/corefx/System.Globalization.Native/calendarData.cpp +++ b/src/corefx/System.Globalization.Native/calendarData.cpp @@ -9,6 +9,7 @@ #include "locale.hpp" #include +#include #include #include @@ -245,7 +246,7 @@ CalendarDataResult GetMonthDayPattern(Locale& locale, UChar* sMonthDay, int32_t if (U_FAILURE(err)) return GetCalendarDataResult(err); - UnicodeString monthDayPattern = generator->getBestPattern(UnicodeString("MMMMd"), err); + UnicodeString monthDayPattern = generator->getBestPattern(UnicodeString(UDAT_MONTH_DAY), err); if (U_FAILURE(err)) return GetCalendarDataResult(err); @@ -297,6 +298,30 @@ extern "C" CalendarDataResult GetCalendarInfo(const UChar* localeName, CalendarI } } +/* +Function: +InvokeCallbackForDatePattern + +Gets the ICU date pattern for the specified locale and EStyle and invokes the callback with the result. +*/ +bool InvokeCallbackForDatePattern(Locale& locale, DateFormat::EStyle style, EnumCalendarInfoCallback callback, const void* context) +{ + LocalPointer dateFormat(DateFormat::createDateInstance(style, locale)); + if (dateFormat.isNull()) + return false; + + // cast to SimpleDateFormat so we can call toPattern() + SimpleDateFormat* sdf = dynamic_cast(dateFormat.getAlias()); + if (sdf == NULL) + return false; + + UnicodeString pattern; + sdf->toPattern(pattern); + + callback(pattern.getTerminatedBuffer(), context); + return true; +} + /* Function: InvokeCallbackForDateTimePattern @@ -451,14 +476,17 @@ extern "C" int32_t EnumCalendarInfo( switch (dataType) { case ShortDates: - return InvokeCallbackForDateTimePattern(locale, "Mdyyyy", callback, context); + // ShortDates to map kShort and kMedium in ICU, but also adding the "yMd" skeleton as well, as this + // closely matches what is used on Windows + return InvokeCallbackForDateTimePattern(locale, UDAT_YEAR_NUM_MONTH_DAY, callback, context) && + InvokeCallbackForDatePattern(locale, DateFormat::kShort, callback, context) && + InvokeCallbackForDatePattern(locale, DateFormat::kMedium, callback, context); case LongDates: - // TODO: need to replace the "EEEE"s with "dddd"s for .net - // Also, "LLLL"s to "MMMM"s - // Also, "G"s to "g"s - return InvokeCallbackForDateTimePattern(locale, "eeeeMMMMddyyyy", callback, context); + // LongDates map to kFull and kLong in ICU. + return InvokeCallbackForDatePattern(locale, DateFormat::kFull, callback, context) && + InvokeCallbackForDatePattern(locale, DateFormat::kLong, callback, context); case YearMonths: - return InvokeCallbackForDateTimePattern(locale, "yyyyMMMM", callback, context); + return InvokeCallbackForDateTimePattern(locale, UDAT_YEAR_MONTH, callback, context); case DayNames: return EnumWeekdays(locale, calendarId, DateFormatSymbols::STANDALONE, DateFormatSymbols::WIDE, callback, context); case AbbrevDayNames: diff --git a/src/mscorlib/corefx/System/Globalization/CalendarData.Unix.cs b/src/mscorlib/corefx/System/Globalization/CalendarData.Unix.cs index 6a62909023f7..cfca8a3533a0 100644 --- a/src/mscorlib/corefx/System/Globalization/CalendarData.Unix.cs +++ b/src/mscorlib/corefx/System/Globalization/CalendarData.Unix.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Generic; +using System.Diagnostics.Contracts; using System.Runtime.InteropServices; using System.Text; @@ -42,10 +43,11 @@ private bool LoadCalendarDataFromSystem(String localeName, CalendarId calendarId bool result = true; result &= GetCalendarInfo(localeName, calendarId, CalendarDataType.NativeName, out this.sNativeName); result &= GetCalendarInfo(localeName, calendarId, CalendarDataType.MonthDay, out this.sMonthDay); + this.sMonthDay = NormalizeDatePattern(this.sMonthDay); - result &= EnumCalendarInfo(localeName, calendarId, CalendarDataType.ShortDates, out this.saShortDates); - result &= EnumCalendarInfo(localeName, calendarId, CalendarDataType.LongDates, out this.saLongDates); - result &= EnumCalendarInfo(localeName, calendarId, CalendarDataType.YearMonths, out this.saYearMonths); + result &= EnumDatePatterns(localeName, calendarId, CalendarDataType.ShortDates, out this.saShortDates); + result &= EnumDatePatterns(localeName, calendarId, CalendarDataType.LongDates, out this.saLongDates); + result &= EnumDatePatterns(localeName, calendarId, CalendarDataType.YearMonths, out this.saYearMonths); result &= EnumCalendarInfo(localeName, calendarId, CalendarDataType.DayNames, out this.saDayNames); result &= EnumCalendarInfo(localeName, calendarId, CalendarDataType.AbbrevDayNames, out this.saAbbrevDayNames); result &= EnumCalendarInfo(localeName, calendarId, CalendarDataType.SuperShortDayNames, out this.saSuperShortDayNames); @@ -128,28 +130,163 @@ private static bool GetCalendarInfo(string localeName, CalendarId calendarId, Ca return false; } - private bool EnumMonthNames(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[] monthNames) + private static bool EnumDatePatterns(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[] datePatterns) + { + datePatterns = null; + + CallbackContext callbackContext = new CallbackContext(); + callbackContext.DisallowDuplicates = true; + bool result = EnumCalendarInfo(localeName, calendarId, dataType, callbackContext); + if (result) + { + List datePatternsList = callbackContext.Results; + + datePatterns = new string[datePatternsList.Count]; + for (int i = 0; i < datePatternsList.Count; i++) + { + datePatterns[i] = NormalizeDatePattern(datePatternsList[i]); + } + } + + return result; + } + + /// + /// The ICU date format characters are not exactly the same as the .NET date format characters. + /// NormalizeDatePattern will take in an ICU date pattern and return the equivalent .NET date pattern. + /// + /// + /// see Date Field Symbol Table in http://userguide.icu-project.org/formatparse/datetime + /// and https://msdn.microsoft.com/en-us/library/8kb3ddd4(v=vs.110).aspx + /// + private static string NormalizeDatePattern(string input) + { + StringBuilder destination = new StringBuilder(input.Length); + + int index = 0; + while (index < input.Length) + { + switch (input[index]) + { + case '\'': + // single quotes escape characters, like 'de' in es-SP + // so read verbatim until the next single quote + destination.Append(input[index++]); + while (index < input.Length) + { + char current = input[index++]; + destination.Append(current); + if (current == '\'') + { + break; + } + } + break; + case 'E': + case 'e': + case 'c': + // 'E' in ICU is the day of the week, which maps to 3 or 4 'd's in .NET + // 'e' in ICU is the local day of the week, which has no representation in .NET, but + // maps closest to 3 or 4 'd's in .NET + // 'c' in ICU is the stand-alone day of the week, which has no representation in .NET, but + // maps closest to 3 or 4 'd's in .NET + NormalizeDayOfWeek(input, destination, ref index); + break; + case 'L': + case 'M': + // 'L' in ICU is the stand-alone name of the month, + // which maps closest to 'M' in .NET since it doesn't support stand-alone month names in patterns + // 'M' in both ICU and .NET is the month, + // but ICU supports 5 'M's, which is the super short month name + int occurrences = CountOccurrences(input, input[index], ref index); + if (occurrences > 4) + { + // 5 'L's or 'M's in ICU is the super short name, which maps closest to MMM in .NET + occurrences = 3; + } + destination.Append('M', occurrences); + break; + case 'G': + // 'G' in ICU is the era, which maps to 'g' in .NET + occurrences = CountOccurrences(input, 'G', ref index); + + // it doesn't matter how many 'G's, since .NET only supports 'g' or 'gg', and they + // have the same meaning + destination.Append('g'); + break; + case 'y': + // a single 'y' in ICU is the year with no padding or trimming. + // a single 'y' in .NET is the year with 1 or 2 digits + // so convert any single 'y' to 'yyyy' + occurrences = CountOccurrences(input, 'y', ref index); + if (occurrences == 1) + { + occurrences = 4; + } + destination.Append('y', occurrences); + break; + default: + const string unsupportedDateFieldSymbols = "YuUrQqwWDFg"; + Contract.Assert(unsupportedDateFieldSymbols.IndexOf(input[index]) == -1, + string.Format(CultureInfo.InvariantCulture, + "Encountered an unexpected date field symbol '{0}' from ICU which has no known corresponding .NET equivalent.", + input[index])); + + destination.Append(input[index++]); + break; + } + } + + return destination.ToString(); + } + + private static void NormalizeDayOfWeek(string input, StringBuilder destination, ref int index) + { + char dayChar = input[index]; + int occurrences = CountOccurrences(input, dayChar, ref index); + occurrences = Math.Max(occurrences, 3); + if (occurrences > 4) + { + // 5 and 6 E/e/c characters in ICU is the super short names, which maps closest to ddd in .NET + occurrences = 3; + } + + destination.Append('d', occurrences); + } + + private static int CountOccurrences(string input, char value, ref int index) + { + int startIndex = index; + while (index < input.Length && input[index] == value) + { + index++; + } + + return index - startIndex; + } + + private static bool EnumMonthNames(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[] monthNames) { monthNames = null; - List monthNameList = new List(13); - bool result = EnumCalendarInfo(localeName, calendarId, dataType, monthNameList); + CallbackContext callbackContext = new CallbackContext(); + bool result = EnumCalendarInfo(localeName, calendarId, dataType, callbackContext); if (result) { // the month-name arrays are expected to have 13 elements. If ICU only returns 12, add an // extra empty string to fill the array. - if (monthNameList.Count == 12) + if (callbackContext.Results.Count == 12) { - monthNameList.Add(string.Empty); + callbackContext.Results.Add(string.Empty); } - monthNames = monthNameList.ToArray(); + monthNames = callbackContext.Results.ToArray(); } return result; } - private bool EnumEraNames(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[] eraNames) + private static bool EnumEraNames(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[] eraNames) { bool result = EnumCalendarInfo(localeName, calendarId, dataType, out eraNames); @@ -168,19 +305,19 @@ internal static bool EnumCalendarInfo(string localeName, CalendarId calendarId, { calendarData = null; - List calendarDataList = new List(); - bool result = EnumCalendarInfo(localeName, calendarId, dataType, calendarDataList); + CallbackContext callbackContext = new CallbackContext(); + bool result = EnumCalendarInfo(localeName, calendarId, dataType, callbackContext); if (result) { - calendarData = calendarDataList.ToArray(); + calendarData = callbackContext.Results.ToArray(); } return result; } - private static bool EnumCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType dataType, List calendarDataList) + private static bool EnumCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType dataType, CallbackContext callbackContext) { - GCHandle context = GCHandle.Alloc(calendarDataList); + GCHandle context = GCHandle.Alloc(callbackContext); try { return Interop.GlobalizationInterop.EnumCalendarInfo(EnumCalendarInfoCallback, localeName, calendarId, dataType, (IntPtr)context); @@ -193,8 +330,34 @@ private static bool EnumCalendarInfo(string localeName, CalendarId calendarId, C private static void EnumCalendarInfoCallback(string calendarString, IntPtr context) { - List calendarDataList = (List)((GCHandle)context).Target; - calendarDataList.Add(calendarString); + CallbackContext callbackContext = (CallbackContext)((GCHandle)context).Target; + + if (callbackContext.DisallowDuplicates) + { + foreach (string existingResult in callbackContext.Results) + { + if (string.Equals(calendarString, existingResult, StringComparison.Ordinal)) + { + // the value is already in the results, so don't add it again + return; + } + } + } + + callbackContext.Results.Add(calendarString); + } + + private class CallbackContext + { + private List _results = new List(); + + public CallbackContext() + { + } + + public List Results { get { return _results; } } + + public bool DisallowDuplicates { get; set; } } } }