* 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.
`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`.
{
[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);
}
}
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
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
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 };
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 };
// 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]
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]
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]
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
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
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 };
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
}
// 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]
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]
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]
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 :
<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>
<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>
{
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;
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 &&
(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);
}
}
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;
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)
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;
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>
new TextInfoToTitleCase(),
new StringCompareMeasurement(),
new StringEqualsMeasurement(),
- new CompareInfoMeasurement(),
+ new CompareInfoCompareMeasurement(),
+ new CompareInfoStartsWithMeasurement(),
+ new CompareInfoEndsWithMeasurement(),
+ new StringStartsWithMeasurement(),
+ new StringEndsWithMeasurement(),
};
}
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";
}
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
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;
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);
}
}
}
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)
{
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);
}
"mono_wasm_change_case_invariant",
"mono_wasm_change_case",
"mono_wasm_compare_string",
+ "mono_wasm_starts_with",
+ "mono_wasm_ends_with",
"icudt68_dat",
];
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 ---
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,
// 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{
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;
}
}
}
-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)
{