CalendarData.Unix.cs 19 KB

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