[browser][non-icu] `HybridGlobalization` checking for prefix/suffix (#85093)
authorIlona Tomkowicz <32700855+ilonatommy@users.noreply.github.com>
Sat, 22 Apr 2023 13:30:57 +0000 (15:30 +0200)
committerGitHub <noreply@github.com>
Sat, 22 Apr 2023 13:30:57 +0000 (15:30 +0200)
* Implementation.

* HG does not belong to legacy code.

* No need to create new instance when existing one is exported.

* TextEncoder's behavior varies between hosts.

* Nit

* Cutting prevents us from using IgnoreSymbols.

* Fixed asserts.

* Fix.

* Match platform with behavior.

* Missing changes to prev commit.

14 files changed:
docs/design/features/hybrid-globalization.md
src/libraries/Common/src/Interop/Browser/Interop.CompareInfo.cs
src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsPrefix.cs
src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsSuffix.cs
src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTestsBase.cs
src/libraries/System.Globalization/tests/Hybrid/Hybrid.WASM.Tests.csproj
src/libraries/System.Private.CoreLib/src/Resources/Strings.resx
src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.WebAssembly.cs
src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs
src/mono/sample/wasm/browser-bench/String.cs
src/mono/wasm/runtime/corebindings.c
src/mono/wasm/runtime/es6/dotnet.es6.lib.js
src/mono/wasm/runtime/exports-linker.ts
src/mono/wasm/runtime/hybrid-globalization.ts [moved from src/mono/wasm/runtime/net6-legacy/hybrid-globalization.ts with 71% similarity]

index b7e74af..629d68e 100644 (file)
@@ -181,3 +181,20 @@ hiraganaBig.localeCompare(katakanaSmall, "en-US", { sensitivity: "base" }) // 0;
 `IgnoreKanaType | IgnoreWidth | IgnoreSymbols | IgnoreNonSpace`
 
 `IgnoreKanaType | IgnoreWidth | IgnoreSymbols | IgnoreNonSpace | IgnoreCase`
+
+
+**String starts with / ends with**
+
+Affected public APIs:
+- CompareInfo.IsPrefix
+- CompareInfo.IsSuffix
+- String.StartsWith
+- String.EndsWith
+
+Web API does not expose locale-sensitive endsWith/startsWith function. As a workaround, both strings get normalized and weightless characters are removed. Resulting strings are cut to the same length and comparison is performed. This approach, beyond having the same compare option limitations as described under **String comparison**, has additional limitations connected with the workaround used. Because we are normalizing strings to be able to cut them, we cannot calculate the match length on the original strings. Methods that calculate this information throw PlatformNotSupported exception:
+
+- [CompareInfo.IsPrefix](https://learn.microsoft.com/en-us/dotnet/api/system.globalization.compareinfo.isprefix?view=net-8.0#system-globalization-compareinfo-isprefix(system-readonlyspan((system-char))-system-readonlyspan((system-char))-system-globalization-compareoptions-system-int32@))
+- [CompareInfo.IsSuffix](https://learn.microsoft.com/en-us/dotnet/api/system.globalization.compareinfo.issuffix?view=net-8.0#system-globalization-compareinfo-issuffix(system-readonlyspan((system-char))-system-readonlyspan((system-char))-system-globalization-compareoptions-system-int32@))
+
+- `IgnoreSymbols`
+Only comparisons that do not skip character types are allowed. E.g. `IgnoreSymbols` skips symbol-chars in comparison/indexing. All `CompareOptions` combinations that include `IgnoreSymbols` throw `PlatformNotSupportedException`.
index 693b908..5294d84 100644 (file)
@@ -9,5 +9,11 @@ internal static partial class Interop
     {
         [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 bool StartsWith(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 bool EndsWith(out string exceptionMessage, in string culture, char* str1, int str1Len, char* str2, int str2Len, global::System.Globalization.CompareOptions options);
     }
 }
index 2b20169..02b959e 100644 (file)
@@ -7,14 +7,8 @@ using Xunit;
 
 namespace System.Globalization.Tests
 {
-    public class CompareInfoIsPrefixTests
+    public class CompareInfoIsPrefixTests : CompareInfoTestsBase
     {
-        private static CompareInfo s_invariantCompare = CultureInfo.InvariantCulture.CompareInfo;
-        private static CompareInfo s_germanCompare = new CultureInfo("de-DE").CompareInfo;
-        private static CompareInfo s_hungarianCompare = new CultureInfo("hu-HU").CompareInfo;
-        private static CompareInfo s_turkishCompare = new CultureInfo("tr-TR").CompareInfo;
-        private static CompareInfo s_frenchCompare = new CultureInfo("fr-FR").CompareInfo;
-
         public static IEnumerable<object[]> IsPrefix_TestData()
         {
             // Empty strings
@@ -31,7 +25,8 @@ namespace System.Globalization.Tests
             yield return new object[] { s_invariantCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.Ordinal, false, 0 };
             yield return new object[] { s_hungarianCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.Ordinal, false, 0 };
             yield return new object[] { s_invariantCompare, "dz", "d", CompareOptions.None, true, 1 };
-            yield return new object[] { s_hungarianCompare, "dz", "d", CompareOptions.None, false, 0 };
+            if (!PlatformDetection.IsHybridGlobalizationOnBrowser)
+                yield return new object[] { s_hungarianCompare, "dz", "d", CompareOptions.None, false, 0 };
             yield return new object[] { s_hungarianCompare, "dz", "d", CompareOptions.Ordinal, true, 1 };
 
             // Turkish
@@ -56,7 +51,7 @@ namespace System.Globalization.Tests
             yield return new object[] { s_invariantCompare, "\u00C0nimal", "a\u0300", CompareOptions.Ordinal, false, 0 };
             yield return new object[] { s_invariantCompare, "\u00C0nimal", "a\u0300", CompareOptions.OrdinalIgnoreCase, false, 0 };
             yield return new object[] { s_invariantCompare, "FooBar", "Foo\u0400Bar", CompareOptions.Ordinal, false, 0 };
-            yield return new object[] { s_invariantCompare, "FooBA\u0300R", "FooB\u00C0R", CompareOptions.IgnoreNonSpace, true, 7 };
+            yield return new object[] { s_invariantCompare, "FooBA\u0300R", "FooB\u00C0R", supportedIgnoreNonSpaceOption, true, 7 };
             yield return new object[] { s_invariantCompare, "o\u0308", "o", CompareOptions.None, false, 0 };
             yield return new object[] { s_invariantCompare, "o\u0308", "o", CompareOptions.Ordinal, true, 1 };
             yield return new object[] { s_invariantCompare, "o\u0000\u0308", "o", CompareOptions.None, true, 1 };
@@ -76,23 +71,32 @@ namespace System.Globalization.Tests
             yield return new object[] { s_invariantCompare, "\uD800\uD800", "\uD800\uD800", CompareOptions.None, true, 2 };
 
             // Ignore symbols
-            yield return new object[] { s_invariantCompare, "Test's can be interesting", "Tests", CompareOptions.IgnoreSymbols, true, 6 };
-            yield return new object[] { s_invariantCompare, "Test's can be interesting", "Tests", CompareOptions.None, false, 0 };
+            if (!PlatformDetection.IsHybridGlobalizationOnBrowser)
+            {
+                yield return new object[] { s_invariantCompare, "Test's can be interesting", "Tests", CompareOptions.IgnoreSymbols, true, 6 };
+                yield return new object[] { s_invariantCompare, "Test's can be interesting", "Tests", CompareOptions.None, false, 0 };
+            }
 
             // Platform differences
-            bool useNls = PlatformDetection.IsNlsGlobalization;
-            if (useNls)
+            // in HybridGlobalization on Browser we use TextEncoder that is not supported for v8 and the manual decoding works like NLS
+            bool behavesLikeNls = PlatformDetection.IsNlsGlobalization || 
+                (PlatformDetection.IsHybridGlobalizationOnBrowser && !PlatformDetection.IsBrowserDomSupportedOrNodeJS);
+            if (behavesLikeNls)
             {
-                yield return new object[] { s_hungarianCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.None, true, 7 };
-                yield return new object[] { s_invariantCompare, "''Tests", "Tests", CompareOptions.IgnoreSymbols, true, 7 };
-                yield return new object[] { s_frenchCompare, "\u0153", "oe", CompareOptions.None, true, 1 };
+                if (!PlatformDetection.IsHybridGlobalizationOnBrowser)
+                {
+                    yield return new object[] { s_hungarianCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.None, true, 7 };
+                    yield return new object[] { s_invariantCompare, "''Tests", "Tests", CompareOptions.IgnoreSymbols, true, 7 };
+                    yield return new object[] { s_frenchCompare, "\u0153", "oe", CompareOptions.None, true, 1 };
+                }
                 yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.None, true, 1 };
                 yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.IgnoreCase, true, 1 };
             }
             else
             {
                 yield return new object[] { s_hungarianCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.None, false, 0 };
-                yield return new object[] { s_invariantCompare, "''Tests", "Tests", CompareOptions.IgnoreSymbols, false, 0 };
+                if (!PlatformDetection.IsHybridGlobalizationOnBrowser)
+                    yield return new object[] { s_invariantCompare, "''Tests", "Tests", CompareOptions.IgnoreSymbols, false, 0 };
                 yield return new object[] { s_frenchCompare, "\u0153", "oe", CompareOptions.None, false, 0 };
                 yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.None, false, 0 };
                 yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.IgnoreCase, false, 0 };
@@ -100,18 +104,24 @@ namespace System.Globalization.Tests
 
             // ICU bugs
             // UInt16 overflow: https://unicode-org.atlassian.net/browse/ICU-20832 fixed in https://github.com/unicode-org/icu/pull/840 (ICU 65)
-            if (useNls || PlatformDetection.ICUVersion.Major >= 65)
+            if (PlatformDetection.IsNlsGlobalization || PlatformDetection.ICUVersion.Major >= 65)
             {
                 yield return new object[] { s_frenchCompare, "b", new string('a', UInt16.MaxValue + 1), CompareOptions.None, false, 0 };
             }
 
             // Prefixes where matched length does not equal value string length
-            yield return new object[] { s_invariantCompare, "dzxyz", "\u01F3", CompareOptions.IgnoreNonSpace, true, 2 };
-            yield return new object[] { s_invariantCompare, "\u01F3xyz", "dz", CompareOptions.IgnoreNonSpace, true, 1 };
-            yield return new object[] { s_germanCompare, "Strasse xyz", "stra\u00DFe", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, true, 7 };
-            yield return new object[] { s_germanCompare, "Strasse xyz", "xtra\u00DFe", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, false, 0 };
-            yield return new object[] { s_germanCompare, "stra\u00DFe xyz", "Strasse", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, true, 6 };
-            yield return new object[] { s_germanCompare, "stra\u00DFe xyz", "Xtrasse", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, false, 0 };
+            if (!PlatformDetection.IsHybridGlobalizationOnBrowser)
+            {
+                yield return new object[] { s_invariantCompare, "dzxyz", "\u01F3", supportedIgnoreNonSpaceOption, true, 2 };
+                yield return new object[] { s_invariantCompare, "\u01F3xyz", "dz", supportedIgnoreNonSpaceOption, true, 1 };
+            }
+            if (!PlatformDetection.IsHybridGlobalizationOnBrowser)
+            {
+                yield return new object[] { s_germanCompare, "Strasse xyz", "stra\u00DFe", supportedIgnoreCaseIgnoreNonSpaceOptions, true, 7 };
+                yield return new object[] { s_germanCompare, "stra\u00DFe xyz", "Strasse", supportedIgnoreCaseIgnoreNonSpaceOptions, true, 6 };
+            }
+            yield return new object[] { s_germanCompare, "Strasse xyz", "xtra\u00DFe", supportedIgnoreCaseIgnoreNonSpaceOptions, false, 0 };
+            yield return new object[] { s_germanCompare, "stra\u00DFe xyz", "Xtrasse", supportedIgnoreCaseIgnoreNonSpaceOptions, false, 0 };
         }
 
         [Theory]
@@ -140,8 +150,11 @@ namespace System.Globalization.Tests
             valueBoundedMemory.MakeReadonly();
 
             Assert.Equal(expected, compareInfo.IsPrefix(sourceBoundedMemory.Span, valueBoundedMemory.Span, options));
-            Assert.Equal(expected, compareInfo.IsPrefix(sourceBoundedMemory.Span, valueBoundedMemory.Span, options, out int actualMatchLength));
-            Assert.Equal(expectedMatchLength, actualMatchLength);
+            if (!PlatformDetection.IsHybridGlobalizationOnBrowser)
+            {
+                Assert.Equal(expected, compareInfo.IsPrefix(sourceBoundedMemory.Span, valueBoundedMemory.Span, options, out int actualMatchLength));
+                Assert.Equal(expectedMatchLength, actualMatchLength);
+            }
         }
 
         [Fact]
@@ -150,7 +163,7 @@ namespace System.Globalization.Tests
             bool result = PlatformDetection.IsNlsGlobalization ? true : false;
             int expectedMatchLength = (result) ? 6 : 0;
             IsPrefix(s_invariantCompare, "FooBar", "Foo\uFFFFBar", CompareOptions.None, result, expectedMatchLength);
-            IsPrefix(s_invariantCompare, "FooBar", "Foo\uFFFFBar", CompareOptions.IgnoreNonSpace, result, expectedMatchLength);
+            IsPrefix(s_invariantCompare, "FooBar", "Foo\uFFFFBar", supportedIgnoreNonSpaceOption, result, expectedMatchLength);
         }
 
         [Fact]
index edac588..e5d8a10 100644 (file)
@@ -7,15 +7,8 @@ using Xunit;
 
 namespace System.Globalization.Tests
 {
-    public class CompareInfoIsSuffixTests
+    public class CompareInfoIsSuffixTests : CompareInfoTestsBase
     {
-        private static CompareInfo s_invariantCompare = CultureInfo.InvariantCulture.CompareInfo;
-        private static CompareInfo s_germanCompare = new CultureInfo("de-DE").CompareInfo;
-        private static CompareInfo s_hungarianCompare = new CultureInfo("hu-HU").CompareInfo;
-        private static CompareInfo s_turkishCompare = new CultureInfo("tr-TR").CompareInfo;
-        private static CompareInfo s_frenchCompare = new CultureInfo("fr-FR").CompareInfo;
-        private static CompareInfo s_slovakCompare = new CultureInfo("sk-SK").CompareInfo;
-
         public static IEnumerable<object[]> IsSuffix_TestData()
         {
             // Empty strings
@@ -32,12 +25,16 @@ namespace System.Globalization.Tests
             yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", CompareOptions.None, false, 0 };
             yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", CompareOptions.Ordinal, false, 0 };
             yield return new object[] { s_invariantCompare, "dz", "z", CompareOptions.None, true, 1 };
-            yield return new object[] { s_hungarianCompare, "dz", "z", CompareOptions.None, false, 0 };
+            if (!PlatformDetection.IsHybridGlobalizationOnBrowser)
+                yield return new object[] { s_hungarianCompare, "dz", "z", CompareOptions.None, false, 0 };
             yield return new object[] { s_hungarianCompare, "dz", "z", CompareOptions.Ordinal, true, 1 };
 
             // Slovak
-            yield return new object[] { s_slovakCompare, "ch", "h", CompareOptions.None, false, 0 };
-            yield return new object[] { s_slovakCompare, "velmi chora", "hora", CompareOptions.None, false, 0 };
+            if (!PlatformDetection.IsHybridGlobalizationOnBrowser)
+            {
+                yield return new object[] { s_slovakCompare, "ch", "h", CompareOptions.None, false, 0 };
+                yield return new object[] { s_slovakCompare, "velmi chora", "hora", CompareOptions.None, false, 0 };
+            }
             yield return new object[] { s_slovakCompare, "chh", "H", CompareOptions.IgnoreCase, true, 1 };
 
             // Turkish
@@ -62,7 +59,7 @@ namespace System.Globalization.Tests
             yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", CompareOptions.Ordinal, false, 0 };
             yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", CompareOptions.OrdinalIgnoreCase, false, 0 };
             yield return new object[] { s_invariantCompare, "FooBar", "Foo\u0400Bar", CompareOptions.Ordinal, false, 0 };
-            yield return new object[] { s_invariantCompare, "FooBA\u0300R", "FooB\u00C0R", CompareOptions.IgnoreNonSpace, true, 7 };
+            yield return new object[] { s_invariantCompare, "FooBA\u0300R", "FooB\u00C0R", supportedIgnoreNonSpaceOption, true, 7 };
             yield return new object[] { s_invariantCompare, "o\u0308", "o", CompareOptions.None, false, 0 };
             yield return new object[] { s_invariantCompare, "o\u0308", "o", CompareOptions.Ordinal, false, 0 };
             yield return new object[] { s_invariantCompare, "o\u0308o", "o", CompareOptions.None, true, 1 };
@@ -83,18 +80,27 @@ namespace System.Globalization.Tests
             yield return new object[] { s_invariantCompare, "\uD800\uD800", "\uD800\uD800", CompareOptions.None, true, 2 };
 
             // Ignore symbols
-            yield return new object[] { s_invariantCompare, "More Test's", "Tests", CompareOptions.IgnoreSymbols, true, 6 };
-            yield return new object[] { s_invariantCompare, "More Test's", "Tests", CompareOptions.None, false, 0 };
+            if (!PlatformDetection.IsHybridGlobalizationOnBrowser)
+            {
+                yield return new object[] { s_invariantCompare, "More Test's", "Tests", CompareOptions.IgnoreSymbols, true, 6 };
+                yield return new object[] { s_invariantCompare, "More Test's", "Tests", CompareOptions.None, false, 0 };
+            }
 
             // NULL character
             yield return new object[] { s_invariantCompare, "a\u0000b", "a\u0000b", CompareOptions.None, true, 3 };
             yield return new object[] { s_invariantCompare, "a\u0000b", "b\u0000b", CompareOptions.None, false, 0 };
 
             // Platform differences
-            if (PlatformDetection.IsNlsGlobalization)
+            // in HybridGlobalization on Browser we use TextEncoder that is not supported for v8 and the manual decoding works like NLS
+            bool behavesLikeNls = PlatformDetection.IsNlsGlobalization || 
+                (PlatformDetection.IsHybridGlobalizationOnBrowser && !PlatformDetection.IsBrowserDomSupportedOrNodeJS);
+            if (behavesLikeNls)
             {
-                yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", CompareOptions.None, true, 7 };
-                yield return new object[] { s_frenchCompare, "\u0153", "oe", CompareOptions.None, true, 1 };
+                if (!PlatformDetection.IsHybridGlobalizationOnBrowser)
+                {
+                    yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", CompareOptions.None, true, 7 };
+                    yield return new object[] { s_frenchCompare, "\u0153", "oe", CompareOptions.None, true, 1 };
+                }
                 yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uDC00", CompareOptions.None, true, 1 };
                 yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uDC00", CompareOptions.IgnoreCase, true, 1 };
             } else
@@ -106,12 +112,15 @@ namespace System.Globalization.Tests
             }
 
             // Suffixes where matched length does not equal value string length
-            yield return new object[] { s_invariantCompare, "xyzdz", "\u01F3", CompareOptions.IgnoreNonSpace, true, 2 };
-            yield return new object[] { s_invariantCompare, "xyz\u01F3", "dz", CompareOptions.IgnoreNonSpace, true, 1 };
-            yield return new object[] { s_germanCompare, "xyz Strasse", "stra\u00DFe", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, true, 7 };
-            yield return new object[] { s_germanCompare, "xyz Strasse", "xtra\u00DFe", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, false, 0 };
-            yield return new object[] { s_germanCompare, "xyz stra\u00DFe", "Strasse", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, true, 6 };
-            yield return new object[] { s_germanCompare, "xyz stra\u00DFe", "Xtrasse", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, false, 0 };
+            yield return new object[] { s_germanCompare, "xyz Strasse", "xtra\u00DFe", supportedIgnoreCaseIgnoreNonSpaceOptions, false, 0 };
+            if (!PlatformDetection.IsHybridGlobalizationOnBrowser)
+            {
+                yield return new object[] { s_invariantCompare, "xyzdz", "\u01F3", supportedIgnoreNonSpaceOption, true, 2 };
+                yield return new object[] { s_invariantCompare, "xyz\u01F3", "dz", supportedIgnoreNonSpaceOption, true, 1 };
+                yield return new object[] { s_germanCompare, "xyz stra\u00DFe", "Strasse", supportedIgnoreCaseIgnoreNonSpaceOptions, true, 6 };
+                yield return new object[] { s_germanCompare, "xyz Strasse", "stra\u00DFe", supportedIgnoreCaseIgnoreNonSpaceOptions, true, 7 };
+            }
+            yield return new object[] { s_germanCompare, "xyz stra\u00DFe", "Xtrasse", supportedIgnoreCaseIgnoreNonSpaceOptions, false, 0 };
         }
 
         [Theory]
@@ -140,8 +149,11 @@ namespace System.Globalization.Tests
             valueBoundedMemory.MakeReadonly();
 
             Assert.Equal(expected, compareInfo.IsSuffix(sourceBoundedMemory.Span, valueBoundedMemory.Span, options));
-            Assert.Equal(expected, compareInfo.IsSuffix(sourceBoundedMemory.Span, valueBoundedMemory.Span, options, out int actualMatchLength));
-            Assert.Equal(expectedMatchLength, actualMatchLength);
+            if (!PlatformDetection.IsHybridGlobalizationOnBrowser)
+            {
+                Assert.Equal(expected, compareInfo.IsSuffix(sourceBoundedMemory.Span, valueBoundedMemory.Span, options, out int actualMatchLength));
+                Assert.Equal(expectedMatchLength, actualMatchLength);
+            }
         }
 
         [Fact]
@@ -151,7 +163,7 @@ namespace System.Globalization.Tests
             int expectedMatchLength = (result) ? 6 : 0;
 
             IsSuffix(s_invariantCompare, "FooBar", "Foo\uFFFFBar", CompareOptions.None, result, expectedMatchLength);
-            IsSuffix(s_invariantCompare, "FooBar", "Foo\uFFFFBar", CompareOptions.IgnoreNonSpace, result, expectedMatchLength);
+            IsSuffix(s_invariantCompare, "FooBar", "Foo\uFFFFBar", supportedIgnoreNonSpaceOption, result, expectedMatchLength);
         }
 
         [Fact]
