File: src\libraries\System.Private.CoreLib\src\System\Globalization\CalendarData.Icu.cs
Web Access
Project: src\src\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj (System.Private.CoreLib)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
 
namespace System.Globalization
{
    // needs to be kept in sync with CalendarDataType in System.Globalization.Native
    internal enum CalendarDataType
    {
        Uninitialized = 0,
        NativeName = 1,
        MonthDay = 2,
        ShortDates = 3,
        LongDates = 4,
        YearMonths = 5,
        DayNames = 6,
        AbbrevDayNames = 7,
        MonthNames = 8,
        AbbrevMonthNames = 9,
        SuperShortDayNames = 10,
        MonthGenitiveNames = 11,
        AbbrevMonthGenitiveNames = 12,
        EraNames = 13,
        AbbrevEraNames = 14,
    }
 
    internal sealed partial class CalendarData
    {
        private bool IcuLoadCalendarDataFromSystem(string localeName, CalendarId calendarId)
        {
            // ToDo: think if not to convert this function with multiple calls to JS into one call with multiple data requested at once
            Debug.Assert(!GlobalizationMode.UseNls);
 
            bool result = true;
 
            // these can return null but are later replaced with String.Empty or other non-nullable value
            result &= GetCalendarInfo(localeName, calendarId, CalendarDataType.NativeName, out this.sNativeName!);
            result &= GetCalendarInfo(localeName, calendarId, CalendarDataType.MonthDay, out this.sMonthDay!);
 
            if (this.sMonthDay != null)
            {
                this.sMonthDay = NormalizeDatePattern(this.sMonthDay);
            }
 
            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!);
 
            string? leapHebrewMonthName = null;
            result &= EnumMonthNames(localeName, calendarId, CalendarDataType.MonthNames, out this.saMonthNames!, ref leapHebrewMonthName);
            if (leapHebrewMonthName != null)
            {
                Debug.Assert(this.saMonthNames != null);
 
                // In Hebrew calendar, get the leap month name Adar II and override the non-leap month 7
                Debug.Assert(calendarId == CalendarId.HEBREW && saMonthNames.Length == 13);
                saLeapYearMonthNames = (string[])saMonthNames.Clone();
                saLeapYearMonthNames[6] = leapHebrewMonthName;
 
                // The returned data from ICU has 6th month name as 'Adar I' and 7th month name as 'Adar'
                // 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'
                // note that when formatting non-leap year dates, 7th month shouldn't get used at all.
                saMonthNames[5] = saMonthNames[6];
                saMonthNames[6] = leapHebrewMonthName;
 
            }
            result &= EnumMonthNames(localeName, calendarId, CalendarDataType.AbbrevMonthNames, out this.saAbbrevMonthNames!, ref leapHebrewMonthName);
            result &= EnumMonthNames(localeName, calendarId, CalendarDataType.MonthGenitiveNames, out this.saMonthGenitiveNames!, ref leapHebrewMonthName);
            result &= EnumMonthNames(localeName, calendarId, CalendarDataType.AbbrevMonthGenitiveNames, out this.saAbbrevMonthGenitiveNames!, ref leapHebrewMonthName);
 
            result &= EnumEraNames(localeName, calendarId, CalendarDataType.EraNames, out this.saEraNames!);
            result &= EnumEraNames(localeName, calendarId, CalendarDataType.AbbrevEraNames, out this.saAbbrevEraNames!);
 
            return result;
        }
 
        // Call native side to figure out which calendars are allowed
        internal static int IcuGetCalendars(string localeName, CalendarId[] calendars)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
            Debug.Assert(!GlobalizationMode.UseNls);
 
            // NOTE: there are no 'user overrides' on Linux
            int count;
#if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS
            if (GlobalizationMode.Hybrid)
                count = Interop.Globalization.GetCalendarsNative(localeName, calendars, calendars.Length);
            else
                count = Interop.Globalization.GetCalendars(localeName, calendars, calendars.Length);
#else
            count = Interop.Globalization.GetCalendars(localeName, calendars, calendars.Length);
#endif
 
            // ensure there is at least 1 calendar returned
            if (count == 0 && calendars.Length > 0)
            {
                calendars[0] = CalendarId.GREGORIAN;
                count = 1;
            }
 
