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
;
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
26 SuperShortDayNames
= 10,
27 MonthGenitiveNames
= 11,
28 AbbrevMonthGenitiveNames
= 12,
33 internal partial class CalendarData
35 private bool LoadCalendarDataFromSystem(string localeName
, CalendarId calendarId
)
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
!);
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.
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
;
108 private static bool SystemSupportsTaiwaneseCalendar()
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
);
133 private static bool EnumDatePatterns(string localeName
, CalendarId calendarId
, CalendarDataType dataType
, out string[]? datePatterns
)
137 EnumCalendarsData callbackContext
= new EnumCalendarsData();
138 callbackContext
.Results
= new List
<string>();
139 callbackContext
.DisallowDuplicates
= true;
140 bool result
= EnumCalendarInfo(localeName
, calendarId
, dataType
, ref callbackContext
);
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();
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)
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.
175 Span
<char> modifiedPattern
= stackalloc char[s
.Length
+ 2];
178 while (index
< s
.Length
)
180 if (s
[index
] == '\'')
184 modifiedPattern
[index
] = s
[index
];
186 } while (index
< s
.Length
&& s
[index
] != '\'');
188 if (index
>= s
.Length
)
191 else if (s
[index
] == 'y')
193 modifiedPattern
[index
] = 'y';
197 modifiedPattern
[index
] = s
[index
];
201 if (index
>= s
.Length
- 1 || s
[index
+ 1] != 'y')
203 // not a 'yy' pattern
207 if (index
+ 2 < s
.Length
&& s
[index
+ 2] == 'y')
209 // we have 'yyy' then nothing to do
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
223 // Now, copy the rest of the pattern to the destination buffer
224 while (index
< s
.Length
)
226 modifiedPattern
[index
+ 2] = s
[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
;
242 // if we come here means the newly constructed pattern not found on the list, then add the original pattern
243 shortDatePatterns
.Add(s
);
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.
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
254 private static string NormalizeDatePattern(string input
)
256 StringBuilder destination
= StringBuilderCache
.Acquire(input
.Length
);
259 while (index
< input
.Length
)
261 switch (input
[index
])
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
);
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
);
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
);
296 // 5 'L's or 'M's in ICU is the super short name, which maps closest to MMM in .NET
299 destination
.Append('M', occurrences
);
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');
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)
318 destination
.Append('y', occurrences
);
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
++]);
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);
340 // 5 and 6 E/e/c characters in ICU is the super short names, which maps closest to ddd in .NET
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)
355 return index
- startIndex
;
358 private static bool EnumMonthNames(string localeName
, CalendarId calendarId
, CalendarDataType dataType
, out string[]? monthNames
, ref string? leapHebrewMonthName
)
362 EnumCalendarsData callbackContext
= new EnumCalendarsData();
363 callbackContext
.Results
= new List
<string>();
364 bool result
= EnumCalendarInfo(localeName
, calendarId
, dataType
, ref callbackContext
);
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();
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
;
406 internal static bool EnumCalendarInfo(string localeName
, CalendarId calendarId
, CalendarDataType dataType
, out string[]? calendarData
)
410 EnumCalendarsData callbackContext
= new EnumCalendarsData();
411 callbackContext
.Results
= new List
<string>();
412 bool result
= EnumCalendarInfo(localeName
, calendarId
, dataType
, ref callbackContext
);
415 calendarData
= callbackContext
.Results
.ToArray();
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
444 callbackContext
.Results
.Add(calendarString
);
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
;