index 617c13e..01983c6 100644 (file)
@@ -28,6 +28,7 @@ namespace System.Globalization.Tests
         protected static CompareInfo s_turkishCompare = new CultureInfo("tr-TR").CompareInfo;
         protected static CompareInfo s_japaneseCompare = new CultureInfo("ja-JP").CompareInfo;
         protected static CompareInfo s_slovakCompare = new CultureInfo("sk-SK").CompareInfo;
+        protected static CompareInfo s_frenchCompare = new CultureInfo("fr-FR").CompareInfo;
         protected static CompareOptions supportedIgnoreNonSpaceOption =
             PlatformDetection.IsHybridGlobalizationOnBrowser ?
             CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreKanaType :
index a65d844..b8a1948 100644 (file)
@@ -10,5 +10,7 @@
     <Compile Include="..\CompareInfo\CompareInfoTests.Compare.cs" />
     <Compile Include="..\CompareInfo\CompareInfoTests.cs" />
     <Compile Include="..\CompareInfo\CompareInfoTestsBase.cs" />
+    <Compile Include="..\CompareInfo\CompareInfoTests.IsPrefix.cs" />
+    <Compile Include="..\CompareInfo\CompareInfoTests.IsSuffix.cs" />
   </ItemGroup>
 </Project>
index 6865608..abe518d 100644 (file)
   <data name="PlatformNotSupported_AssemblyName_GetAssemblyName" xml:space="preserve">
     <value>AssemblyName.GetAssemblyName() is not supported on this platform.</value>
   </data>
