diff --git a/docs/design/features/globalization-hybrid-mode.md b/docs/design/features/globalization-hybrid-mode.md index 373804adfd9ff9..7966be7224710b 100644 --- a/docs/design/features/globalization-hybrid-mode.md +++ b/docs/design/features/globalization-hybrid-mode.md @@ -478,3 +478,34 @@ Below function are used from apple native functions: - [uppercaseStringWithLocale](https://developer.apple.com/documentation/foundation/nsstring/1413316-uppercasestringwithlocale?language=objc) - [lowercaseStringWithLocale](https://developer.apple.com/documentation/foundation/nsstring/1417298-lowercasestringwithlocale?language=objc) +## Calandars + +Affected public APIs: +- DateTimeFormatInfo.AbbreviatedDayNames +- DateTimeFormatInfo.GetAbbreviatedDayName() +- DateTimeFormatInfo.AbbreviatedMonthGenitiveNames +- DateTimeFormatInfo.AbbreviatedMonthNames +- DateTimeFormatInfo.GetAbbreviatedMonthName() +- DateTimeFormatInfo.AMDesignator +- DateTimeFormatInfo.CalendarWeekRule +- DateTimeFormatInfo.DayNames +- DateTimeFormatInfo.GetDayName() +- DateTimeFormatInfo.GetEraName() +- DateTimeFormatInfo.FirstDayOfWeek +- DateTimeFormatInfo.FullDateTimePattern +- DateTimeFormatInfo.LongDatePattern +- DateTimeFormatInfo.LongTimePattern +- DateTimeFormatInfo.MonthDayPattern +- DateTimeFormatInfo.MonthGenitiveNames +- DateTimeFormatInfo.MonthNames +- DateTimeFormatInfo.GetMonthName() +- DateTimeFormatInfo.NativeCalendarName +- DateTimeFormatInfo.PMDesignator +- DateTimeFormatInfo.ShortDatePattern +- DateTimeFormatInfo.ShortestDayNames +- DateTimeFormatInfo.GetShortestDayName() +- DateTimeFormatInfo.ShortTimePattern +- DateTimeFormatInfo.YearMonthPattern + +Apple Native API does not have an equivalent for abbreviated era name and will return empty string +- DateTimeFormatInfo.GetAbbreviatedEraName() diff --git a/src/libraries/Common/src/Interop/Interop.Calendar.iOS.cs b/src/libraries/Common/src/Interop/Interop.Calendar.iOS.cs new file mode 100644 index 00000000000000..11a6246a1d700b --- /dev/null +++ b/src/libraries/Common/src/Interop/Interop.Calendar.iOS.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using System.Runtime.InteropServices; + +internal static partial class Interop +{ + internal static partial class Globalization + { + [LibraryImport(Libraries.GlobalizationNative, EntryPoint = "GlobalizationNative_GetCalendarInfoNative", StringMarshalling = StringMarshalling.Utf8)] + internal static partial string GetCalendarInfoNative(string localeName, CalendarId calendarId, CalendarDataType calendarDataType); + } +} diff --git a/src/libraries/System.Globalization.Calendars/tests/Hybrid/System.Globalization.Calendars.IOS.Tests.csproj b/src/libraries/System.Globalization.Calendars/tests/Hybrid/System.Globalization.Calendars.IOS.Tests.csproj new file mode 100644 index 00000000000000..614c0d2a47f65a --- /dev/null +++ b/src/libraries/System.Globalization.Calendars/tests/Hybrid/System.Globalization.Calendars.IOS.Tests.csproj @@ -0,0 +1,108 @@ + + + $(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos;$(NetCoreAppCurrent)-maccatalyst + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/libraries/System.Globalization.Calendars/tests/System/Globalization/CalendarTestBase.cs b/src/libraries/System.Globalization.Calendars/tests/System/Globalization/CalendarTestBase.cs index c99b883aa77280..563af14ef500e1 100644 --- a/src/libraries/System.Globalization.Calendars/tests/System/Globalization/CalendarTestBase.cs +++ b/src/libraries/System.Globalization.Calendars/tests/System/Globalization/CalendarTestBase.cs @@ -430,7 +430,7 @@ public void GetEra_Invalid_ThrowsArgumentOutOfRangeException() Assert.All(DateTime_TestData(calendar), dt => { // JapaneseCalendar throws on ICU, but not on NLS - if ((calendar is JapaneseCalendar && PlatformDetection.IsNlsGlobalization) || + if ((calendar is JapaneseCalendar && (PlatformDetection.IsNlsGlobalization || PlatformDetection.IsHybridGlobalizationOnOSX)) || calendar is HebrewCalendar || calendar is TaiwanLunisolarCalendar || calendar is JapaneseLunisolarCalendar) diff --git a/src/libraries/System.Globalization/tests/Hybrid/System.Globalization.IOS.Tests.csproj b/src/libraries/System.Globalization/tests/Hybrid/System.Globalization.IOS.Tests.csproj index f21ff6dafff2b8..eac50b0d2f3e0f 100644 --- a/src/libraries/System.Globalization/tests/Hybrid/System.Globalization.IOS.Tests.csproj +++ b/src/libraries/System.Globalization/tests/Hybrid/System.Globalization.IOS.Tests.csproj @@ -18,9 +18,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 09f13e17a07291..26d78479c72d8a 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -320,6 +320,7 @@ + @@ -1295,6 +1296,9 @@ Common\Interop\Interop.Calendar.cs + + Common\Interop\Interop.Calendar.iOS.cs + Common\Interop\Interop.Casing.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CalendarData.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CalendarData.Unix.cs index cb15665c979ac6..295a58e9882e17 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CalendarData.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CalendarData.Unix.cs @@ -13,6 +13,10 @@ private bool LoadCalendarDataFromSystemCore(string localeName, CalendarId calend return GlobalizationMode.Hybrid ? JSLoadCalendarDataFromBrowser(localeName, calendarId) : IcuLoadCalendarDataFromSystem(localeName, calendarId); +#elif TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS + return GlobalizationMode.Hybrid ? + LoadCalendarDataFromNative(localeName, calendarId) : + IcuLoadCalendarDataFromSystem(localeName, calendarId); #else return IcuLoadCalendarDataFromSystem(localeName, calendarId); #endif diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CalendarData.iOS.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CalendarData.iOS.cs new file mode 100644 index 00000000000000..46bfb3d481ca2d --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CalendarData.iOS.cs @@ -0,0 +1,108 @@ +// 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 +{ + internal sealed partial class CalendarData + { + private bool LoadCalendarDataFromNative(string localeName, CalendarId calendarId) + { + Debug.Assert(!GlobalizationMode.UseNls); + + sNativeName = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.NativeName); + sMonthDay = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.MonthDay); + saShortDates = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.ShortDates).Split("||"); + saLongDates = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.LongDates).Split("||"); + saYearMonths = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.YearMonths).Split("||"); + saDayNames = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.DayNames).Split("||"); + saAbbrevDayNames = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.AbbrevDayNames).Split("||"); + saSuperShortDayNames = GetCalendarInfoNative(localeName, calendarId, CalendarDataType.SuperShortDayNames).Split("||"); + + string? leapHebrewMonthName = null; + saMonthNames = NormalizeMonthArray(GetCalendarInfoNative(localeName, calendarId, CalendarDataType.MonthNames).Split("||"), calendarId, ref leapHebrewMonthName); + if (leapHebrewMonthName != null) + { + Debug.Assert(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 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; + + } + saAbbrevMonthNames = NormalizeMonthArray(GetCalendarInfoNative(localeName, calendarId, CalendarDataType.AbbrevMonthNames).Split("||"), calendarId, ref leapHebrewMonthName); + saMonthGenitiveNames = NormalizeMonthArray(GetCalendarInfoNative(localeName, calendarId, CalendarDataType.MonthGenitiveNames).Split("||"), calendarId, ref leapHebrewMonthName); + saAbbrevMonthGenitiveNames = NormalizeMonthArray(GetCalendarInfoNative(localeName, calendarId, CalendarDataType.AbbrevMonthGenitiveNames).Split("||"), calendarId, ref leapHebrewMonthName); + + saEraNames = NormalizeEraNames(calendarId, GetCalendarInfoNative(localeName, calendarId, CalendarDataType.EraNames).Split("||")); + saAbbrevEraNames = Array.Empty(); + + return sNativeName != null && saShortDates != null && saLongDates != null && saYearMonths != null && + saDayNames != null && saAbbrevDayNames != null && saSuperShortDayNames != null && saMonthNames != null && + saAbbrevMonthNames != null && saMonthGenitiveNames != null && saAbbrevMonthGenitiveNames != null && + saEraNames != null && saAbbrevEraNames != null; + } + + private static string[] NormalizeEraNames(CalendarId calendarId, string[]? 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) + return new string[] { eraNames![eraNames.Length - 1] }; + + return eraNames ?? Array.Empty(); + } + + private static string[] NormalizeMonthArray(string[] months, CalendarId calendarId, ref string? leapHebrewMonthName) + { + if (months.Length == 13) + return months; + + string[] normalizedMonths = new string[13]; + // the month-name arrays are expected to have 13 elements. If only returns 12, add an + // extra empty string to fill the array. + if (months.Length == 12) + { + normalizedMonths[12] = ""; + months.CopyTo(normalizedMonths, 0); + return normalizedMonths; + } + + if (months.Length > 13) + { + Debug.Assert(calendarId == CalendarId.HEBREW && months.Length == 14); + + if (calendarId == CalendarId.HEBREW) + { + leapHebrewMonthName = months[13]; + } + for (int i = 0; i < 13; i++) + { + normalizedMonths[i] = months[i]; + } + return normalizedMonths; + } + + throw new Exception("CalendarData.GetCalendarInfoNative() returned an unexpected number of month names."); + } + + private static string GetCalendarInfoNative(string localeName, CalendarId calendarId, CalendarDataType calendarDataType) + { + Debug.Assert(localeName != null); + + return Interop.Globalization.GetCalendarInfoNative(localeName, calendarId, calendarDataType); + } + } +} diff --git a/src/mono/mono/mini/CMakeLists.txt b/src/mono/mono/mini/CMakeLists.txt index adb4922b9b36cf..47a6abcb7c71e3 100644 --- a/src/mono/mono/mini/CMakeLists.txt +++ b/src/mono/mono/mini/CMakeLists.txt @@ -72,7 +72,8 @@ if(HAVE_SYS_ICU) ${icu_shim_sources_base} pal_locale.m pal_collation.m - pal_casing.m) + pal_casing.m + pal_calendarData.m) endif() addprefix(icu_shim_sources "${ICU_SHIM_PATH}" "${icu_shim_sources_base}") diff --git a/src/native/libs/System.Globalization.Native/CMakeLists.txt b/src/native/libs/System.Globalization.Native/CMakeLists.txt index b3e70083f32f4f..cf06b6c80482a4 100644 --- a/src/native/libs/System.Globalization.Native/CMakeLists.txt +++ b/src/native/libs/System.Globalization.Native/CMakeLists.txt @@ -93,8 +93,8 @@ else() endif() if (CLR_CMAKE_TARGET_APPLE) - set(NATIVEGLOBALIZATION_SOURCES ${NATIVEGLOBALIZATION_SOURCES} pal_locale.m pal_collation.m pal_casing.m) - set_source_files_properties(pal_locale.m pal_collation.m pal_casing.m PROPERTIES COMPILE_FLAGS ${CLR_CMAKE_COMMON_OBJC_FLAGS}) + set(NATIVEGLOBALIZATION_SOURCES ${NATIVEGLOBALIZATION_SOURCES} pal_locale.m pal_collation.m pal_casing.m pal_calendarData.m) + set_source_files_properties(pal_locale.m pal_collation.m pal_casing.m pal_calendarData.m PROPERTIES COMPILE_FLAGS ${CLR_CMAKE_COMMON_OBJC_FLAGS}) endif() # time zone names are filtered out of icu data for the browser and associated functionality is disabled diff --git a/src/native/libs/System.Globalization.Native/entrypoints.c b/src/native/libs/System.Globalization.Native/entrypoints.c index 1ba348b910b5ff..cffad72a023721 100644 --- a/src/native/libs/System.Globalization.Native/entrypoints.c +++ b/src/native/libs/System.Globalization.Native/entrypoints.c @@ -63,6 +63,7 @@ static const Entry s_globalizationNative[] = DllImportEntry(GlobalizationNative_ChangeCaseNative) DllImportEntry(GlobalizationNative_CompareStringNative) DllImportEntry(GlobalizationNative_EndsWithNative) + DllImportEntry(GlobalizationNative_GetCalendarInfoNative) DllImportEntry(GlobalizationNative_GetLocaleInfoIntNative) DllImportEntry(GlobalizationNative_GetLocaleInfoPrimaryGroupingSizeNative) DllImportEntry(GlobalizationNative_GetLocaleInfoSecondaryGroupingSizeNative) diff --git a/src/native/libs/System.Globalization.Native/pal_calendarData.h b/src/native/libs/System.Globalization.Native/pal_calendarData.h index f6cfbdef448a9f..50458ac471a609 100644 --- a/src/native/libs/System.Globalization.Native/pal_calendarData.h +++ b/src/native/libs/System.Globalization.Native/pal_calendarData.h @@ -91,3 +91,9 @@ PALEXPORT int32_t GlobalizationNative_GetJapaneseEraStartDate(int32_t era, int32_t* startYear, int32_t* startMonth, int32_t* startDay); + +#ifdef __APPLE__ +PALEXPORT const char* GlobalizationNative_GetCalendarInfoNative(const char* localeName, + CalendarId calendarId, + CalendarDataType dataType); +#endif diff --git a/src/native/libs/System.Globalization.Native/pal_calendarData.m b/src/native/libs/System.Globalization.Native/pal_calendarData.m new file mode 100644 index 00000000000000..307590dfc7a9be --- /dev/null +++ b/src/native/libs/System.Globalization.Native/pal_calendarData.m @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include +#include "pal_icushim_internal.h" +#include "pal_calendarData.h" +#import + +#if defined(TARGET_MACCATALYST) || defined(TARGET_IOS) || defined(TARGET_TVOS) + +/* +Function: +GetCalendarIdentifier + +Gets the associated NSCalendarIdentifier for the CalendarId. +*/ +static NSString* GetCalendarIdentifier(CalendarId calendarId) +{ + NSString *calendarIdentifier = NSCalendarIdentifierGregorian; + switch (calendarId) + { + case JAPAN: + calendarIdentifier = NSCalendarIdentifierJapanese; + break; + case THAI: + calendarIdentifier = NSCalendarIdentifierBuddhist; + break; + case HEBREW: + calendarIdentifier = NSCalendarIdentifierHebrew; + break; + case PERSIAN: + calendarIdentifier = NSCalendarIdentifierPersian; + break; + case HIJRI: + calendarIdentifier = NSCalendarIdentifierIslamic; + break; + case UMALQURA: + calendarIdentifier = NSCalendarIdentifierIslamicUmmAlQura; + break; + case TAIWAN: + calendarIdentifier = NSCalendarIdentifierRepublicOfChina; + break; + default: + break; + } + return calendarIdentifier; +} + +/* +Function: +GlobalizationNative_GetCalendarInfoNative + +Gets a single string of calendar information for a given locale, calendar, and calendar data type. +with the requested value. +*/ +const char* GlobalizationNative_GetCalendarInfoNative(const char* localeName, CalendarId calendarId, CalendarDataType dataType) +{ + NSString *locName = [NSString stringWithFormat:@"%s", localeName]; + NSLocale *currentLocale = [[NSLocale alloc] initWithLocaleIdentifier:locName]; + + if (dataType == CalendarData_MonthDay) + { + NSString *formatString = [NSDateFormatter dateFormatFromTemplate:@"MMMMd" options:0 locale:currentLocale]; + return formatString ? strdup([formatString UTF8String]) : NULL; + } + else if (dataType == CalendarData_YearMonths) + { + NSString *formatString = [NSDateFormatter dateFormatFromTemplate:@"MMMM yyyy" options:0 locale:currentLocale]; + return formatString ? strdup([formatString UTF8String]) : NULL; + } + + NSString *calendarIdentifier = GetCalendarIdentifier(calendarId); + NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:calendarIdentifier]; + + if (dataType == CalendarData_NativeName) + return calendar ? strdup([[calendar calendarIdentifier] UTF8String]) : NULL; + + NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init]; + dateFormat.locale = currentLocale; + dateFormat.calendar = calendar; + + NSArray *result; + switch (dataType) + { + case CalendarData_ShortDates: + { + [dateFormat setDateStyle:NSDateFormatterShortStyle]; + NSString *shortFormatString = [dateFormat dateFormat]; + [dateFormat setDateStyle:NSDateFormatterMediumStyle]; + NSString *mediumFormatString = [dateFormat dateFormat]; + NSString *yearMonthDayFormat = [NSDateFormatter dateFormatFromTemplate:@"yMd" options:0 locale:currentLocale]; + result = @[shortFormatString, mediumFormatString, yearMonthDayFormat]; + break; + } + case CalendarData_LongDates: + { + [dateFormat setDateStyle:NSDateFormatterLongStyle]; + NSString *longFormatString = [dateFormat dateFormat]; + [dateFormat setDateStyle:NSDateFormatterFullStyle]; + NSString *fullFormatString = [dateFormat dateFormat]; + result = @[longFormatString, fullFormatString]; + break; + } + case CalendarData_DayNames: + result = [dateFormat standaloneWeekdaySymbols]; + break; + case CalendarData_AbbrevDayNames: + result = [dateFormat shortStandaloneWeekdaySymbols]; + break; + case CalendarData_MonthNames: + result = [dateFormat standaloneMonthSymbols]; + break; + case CalendarData_AbbrevMonthNames: + result = [dateFormat shortStandaloneMonthSymbols]; + break; + case CalendarData_SuperShortDayNames: + result = [dateFormat veryShortStandaloneWeekdaySymbols]; + break; + case CalendarData_MonthGenitiveNames: + result = [dateFormat monthSymbols]; + break; + case CalendarData_AbbrevMonthGenitiveNames: + result = [dateFormat shortMonthSymbols]; + break; + case CalendarData_EraNames: + case CalendarData_AbbrevEraNames: + result = [dateFormat eraSymbols]; + break; + default: + assert(false); + return NULL; + } + + NSString *arrayToString = [[result valueForKey:@"description"] componentsJoinedByString:@"||"]; + return arrayToString ? strdup([arrayToString UTF8String]) : NULL; +} +#endif