            return count;
        }
 
        private static bool IcuSystemSupportsTaiwaneseCalendar()
        {
            Debug.Assert(!GlobalizationMode.UseNls);
            return true;
        }
 
        // PAL Layer ends here
 
        private static unsafe bool GetCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType dataType, out string? calendarString)
        {
            Debug.Assert(!GlobalizationMode.Invariant);
 
            return Interop.CallStringMethod(
                static (buffer, locale, id, type) =>
                {
                    fixed (char* bufferPtr = buffer)
                    {
                        return Interop.Globalization.GetCalendarInfo(locale, id, type, bufferPtr, buffer.Length);
                    }
                },
                localeName,
                calendarId,
                dataType,
                out calendarString);
        }
 
        private static unsafe bool EnumDatePatterns(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[]? datePatterns)
        {
            datePatterns = null;
 
            IcuEnumCalendarsData callbackContext = default;
            callbackContext.Results = new List<string>();
            callbackContext.DisallowDuplicates = true;
#pragma warning disable CS8500 // takes address of managed type
            bool result = EnumCalendarInfo(localeName, calendarId, dataType, &callbackContext);
#pragma warning restore CS8500
            if (result)
            {
                List<string> datePatternsList = callbackContext.Results;
 
                for (int i = 0; i < datePatternsList.Count; i++)
                {
                    datePatternsList[i] = NormalizeDatePattern(datePatternsList[i]);
                }
 
                if (dataType == CalendarDataType.ShortDates)
                    FixDefaultShortDatePattern(datePatternsList);
 
                datePatterns = datePatternsList.ToArray();
            }
 
            return result;
        }
 
        // FixDefaultShortDatePattern will convert the default short date pattern from using 'yy' to using 'yyyy'
        // And will ensure the original pattern still exist in the list.
        // doing that will have the short date pattern format the year as 4-digit number and not just 2-digit number.
        // Example: June 5, 2018 will be formatted to something like 6/5/2018 instead of 6/5/18 fro en-US culture.
        private static void FixDefaultShortDatePattern(List<string> shortDatePatterns)
        {
            if (shortDatePatterns.Count == 0)
                return;
 
            string s = shortDatePatterns[0];
 
            // We are not expecting any pattern have length more than 100.
            // We have to do this check to prevent stack overflow as we allocate the buffer on the stack.
            if (s.Length > 100)
                return;
 
            Span<char> modifiedPattern = stackalloc char[s.Length + 2];
            int index = 0;
 
            while (index < s.Length)
            {
                if (s[index] == '\'')
                {
                    do
                    {
                        modifiedPattern[index] = s[index];
                        index++;
                    } while (index < s.Length && s[index] != '\'');
 
                    if (index >= s.Length)
                        return;
                }
                else if (s[index] == 'y')
                {
                    modifiedPattern[index] = 'y';
                    break;
                }
 
                modifiedPattern[index] = s[index];
                index++;
            }
 
            if (index >= s.Length - 1 || s[index + 1] != 'y')
            {
                // not a 'yy' pattern
                return;
            }
 
            if (index + 2 < s.Length && s[index + 2] == 'y')
            {
                // we have 'yyy' then nothing to do
                return;
            }
 
            // we are sure now we have 'yy' pattern
 
            Debug.Assert(index + 3 < modifiedPattern.Length);
 
            modifiedPattern[index + 1] = 'y'; // second y
            modifiedPattern[index + 2] = 'y'; // third y
            modifiedPattern[index + 3] = 'y'; // fourth y
 
            index += 2;
 
            // Now, copy the rest of the pattern to the destination buffer
            while (index < s.Length)
            {
                modifiedPattern[index + 2] = s[index];
                index++;
            }
 
            shortDatePatterns[0] = modifiedPattern.ToString();
 
            for (int i = 1; i < shortDatePatterns.Count; i++)
            {
                if (shortDatePatterns[i] == shortDatePatterns[0])
                {
                    // Found match in the list to the new constructed pattern, then replace it with the original modified pattern
                    shortDatePatterns[i] = s;
                    return;
                }
            }
 
            // if we come here means the newly constructed pattern not found on the list, then add the original pattern
            shortDatePatterns.Add(s);
        }
 
        /// <summary>
        /// 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.
        /// </summary>
        /// <remarks>
        /// 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
        /// </remarks>
        private static string NormalizeDatePattern(string input)
        {
            var destination = input.Length < 128 ?
                new ValueStringBuilder(stackalloc char[128]) :
                new ValueStringBuilder(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, ref 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
                        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";
                        Debug.Assert(!unsupportedDateFieldSymbols.Contains(input[index]),
                            $"Encountered an unexpected date field symbol '{input[index]}' from ICU which has no known corresponding .NET equivalent.");
 
                        destination.Append(input[index++]);
                        break;
                }
            }
 
            return destination.ToString();
        }
 
        private static void NormalizeDayOfWeek(string input, ref ValueStringBuilder 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 unsafe bool EnumMonthNames(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[]? monthNames, ref string? leapHebrewMonthName)
        {
            monthNames = null;
 
            IcuEnumCalendarsData callbackContext = default;
            callbackContext.Results = new List<string>();
#pragma warning disable CS8500 // takes address of managed type
            bool result = EnumCalendarInfo(localeName, calendarId, dataType, &callbackContext);
#pragma warning restore CS8500
            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 (callbackContext.Results.Count == 12)
                {
                    callbackContext.Results.Add(string.Empty);
                }
 
                if (callbackContext.Results.Count > 13)
                {
                    Debug.Assert(calendarId == CalendarId.HEBREW && callbackContext.Results.Count == 14);
 
                    if (calendarId == CalendarId.HEBREW)
                    {
                        leapHebrewMonthName = callbackContext.Results[13];
                    }
                    callbackContext.Results.RemoveRange(13, callbackContext.Results.Count - 13);
                }
 
                monthNames = callbackContext.Results.ToArray();
            }
 
            return result;
        }
 
        private static bool EnumEraNames(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[]? eraNames)
        {
            bool result = EnumCalendarInfo(localeName, calendarId, dataType, out eraNames);
 
            // .NET expects that only the Japanese calendars have more than 1 era.
            // So for other calendars, only return the latest era.
            if (calendarId != CalendarId.JAPAN && calendarId != CalendarId.JAPANESELUNISOLAR && eraNames?.Length > 0)
            {
                string[] latestEraName = new string[] { eraNames![eraNames.Length - 1] };
                eraNames = latestEraName;
            }
 
            return result;
        }
 
        internal static unsafe bool EnumCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType dataType, out string[]? calendarData)
        {
            calendarData = null;
 
            IcuEnumCalendarsData callbackContext = default;
            callbackContext.Results = new List<string>();
#pragma warning disable CS8500 // takes address of managed type
            bool result = EnumCalendarInfo(localeName, calendarId, dataType, &callbackContext);
#pragma warning restore CS8500
            if (result)
            {
                calendarData = callbackContext.Results.ToArray();
            }
 
            return result;
        }
 