+  <data name="PlatformNotSupported_HybridGlobalizationWithCompareOptions" xml:space="preserve">
+    <value>CompareOptions = {0} are not supported when HybridGlobalization=true. Disable it to load larger ICU bundle, then use this option.</value>
+  </data>
+  <data name="PlatformNotSupported_HybridGlobalizationWithCompareOptionsForCulture" xml:space="preserve">
+    <value>CompareOptions = {0} are not supported for culture = {1} when HybridGlobalization=true. Disable it to load larger ICU bundle, then use this option.</value>
+  </data>
+  <data name="PlatformNotSupported_HybridGlobalizationWithMatchLength" xml:space="preserve">
+    <value>Match length calculation is not supported when HybridGlobalization=true. Disable it to load larger ICU bundle, then use this function.</value>
+  </data>
   <data name="PlatformNotSupported_NonZeroLowerBound" xml:space="preserve">
     <value>Arrays with non-zero lower bounds are not supported.</value>
   </data>
index 3ca001a..474935f 100644 (file)
@@ -8,21 +8,37 @@ namespace System.Globalization
 {
     public partial class CompareInfo
     {
-        private unsafe int JsCompareString(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2, CompareOptions options)
+        private static void AssertHybridOnWasm(CompareOptions options)
         {
             Debug.Assert(!GlobalizationMode.Invariant);
             Debug.Assert(!GlobalizationMode.UseNls);
             Debug.Assert(GlobalizationMode.Hybrid);
             Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
+        }
 
-
+        private static void AssertComparisonSupported(CompareOptions options, string cultureName)
+        {
             if (CompareOptionsNotSupported(options))
                 throw new PlatformNotSupportedException(GetPNSE(options));
 
-            string cultureName = m_name;
+            if (CompareOptionsNotSupportedForCulture(options, cultureName))
+                throw new PlatformNotSupportedException(GetPNSEForCulture(options, cultureName));
+        }
+
+        private static void AssertIndexingSupported(CompareOptions options, string cultureName)
+        {
+            if (IndexingOptionsNotSupported(options) || CompareOptionsNotSupported(options))
+                throw new PlatformNotSupportedException(GetPNSE(options));
 
             if (CompareOptionsNotSupportedForCulture(options, cultureName))
                 throw new PlatformNotSupportedException(GetPNSEForCulture(options, cultureName));
+        }
+
+        private unsafe int JsCompareString(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2, CompareOptions options)
+        {
+            AssertHybridOnWasm(options);
+            string cultureName = m_name;
+            AssertComparisonSupported(options, cultureName);
 
             string exceptionMessage;
             int cmpResult;
@@ -38,14 +54,57 @@ namespace System.Globalization
             return cmpResult;
         }
 
+        private unsafe bool JsStartsWith(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options)
+        {
+            AssertHybridOnWasm(options);
+            Debug.Assert(!prefix.IsEmpty);
+            string cultureName = m_name;
+            AssertIndexingSupported(options, cultureName);
+
+            string exceptionMessage;
+            bool result;
+            fixed (char* pSource = &MemoryMarshal.GetReference(source))
+            fixed (char* pPrefix = &MemoryMarshal.GetReference(prefix))
+            {
+                result = Interop.JsGlobalization.StartsWith(out exceptionMessage, cultureName, pSource, source.Length, pPrefix, prefix.Length, options);
+            }
+
+            if (!string.IsNullOrEmpty(exceptionMessage))
+                throw new Exception(exceptionMessage);
+
+            return result;
+        }
+
+        private unsafe bool JsEndsWith(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options)
+        {
+            AssertHybridOnWasm(options);
+            Debug.Assert(!prefix.IsEmpty);
+            string cultureName = m_name;
+            AssertIndexingSupported(options, cultureName);
+
+            string exceptionMessage;
+            bool result;
+            fixed (char* pSource = &MemoryMarshal.GetReference(source))
+            fixed (char* pPrefix = &MemoryMarshal.GetReference(prefix))
+            {
+                result = Interop.JsGlobalization.EndsWith(out exceptionMessage, cultureName, pSource, source.Length, pPrefix, prefix.Length, options);
+            }
+
+            if (!string.IsNullOrEmpty(exceptionMessage))
+                throw new Exception(exceptionMessage);
+
+            return result;
+        }
+
+        private static bool IndexingOptionsNotSupported(CompareOptions options) =>
+            (options & CompareOptions.IgnoreSymbols) == CompareOptions.IgnoreSymbols;
+
         private static bool CompareOptionsNotSupported(CompareOptions options) =>
             (options & CompareOptions.IgnoreWidth) == CompareOptions.IgnoreWidth ||
             ((options & CompareOptions.IgnoreNonSpace) == CompareOptions.IgnoreNonSpace && (options & CompareOptions.IgnoreKanaType) != CompareOptions.IgnoreKanaType);
 
-
         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.";
-
+            SR.Format(SR.PlatformNotSupported_HybridGlobalizationWithCompareOptions, options);
 
         private static bool CompareOptionsNotSupportedForCulture(CompareOptions options, string cultureName) =>
             (options == CompareOptions.IgnoreKanaType &&
@@ -53,8 +112,7 @@ namespace System.Globalization
             (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.";
+            SR.Format(SR.PlatformNotSupported_HybridGlobalizationWithCompareOptions, options, cultureName);
     }
 }
index 3a3a5ba..f205901 100644 (file)
@@ -612,7 +612,12 @@ namespace System.Globalization
             else
             {
                 // Linguistic comparison requested and we don't need to special-case any args.
-
+#if TARGET_BROWSER
+                if (GlobalizationMode.Hybrid)
+                {
+                    throw new PlatformNotSupportedException(SR.PlatformNotSupported_HybridGlobalizationWithMatchLength);
+                }
+#endif
                 int tempMatchLength = 0;
                 matched = StartsWithCore(source, prefix, options, &tempMatchLength);
                 matchLength = tempMatchLength;
@@ -624,6 +629,10 @@ namespace System.Globalization
         private unsafe bool StartsWithCore(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options, int* matchLengthPtr) =>
             GlobalizationMode.UseNls ?
                 NlsStartsWith(source, prefix, options, matchLengthPtr) :
+#if TARGET_BROWSER
+            GlobalizationMode.Hybrid ?
+                JsStartsWith(source, prefix, options) :
+#endif
                 IcuStartsWith(source, prefix, options, matchLengthPtr);
 
         public bool IsPrefix(string source, string prefix)
@@ -750,7 +759,12 @@ namespace System.Globalization
             else
             {
                 // Linguistic comparison requested and we don't need to special-case any args.
-
+#if TARGET_BROWSER
+                if (GlobalizationMode.Hybrid)
+                {
+                    throw new PlatformNotSupportedException(SR.PlatformNotSupported_HybridGlobalizationWithMatchLength);
+                }
+#endif
                 int tempMatchLength = 0;
                 matched = EndsWithCore(source, suffix, options, &tempMatchLength);
                 matchLength = tempMatchLength;
@@ -767,6 +781,10 @@ namespace System.Globalization
         private unsafe bool EndsWithCore(ReadOnlySpan<char> source, ReadOnlySpan<char> suffix, CompareOptions options, int* matchLengthPtr) =>
             GlobalizationMode.UseNls ?
                 NlsEndsWith(source, suffix, options, matchLengthPtr) :
+#if TARGET_BROWSER
+            GlobalizationMode.Hybrid ?
+                JsEndsWith(source, suffix, options) :
+#endif
                 IcuEndsWith(source, suffix, options, matchLengthPtr);
 
         /// <summary>
index 608801c..16cc2c6 100644 (file)
@@ -23,7 +23,11 @@ namespace Sample
                 new TextInfoToTitleCase(),
                 new StringCompareMeasurement(),
                 new StringEqualsMeasurement(),
-                new CompareInfoMeasurement(),
+                new CompareInfoCompareMeasurement(),
+                new CompareInfoStartsWithMeasurement(),
+                new CompareInfoEndsWithMeasurement(),
+                new StringStartsWithMeasurement(),
+                new StringEndsWithMeasurement(),
             };
         }
 
@@ -124,16 +128,21 @@ namespace Sample
             public override void RunStep() => textInfo.ToTitleCase(str);
         }
 
-        public class StringsCompare : StringMeasurement
+        public abstract class StringsCompare : StringMeasurement
         {
-            protected string str2;
+            protected string strDifferentSuffix;
+            protected string strDifferentPrefix;
 
             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);
+                strDifferentSuffix = new string(data);
+                data[len-1] = originalLastChar;
+                data[0] = (char)random.Next(0x80);
+                strDifferentPrefix = new string(data);
             }
             public override string Name => "Strings Compare Base";
         }
