CalendarData.Unix.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. // See the LICENSE file in the project root for more information.
  4. using System.Collections.Generic;
  5. using System.Diagnostics;
  6. using System.Security;
  7. using System.Text;
  8. using Internal.Runtime.CompilerServices;
  9. namespace System.Globalization
  10. {
  11. // needs to be kept in sync with CalendarDataType in System.Globalization.Native
  12. internal enum CalendarDataType
  13. {
  14. Uninitialized = 0,
  15. NativeName = 1,
  16. MonthDay = 2,
  17. ShortDates = 3,
  18. LongDates = 4,
  19. YearMonths = 5,
  20. DayNames = 6,
  21. AbbrevDayNames = 7,
  22. MonthNames = 8,
  23. AbbrevMonthNames = 9,
  24. SuperShortDayNames = 10,
  25. MonthGenitiveNames = 11,
  26. AbbrevMonthGenitiveNames = 12,
  27. EraNames = 13,
  28. AbbrevEraNames = 14,
  29. }
  30. internal partial class CalendarData
  31. {
  32. private bool LoadCalendarDataFromSystem(string localeName, CalendarId calendarId)
  33. {
  34. bool result = true;
  35. result &= GetCalendarInfo(localeName, calendarId, CalendarDataType.NativeName, out this.sNativeName);
  36. result &= GetCalendarInfo(localeName, calendarId, CalendarDataType.MonthDay, out this.sMonthDay);
  37. this.sMonthDay = NormalizeDatePattern(this.sMonthDay);
  38. result &= EnumDatePatterns(localeName, calendarId, CalendarDataType.ShortDates, out this.saShortDates);
  39. result &= EnumDatePatterns(localeName, calendarId, CalendarDataType.LongDates, out this.saLongDates);
  40. result &= EnumDatePatterns(localeName, calendarId, CalendarDataType.YearMonths, out this.saYearMonths);
  41. result &= EnumCalendarInfo(localeName, calendarId, CalendarDataType.DayNames, out this.saDayNames);
  42. result &= EnumCalendarInfo(localeName, calendarId, CalendarDataType.AbbrevDayNames, out this.saAbbrevDayNames);
  43. result &= EnumCalendarInfo(localeName, calendarId, CalendarDataType.SuperShortDayNames, out this.saSuperShortDayNames);
  44. string leapHebrewMonthName = null;
  45. result &= EnumMonthNames(localeName, calendarId, CalendarDataType.MonthNames, out this.saMonthNames, ref leapHebrewMonthName);
  46. if (leapHebrewMonthName != null)
  47. {
  48. // In Hebrew calendar, get the leap month name Adar II and override the non-leap month 7
  49. Debug.Assert(calendarId == CalendarId.HEBREW && saMonthNames.Length == 13);
  50. saLeapYearMonthNames = (string[]) saMonthNames.Clone();
  51. saLeapYearMonthNames[6] = leapHebrewMonthName;
  52. // The returned data from ICU has 6th month name as 'Adar I' and 7th month name as 'Adar'
  53. // We need to adjust that in the list used with non-leap year to have 6th month as 'Adar' and 7th month as 'Adar II'
  54. // note that when formatting non-leap year dates, 7th month shouldn't get used at all.
  55. saMonthNames[5] = saMonthNames[6];
  56. saMonthNames[6] = leapHebrewMonthName;
  57. }
  58. result &= EnumMonthNames(localeName, calendarId, CalendarDataType.AbbrevMonthNames, out this.saAbbrevMonthNames, ref leapHebrewMonthName);
  59. result &= EnumMonthNames(localeName, calendarId, CalendarDataType.MonthGenitiveNames, out this.saMonthGenitiveNames, ref leapHebrewMonthName);
  60. result &= EnumMonthNames(localeName, calendarId, CalendarDataType.AbbrevMonthGenitiveNames, out this.saAbbrevMonthGenitiveNames, ref leapHebrewMonthName);
  61. result &= EnumEraNames(localeName, calendarId, CalendarDataType.EraNames, out this.saEraNames);
  62. result &= EnumEraNames(localeName, calendarId, CalendarDataType.AbbrevEraNames, out this.saAbbrevEraNames);
  63. return result;
  64. }
  65. internal static int GetTwoDigitYearMax(CalendarId calendarId)
  66. {
  67. // There is no user override for this value on Linux or in ICU.
  68. // So just return -1 to use the hard-coded defaults.
  69. return -1;
  70. }
  71. // Call native side to figure out which calendars are allowed
  72. internal static int GetCalendars(string localeName, bool useUserOverride, CalendarId[] calendars)
  73. {
  74. Debug.Assert(!GlobalizationMode.Invariant);
  75. // NOTE: there are no 'user overrides' on Linux
  76. int count = Interop.Globalization.GetCalendars(localeName, calendars, calendars.Length);
  77. // ensure there is at least 1 calendar returned
  78. if (count == 0 && calendars.Length > 0)
  79. {
  80. calendars[0] = CalendarId.GREGORIAN;
  81. count = 1;
  82. }
  83. return count;
  84. }
  85. private static bool SystemSupportsTaiwaneseCalendar()
  86. {
  87. return true;
  88. }
  89. // PAL Layer ends here
  90. private static unsafe bool GetCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType dataType, out string calendarString)
  91. {
  92. Debug.Assert(!GlobalizationMode.Invariant);
  93. return Interop.CallStringMethod(
  94. (buffer, locale, id, type) =>
  95. {
  96. fixed (char* bufferPtr = buffer)
  97. {
  98. return Interop.Globalization.GetCalendarInfo(locale, id, type, bufferPtr, buffer.Length);
  99. }
  100. },
  101. localeName,
  102. calendarId,
  103. dataType,
  104. out calendarString);
  105. }
  106. private static bool EnumDatePatterns(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[] datePatterns)
  107. {
  108. datePatterns = null;
  109. EnumCalendarsData callbackContext = new EnumCalendarsData();
  110. callbackContext.Results = new List<string>();
  111. callbackContext.DisallowDuplicates = true;
  112. bool result = EnumCalendarInfo(localeName, calendarId, dataType, ref callbackContext);
  113. if (result)
  114. {
  115. List<string> datePatternsList = callbackContext.Results;
  116. for (int i = 0; i < datePatternsList.Count; i++)
  117. {
  118. datePatternsList[i] = NormalizeDatePattern(datePatternsList[i]);
  119. }
  120. if (dataType == CalendarDataType.ShortDates)
  121. FixDefaultShortDatePattern(datePatternsList);
  122. datePatterns = datePatternsList.ToArray();
  123. }
  124. return result;
  125. }
  126. // FixDefaultShortDatePattern will convert the default short date pattern from using 'yy' to using 'yyyy'
  127. // And will ensure the original pattern still exist in the list.
  128. // doing that will have the short date pattern format the year as 4-digit number and not just 2-digit number.
  129. // Example: June 5, 2018 will be formatted to something like 6/5/2018 instead of 6/5/18 fro en-US culture.
  130. private static void FixDefaultShortDatePattern(List<string> shortDatePatterns)
  131. {
  132. if (shortDatePatterns.Count == 0)
  133. return;
  134. string s = shortDatePatterns[0];
  135. // We are not expecting any pattern have length more than 100.
  136. // We have to do this check to prevent stack overflow as we allocate the buffer on the stack.
  137. if (s.Length > 100)
  138. return;
  139. Span<char> modifiedPattern = stackalloc char[s.Length + 2];
  140. int index = 0;
  141. while (index < s.Length)
  142. {
  143. if (s[index] == '\'')
  144. {
  145. do
  146. {
  147. modifiedPattern[index] = s[index];
  148. index++;
  149. } while (index < s.Length && s[index] != '\'');
  150. if (index >= s.Length)
  151. return;
  152. }
  153. else if (s[index] == 'y')
  154. {
  155. modifiedPattern[index] = 'y';
  156. break;
  157. }
  158. modifiedPattern[index] = s[index];
  159. index++;
  160. }
  161. if (index >= s.Length - 1 || s[index + 1] != 'y')
  162. {
  163. // not a 'yy' pattern
  164. return;
  165. }
  166. if (index + 2 < s.Length && s[index + 2] == 'y')
  167. {
  168. // we have 'yyy' then nothing to do
  169. return;
  170. }
  171. // we are sure now we have 'yy' pattern
  172. Debug.Assert(index + 3 < modifiedPattern.Length);
  173. modifiedPattern[index + 1] = 'y'; // second y
  174. modifiedPattern[index + 2] = 'y'; // third y
  175. modifiedPattern[index + 3] = 'y'; // fourth y
  176. index += 2;
  177. // Now, copy the rest of the pattern to the destination buffer
  178. while (index < s.Length)
  179. {
  180. modifiedPattern[index + 2] = s[index];
  181. index++;
  182. }
  183. shortDatePatterns[0] = modifiedPattern.ToString();
  184. for (int i = 1; i < shortDatePatterns.Count; i++)
  185. {
  186. if (shortDatePatterns[i] == shortDatePatterns[0])
  187. {
  188. // Found match in the list to the new constructed pattern, then replace it with the original modified pattern
  189. shortDatePatterns[i] = s;
  190. return;
  191. }
  192. }
  193. // if we come here means the newly constructed pattern not found on the list, then add the original pattern
  194. shortDatePatterns.Add(s);
  195. }
  196. /// <summary>
  197. /// The ICU date format characters are not exactly the same as the .NET date format characters.
  198. /// NormalizeDatePattern will take in an ICU date pattern and return the equivalent .NET date pattern.
  199. /// </summary>
  200. /// <remarks>
  201. /// see Date Field Symbol Table in http://userguide.icu-project.org/formatparse/datetime
  202. /// and https://msdn.microsoft.com/en-us/library/8kb3ddd4(v=vs.110).aspx
  203. /// </remarks>
  204. private static string NormalizeDatePattern(string input)
  205. {
  206. StringBuilder destination = StringBuilderCache.Acquire(input.Length);
  207. int index = 0;
  208. while (index < input.Length)
  209. {
  210. switch (input[index])
  211. {
  212. case '\'':
  213. // single quotes escape characters, like 'de' in es-SP
  214. // so read verbatim until the next single quote
  215. destination.Append(input[index++]);
  216. while (index < input.Length)
  217. {
  218. char current = input[index++];
  219. destination.Append(current);
  220. if (current == '\'')
  221. {
  222. break;
  223. }
  224. }
  225. break;
  226. case 'E':
  227. case 'e':
  228. case 'c':
  229. // 'E' in ICU is the day of the week, which maps to 3 or 4 'd's in .NET
  230. // 'e' in ICU is the local day of the week, which has no representation in .NET, but
  231. // maps closest to 3 or 4 'd's in .NET
  232. // 'c' in ICU is the stand-alone day of the week, which has no representation in .NET, but
  233. // maps closest to 3 or 4 'd's in .NET
  234. NormalizeDayOfWeek(input, destination, ref index);
  235. break;
  236. case 'L':
  237. case 'M':
  238. // 'L' in ICU is the stand-alone name of the month,
  239. // which maps closest to 'M' in .NET since it doesn't support stand-alone month names in patterns
  240. // 'M' in both ICU and .NET is the month,
  241. // but ICU supports 5 'M's, which is the super short month name
  242. int occurrences = CountOccurrences(input, input[index], ref index);
  243. if (occurrences > 4)
  244. {
  245. // 5 'L's or 'M's in ICU is the super short name, which maps closest to MMM in .NET
  246. occurrences = 3;
  247. }
  248. destination.Append('M', occurrences);
  249. break;
  250. case 'G':
  251. // 'G' in ICU is the era, which maps to 'g' in .NET
  252. occurrences = CountOccurrences(input, 'G', ref index);
  253. // it doesn't matter how many 'G's, since .NET only supports 'g' or 'gg', and they
  254. // have the same meaning
  255. destination.Append('g');
  256. break;
  257. case 'y':
  258. // a single 'y' in ICU is the year with no padding or trimming.
  259. // a single 'y' in .NET is the year with 1 or 2 digits
  260. // so convert any single 'y' to 'yyyy'
  261. occurrences = CountOccurrences(input, 'y', ref index);
  262. if (occurrences == 1)
  263. {
  264. occurrences = 4;
  265. }
  266. destination.Append('y', occurrences);
  267. break;
  268. default:
  269. const string unsupportedDateFieldSymbols = "YuUrQqwWDFg";
  270. Debug.Assert(!unsupportedDateFieldSymbols.Contains(input[index]),
  271. string.Format(CultureInfo.InvariantCulture,
  272. "Encountered an unexpected date field symbol '{0}' from ICU which has no known corresponding .NET equivalent.",
  273. input[index]));
  274. destination.Append(input[index++]);
  275. break;
  276. }
  277. }
  278. return StringBuilderCache.GetStringAndRelease(destination);
  279. }
  280. private static void NormalizeDayOfWeek(string input, StringBuilder destination, ref int index)
  281. {
  282. char dayChar = input[index];
  283. int occurrences = CountOccurrences(input, dayChar, ref index);
  284. occurrences = Math.Max(occurrences, 3);
  285. if (occurrences > 4)
  286. {
  287. // 5 and 6 E/e/c characters in ICU is the super short names, which maps closest to ddd in .NET
  288. occurrences = 3;
  289. }
  290. destination.Append('d', occurrences);
  291. }
  292. private static int CountOccurrences(string input, char value, ref int index)
  293. {
  294. int startIndex = index;
  295. while (index < input.Length && input[index] == value)
  296. {
  297. index++;
  298. }
  299. return index - startIndex;
  300. }
  301. private static bool EnumMonthNames(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[] monthNames, ref string leapHebrewMonthName)
  302. {
  303. monthNames = null;
  304. EnumCalendarsData callbackContext = new EnumCalendarsData();
  305. callbackContext.Results = new List<string>();
  306. bool result = EnumCalendarInfo(localeName, calendarId, dataType, ref callbackContext);
  307. if (result)
  308. {
  309. // the month-name arrays are expected to have 13 elements. If ICU only returns 12, add an
  310. // extra empty string to fill the array.
  311. if (callbackContext.Results.Count == 12)
  312. {
  313. callbackContext.Results.Add(string.Empty);
  314. }
  315. if (callbackContext.Results.Count > 13)
  316. {
  317. Debug.Assert(calendarId == CalendarId.HEBREW && callbackContext.Results.Count == 14);
  318. if (calendarId == CalendarId.HEBREW)
  319. {
  320. leapHebrewMonthName = callbackContext.Results[13];
  321. }
  322. callbackContext.Results.RemoveRange(13, callbackContext.Results.Count - 13);
  323. }
  324. monthNames = callbackContext.Results.ToArray();
  325. }
  326. return result;
  327. }
  328. private static bool EnumEraNames(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[] eraNames)
  329. {
  330. bool result = EnumCalendarInfo(localeName, calendarId, dataType, out eraNames);
  331. // .NET expects that only the Japanese calendars have more than 1 era.
  332. // So for other calendars, only return the latest era.
  333. if (calendarId != CalendarId.JAPAN && calendarId != CalendarId.JAPANESELUNISOLAR && eraNames.Length > 0)
  334. {
  335. string[] latestEraName = new string[] { eraNames[eraNames.Length - 1] };
  336. eraNames = latestEraName;
  337. }
  338. return result;
  339. }
  340. internal static bool EnumCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[] calendarData)
  341. {
  342. calendarData = null;
  343. EnumCalendarsData callbackContext = new EnumCalendarsData();
  344. callbackContext.Results = new List<string>();
  345. bool result = EnumCalendarInfo(localeName, calendarId, dataType, ref callbackContext);
  346. if (result)
  347. {
  348. calendarData = callbackContext.Results.ToArray();
  349. }
  350. return result;
  351. }
  352. private static unsafe bool EnumCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType dataType, ref EnumCalendarsData callbackContext)
  353. {
  354. return Interop.Globalization.EnumCalendarInfo(EnumCalendarInfoCallback, localeName, calendarId, dataType, (IntPtr)Unsafe.AsPointer(ref callbackContext));
  355. }
  356. private static unsafe void EnumCalendarInfoCallback(string calendarString, IntPtr context)
  357. {
  358. try
  359. {
  360. ref EnumCalendarsData callbackContext = ref Unsafe.As<byte, EnumCalendarsData>(ref *(byte*)context);
  361. if (callbackContext.DisallowDuplicates)
  362. {
  363. foreach (string existingResult in callbackContext.Results)
  364. {
  365. if (string.Equals(calendarString, existingResult, StringComparison.Ordinal))
  366. {
  367. // the value is already in the results, so don't add it again
  368. return;
  369. }
  370. }
  371. }
  372. callbackContext.Results.Add(calendarString);
  373. }
  374. catch (Exception e)
  375. {
  376. Debug.Fail(e.ToString());
  377. // we ignore the managed exceptions here because EnumCalendarInfoCallback will get called from the native code.
  378. // If we don't ignore the exception here that can cause the runtime to fail fast.
  379. }
  380. }
  381. private struct EnumCalendarsData
  382. {
  383. public List<string> Results;
  384. public bool DisallowDuplicates;
  385. }
  386. }
  387. }