Add and apply nullable attributes (dotnet/coreclr#24679)
[mono-project.git] / netcore / System.Private.CoreLib / shared / System / Globalization / CalendarData.Unix.cs
blobc1495706caef439c1f344386d8c4aabfe898a4cf
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.
5 using System.Collections.Generic;
6 using System.Diagnostics;
7 using System.Security;
8 using System.Text;
9 using Internal.Runtime.CompilerServices;
11 namespace System.Globalization
13 // needs to be kept in sync with CalendarDataType in System.Globalization.Native
14 internal enum CalendarDataType
16 Uninitialized = 0,
17 NativeName = 1,
18 MonthDay = 2,
19 ShortDates = 3,
20 LongDates = 4,
21 YearMonths = 5,
22 DayNames = 6,
23 AbbrevDayNames = 7,
24 MonthNames = 8,
25 AbbrevMonthNames = 9,
26 SuperShortDayNames = 10,
27 MonthGenitiveNames = 11,
28 AbbrevMonthGenitiveNames = 12,
29 EraNames = 13,
30 AbbrevEraNames = 14,
33 internal partial class CalendarData
35 private bool LoadCalendarDataFromSystem(string localeName, CalendarId calendarId)
37 bool result = true;
39 // these can return null but are later replaced with String.Empty or other non-nullable value
40 result &= GetCalendarInfo(localeName, calendarId, CalendarDataType.NativeName, out this.sNativeName!);
41 result &= GetCalendarInfo(localeName, calendarId, CalendarDataType.MonthDay, out this.sMonthDay!);
43 if (this.sMonthDay != null)
45 this.sMonthDay = NormalizeDatePattern(this.sMonthDay);
48 result &= EnumDatePatterns(localeName, calendarId, CalendarDataType.ShortDates, out this.saShortDates!);
49 result &= EnumDatePatterns(localeName, calendarId, CalendarDataType.LongDates, out this.saLongDates!);
50 result &= EnumDatePatterns(localeName, calendarId, CalendarDataType.YearMonths, out this.saYearMonths!);
51 result &= EnumCalendarInfo(localeName, calendarId, CalendarDataType.DayNames, out this.saDayNames!);
52 result &= EnumCalendarInfo(localeName, calendarId, CalendarDataType.AbbrevDayNames, out this.saAbbrevDayNames!);
53 result &= EnumCalendarInfo(localeName, calendarId, CalendarDataType.SuperShortDayNames, out this.saSuperShortDayNames!);
55 string? leapHebrewMonthName = null;
56 result &= EnumMonthNames(localeName, calendarId, CalendarDataType.MonthNames, out this.saMonthNames!, ref leapHebrewMonthName);
57 if (leapHebrewMonthName != null)
59 Debug.Assert(this.saMonthNames != null);
61 // In Hebrew calendar, get the leap month name Adar II and override the non-leap month 7
62 Debug.Assert(calendarId == CalendarId.HEBREW && saMonthNames.Length == 13);
63 saLeapYearMonthNames = (string[]) saMonthNames.Clone();
64 saLeapYearMonthNames[6] = leapHebrewMonthName;
66 // The returned data from ICU has 6th month name as 'Adar I' and 7th month name as 'Adar'
67 // 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'
68 // note that when formatting non-leap year dates, 7th month shouldn't get used at all.
69 saMonthNames[5] = saMonthNames[6];
70 saMonthNames[6] = leapHebrewMonthName;
73 result &= EnumMonthNames(localeName, calendarId, CalendarDataType.AbbrevMonthNames, out this.saAbbrevMonthNames!, ref leapHebrewMonthName);
74 result &= EnumMonthNames(localeName, calendarId, CalendarDataType.MonthGenitiveNames, out this.saMonthGenitiveNames!, ref leapHebrewMonthName);
75 result &= EnumMonthNames(localeName, calendarId, CalendarDataType.AbbrevMonthGenitiveNames, out this.saAbbrevMonthGenitiveNames!, ref leapHebrewMonthName);
77 result &= EnumEraNames(localeName, calendarId, CalendarDataType.EraNames, out this.saEraNames!);
78 result &= EnumEraNames(localeName, calendarId, CalendarDataType.AbbrevEraNames, out this.saAbbrevEraNames!);
80 return result;
83 internal static int GetTwoDigitYearMax(CalendarId calendarId)
85 // There is no user override for this value on Linux or in ICU.
86 // So just return -1 to use the hard-coded defaults.
87 return -1;
90 // Call native side to figure out which calendars are allowed
91 internal static int GetCalendars(string localeName, bool useUserOverride, CalendarId[] calendars)
93 Debug.Assert(!GlobalizationMode.Invariant);
95 // NOTE: there are no 'user overrides' on Linux
96 int count = Interop.Globalization.GetCalendars(localeName, calendars, calendars.Length);
98 // ensure there is at least 1 calendar returned
99 if (count == 0 && calendars.Length > 0)
101 calendars[0] = CalendarId.GREGORIAN;
102 count = 1;
105 return count;
108 private static bool SystemSupportsTaiwaneseCalendar()
110 return true;
113 // PAL Layer ends here
115 private static unsafe bool GetCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType dataType, out string? calendarString)
117 Debug.Assert(!GlobalizationMode.Invariant);
119 return Interop.CallStringMethod(
120 (buffer, locale, id, type) =>
122 fixed (char* bufferPtr = buffer)
124 return Interop.Globalization.GetCalendarInfo(locale, id, type, bufferPtr, buffer.Length);
127 localeName,
128 calendarId,
129 dataType,
130 out calendarString);
133 private static bool EnumDatePatterns(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[]? datePatterns)
135 datePatterns = null;
137 EnumCalendarsData callbackContext = new EnumCalendarsData();
138 callbackContext.Results = new List<string>();
139 callbackContext.DisallowDuplicates = true;
140 bool result = EnumCalendarInfo(localeName, calendarId, dataType, ref callbackContext);
141 if (result)
143 List<string> datePatternsList = callbackContext.Results;
145 for (int i = 0; i < datePatternsList.Count; i++)
147 datePatternsList[i] = NormalizeDatePattern(datePatternsList[i]);
150 if (dataType == CalendarDataType.ShortDates)
151 FixDefaultShortDatePattern(datePatternsList);
153 datePatterns = datePatternsList.ToArray();
156 return result;
159 // FixDefaultShortDatePattern will convert the default short date pattern from using 'yy' to using 'yyyy'
160 // And will ensure the original pattern still exist in the list.
161 // doing that will have the short date pattern format the year as 4-digit number and not just 2-digit number.
162 // Example: June 5, 2018 will be formatted to something like 6/5/2018 instead of 6/5/18 fro en-US culture.
163 private static void FixDefaultShortDatePattern(List<string> shortDatePatterns)
165 if (shortDatePatterns.Count == 0)
166 return;
168 string s = shortDatePatterns[0];
170 // We are not expecting any pattern have length more than 100.
171 // We have to do this check to prevent stack overflow as we allocate the buffer on the stack.
172 if (s.Length > 100)
173 return;
175 Span<char> modifiedPattern = stackalloc char[s.Length + 2];
176 int index = 0;
178 while (index < s.Length)
180 if (s[index] == '\'')
184 modifiedPattern[index] = s[index];
185 index++;
186 } while (index < s.Length && s[index] != '\'');
188 if (index >= s.Length)
189 return;
191 else if (s[index] == 'y')
193 modifiedPattern[index] = 'y';
194 break;
197 modifiedPattern[index] = s[index];
198 index++;
201 if (index >= s.Length - 1 || s[index + 1] != 'y')
203 // not a 'yy' pattern
204 return;
207 if (index + 2 < s.Length && s[index + 2] == 'y')
209 // we have 'yyy' then nothing to do
210 return;
213 // we are sure now we have 'yy' pattern
215 Debug.Assert(index + 3 < modifiedPattern.Length);
217 modifiedPattern[index + 1] = 'y'; // second y
218 modifiedPattern[index + 2] = 'y'; // third y
219 modifiedPattern[index + 3] = 'y'; // fourth y
221 index += 2;
223 // Now, copy the rest of the pattern to the destination buffer
224 while (index < s.Length)
226 modifiedPattern[index + 2] = s[index];
227 index++;
230 shortDatePatterns[0] = modifiedPattern.ToString();
232 for (int i = 1; i < shortDatePatterns.Count; i++)
234 if (shortDatePatterns[i] == shortDatePatterns[0])
236 // Found match in the list to the new constructed pattern, then replace it with the original modified pattern
237 shortDatePatterns[i] = s;
238 return;
242 // if we come here means the newly constructed pattern not found on the list, then add the original pattern
243 shortDatePatterns.Add(s);
246 /// <summary>
247 /// The ICU date format characters are not exactly the same as the .NET date format characters.
248 /// NormalizeDatePattern will take in an ICU date pattern and return the equivalent .NET date pattern.
249 /// </summary>
250 /// <remarks>
251 /// see Date Field Symbol Table in http://userguide.icu-project.org/formatparse/datetime
252 /// and https://msdn.microsoft.com/en-us/library/8kb3ddd4(v=vs.110).aspx
253 /// </remarks>
254 private static string NormalizeDatePattern(string input)
256 StringBuilder destination = StringBuilderCache.Acquire(input.Length);
258 int index = 0;
259 while (index < input.Length)
261 switch (input[index])
263 case '\'':
264 // single quotes escape characters, like 'de' in es-SP
265 // so read verbatim until the next single quote
266 destination.Append(input[index++]);
267 while (index < input.Length)
269 char current = input[index++];
270 destination.Append(current);
271 if (current == '\'')
273 break;
276 break;
277 case 'E':
278 case 'e':
279 case 'c':
280 // 'E' in ICU is the day of the week, which maps to 3 or 4 'd's in .NET
281 // 'e' in ICU is the local day of the week, which has no representation in .NET, but
282 // maps closest to 3 or 4 'd's in .NET
283 // 'c' in ICU is the stand-alone day of the week, which has no representation in .NET, but
284 // maps closest to 3 or 4 'd's in .NET
285 NormalizeDayOfWeek(input, destination, ref index);
286 break;
287 case 'L':
288 case 'M':
289 // 'L' in ICU is the stand-alone name of the month,
290 // which maps closest to 'M' in .NET since it doesn't support stand-alone month names in patterns
291 // 'M' in both ICU and .NET is the month,
292 // but ICU supports 5 'M's, which is the super short month name
293 int occurrences = CountOccurrences(input, input[index], ref index);
294 if (occurrences > 4)
296 // 5 'L's or 'M's in ICU is the super short name, which maps closest to MMM in .NET
297 occurrences = 3;
299 destination.Append('M', occurrences);
300 break;
301 case 'G':
302 // 'G' in ICU is the era, which maps to 'g' in .NET
303 occurrences = CountOccurrences(input, 'G', ref index);
305 // it doesn't matter how many 'G's, since .NET only supports 'g' or 'gg', and they
306 // have the same meaning
307 destination.Append('g');
308 break;
309 case 'y':
310 // a single 'y' in ICU is the year with no padding or trimming.
311 // a single 'y' in .NET is the year with 1 or 2 digits
312 // so convert any single 'y' to 'yyyy'
313 occurrences = CountOccurrences(input, 'y', ref index);
314 if (occurrences == 1)
316 occurrences = 4;
318 destination.Append('y', occurrences);
319 break;
320 default:
321 const string unsupportedDateFieldSymbols = "YuUrQqwWDFg";
322 Debug.Assert(!unsupportedDateFieldSymbols.Contains(input[index]),
323 $"Encountered an unexpected date field symbol '{input[index]}' from ICU which has no known corresponding .NET equivalent.");
325 destination.Append(input[index++]);
326 break;
330 return StringBuilderCache.GetStringAndRelease(destination);
333 private static void NormalizeDayOfWeek(string input, StringBuilder destination, ref int index)
335 char dayChar = input[index];
336 int occurrences = CountOccurrences(input, dayChar, ref index);
337 occurrences = Math.Max(occurrences, 3);
338 if (occurrences > 4)
340 // 5 and 6 E/e/c characters in ICU is the super short names, which maps closest to ddd in .NET
341 occurrences = 3;
344 destination.Append('d', occurrences);
347 private static int CountOccurrences(string input, char value, ref int index)
349 int startIndex = index;
350 while (index < input.Length && input[index] == value)
352 index++;
355 return index - startIndex;
358 private static bool EnumMonthNames(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[]? monthNames, ref string? leapHebrewMonthName)
360 monthNames = null;
362 EnumCalendarsData callbackContext = new EnumCalendarsData();
363 callbackContext.Results = new List<string>();
364 bool result = EnumCalendarInfo(localeName, calendarId, dataType, ref callbackContext);
365 if (result)
367 // the month-name arrays are expected to have 13 elements. If ICU only returns 12, add an
368 // extra empty string to fill the array.
369 if (callbackContext.Results.Count == 12)
371 callbackContext.Results.Add(string.Empty);
374 if (callbackContext.Results.Count > 13)
376 Debug.Assert(calendarId == CalendarId.HEBREW && callbackContext.Results.Count == 14);
378 if (calendarId == CalendarId.HEBREW)
380 leapHebrewMonthName = callbackContext.Results[13];
382 callbackContext.Results.RemoveRange(13, callbackContext.Results.Count - 13);
385 monthNames = callbackContext.Results.ToArray();
388 return result;
391 private static bool EnumEraNames(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[]? eraNames)
393 bool result = EnumCalendarInfo(localeName, calendarId, dataType, out eraNames);
395 // .NET expects that only the Japanese calendars have more than 1 era.
396 // So for other calendars, only return the latest era.
397 if (calendarId != CalendarId.JAPAN && calendarId != CalendarId.JAPANESELUNISOLAR && eraNames?.Length > 0)
399 string[] latestEraName = new string[] { eraNames![eraNames.Length - 1] };
400 eraNames = latestEraName;
403 return result;
406 internal static bool EnumCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[]? calendarData)
408 calendarData = null;
410 EnumCalendarsData callbackContext = new EnumCalendarsData();
411 callbackContext.Results = new List<string>();
412 bool result = EnumCalendarInfo(localeName, calendarId, dataType, ref callbackContext);
413 if (result)
415 calendarData = callbackContext.Results.ToArray();
418 return result;
421 private static unsafe bool EnumCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType dataType, ref EnumCalendarsData callbackContext)
423 return Interop.Globalization.EnumCalendarInfo(EnumCalendarInfoCallback, localeName, calendarId, dataType, (IntPtr)Unsafe.AsPointer(ref callbackContext));
426 private static unsafe void EnumCalendarInfoCallback(string calendarString, IntPtr context)
430 ref EnumCalendarsData callbackContext = ref Unsafe.As<byte, EnumCalendarsData>(ref *(byte*)context);
432 if (callbackContext.DisallowDuplicates)
434 foreach (string existingResult in callbackContext.Results)
436 if (string.Equals(calendarString, existingResult, StringComparison.Ordinal))
438 // the value is already in the results, so don't add it again
439 return;
444 callbackContext.Results.Add(calendarString);
446 catch (Exception e)
448 Debug.Fail(e.ToString());
449 // we ignore the managed exceptions here because EnumCalendarInfoCallback will get called from the native code.
450 // If we don't ignore the exception here that can cause the runtime to fail fast.
454 private struct EnumCalendarsData
456 public List<string> Results;
457 public bool DisallowDuplicates;