@@ -149,7 +158,7 @@ namespace Sample
                 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, strDifferentSuffix, cultureInfo, CompareOptions.None);
         }
 
         public class StringEqualsMeasurement : StringsCompare
@@ -160,10 +169,10 @@ namespace Sample
                 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, strDifferentSuffix, StringComparison.InvariantCulture);
         }
 
-        public class CompareInfoMeasurement : StringsCompare
+        public class CompareInfoCompareMeasurement : StringsCompare
         {
             protected CompareInfo compareInfo;
 
@@ -174,7 +183,63 @@ namespace Sample
                 return Task.CompletedTask;
             }
             public override string Name => "CompareInfo Compare";
-            public override void RunStep() => compareInfo.Compare(str, str2);
+            public override void RunStep() => compareInfo.Compare(str, strDifferentSuffix);
+        }
+
+        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, strDifferentSuffix);
+        }
+
+        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, strDifferentPrefix);
+        }
+
+        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(strDifferentSuffix, 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(strDifferentPrefix, false, cultureInfo);
         }
     }
 }
index ded8b02..0b196d0 100644 (file)
@@ -46,6 +46,8 @@ 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 mono_bool mono_wasm_starts_with(MonoString **exceptionMessage, MonoString **culture, const uint16_t* str1, int32_t str1Length, const uint16_t* str2, int32_t str2Length, int32_t options);
+extern mono_bool mono_wasm_ends_with(MonoString **exceptionMessage, MonoString **culture, const uint16_t* str1, int32_t str1Length, const uint16_t* str2, int32_t str2Length, int32_t options);
 
 void bindings_initialize_internals (void)
 {
@@ -77,4 +79,6 @@ 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::StartsWith", mono_wasm_starts_with);
+       mono_add_internal_call ("Interop/JsGlobalization::EndsWith", mono_wasm_ends_with);
 }