#pragma warning disable CS8500 // takes address of managed type
        private static unsafe bool EnumCalendarInfo(string localeName, CalendarId calendarId, CalendarDataType dataType, IcuEnumCalendarsData* callbackContext)
        {
            return Interop.Globalization.EnumCalendarInfo(&EnumCalendarInfoCallback, localeName, calendarId, dataType, (IntPtr)callbackContext);
        }
#pragma warning restore CS8500
 
        [UnmanagedCallersOnly]
        private static unsafe void EnumCalendarInfoCallback(char* calendarStringPtr, IntPtr context)
        {
            try
            {
                ReadOnlySpan<char> calendarStringSpan = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(calendarStringPtr);
#pragma warning disable 8500
                IcuEnumCalendarsData* callbackContext = (IcuEnumCalendarsData*)context;
#pragma warning restore 8500
 
                if (callbackContext->DisallowDuplicates)
                {
                    foreach (string existingResult in callbackContext->Results)
                    {
                        if (calendarStringSpan.SequenceEqual(existingResult))
                        {
                            // the value is already in the results, so don't add it again
                            return;
                        }
                    }
                }
 
                callbackContext->Results.Add(calendarStringSpan.ToString());
            }
            catch (Exception e)
            {
                Debug.Fail(e.ToString());
                // we ignore the managed exceptions here because EnumCalendarInfoCallback will get called from the native code.
                // If we don't ignore the exception here that can cause the runtime to fail fast.
            }
        }
 
        private struct IcuEnumCalendarsData
        {
            public List<string> Results;
            public bool DisallowDuplicates;
        }
    }
}