1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
6 using System.Diagnostics;
8 using System.Runtime.CompilerServices;
9 using System.Runtime.InteropServices;
11 namespace System.Globalization
13 public partial class CompareInfo
15 private unsafe void InitSort(CultureInfo culture)
17 _sortName = culture.SortName;
19 m_name = culture._name;
20 _sortName = culture.SortName;
24 _sortHandle = IntPtr.Zero;
28 const uint LCMAP_SORTHANDLE = 0x20000000;
30 int ret = Interop.Kernel32.LCMapStringEx(_sortName, LCMAP_SORTHANDLE, null, 0, &handle, IntPtr.Size, null, null, IntPtr.Zero);
31 _sortHandle = ret > 0 ? handle : IntPtr.Zero;
35 private static unsafe int FindStringOrdinal(
36 uint dwFindStringOrdinalFlags,
44 Debug.Assert(!GlobalizationMode.Invariant);
46 fixed (char* pSource = stringSource)
47 fixed (char* pValue = value)
49 int ret = Interop.Kernel32.FindStringOrdinal(
50 dwFindStringOrdinalFlags,
56 return ret < 0 ? ret : ret + offset;
60 private static unsafe int FindStringOrdinal(
61 uint dwFindStringOrdinalFlags,
62 ReadOnlySpan<char> source,
63 ReadOnlySpan<char> value,
66 Debug.Assert(!GlobalizationMode.Invariant);
68 fixed (char* pSource = &MemoryMarshal.GetReference(source))
69 fixed (char* pValue = &MemoryMarshal.GetReference(value))
71 int ret = Interop.Kernel32.FindStringOrdinal(
72 dwFindStringOrdinalFlags,
82 internal static int IndexOfOrdinalCore(string source, string value, int startIndex, int count, bool ignoreCase)
84 Debug.Assert(!GlobalizationMode.Invariant);
86 Debug.Assert(source != null);
87 Debug.Assert(value != null);
89 return FindStringOrdinal(FIND_FROMSTART, source, startIndex, count, value, value.Length, ignoreCase);
92 internal static int IndexOfOrdinalCore(ReadOnlySpan<char> source, ReadOnlySpan<char> value, bool ignoreCase)
94 Debug.Assert(!GlobalizationMode.Invariant);
96 Debug.Assert(source.Length != 0);
97 Debug.Assert(value.Length != 0);
99 return FindStringOrdinal(FIND_FROMSTART, source, value, ignoreCase);
102 internal static int LastIndexOfOrdinalCore(string source, string value, int startIndex, int count, bool ignoreCase)
104 Debug.Assert(!GlobalizationMode.Invariant);
106 Debug.Assert(source != null);
107 Debug.Assert(value != null);
109 return FindStringOrdinal(FIND_FROMEND, source, startIndex - count + 1, count, value, value.Length, ignoreCase);
112 private unsafe int GetHashCodeOfStringCore(string source, CompareOptions options)
114 Debug.Assert(!_invariantMode);
116 Debug.Assert(source != null);
117 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
119 if (source.Length == 0)
124 uint flags = LCMAP_SORTKEY | (uint)GetNativeCompareFlags(options);
126 fixed (char* pSource = source)
128 int sortKeyLength = Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
130 pSource, source.Length,
132 null, null, _sortHandle);
133 if (sortKeyLength == 0)
135 throw new ArgumentException(SR.Arg_ExternalException);
138 byte[] borrowedArr = null;
139 Span<byte> span = sortKeyLength <= 512 ?
140 stackalloc byte[512] :
141 (borrowedArr = ArrayPool<byte>.Shared.Rent(sortKeyLength));
143 fixed (byte* pSortKey = &MemoryMarshal.GetReference(span))
145 if (Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
147 pSource, source.Length,
149 null, null, _sortHandle) != sortKeyLength)
151 throw new ArgumentException(SR.Arg_ExternalException);
155 int hash = Marvin.ComputeHash32(span.Slice(0, sortKeyLength), Marvin.DefaultSeed);
157 // Return the borrowed array if necessary.
158 if (borrowedArr != null)
160 ArrayPool<byte>.Shared.Return(borrowedArr);
167 private static unsafe int CompareStringOrdinalIgnoreCase(char* string1, int count1, char* string2, int count2)
169 Debug.Assert(!GlobalizationMode.Invariant);
171 // Use the OS to compare and then convert the result to expected value by subtracting 2
172 return Interop.Kernel32.CompareStringOrdinal(string1, count1, string2, count2, true) - 2;
175 // TODO https://github.com/dotnet/coreclr/issues/13827:
176 // This method shouldn't be necessary, as we should be able to just use the overload
177 // that takes two spans. But due to this issue, that's adding significant overhead.
178 private unsafe int CompareString(ReadOnlySpan<char> string1, string string2, CompareOptions options)
180 Debug.Assert(string2 != null);
181 Debug.Assert(!_invariantMode);
182 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
184 string localeName = _sortHandle != IntPtr.Zero ? null : _sortName;
186 fixed (char* pLocaleName = localeName)
187 fixed (char* pString1 = &MemoryMarshal.GetReference(string1))
188 fixed (char* pString2 = &string2.GetRawStringData())
190 int result = Interop.Kernel32.CompareStringEx(
192 (uint)GetNativeCompareFlags(options),
203 Environment.FailFast("CompareStringEx failed");
206 // Map CompareStringEx return value to -1, 0, 1.
211 private unsafe int CompareString(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2, CompareOptions options)
213 Debug.Assert(!_invariantMode);
214 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
216 string localeName = _sortHandle != IntPtr.Zero ? null : _sortName;
218 fixed (char* pLocaleName = localeName)
219 fixed (char* pString1 = &MemoryMarshal.GetReference(string1))
220 fixed (char* pString2 = &MemoryMarshal.GetReference(string2))
222 int result = Interop.Kernel32.CompareStringEx(
224 (uint)GetNativeCompareFlags(options),
235 Environment.FailFast("CompareStringEx failed");
238 // Map CompareStringEx return value to -1, 0, 1.
243 private unsafe int FindString(
244 uint dwFindNLSStringFlags,
245 ReadOnlySpan<char> lpStringSource,
246 ReadOnlySpan<char> lpStringValue,
249 Debug.Assert(!_invariantMode);
251 string localeName = _sortHandle != IntPtr.Zero ? null : _sortName;
253 fixed (char* pLocaleName = localeName)
254 fixed (char* pSource = &MemoryMarshal.GetReference(lpStringSource))
255 fixed (char* pValue = &MemoryMarshal.GetReference(lpStringValue))
257 return Interop.Kernel32.FindNLSStringEx(
259 dwFindNLSStringFlags,
261 lpStringSource.Length,
263 lpStringValue.Length,
271 private unsafe int FindString(
272 uint dwFindNLSStringFlags,
273 string lpStringSource,
276 string lpStringValue,
281 Debug.Assert(!_invariantMode);
283 string localeName = _sortHandle != IntPtr.Zero ? null : _sortName;
285 fixed (char* pLocaleName = localeName)
286 fixed (char* pSource = lpStringSource)
287 fixed (char* pValue = lpStringValue)
289 char* pS = pSource + startSource;
290 char* pV = pValue + startValue;
292 return Interop.Kernel32.FindNLSStringEx(
294 dwFindNLSStringFlags,
306 internal unsafe int IndexOfCore(String source, String target, int startIndex, int count, CompareOptions options, int* matchLengthPtr)
308 Debug.Assert(!_invariantMode);
310 Debug.Assert(source != null);
311 Debug.Assert(target != null);
312 Debug.Assert((options & CompareOptions.OrdinalIgnoreCase) == 0);
314 if (target.Length == 0)
316 if (matchLengthPtr != null)
321 if (source.Length == 0)
326 if ((options & CompareOptions.Ordinal) != 0)
328 int retValue = FastIndexOfString(source, target, startIndex, count, target.Length, findLastIndex: false);
331 if (matchLengthPtr != null)
332 *matchLengthPtr = target.Length;
338 int retValue = FindString(FIND_FROMSTART | (uint)GetNativeCompareFlags(options), source, startIndex, count,
339 target, 0, target.Length, matchLengthPtr);
342 return retValue + startIndex;
349 internal unsafe int IndexOfCore(ReadOnlySpan<char> source, ReadOnlySpan<char> target, CompareOptions options, int* matchLengthPtr)
351 Debug.Assert(!_invariantMode);
353 Debug.Assert(source.Length != 0);
354 Debug.Assert(target.Length != 0);
355 Debug.Assert((options == CompareOptions.None || options == CompareOptions.IgnoreCase));
357 int retValue = FindString(FIND_FROMSTART | (uint)GetNativeCompareFlags(options), source, target, matchLengthPtr);
361 private unsafe int LastIndexOfCore(string source, string target, int startIndex, int count, CompareOptions options)
363 Debug.Assert(!_invariantMode);
365 Debug.Assert(!string.IsNullOrEmpty(source));
366 Debug.Assert(target != null);
367 Debug.Assert((options & CompareOptions.OrdinalIgnoreCase) == 0);
369 // TODO: Consider moving this up to the relevent APIs we need to ensure this behavior for
370 // and add a precondition that target is not empty.
371 if (target.Length == 0)
372 return startIndex; // keep Whidbey compatibility
374 if ((options & CompareOptions.Ordinal) != 0)
376 return FastIndexOfString(source, target, startIndex, count, target.Length, findLastIndex: true);
380 int retValue = FindString(FIND_FROMEND | (uint)GetNativeCompareFlags(options), source, startIndex - count + 1,
381 count, target, 0, target.Length, null);
385 return retValue + startIndex - (count - 1);
392 private unsafe bool StartsWith(string source, string prefix, CompareOptions options)
394 Debug.Assert(!_invariantMode);
396 Debug.Assert(!string.IsNullOrEmpty(source));
397 Debug.Assert(!string.IsNullOrEmpty(prefix));
398 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
400 return FindString(FIND_STARTSWITH | (uint)GetNativeCompareFlags(options), source, 0, source.Length,
401 prefix, 0, prefix.Length, null) >= 0;
404 private unsafe bool StartsWith(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options)
406 Debug.Assert(!_invariantMode);
408 Debug.Assert(!source.IsEmpty);
409 Debug.Assert(!prefix.IsEmpty);
410 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
412 return FindString(FIND_STARTSWITH | (uint)GetNativeCompareFlags(options), source, prefix, null) >= 0;
415 private unsafe bool EndsWith(string source, string suffix, CompareOptions options)
417 Debug.Assert(!_invariantMode);
419 Debug.Assert(!string.IsNullOrEmpty(source));
420 Debug.Assert(!string.IsNullOrEmpty(suffix));
421 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
423 return FindString(FIND_ENDSWITH | (uint)GetNativeCompareFlags(options), source, 0, source.Length,
424 suffix, 0, suffix.Length, null) >= 0;
427 private unsafe bool EndsWith(ReadOnlySpan<char> source, ReadOnlySpan<char> suffix, CompareOptions options)
429 Debug.Assert(!_invariantMode);
431 Debug.Assert(!source.IsEmpty);
432 Debug.Assert(!suffix.IsEmpty);
433 Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
435 return FindString(FIND_ENDSWITH | (uint)GetNativeCompareFlags(options), source, suffix, null) >= 0;
440 private IntPtr _sortHandle;
442 private const uint LCMAP_SORTKEY = 0x00000400;
443 private const uint LCMAP_HASH = 0x00040000;
445 private const int FIND_STARTSWITH = 0x00100000;
446 private const int FIND_ENDSWITH = 0x00200000;
447 private const int FIND_FROMSTART = 0x00400000;
448 private const int FIND_FROMEND = 0x00800000;
450 // TODO: Instead of this method could we just have upstack code call IndexOfOrdinal with ignoreCase = false?
451 private static unsafe int FastIndexOfString(string source, string target, int startIndex, int sourceCount, int targetCount, bool findLastIndex)
455 int sourceStartIndex = findLastIndex ? startIndex - sourceCount + 1 : startIndex;
457 fixed (char* pSource = source, spTarget = target)
459 char* spSubSource = pSource + sourceStartIndex;
463 int startPattern = (sourceCount - 1) - targetCount + 1;
464 if (startPattern < 0)
467 char patternChar0 = spTarget[0];
468 for (int ctrSrc = startPattern; ctrSrc >= 0; ctrSrc--)
470 if (spSubSource[ctrSrc] != patternChar0)
474 for (ctrPat = 1; ctrPat < targetCount; ctrPat++)
476 if (spSubSource[ctrSrc + ctrPat] != spTarget[ctrPat])
479 if (ctrPat == targetCount)
488 retValue += startIndex - sourceCount + 1;
493 int endPattern = (sourceCount - 1) - targetCount + 1;
497 char patternChar0 = spTarget[0];
498 for (int ctrSrc = 0; ctrSrc <= endPattern; ctrSrc++)
500 if (spSubSource[ctrSrc] != patternChar0)
503 for (ctrPat = 1; ctrPat < targetCount; ctrPat++)
505 if (spSubSource[ctrSrc + ctrPat] != spTarget[ctrPat])
508 if (ctrPat == targetCount)
517 retValue += startIndex;
525 private unsafe SortKey CreateSortKey(String source, CompareOptions options)
527 Debug.Assert(!_invariantMode);
529 if (source == null) { throw new ArgumentNullException(nameof(source)); }
531 if ((options & ValidSortkeyCtorMaskOffFlags) != 0)
533 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
536 byte [] keyData = null;
537 if (source.Length == 0)
539 keyData = Array.Empty<byte>();
543 uint flags = LCMAP_SORTKEY | (uint)GetNativeCompareFlags(options);
545 fixed (char *pSource = source)
547 int sortKeyLength = Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
549 pSource, source.Length,
551 null, null, _sortHandle);
552 if (sortKeyLength == 0)
554 throw new ArgumentException(SR.Arg_ExternalException);
557 keyData = new byte[sortKeyLength];
559 fixed (byte* pBytes = keyData)
561 if (Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
563 pSource, source.Length,
564 pBytes, keyData.Length,
565 null, null, _sortHandle) != sortKeyLength)
567 throw new ArgumentException(SR.Arg_ExternalException);
573 return new SortKey(Name, source, options, keyData);
576 private static unsafe bool IsSortable(char* text, int length)
578 Debug.Assert(!GlobalizationMode.Invariant);
580 return Interop.Kernel32.IsNLSDefinedString(Interop.Kernel32.COMPARE_STRING, 0, IntPtr.Zero, text, length);
583 private const int COMPARE_OPTIONS_ORDINAL = 0x40000000; // Ordinal
584 private const int NORM_IGNORECASE = 0x00000001; // Ignores case. (use LINGUISTIC_IGNORECASE instead)
585 private const int NORM_IGNOREKANATYPE = 0x00010000; // Does not differentiate between Hiragana and Katakana characters. Corresponding Hiragana and Katakana will compare as equal.
586 private const int NORM_IGNORENONSPACE = 0x00000002; // Ignores nonspacing. This flag also removes Japanese accent characters. (use LINGUISTIC_IGNOREDIACRITIC instead)
587 private const int NORM_IGNORESYMBOLS = 0x00000004; // Ignores symbols.
588 private const int NORM_IGNOREWIDTH = 0x00020000; // Does not differentiate between a single-byte character and the same character as a double-byte character.
589 private const int NORM_LINGUISTIC_CASING = 0x08000000; // use linguistic rules for casing
590 private const int SORT_STRINGSORT = 0x00001000; // Treats punctuation the same as symbols.
592 private static int GetNativeCompareFlags(CompareOptions options)
594 // Use "linguistic casing" by default (load the culture's casing exception tables)
595 int nativeCompareFlags = NORM_LINGUISTIC_CASING;
597 if ((options & CompareOptions.IgnoreCase) != 0) { nativeCompareFlags |= NORM_IGNORECASE; }
598 if ((options & CompareOptions.IgnoreKanaType) != 0) { nativeCompareFlags |= NORM_IGNOREKANATYPE; }
599 if ((options & CompareOptions.IgnoreNonSpace) != 0) { nativeCompareFlags |= NORM_IGNORENONSPACE; }
600 if ((options & CompareOptions.IgnoreSymbols) != 0) { nativeCompareFlags |= NORM_IGNORESYMBOLS; }
601 if ((options & CompareOptions.IgnoreWidth) != 0) { nativeCompareFlags |= NORM_IGNOREWIDTH; }
602 if ((options & CompareOptions.StringSort) != 0) { nativeCompareFlags |= SORT_STRINGSORT; }
604 // TODO: Can we try for GetNativeCompareFlags to never
605 // take Ordinal or OrdinalIgnoreCase. This value is not part of Win32, we just handle it special
607 // Suffix & Prefix shouldn't use this, make sure to turn off the NORM_LINGUISTIC_CASING flag
608 if (options == CompareOptions.Ordinal) { nativeCompareFlags = COMPARE_OPTIONS_ORDINAL; }
610 Debug.Assert(((options & ~(CompareOptions.IgnoreCase |
611 CompareOptions.IgnoreKanaType |
612 CompareOptions.IgnoreNonSpace |
613 CompareOptions.IgnoreSymbols |
614 CompareOptions.IgnoreWidth |
615 CompareOptions.StringSort)) == 0) ||
616 (options == CompareOptions.Ordinal), "[CompareInfo.GetNativeCompareFlags]Expected all flags to be handled");
618 return nativeCompareFlags;
621 private unsafe SortVersion GetSortVersion()
623 Debug.Assert(!_invariantMode);
625 Interop.Kernel32.NlsVersionInfoEx nlsVersion = new Interop.Kernel32.NlsVersionInfoEx();
626 nlsVersion.dwNLSVersionInfoSize = Marshal.SizeOf(typeof(Interop.Kernel32.NlsVersionInfoEx));
627 Interop.Kernel32.GetNLSVersionEx(Interop.Kernel32.COMPARE_STRING, _sortName, &nlsVersion);
628 return new SortVersion(
629 nlsVersion.dwNLSVersion,
630 nlsVersion.dwEffectiveId == 0 ? LCID : nlsVersion.dwEffectiveId,
631 nlsVersion.guidCustomVersion);