index 2403db2..326f539 100644 (file)
@@ -101,6 +101,8 @@ let linked_functions = [
     "mono_wasm_change_case_invariant",
     "mono_wasm_change_case",
     "mono_wasm_compare_string",
+    "mono_wasm_starts_with",
+    "mono_wasm_ends_with",
 
     "icudt68_dat",
 ];
index ed75ea9..417cb51 100644 (file)
@@ -27,7 +27,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_ends_with, mono_wasm_starts_with } from "./hybrid-globalization";
 
 // the methods would be visible to EMCC linker
 // --- keep in sync with dotnet.cjs.lib.js ---
@@ -100,6 +100,8 @@ export function export_linker(): any {
         mono_wasm_change_case_invariant,
         mono_wasm_change_case,
         mono_wasm_compare_string,
+        mono_wasm_starts_with,
+        mono_wasm_ends_with,
 
         // threading exports, if threading is enabled
         ...mono_wasm_threads_exports,
@@ -1,12 +1,12 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-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, string_decoder } from "../strings";
-import { setU16 } from "../memory";
+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, string_decoder } from "./strings";
+import { setU16 } from "./memory";
 
 export function mono_wasm_change_case_invariant(exceptionMessage: Int32Ptr, src: number, srcLength: number, dst: number, dstLength: number, toUpper: number) : void{
     try{
@@ -51,7 +51,7 @@ function get_utf16_string(ptr: number, length: number): string{
     const view = new Uint16Array(Module.HEAPU16.buffer, ptr, length);
     let string = "";
     for (let i = 0; i < length; i++)
-        string += String.fromCharCode(view[i]);        
+        string += String.fromCharCode(view[i]);
     return string;
 }
 
@@ -77,13 +77,80 @@ export function mono_wasm_compare_string(exceptionMessage: Int32Ptr, culture: Mo
     }
 }
 
-export function pass_exception_details(ex: any, exceptionMessage: Int32Ptr){
+function pass_exception_details(ex: any, exceptionMessage: Int32Ptr){
     const exceptionJsString = ex.message + "\n" + ex.stack;
     const exceptionRoot = mono_wasm_new_external_root<MonoString>(<any>exceptionMessage);
     js_string_to_mono_string_root(exceptionJsString, exceptionRoot);
     exceptionRoot.release();
 }
 
+export function mono_wasm_starts_with(exceptionMessage: Int32Ptr, culture: MonoStringRef, str1: number, str1Length: number, str2: number, str2Length: number, options: number): number{
+    const cultureRoot = mono_wasm_new_external_root<MonoString>(culture);
+    try{
+        const cultureName = conv_string_root(cultureRoot);
+        const prefix = get_clean_string(str2, str2Length);
+        // no need to look for an empty string
+        if (prefix.length == 0)
+            return 1; // true
+
+        const source = get_clean_string(str1, str1Length);
+        if (source.length < prefix.length)
+            return 0; //false
+        const sourceOfPrefixLength = source.slice(0, prefix.length);
+
+        const casePicker = (options & 0x1f);
+        const locale = cultureName ? cultureName : undefined;
+        const result = compare_strings(sourceOfPrefixLength, prefix, locale, casePicker);
+        if (result == -2)
+            throw new Error("$Invalid comparison option.");
+        return result === 0 ? 1 : 0; // equals ? true : false
+    }
+    catch (ex: any) {
+        pass_exception_details(ex, exceptionMessage);
+        return -1;
+    }
+    finally {
+        cultureRoot.release();
+    }
+}
+
+export function mono_wasm_ends_with(exceptionMessage: Int32Ptr, culture: MonoStringRef, str1: number, str1Length: number, str2: number, str2Length: number, options: number): number{
+    const cultureRoot = mono_wasm_new_external_root<MonoString>(culture);
+    try{
+        const cultureName = conv_string_root(cultureRoot);
+        const suffix = get_clean_string(str2, str2Length);
+        if (suffix.length == 0)
+            return 1; // true
+
+        const source = get_clean_string(str1, str1Length);
+        const diff = source.length - suffix.length;
+        if (diff < 0)
+            return 0; //false
+        const sourceOfSuffixLength = source.slice(diff, source.length);
+
+        const casePicker = (options & 0x1f);
+        const locale = cultureName ? cultureName : undefined;
+        const result = compare_strings(sourceOfSuffixLength, suffix, locale, casePicker);
+        if (result == -2)
+            throw new Error("$Invalid comparison option.");
+        return result === 0 ? 1 : 0; // equals ? true : false
+    }
+    catch (ex: any) {
+        pass_exception_details(ex, exceptionMessage);
+        return -1;
+    }
+    finally {
+        cultureRoot.release();
+    }
+}
+
+function get_clean_string(strPtr: number, strLen: number)
+{
+    const str = string_decoder.decode(<any>strPtr, <any>(strPtr + 2*strLen));
+    const nStr = str.normalize();
+    return nStr.replace(/[\u200B-\u200D\uFEFF\0]/g, "");
+}
+
 export function compare_strings(string1: string, string2: string, locale: string | undefined, casePicker: number) : number{
     switch (casePicker)
     {