diff --git a/docs/design/features/hybrid-globalization.md b/docs/design/features/hybrid-globalization.md index b7e74af942007..bc0c721950929 100644 --- a/docs/design/features/hybrid-globalization.md +++ b/docs/design/features/hybrid-globalization.md @@ -19,6 +19,7 @@ Affected public APIs: Web API does not have an equivalent, so they throw `PlatformNotSupportedException`. + **Case change** Affected public APIs: @@ -28,6 +29,7 @@ Affected public APIs: Case change with invariant culture uses `toUpperCase` / `toLoweCase` functions that do not guarantee a full match with the original invariant culture. + **String comparison** Affected public APIs: @@ -42,7 +44,6 @@ The number of `CompareOptions` and `StringComparison` combinations is limited. O let high = String.fromCharCode(65281) // %uff83 = テ let low = String.fromCharCode(12486) // %u30c6 = テ high.localeCompare(low, "ja-JP", { sensitivity: "case" }) // -1 ; case: a ≠ b, a = á, a ≠ A; expected: 0 - let wide = String.fromCharCode(65345) // %uFF41 = a let narrow = "a" wide.localeCompare(narrow, "en-US", { sensitivity: "accent" }) // 0; accent: a ≠ b, a ≠ á, a = A; expected: -1 @@ -181,3 +182,46 @@ hiraganaBig.localeCompare(katakanaSmall, "en-US", { sensitivity: "base" }) // 0; `IgnoreKanaType | IgnoreWidth | IgnoreSymbols | IgnoreNonSpace` `IgnoreKanaType | IgnoreWidth | IgnoreSymbols | IgnoreNonSpace | IgnoreCase` + + +**String indexing** + +Affected public APIs: +- CompareInfo.IndexOf +- CompareInfo.LastIndexOf +- String.IndexOf +- String.LastIndexOf + +Web API does not expose locale-sensitive indexing function. There is a discussion on adding it: https://github.com/tc39/ecma402/issues/506. In the current state, as a workaround, locale-sensitive string segmenter combined with locale-sensitive comparison is used. This approach, beyond having the same compare option limitations as described under **String comparison**, has additional limitations connected with the workaround used. Information about additional limitations: + +- `IgnoreSymbols` +Only comparisons that ignore types of characters but do not skip them are allowed. E.g. `IgnoreCase` ignores type (case) of characters but `IgnoreSymbols` skips symbol-chars in comparison/indexing. All `CompareOptions` combinations that include `IgnoreSymbols` throw `PlatformNotSupportedException`. + +- Some letters consist of more than one grapheme. +Using locale-sensitive segmenter `Intl.Segmenter(locale, { granularity: "grapheme" })` does not guarantee that string will be segmented by letters but by graphemes. E.g. in `cs-CZ` and `sk-SK` "ch" is 1 letter, 2 graphemes. The following code with `HybridGlobalization` switched off returns -1 (not found) while with `HybridGlobalization` switched on, it returns 1. + +``` C# +new CultureInfo("sk-SK").CompareInfo.IndexOf("ch", "h"); // -1 or 1 +``` + +- Some graphemes consist of more than one character. +E.g. `\r\n` that represents two characters in C#, is treated as one grapheme by the segmenter: + +``` JS +const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" }); +Array.from(segmenter.segment("\r\n")) // {segment: '\r\n', index: 0, input: '\r\n'} +``` + +Because we are comparing grapheme-by-grapheme, character `\r` or character `\n` will not be found in `\r\n` string when `HybridGlobalization` is switched on. + +- Some graphemes have multi-grapheme equivalents. +E.g. in `de-DE` ß (%u00DF) is one letter and one grapheme and "ss" is one letter and is recognized as two graphemes. Web API's equivalent of `IgnoreNonSpace` treats them as the same letter when comparing. Similar case: dz (%u01F3) and dz. +``` JS +"ß".localeCompare("ss", "de-DE", { sensitivity: "case" }); // 0 +``` + +Using `IgnoreNonSpace` for these two with `HybridGlobalization` off, also returns 0 (they are equal). However, the workaround used in `HybridGlobalization` will compare them grapheme-by-grapheme and will return -1. + +``` C# +new CultureInfo("de-DE").CompareInfo.IndexOf("strasse", "stra\u00DFe", 0, CompareOptions.IgnoreNonSpace); // 0 or -1 +``` diff --git a/src/libraries/Common/src/Interop/Browser/Interop.CompareInfo.cs b/src/libraries/Common/src/Interop/Browser/Interop.CompareInfo.cs index 693b908a0c691..5b0a82acc1bbc 100644 --- a/src/libraries/Common/src/Interop/Browser/Interop.CompareInfo.cs +++ b/src/libraries/Common/src/Interop/Browser/Interop.CompareInfo.cs @@ -9,5 +9,8 @@ internal static unsafe partial class JsGlobalization { [MethodImplAttribute(MethodImplOptions.InternalCall)] internal static extern unsafe int CompareString(out string exceptionMessage, in string culture, char* str1, int str1Len, char* str2, int str2Len, global::System.Globalization.CompareOptions options); + + [MethodImplAttribute(MethodImplOptions.InternalCall)] + internal static extern unsafe int IndexOf(out string exceptionMessage, in string culture, char* str1, int str1Len, char* str2, int str2Len, global::System.Globalization.CompareOptions options, int* matchLengthPtr, bool fromBeginning); } } diff --git a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IndexOf.cs b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IndexOf.cs index 256e394f3673e..af8bf59a66588 100644 --- a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IndexOf.cs +++ b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IndexOf.cs @@ -33,13 +33,29 @@ public static IEnumerable IndexOf_TestData() yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", 0, 12, CompareOptions.Ordinal, -1, 0 }; // Slovak - yield return new object[] { s_slovakCompare, "ch", "h", 0, 2, CompareOptions.None, -1, 0 }; + // HybridGlobalization on WASM treats "ch" in Slovak like 2 separate letters + if (PlatformDetection.IsHybridGlobalizationOnBrowser) + { + yield return new object[] { s_slovakCompare, "ch", "h", 0, 2, CompareOptions.None, 1, 1 }; + yield return new object[] { s_slovakCompare, "chh", "h", 0, 3, CompareOptions.None, 1, 1 }; + } + else + { + yield return new object[] { s_slovakCompare, "ch", "h", 0, 2, CompareOptions.None, -1, 0 }; + yield return new object[] { s_slovakCompare, "chh", "h", 0, 3, CompareOptions.None, 2, 1 }; + } // Android has its own ICU, which doesn't work well with slovak if (!PlatformDetection.IsAndroid && !PlatformDetection.IsLinuxBionic) { - yield return new object[] { s_slovakCompare, "chodit hore", "HO", 0, 11, CompareOptions.IgnoreCase, 7, 2 }; + if (PlatformDetection.IsHybridGlobalizationOnBrowser) + { + yield return new object[] { s_slovakCompare, "chodit hore", "HO", 0, 11, CompareOptions.IgnoreCase, 1, 2 }; + } + else + { + yield return new object[] { s_slovakCompare, "chodit hore", "HO", 0, 11, CompareOptions.IgnoreCase, 7, 2 }; + } } - yield return new object[] { s_slovakCompare, "chh", "h", 0, 3, CompareOptions.None, 2, 1 }; // Turkish // Android has its own ICU, which doesn't work well with tr @@ -63,16 +79,24 @@ public static IEnumerable IndexOf_TestData() yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 0, 9, CompareOptions.IgnoreCase, 8, 1 }; yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 0, 9, CompareOptions.OrdinalIgnoreCase, -1, 0 }; yield return new object[] { s_invariantCompare, "FooBar", "Foo\u0400Bar", 0, 6, CompareOptions.Ordinal, -1, 0 }; - yield return new object[] { s_invariantCompare, "TestFooBA\u0300R", "FooB\u00C0R", 0, 11, CompareOptions.IgnoreNonSpace, 4, 7 }; + yield return new object[] { s_invariantCompare, "TestFooBA\u0300R", "FooB\u00C0R", 0, 11, supportedIgnoreNonSpaceOption, 4, 7 }; yield return new object[] { s_invariantCompare, "o\u0308", "o", 0, 2, CompareOptions.None, -1, 0 }; - yield return new object[] { s_invariantCompare, "\r\n", "\n", 0, 2, CompareOptions.None, 1, 1 }; + if (PlatformDetection.IsHybridGlobalizationOnBrowser) + { + yield return new object[] { s_invariantCompare, "\r\n", "\n", 0, 2, CompareOptions.None, -1, 0 }; + } + else + { + yield return new object[] { s_invariantCompare, "\r\n", "\n", 0, 2, CompareOptions.None, 1, 1 }; + } // Weightless characters yield return new object[] { s_invariantCompare, "", "\u200d", 0, 0, CompareOptions.None, 0, 0 }; yield return new object[] { s_invariantCompare, "hello", "\u200d", 1, 3, CompareOptions.IgnoreCase, 1, 0 }; - // Ignore symbols - yield return new object[] { s_invariantCompare, "More Test's", "Tests", 0, 11, CompareOptions.IgnoreSymbols, 5, 6 }; + // Ignore symbols is not supported with HybridGlobalization on WASM + if (!PlatformDetection.IsHybridGlobalizationOnBrowser) + yield return new object[] { s_invariantCompare, "More Test's", "Tests", 0, 11, CompareOptions.IgnoreSymbols, 5, 6 }; yield return new object[] { s_invariantCompare, "More Test's", "Tests", 0, 11, CompareOptions.None, -1, 0 }; yield return new object[] { s_invariantCompare, "cbabababdbaba", "ab", 0, 13, CompareOptions.None, 2, 2 }; @@ -127,12 +151,23 @@ public static IEnumerable IndexOf_TestData() } // Inputs where matched length does not equal value string length - yield return new object[] { s_invariantCompare, "abcdzxyz", "\u01F3", 0, 8, CompareOptions.IgnoreNonSpace, 3, 2 }; - yield return new object[] { s_invariantCompare, "abc\u01F3xyz", "dz", 0, 7, CompareOptions.IgnoreNonSpace, 3, 1 }; - yield return new object[] { s_germanCompare, "abc Strasse Strasse xyz", "stra\u00DFe", 0, 23, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, 4, 7 }; - yield return new object[] { s_germanCompare, "abc Strasse Strasse xyz", "xtra\u00DFe", 0, 23, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, -1, 0 }; - yield return new object[] { s_germanCompare, "abc stra\u00DFe stra\u00DFe xyz", "Strasse", 0, 21, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, 4, 6 }; - yield return new object[] { s_germanCompare, "abc stra\u00DFe stra\u00DFe xyz", "Xtrasse", 0, 21, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, -1, 0 }; + if (PlatformDetection.IsHybridGlobalizationOnBrowser) + { + yield return new object[] { s_invariantCompare, "abcdzxyz", "\u01F3", 0, 8, supportedIgnoreNonSpaceOption, -1, 0 }; + yield return new object[] { s_invariantCompare, "abc\u01F3xyz", "dz", 0, 7, supportedIgnoreNonSpaceOption, -1, 0 }; + yield return new object[] { s_germanCompare, "abc Strasse Strasse xyz", "stra\u00DFe", 0, 23, supportedIgnoreCaseIgnoreNonSpaceOptions, -1, 0 }; + yield return new object[] { s_germanCompare, "abc stra\u00DFe stra\u00DFe xyz", "Strasse", 0, 21, supportedIgnoreCaseIgnoreNonSpaceOptions, -1, 0 }; + } + else + { + yield return new object[] { s_invariantCompare, "abcdzxyz", "\u01F3", 0, 8, supportedIgnoreNonSpaceOption, 3, 2 }; + yield return new object[] { s_invariantCompare, "abc\u01F3xyz", "dz", 0, 7, supportedIgnoreNonSpaceOption, 3, 1 }; + yield return new object[] { s_germanCompare, "abc Strasse Strasse xyz", "stra\u00DFe", 0, 23, supportedIgnoreCaseIgnoreNonSpaceOptions, 4, 7 }; + yield return new object[] { s_germanCompare, "abc stra\u00DFe stra\u00DFe xyz", "Strasse", 0, 21, supportedIgnoreCaseIgnoreNonSpaceOptions, 4, 6 }; + } + + yield return new object[] { s_germanCompare, "abc Strasse Strasse xyz", "xtra\u00DFe", 0, 23, supportedIgnoreCaseIgnoreNonSpaceOptions, -1, 0 }; + yield return new object[] { s_germanCompare, "abc stra\u00DFe stra\u00DFe xyz", "Xtrasse", 0, 21, supportedIgnoreCaseIgnoreNonSpaceOptions, -1, 0 }; } public static IEnumerable IndexOf_Aesc_Ligature_TestData() @@ -273,7 +308,7 @@ public void IndexOf_UnassignedUnicode() bool useNls = PlatformDetection.IsNlsGlobalization; int expectedMatchLength = (useNls) ? 6 : 0; IndexOf_String(s_invariantCompare, "FooBar", "Foo\uFFFFBar", 0, 6, CompareOptions.None, useNls ? 0 : -1, expectedMatchLength); - IndexOf_String(s_invariantCompare, "~FooBar", "Foo\uFFFFBar", 0, 7, CompareOptions.IgnoreNonSpace, useNls ? 1 : -1, expectedMatchLength); + IndexOf_String(s_invariantCompare, "~FooBar", "Foo\uFFFFBar", 0, 7, supportedIgnoreNonSpaceOption, useNls ? 1 : -1, expectedMatchLength); } [Fact] diff --git a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs index 3ef9b9143b47f..6a4c1666e51a5 100644 --- a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs +++ b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs @@ -51,7 +51,14 @@ public static IEnumerable LastIndexOf_TestData() // Android has its own ICU, which doesn't work well with slovak if (!PlatformDetection.IsAndroid && !PlatformDetection.IsLinuxBionic) { - yield return new object[] { s_slovakCompare, "hore chodit", "HO", 11, 12, CompareOptions.IgnoreCase, 0, 2 }; + if (PlatformDetection.IsHybridGlobalizationOnBrowser) + { + yield return new object[] { s_slovakCompare, "hore chodit", "HO", 11, 12, CompareOptions.IgnoreCase, 6, 2 }; + } + else + { + yield return new object[] { s_slovakCompare, "hore chodit", "HO", 11, 12, CompareOptions.IgnoreCase, 0, 2 }; + } } yield return new object[] { s_slovakCompare, "chh", "h", 2, 2, CompareOptions.None, 2, 1 }; @@ -78,9 +85,16 @@ public static IEnumerable LastIndexOf_TestData() yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 8, 9, CompareOptions.OrdinalIgnoreCase, -1, 0 }; yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 8, 9, CompareOptions.Ordinal, -1, 0 }; yield return new object[] { s_invariantCompare, "FooBar", "Foo\u0400Bar", 5, 6, CompareOptions.Ordinal, -1, 0 }; - yield return new object[] { s_invariantCompare, "TestFooBA\u0300R", "FooB\u00C0R", 10, 11, CompareOptions.IgnoreNonSpace, 4, 7 }; + yield return new object[] { s_invariantCompare, "TestFooBA\u0300R", "FooB\u00C0R", 10, 11, supportedIgnoreNonSpaceOption, 4, 7 }; yield return new object[] { s_invariantCompare, "o\u0308", "o", 1, 2, CompareOptions.None, -1, 0 }; - yield return new object[] { s_invariantCompare, "\r\n", "\n", 1, 2, CompareOptions.None, 1, 1 }; + if (PlatformDetection.IsHybridGlobalizationOnBrowser) + { + yield return new object[] { s_invariantCompare, "\r\n", "\n", 1, 2, CompareOptions.None, -1, 0 }; + } + else + { + yield return new object[] { s_invariantCompare, "\r\n", "\n", 1, 2, CompareOptions.None, 1, 1 }; + } // Weightless characters // NLS matches weightless characters at the end of the string @@ -96,7 +110,8 @@ public static IEnumerable LastIndexOf_TestData() yield return new object[] { s_invariantCompare, "AA\u200DA", "\u200d", 3, 4, CompareOptions.None, 4, 0}; // Ignore symbols - yield return new object[] { s_invariantCompare, "More Test's", "Tests", 10, 11, CompareOptions.IgnoreSymbols, 5, 6 }; + if (!PlatformDetection.IsHybridGlobalizationOnBrowser) + yield return new object[] { s_invariantCompare, "More Test's", "Tests", 10, 11, CompareOptions.IgnoreSymbols, 5, 6 }; yield return new object[] { s_invariantCompare, "More Test's", "Tests", 10, 11, CompareOptions.None, -1, 0 }; yield return new object[] { s_invariantCompare, "cbabababdbaba", "ab", 12, 13, CompareOptions.None, 10, 2 }; @@ -111,12 +126,22 @@ public static IEnumerable LastIndexOf_TestData() } // Inputs where matched length does not equal value string length - yield return new object[] { s_invariantCompare, "abcdzxyz", "\u01F3", 7, 8, CompareOptions.IgnoreNonSpace, 3, 2 }; - yield return new object[] { s_invariantCompare, "abc\u01F3xyz", "dz", 6, 7, CompareOptions.IgnoreNonSpace, 3, 1 }; - yield return new object[] { s_germanCompare, "abc Strasse Strasse xyz", "stra\u00DFe", 22, 23, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, 12, 7 }; - yield return new object[] { s_germanCompare, "abc Strasse Strasse xyz", "xtra\u00DFe", 22, 23, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, -1, 0 }; - yield return new object[] { s_germanCompare, "abc stra\u00DFe stra\u00DFe xyz", "Strasse", 20, 21, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, 11, 6 }; - yield return new object[] { s_germanCompare, "abc stra\u00DFe stra\u00DFe xyz", "Xtrasse", 20, 21, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, -1, 0 }; + if (PlatformDetection.IsHybridGlobalizationOnBrowser) + { + yield return new object[] { s_invariantCompare, "abcdzxyz", "\u01F3", 7, 8, supportedIgnoreNonSpaceOption, -1, 0 }; + yield return new object[] { s_invariantCompare, "abc\u01F3xyz", "dz", 6, 7, supportedIgnoreNonSpaceOption, -1, 0 }; + yield return new object[] { s_germanCompare, "abc Strasse Strasse xyz", "stra\u00DFe", 22, 23, supportedIgnoreCaseIgnoreNonSpaceOptions, -1, 0 }; + yield return new object[] { s_germanCompare, "abc stra\u00DFe stra\u00DFe xyz", "Strasse", 20, 21, supportedIgnoreCaseIgnoreNonSpaceOptions, -1, 0 }; + } + else + { + yield return new object[] { s_invariantCompare, "abcdzxyz", "\u01F3", 7, 8, supportedIgnoreNonSpaceOption, 3, 2 }; + yield return new object[] { s_invariantCompare, "abc\u01F3xyz", "dz", 6, 7, supportedIgnoreNonSpaceOption, 3, 1 }; + yield return new object[] { s_germanCompare, "abc Strasse Strasse xyz", "stra\u00DFe", 22, 23, supportedIgnoreCaseIgnoreNonSpaceOptions, 12, 7 }; + yield return new object[] { s_germanCompare, "abc stra\u00DFe stra\u00DFe xyz", "Strasse", 20, 21, supportedIgnoreCaseIgnoreNonSpaceOptions, 11, 6 }; + } + yield return new object[] { s_germanCompare, "abc Strasse Strasse xyz", "xtra\u00DFe", 22, 23, supportedIgnoreCaseIgnoreNonSpaceOptions, -1, 0 }; + yield return new object[] { s_germanCompare, "abc stra\u00DFe stra\u00DFe xyz", "Xtrasse", 20, 21, supportedIgnoreCaseIgnoreNonSpaceOptions, -1, 0 }; } public static IEnumerable LastIndexOf_Aesc_Ligature_TestData() @@ -292,7 +317,7 @@ public void LastIndexOf_UnassignedUnicode() bool useNls = PlatformDetection.IsNlsGlobalization; int expectedMatchLength = (useNls) ? 6 : 0; LastIndexOf_String(s_invariantCompare, "FooBar", "Foo\uFFFFBar", 5, 6, CompareOptions.None, useNls ? 0 : -1, expectedMatchLength); - LastIndexOf_String(s_invariantCompare, "~FooBar", "Foo\uFFFFBar", 6, 7, CompareOptions.IgnoreNonSpace, useNls ? 1 : -1, expectedMatchLength); + LastIndexOf_String(s_invariantCompare, "~FooBar", "Foo\uFFFFBar", 6, 7, supportedIgnoreNonSpaceOption, useNls ? 1 : -1, expectedMatchLength); } [Fact] diff --git a/src/libraries/System.Globalization/tests/Hybrid/Hybrid.WASM.Tests.csproj b/src/libraries/System.Globalization/tests/Hybrid/Hybrid.WASM.Tests.csproj index a65d8448c4d70..d89cd9350a5c6 100644 --- a/src/libraries/System.Globalization/tests/Hybrid/Hybrid.WASM.Tests.csproj +++ b/src/libraries/System.Globalization/tests/Hybrid/Hybrid.WASM.Tests.csproj @@ -10,5 +10,7 @@ + + diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs index d4363b3ad1670..fb89c6b4a7751 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs @@ -21,24 +21,24 @@ public partial class CompareInfo private void IcuInitSortHandle(string interopCultureName) { - if (GlobalizationMode.Invariant) - { - _isAsciiEqualityOrdinal = true; - } - else - { - Debug.Assert(!GlobalizationMode.UseNls); - Debug.Assert(interopCultureName != null); - - // Inline the following condition to avoid potential implementation cycles within globalization - // - // _isAsciiEqualityOrdinal = _sortName == "" || _sortName == "en" || _sortName.StartsWith("en-", StringComparison.Ordinal); - // - _isAsciiEqualityOrdinal = _sortName.Length == 0 || - (_sortName.Length >= 2 && _sortName[0] == 'e' && _sortName[1] == 'n' && (_sortName.Length == 2 || _sortName[2] == '-')); + _isAsciiEqualityOrdinal = GetIsAsciiEqualityOrdinal(interopCultureName); + if (!GlobalizationMode.Invariant) + _sortHandle = SortHandleCache.GetCachedSortHandle(interopCultureName); + } - _sortHandle = SortHandleCache.GetCachedSortHandle(interopCultureName); - } + private bool GetIsAsciiEqualityOrdinal(string interopCultureName) + { + if (GlobalizationMode.Invariant) + return true; + Debug.Assert(!GlobalizationMode.UseNls); + Debug.Assert(interopCultureName != null); + + // Inline the following condition to avoid potential implementation cycles within globalization + // + // _isAsciiEqualityOrdinal = _sortName == "" || _sortName == "en" || _sortName.StartsWith("en-", StringComparison.Ordinal); + // + return _sortName.Length == 0 || + (_sortName.Length >= 2 && _sortName[0] == 'e' && _sortName[1] == 'n' && (_sortName.Length == 2 || _sortName[2] == '-')); } private unsafe int IcuCompareString(ReadOnlySpan string1, ReadOnlySpan string2, CompareOptions options) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs index 3ca001a9573bd..84980806be413 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs @@ -16,12 +16,12 @@ private unsafe int JsCompareString(ReadOnlySpan string1, ReadOnlySpan string1, ReadOnlySpan + private void JsInit(string interopCultureName) + { + _isAsciiEqualityOrdinal = GetIsAsciiEqualityOrdinal(interopCultureName); + } + + private unsafe int JsIndexOfCore(ReadOnlySpan source, ReadOnlySpan target, CompareOptions options, int* matchLengthPtr, bool fromBeginning) + { + Debug.Assert(!GlobalizationMode.Invariant); + Debug.Assert(!GlobalizationMode.UseNls); + Debug.Assert(GlobalizationMode.Hybrid); + Debug.Assert(target.Length != 0); + + if (IndexingOptionsNotSupported(options) || ComparisonOptionsNotSupported(options)) + throw new PlatformNotSupportedException(GetPNSE(options)); + + string cultureName = m_name; + + if (ComparisonOptionsNotSupportedForCulture(options, cultureName)) + throw new PlatformNotSupportedException(GetPNSEForCulture(options, cultureName)); + + string exceptionMessage; + int idx; + if (_isAsciiEqualityOrdinal && CanUseAsciiOrdinalForOptions(options)) + { + idx = (options & CompareOptions.IgnoreCase) != 0 ? + IndexOfOrdinalIgnoreCaseHelperJS(out exceptionMessage, source, target, options, matchLengthPtr, fromBeginning) : + IndexOfOrdinalHelperJS(out exceptionMessage, source, target, options, matchLengthPtr, fromBeginning); + } + else + { + fixed (char* pSource = &MemoryMarshal.GetReference(source)) + fixed (char* pTarget = &MemoryMarshal.GetReference(target)) + { + idx = Interop.JsGlobalization.IndexOf(out exceptionMessage, m_name, pTarget, target.Length, pSource, source.Length, options, matchLengthPtr, fromBeginning); + } + } + + if (!string.IsNullOrEmpty(exceptionMessage)) + throw new Exception(exceptionMessage); + + return idx; + } + + /// + /// Duplicate of IndexOfOrdinalHelperJS that also handles ignore case. Can't converge both methods + /// as the JIT wouldn't be able to optimize the ignoreCase path away. + /// + /// + // ToDo: clean up with IndexOfOrdinalHelper from .Icu + private unsafe int IndexOfOrdinalIgnoreCaseHelperJS(out string exceptionMessage, ReadOnlySpan source, ReadOnlySpan target, CompareOptions options, int* matchLengthPtr, bool fromBeginning) + { + Debug.Assert(!GlobalizationMode.Invariant); + + Debug.Assert(!target.IsEmpty); + Debug.Assert(_isAsciiEqualityOrdinal && CanUseAsciiOrdinalForOptions(options)); + + exceptionMessage = ""; + + fixed (char* ap = &MemoryMarshal.GetReference(source)) + fixed (char* bp = &MemoryMarshal.GetReference(target)) + { + char* a = ap; + char* b = bp; + + for (int j = 0; j < target.Length; j++) + { + char targetChar = *(b + j); + if (targetChar >= 0x80 || HighCharTable[targetChar]) + goto InteropCall; + } + + if (target.Length > source.Length) + { + for (int k = 0; k < source.Length; k++) + { + char targetChar = *(a + k); + if (targetChar >= 0x80 || HighCharTable[targetChar]) + goto InteropCall; + } + return -1; + } + + int startIndex, endIndex, jump; + if (fromBeginning) + { + // Left to right, from zero to last possible index in the source string. + // Incrementing by one after each iteration. Stop condition is last possible index plus 1. + startIndex = 0; + endIndex = source.Length - target.Length + 1; + jump = 1; + } + else + { + // Right to left, from first possible index in the source string to zero. + // Decrementing by one after each iteration. Stop condition is last possible index minus 1. + startIndex = source.Length - target.Length; + endIndex = -1; + jump = -1; + } + + for (int i = startIndex; i != endIndex; i += jump) + { + int targetIndex = 0; + int sourceIndex = i; + + for (; targetIndex < target.Length; targetIndex++, sourceIndex++) + { + char valueChar = *(a + sourceIndex); + char targetChar = *(b + targetIndex); + + if (valueChar >= 0x80 || HighCharTable[valueChar]) + goto InteropCall; + + if (valueChar == targetChar) + { + continue; + } + + // uppercase both chars - notice that we need just one compare per char + if (char.IsAsciiLetterLower(valueChar)) + valueChar = (char)(valueChar - 0x20); + if (char.IsAsciiLetterLower(targetChar)) + targetChar = (char)(targetChar - 0x20); + + if (valueChar == targetChar) + { + continue; + } + + // The match may be affected by special character. Verify that the following character is regular ASCII. + if (sourceIndex < source.Length - 1 && *(a + sourceIndex + 1) >= 0x80) + goto InteropCall; + goto Next; + } + + // The match may be affected by special character. Verify that the following character is regular ASCII. + if (sourceIndex < source.Length && *(a + sourceIndex) >= 0x80) + goto InteropCall; + if (matchLengthPtr != null) + *matchLengthPtr = target.Length; + return i; + + Next: ; + } + + return -1; + + InteropCall: + return Interop.JsGlobalization.IndexOf(out exceptionMessage, m_name, b, target.Length, a, source.Length, options, matchLengthPtr, fromBeginning); + } + } + + private unsafe int IndexOfOrdinalHelperJS(out string exceptionMessage, ReadOnlySpan source, ReadOnlySpan target, CompareOptions options, int* matchLengthPtr, bool fromBeginning) + { + Debug.Assert(!GlobalizationMode.Invariant); + + Debug.Assert(!target.IsEmpty); + Debug.Assert(_isAsciiEqualityOrdinal && CanUseAsciiOrdinalForOptions(options)); + + exceptionMessage = ""; + + fixed (char* ap = &MemoryMarshal.GetReference(source)) + fixed (char* bp = &MemoryMarshal.GetReference(target)) + { + char* a = ap; + char* b = bp; + + for (int j = 0; j < target.Length; j++) + { + char targetChar = *(b + j); + if (targetChar >= 0x80 || HighCharTable[targetChar]) + goto InteropCall; + } + + if (target.Length > source.Length) + { + for (int k = 0; k < source.Length; k++) + { + char targetChar = *(a + k); + if (targetChar >= 0x80 || HighCharTable[targetChar]) + goto InteropCall; + } + return -1; + } + + int startIndex, endIndex, jump; + if (fromBeginning) + { + // Left to right, from zero to last possible index in the source string. + // Incrementing by one after each iteration. Stop condition is last possible index plus 1. + startIndex = 0; + endIndex = source.Length - target.Length + 1; + jump = 1; + } + else + { + // Right to left, from first possible index in the source string to zero. + // Decrementing by one after each iteration. Stop condition is last possible index minus 1. + startIndex = source.Length - target.Length; + endIndex = -1; + jump = -1; + } + + for (int i = startIndex; i != endIndex; i += jump) + { + int targetIndex = 0; + int sourceIndex = i; + + for (; targetIndex < target.Length; targetIndex++, sourceIndex++) + { + char valueChar = *(a + sourceIndex); + char targetChar = *(b + targetIndex); + + if (valueChar >= 0x80 || HighCharTable[valueChar]) + goto InteropCall; + + if (valueChar == targetChar) + { + continue; + } + + // The match may be affected by special character. Verify that the following character is regular ASCII. + if (sourceIndex < source.Length - 1 && *(a + sourceIndex + 1) >= 0x80) + goto InteropCall; + goto Next; + } + + // The match may be affected by special character. Verify that the following character is regular ASCII. + if (sourceIndex < source.Length && *(a + sourceIndex) >= 0x80) + goto InteropCall; + if (matchLengthPtr != null) + *matchLengthPtr = target.Length; + return i; + + Next: ; + } + + return -1; + + InteropCall: + return Interop.JsGlobalization.IndexOf(out exceptionMessage, m_name, b, target.Length, a, source.Length, options, matchLengthPtr, fromBeginning); + } + } + + private static bool ComparisonOptionsNotSupported(CompareOptions options) => (options & CompareOptions.IgnoreWidth) == CompareOptions.IgnoreWidth || ((options & CompareOptions.IgnoreNonSpace) == CompareOptions.IgnoreNonSpace && (options & CompareOptions.IgnoreKanaType) != CompareOptions.IgnoreKanaType); + private static bool IndexingOptionsNotSupported(CompareOptions options) => + (options & CompareOptions.IgnoreSymbols) == CompareOptions.IgnoreSymbols; private static string GetPNSE(CompareOptions options) => $"CompareOptions = {options} are not supported when HybridGlobalization=true. Disable it to load larger ICU bundle, then use this option."; - - private static bool CompareOptionsNotSupportedForCulture(CompareOptions options, string cultureName) => + private static bool ComparisonOptionsNotSupportedForCulture(CompareOptions options, string cultureName) => (options == CompareOptions.IgnoreKanaType && (string.IsNullOrEmpty(cultureName) || cultureName.Split('-')[0] != "ja")) || (options == CompareOptions.None && (cultureName.Split('-')[0] == "ja")); - private static string GetPNSEForCulture(CompareOptions options, string cultureName) => $"CompareOptions = {options} are not supported for culture = {cultureName} when HybridGlobalization=true. Disable it to load larger ICU bundle, then use this option."; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs index 3a3a5ba955375..30851bd7b65d3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs @@ -173,11 +173,16 @@ private void InitSort(CultureInfo culture) if (GlobalizationMode.UseNls) { NlsInitSortHandle(); + return; } - else +#if TARGET_BROWSER + if (GlobalizationMode.Hybrid) { - IcuInitSortHandle(culture.InteropName!); + JsInit(culture.InteropName!); + return; } +#endif + IcuInitSortHandle(culture.InteropName!); } [OnDeserializing] @@ -1100,6 +1105,10 @@ private unsafe int IndexOf(ReadOnlySpan source, ReadOnlySpan value, private unsafe int IndexOfCore(ReadOnlySpan source, ReadOnlySpan target, CompareOptions options, int* matchLengthPtr, bool fromBeginning) => GlobalizationMode.UseNls ? NlsIndexOfCore(source, target, options, matchLengthPtr, fromBeginning) : +#if TARGET_BROWSER + GlobalizationMode.Hybrid ? + JsIndexOfCore(source, target, options, matchLengthPtr, fromBeginning) : +#endif IcuIndexOfCore(source, target, options, matchLengthPtr, fromBeginning); /// diff --git a/src/mono/sample/wasm/browser-bench/String.cs b/src/mono/sample/wasm/browser-bench/String.cs index 608801c5177b0..7a72abb3d4a54 100644 --- a/src/mono/sample/wasm/browser-bench/String.cs +++ b/src/mono/sample/wasm/browser-bench/String.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -24,6 +24,7 @@ public StringTask() new StringCompareMeasurement(), new StringEqualsMeasurement(), new CompareInfoMeasurement(), + new StringIndexOfMeasurement(), }; } @@ -124,16 +125,24 @@ public class TextInfoToTitleCase : TextInfoMeasurement public override void RunStep() => textInfo.ToTitleCase(str); } - public class StringsCompare : StringMeasurement + public abstract class StringsCompare : StringMeasurement { - protected string str2; + protected string strAsciiSuffix; + protected string strAsciiPrefix; + protected string halfStrLenAsciiSuffix; public void InitializeStringsForComparison() { InitializeString(); - // worst case: strings may differ only with the last char + // worst case: strings may differ only with the last/first char + char originalLastChar = data[len-1]; data[len-1] = (char)random.Next(0x80); - str2 = new string(data); + strAsciiSuffix = new string(data); + int middleIdx = (int)(len/2); + halfStrLenAsciiSuffix = new string(new ArraySegment(data, middleIdx, len - middleIdx)); + data[len-1] = originalLastChar; + data[0] = (char)random.Next(0x80); + strAsciiPrefix = new string(data); } public override string Name => "Strings Compare Base"; } @@ -149,7 +158,7 @@ public override Task BeforeBatch() return Task.CompletedTask; } public override string Name => "String Compare"; - public override void RunStep() => string.Compare(str, str2, cultureInfo, CompareOptions.None); + public override void RunStep() => string.Compare(str, strAsciiSuffix, cultureInfo, CompareOptions.None); } public class StringEqualsMeasurement : StringsCompare @@ -160,10 +169,10 @@ public override Task BeforeBatch() return Task.CompletedTask; } public override string Name => "String Equals"; - public override void RunStep() => string.Equals(str, str2, StringComparison.InvariantCulture); + public override void RunStep() => string.Equals(str, strAsciiSuffix, StringComparison.InvariantCulture); } - public class CompareInfoMeasurement : StringsCompare + public class CompareInfoCompareMeasurement : StringsCompare { protected CompareInfo compareInfo; @@ -174,7 +183,77 @@ public override Task BeforeBatch() return Task.CompletedTask; } public override string Name => "CompareInfo Compare"; - public override void RunStep() => compareInfo.Compare(str, str2); + public override void RunStep() => compareInfo.Compare(str, strAsciiSuffix); + } + + public class CompareInfoStartsWithMeasurement : StringsCompare + { + protected CompareInfo compareInfo; + + public override Task BeforeBatch() + { + compareInfo = new CultureInfo("hy-AM").CompareInfo; + InitializeStringsForComparison(); + return Task.CompletedTask; + } + public override string Name => "CompareInfo IsPrefix"; + public override void RunStep() => compareInfo.IsPrefix(str, strAsciiSuffix); + } + + public class CompareInfoEndsWithMeasurement : StringsCompare + { + protected CompareInfo compareInfo; + + public override Task BeforeBatch() + { + compareInfo = new CultureInfo("it-IT").CompareInfo; + InitializeStringsForComparison(); + return Task.CompletedTask; + } + public override string Name => "CompareInfo IsSuffix"; + public override void RunStep() => compareInfo.IsSuffix(str, strAsciiPrefix); + } + + public class StringStartsWithMeasurement : StringsCompare + { + protected CultureInfo cultureInfo; + + public override Task BeforeBatch() + { + cultureInfo = new CultureInfo("bs-BA"); + InitializeStringsForComparison(); + return Task.CompletedTask; + } + public override string Name => "String StartsWith"; + public override void RunStep() => str.StartsWith(strAsciiSuffix, false, cultureInfo); + } + + public class StringEndsWithMeasurement : StringsCompare + { + protected CultureInfo cultureInfo; + + public override Task BeforeBatch() + { + cultureInfo = new CultureInfo("nb-NO"); + InitializeStringsForComparison(); + return Task.CompletedTask; + } + public override string Name => "String EndsWith"; + public override void RunStep() => str.EndsWith(strAsciiPrefix, false, cultureInfo); + } + + public class StringIndexOfMeasurement : StringsCompare + { + protected CompareInfo compareInfo; + + public override Task BeforeBatch() + { + compareInfo = new CultureInfo("nb-NO").CompareInfo; + InitializeStringsForComparison(); + return Task.CompletedTask; + } + public override string Name => "String IndexOf"; + public override void RunStep() => compareInfo.IndexOf(str, halfStrLenAsciiSuffix, CompareOptions.None); } } } diff --git a/src/mono/wasm/runtime/corebindings.c b/src/mono/wasm/runtime/corebindings.c index d07e106668c3e..f5a9e4887b569 100644 --- a/src/mono/wasm/runtime/corebindings.c +++ b/src/mono/wasm/runtime/corebindings.c @@ -46,6 +46,7 @@ extern void* mono_wasm_invoke_js_blazor (MonoString **exceptionMessage, void *ca extern void mono_wasm_change_case_invariant(MonoString **exceptionMessage, const uint16_t* src, int32_t srcLength, uint16_t* dst, int32_t dstLength, mono_bool bToUpper); extern void mono_wasm_change_case(MonoString **exceptionMessage, MonoString **culture, const uint16_t* src, int32_t srcLength, uint16_t* dst, int32_t dstLength, mono_bool bToUpper); extern int mono_wasm_compare_string(MonoString **exceptionMessage, MonoString **culture, const uint16_t* str1, int32_t str1Length, const uint16_t* str2, int32_t str2Length, int32_t options); +extern int mono_wasm_index_of(MonoString **exceptionMessage, MonoString **culture, const uint16_t* str1, int32_t str1Length, const uint16_t* str2, int32_t str2Length, int32_t options, int32_t* matchLengthPointer, mono_bool fromBeginning); void bindings_initialize_internals (void) { @@ -77,4 +78,5 @@ void bindings_initialize_internals (void) mono_add_internal_call ("Interop/JsGlobalization::ChangeCaseInvariant", mono_wasm_change_case_invariant); mono_add_internal_call ("Interop/JsGlobalization::ChangeCase", mono_wasm_change_case); mono_add_internal_call ("Interop/JsGlobalization::CompareString", mono_wasm_compare_string); + mono_add_internal_call ("Interop/JsGlobalization::IndexOf", mono_wasm_index_of); } diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index 19900b76cac87..b06c326536030 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -100,6 +100,7 @@ let linked_functions = [ "mono_wasm_change_case_invariant", "mono_wasm_change_case", "mono_wasm_compare_string", + "mono_wasm_index_of", "icudt68_dat", ]; diff --git a/src/mono/wasm/runtime/exports-linker.ts b/src/mono/wasm/runtime/exports-linker.ts index ae0305551a42c..d3989e0f040c1 100644 --- a/src/mono/wasm/runtime/exports-linker.ts +++ b/src/mono/wasm/runtime/exports-linker.ts @@ -26,7 +26,7 @@ import { mono_wasm_invoke_js_blazor, mono_wasm_invoke_js_with_args_ref, mono_wasm_get_object_property_ref, mono_wasm_set_object_property_ref, mono_wasm_get_by_index_ref, mono_wasm_set_by_index_ref, mono_wasm_get_global_object_ref } from "./net6-legacy/method-calls"; -import { mono_wasm_change_case, mono_wasm_change_case_invariant, mono_wasm_compare_string } from "./net6-legacy/hybrid-globalization"; +import { mono_wasm_change_case, mono_wasm_change_case_invariant, mono_wasm_compare_string, mono_wasm_index_of } from "./net6-legacy/hybrid-globalization"; // the methods would be visible to EMCC linker // --- keep in sync with dotnet.cjs.lib.js --- @@ -97,6 +97,7 @@ export function export_linker(): any { mono_wasm_change_case_invariant, mono_wasm_change_case, mono_wasm_compare_string, + mono_wasm_index_of, // threading exports, if threading is enabled ...mono_wasm_threads_exports, diff --git a/src/mono/wasm/runtime/net6-legacy/hybrid-globalization.ts b/src/mono/wasm/runtime/net6-legacy/hybrid-globalization.ts index ae3ee2ac648f6..7756b4a528c88 100644 --- a/src/mono/wasm/runtime/net6-legacy/hybrid-globalization.ts +++ b/src/mono/wasm/runtime/net6-legacy/hybrid-globalization.ts @@ -5,8 +5,8 @@ import { Module } from "../imports"; import { mono_wasm_new_external_root } from "../roots"; import {MonoString, MonoStringRef } from "../types"; import { Int32Ptr } from "../types/emscripten"; -import { conv_string_root, js_string_to_mono_string_root } from "../strings"; -import { setU16 } from "../memory"; +import { conv_string_root, js_string_to_mono_string_root, string_decoder } from "../strings"; +import { setU16, setU32 } from "../memory"; export function mono_wasm_change_case_invariant(exceptionMessage: Int32Ptr, src: number, srcLength: number, dst: number, dstLength: number, toUpper: number) : void{ try{ @@ -84,6 +84,107 @@ export function pass_exception_details(ex: any, exceptionMessage: Int32Ptr){ exceptionRoot.release(); } + +export function mono_wasm_index_of(exceptionMessage: Int32Ptr, culture: MonoStringRef, str1: number, str1Length: number, str2: number, str2Length: number, options: number, matchLengthPointer: number, fromBeginning: boolean): number{ + const cultureRoot = mono_wasm_new_external_root(culture); + try{ + const ignoreSymbols = (options & 0x4) == 0x4; + if (ignoreSymbols) + throw new Error("$Invalid comparison option."); + const value = string_decoder.decode(str1, (str1 + 2*str1Length)); + // no need to look for an empty string + const result = "".localeCompare(value, undefined); + if (result === 0) + return fromBeginning ? 0 : str2Length; + + const source = string_decoder.decode(str2, (str2 + 2*str2Length)); + const cultureName = conv_string_root(cultureRoot); + const locale = cultureName ? cultureName : undefined; + const graphemesSource = segment_string_locale_sensitive(source, locale); + const graphemesValue = segment_string_locale_sensitive(value, locale); + const casePicker = (options & 0x1f); + if (fromBeginning) + return get_index_of(graphemesSource, graphemesValue, locale, casePicker, matchLengthPointer, source.length); + else + return get_last_index_of(graphemesSource, graphemesValue, locale, casePicker, matchLengthPointer, source.length); + } + catch (ex: any) { + pass_exception_details(ex, exceptionMessage); + return -1; + } + finally { + cultureRoot.release(); + } +} + +export function segment_string_locale_sensitive(string: string, locale: string | undefined) : Intl.SegmentData[] +{ + const segmenter = new Intl.Segmenter(locale, { granularity: "grapheme" }); + return Array.from(segmenter.segment(string)); +} + +function get_index_of(graphemesSource: Intl.SegmentData[], graphemesValue: Intl.SegmentData[], locale: string | undefined, casePicker: number, matchLengthPointer: number, srcOriginalLen: number){ + const lenDifference = graphemesSource.length - graphemesValue.length; + for (let i = 0; i <= lenDifference; i++) + { + let index = -1; + for (let j = 0; j < graphemesValue.length; j++) + { + const isEqual = compare_strings(graphemesSource[i + j].segment, graphemesValue[j].segment, locale, casePicker); + if (isEqual !== 0) + { + index = -1; + break; + } + if (index === -1 && isEqual === 0) + index = graphemesSource[i].index; + } + if (index !== -1) + { + const lastGraphemeOriginalStartIxd = graphemesSource[i + graphemesValue.length - 1].index; + const lastGraphemeLen = (index + 1 < graphemesSource.length) ? + graphemesSource[index + 1].index - graphemesSource[index].index : + srcOriginalLen - graphemesSource[index].index; + const matchLen = (lastGraphemeOriginalStartIxd + lastGraphemeLen) - index ; + setU32(matchLengthPointer, matchLen); + return index; + } + } + return -1; +} + +function get_last_index_of(graphemesSource: Intl.SegmentData[], graphemesValue: Intl.SegmentData[], locale: string | undefined, casePicker: number, matchLengthPointer: number, srcOriginalLen: number){ + + for (let i = graphemesSource.length - 1; i >= graphemesValue.length - 1; i--) + { + let index = -1; + const lastGraphemeIdx = graphemesValue.length - 1; + const firstGraphemeId = i - graphemesValue.length + 1; + for (let j = lastGraphemeIdx; j >= 0; j--) + { + const isEqual = compare_strings(graphemesSource[i - (lastGraphemeIdx - j)].segment, graphemesValue[j].segment, locale, casePicker); + if (isEqual !== 0) + { + index = -1; + break; + } + if (index === -1 && isEqual === 0) + index = graphemesSource[firstGraphemeId].index; + } + if (index !== -1) + { + const lastGraphemeOriginalStartIxd = graphemesSource[i].index; + const lastGraphemeLen = (i + 1 < graphemesSource.length) ? + graphemesSource[i + 1].index - graphemesSource[i].index : + srcOriginalLen - graphemesSource[i].index; + const matchLen = (lastGraphemeOriginalStartIxd + lastGraphemeLen) - index ; + setU32(matchLengthPointer, matchLen); + return index; + } + } + return -1; +} + export function compare_strings(string1: string, string2: string, locale: string | undefined, casePicker: number) : number{ switch (casePicker) {