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
;
7 using System
.Runtime
.InteropServices
;
9 using Internal
.Runtime
.CompilerServices
;
11 namespace System
.Globalization
13 public partial class CompareInfo
15 private unsafe void InitSort(CultureInfo culture
)
17 _sortName
= culture
.SortName
;
19 if (GlobalizationMode
.Invariant
)
21 _sortHandle
= IntPtr
.Zero
;
25 const uint LCMAP_SORTHANDLE
= 0x20000000;
28 int ret
= Interop
.Kernel32
.LCMapStringEx(_sortName
, LCMAP_SORTHANDLE
, null, 0, &handle
, IntPtr
.Size
, null, null, IntPtr
.Zero
);
29 _sortHandle
= ret
> 0 ? handle
: IntPtr
.Zero
;
33 private static unsafe int FindStringOrdinal(
34 uint dwFindStringOrdinalFlags
,
42 Debug
.Assert(!GlobalizationMode
.Invariant
);
43 Debug
.Assert(stringSource
!= null);
44 Debug
.Assert(value != null);
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
);
67 Debug
.Assert(!source
.IsEmpty
);
68 Debug
.Assert(!value.IsEmpty
);
70 fixed (char* pSource
= &MemoryMarshal
.GetReference(source
))
71 fixed (char* pValue
= &MemoryMarshal
.GetReference(value))
73 int ret
= Interop
.Kernel32
.FindStringOrdinal(
74 dwFindStringOrdinalFlags
,
84 internal static int IndexOfOrdinalCore(string source
, string value, int startIndex
, int count
, bool ignoreCase
)
86 Debug
.Assert(!GlobalizationMode
.Invariant
);
88 Debug
.Assert(source
!= null);
89 Debug
.Assert(value != null);
91 return FindStringOrdinal(FIND_FROMSTART
, source
, startIndex
, count
, value, value.Length
, ignoreCase
);
94 internal static int IndexOfOrdinalCore(ReadOnlySpan
<char> source
, ReadOnlySpan
<char> value, bool ignoreCase
, bool fromBeginning
)
96 Debug
.Assert(!GlobalizationMode
.Invariant
);
98 Debug
.Assert(source
.Length
!= 0);
99 Debug
.Assert(value.Length
!= 0);
101 uint positionFlag
= fromBeginning
? (uint)FIND_FROMSTART
: FIND_FROMEND
;
102 return FindStringOrdinal(positionFlag
, source
, value, ignoreCase
);
105 internal static int LastIndexOfOrdinalCore(string source
, string value, int startIndex
, int count
, bool ignoreCase
)
107 Debug
.Assert(!GlobalizationMode
.Invariant
);
109 Debug
.Assert(source
!= null);
110 Debug
.Assert(value != null);
112 return FindStringOrdinal(FIND_FROMEND
, source
, startIndex
- count
+ 1, count
, value, value.Length
, ignoreCase
);
115 private unsafe int GetHashCodeOfStringCore(ReadOnlySpan
<char> source
, CompareOptions options
)
117 Debug
.Assert(!GlobalizationMode
.Invariant
);
118 Debug
.Assert((options
& (CompareOptions
.Ordinal
| CompareOptions
.OrdinalIgnoreCase
)) == 0);
120 if (source
.Length
== 0)
125 uint flags
= LCMAP_SORTKEY
| (uint)GetNativeCompareFlags(options
);
127 fixed (char* pSource
= source
)
129 int sortKeyLength
= Interop
.Kernel32
.LCMapStringEx(_sortHandle
!= IntPtr
.Zero
? null : _sortName
,
131 pSource
, source
.Length
/* in chars */,
133 null, null, _sortHandle
);
134 if (sortKeyLength
== 0)
136 throw new ArgumentException(SR
.Arg_ExternalException
);
139 // Note in calls to LCMapStringEx below, the input buffer is specified in wchars (and wchar count),
140 // but the output buffer is specified in bytes (and byte count). This is because when generating
141 // sort keys, LCMapStringEx treats the output buffer as containing opaque binary data.
142 // See https://docs.microsoft.com/en-us/windows/desktop/api/winnls/nf-winnls-lcmapstringex.
144 byte[]? borrowedArr
= null;
145 Span
<byte> span
= sortKeyLength
<= 512 ?
146 stackalloc byte[512] :
147 (borrowedArr
= ArrayPool
<byte>.Shared
.Rent(sortKeyLength
));
149 fixed (byte* pSortKey
= &MemoryMarshal
.GetReference(span
))
151 if (Interop
.Kernel32
.LCMapStringEx(_sortHandle
!= IntPtr
.Zero
? null : _sortName
,
153 pSource
, source
.Length
/* in chars */,
154 pSortKey
, sortKeyLength
,
155 null, null, _sortHandle
) != sortKeyLength
)
157 throw new ArgumentException(SR
.Arg_ExternalException
);
161 int hash
= Marvin
.ComputeHash32(span
.Slice(0, sortKeyLength
), Marvin
.DefaultSeed
);
163 // Return the borrowed array if necessary.
164 if (borrowedArr
!= null)
166 ArrayPool
<byte>.Shared
.Return(borrowedArr
);
173 private static unsafe int CompareStringOrdinalIgnoreCase(ref char string1
, int count1
, ref char string2
, int count2
)
175 Debug
.Assert(!GlobalizationMode
.Invariant
);
177 fixed (char* char1
= &string1
)
178 fixed (char* char2
= &string2
)
180 // Use the OS to compare and then convert the result to expected value by subtracting 2
181 return Interop
.Kernel32
.CompareStringOrdinal(char1
, count1
, char2
, count2
, true) - 2;
185 // TODO https://github.com/dotnet/coreclr/issues/13827:
186 // This method shouldn't be necessary, as we should be able to just use the overload
187 // that takes two spans. But due to this issue, that's adding significant overhead.
188 private unsafe int CompareString(ReadOnlySpan
<char> string1
, string string2
, CompareOptions options
)
190 Debug
.Assert(string2
!= null);
191 Debug
.Assert(!GlobalizationMode
.Invariant
);
192 Debug
.Assert((options
& (CompareOptions
.Ordinal
| CompareOptions
.OrdinalIgnoreCase
)) == 0);
194 string? localeName
= _sortHandle
!= IntPtr
.Zero
? null : _sortName
;
196 fixed (char* pLocaleName
= localeName
)
197 fixed (char* pString1
= &MemoryMarshal
.GetReference(string1
))
198 fixed (char* pString2
= &string2
.GetRawStringData())
200 Debug
.Assert(pString1
!= null);
201 int result
= Interop
.Kernel32
.CompareStringEx(
203 (uint)GetNativeCompareFlags(options
),
214 throw new ArgumentException(SR
.Arg_ExternalException
);
217 // Map CompareStringEx return value to -1, 0, 1.
222 private unsafe int CompareString(ReadOnlySpan
<char> string1
, ReadOnlySpan
<char> string2
, CompareOptions options
)
224 Debug
.Assert(!GlobalizationMode
.Invariant
);
225 Debug
.Assert((options
& (CompareOptions
.Ordinal
| CompareOptions
.OrdinalIgnoreCase
)) == 0);
227 string? localeName
= _sortHandle
!= IntPtr
.Zero
? null : _sortName
;
229 fixed (char* pLocaleName
= localeName
)
230 fixed (char* pString1
= &MemoryMarshal
.GetReference(string1
))
231 fixed (char* pString2
= &MemoryMarshal
.GetReference(string2
))
233 Debug
.Assert(pString1
!= null);
234 Debug
.Assert(pString2
!= null);
235 int result
= Interop
.Kernel32
.CompareStringEx(
237 (uint)GetNativeCompareFlags(options
),
248 throw new ArgumentException(SR
.Arg_ExternalException
);
251 // Map CompareStringEx return value to -1, 0, 1.
256 private unsafe int FindString(
257 uint dwFindNLSStringFlags
,
258 ReadOnlySpan
<char> lpStringSource
,
259 ReadOnlySpan
<char> lpStringValue
,
262 Debug
.Assert(!GlobalizationMode
.Invariant
);
263 Debug
.Assert(!lpStringSource
.IsEmpty
);
264 Debug
.Assert(!lpStringValue
.IsEmpty
);
266 string? localeName
= _sortHandle
!= IntPtr
.Zero
? null : _sortName
;
268 fixed (char* pLocaleName
= localeName
)
269 fixed (char* pSource
= &MemoryMarshal
.GetReference(lpStringSource
))
270 fixed (char* pValue
= &MemoryMarshal
.GetReference(lpStringValue
))
272 return Interop
.Kernel32
.FindNLSStringEx(
274 dwFindNLSStringFlags
,
276 lpStringSource
.Length
,
278 lpStringValue
.Length
,
286 private unsafe int FindString(
287 uint dwFindNLSStringFlags
,
288 string lpStringSource
,
291 string lpStringValue
,
296 Debug
.Assert(!GlobalizationMode
.Invariant
);
297 Debug
.Assert(lpStringSource
!= null);
298 Debug
.Assert(lpStringValue
!= null);
300 string? localeName
= _sortHandle
!= IntPtr
.Zero
? null : _sortName
;
302 fixed (char* pLocaleName
= localeName
)
303 fixed (char* pSource
= lpStringSource
)
304 fixed (char* pValue
= lpStringValue
)
306 char* pS
= pSource
+ startSource
;
307 char* pV
= pValue
+ startValue
;
309 return Interop
.Kernel32
.FindNLSStringEx(
311 dwFindNLSStringFlags
,
323 internal unsafe int IndexOfCore(string source
, string target
, int startIndex
, int count
, CompareOptions options
, int* matchLengthPtr
)
325 Debug
.Assert(!GlobalizationMode
.Invariant
);
327 Debug
.Assert(source
!= null);
328 Debug
.Assert(target
!= null);
329 Debug
.Assert((options
& CompareOptions
.OrdinalIgnoreCase
) == 0);
330 Debug
.Assert((options
& CompareOptions
.Ordinal
) == 0);
332 int retValue
= FindString(FIND_FROMSTART
| (uint)GetNativeCompareFlags(options
), source
, startIndex
, count
,
333 target
, 0, target
.Length
, matchLengthPtr
);
336 return retValue
+ startIndex
;
342 internal unsafe int IndexOfCore(ReadOnlySpan
<char> source
, ReadOnlySpan
<char> target
, CompareOptions options
, int* matchLengthPtr
, bool fromBeginning
)
344 Debug
.Assert(!GlobalizationMode
.Invariant
);
346 Debug
.Assert(source
.Length
!= 0);
347 Debug
.Assert(target
.Length
!= 0);
348 Debug
.Assert((options
== CompareOptions
.None
|| options
== CompareOptions
.IgnoreCase
));
350 uint positionFlag
= fromBeginning
? (uint)FIND_FROMSTART
: FIND_FROMEND
;
351 return FindString(positionFlag
| (uint)GetNativeCompareFlags(options
), source
, target
, matchLengthPtr
);
354 private unsafe int LastIndexOfCore(string source
, string target
, int startIndex
, int count
, CompareOptions options
)
356 Debug
.Assert(!GlobalizationMode
.Invariant
);
358 Debug
.Assert(!string.IsNullOrEmpty(source
));
359 Debug
.Assert(target
!= null);
360 Debug
.Assert((options
& CompareOptions
.OrdinalIgnoreCase
) == 0);
362 if (target
.Length
== 0)
365 if ((options
& CompareOptions
.Ordinal
) != 0)
367 return FastLastIndexOfString(source
, target
, startIndex
, count
, target
.Length
);
371 int retValue
= FindString(FIND_FROMEND
| (uint)GetNativeCompareFlags(options
), source
, startIndex
- count
+ 1,
372 count
, target
, 0, target
.Length
, null);
376 return retValue
+ startIndex
- (count
- 1);
383 private unsafe bool StartsWith(string source
, string prefix
, CompareOptions options
)
385 Debug
.Assert(!GlobalizationMode
.Invariant
);
387 Debug
.Assert(!string.IsNullOrEmpty(source
));
388 Debug
.Assert(!string.IsNullOrEmpty(prefix
));
389 Debug
.Assert((options
& (CompareOptions
.Ordinal
| CompareOptions
.OrdinalIgnoreCase
)) == 0);
391 return FindString(FIND_STARTSWITH
| (uint)GetNativeCompareFlags(options
), source
, 0, source
.Length
,
392 prefix
, 0, prefix
.Length
, null) >= 0;
395 private unsafe bool StartsWith(ReadOnlySpan
<char> source
, ReadOnlySpan
<char> prefix
, CompareOptions options
)
397 Debug
.Assert(!GlobalizationMode
.Invariant
);
399 Debug
.Assert(!source
.IsEmpty
);
400 Debug
.Assert(!prefix
.IsEmpty
);
401 Debug
.Assert((options
& (CompareOptions
.Ordinal
| CompareOptions
.OrdinalIgnoreCase
)) == 0);
403 return FindString(FIND_STARTSWITH
| (uint)GetNativeCompareFlags(options
), source
, prefix
, null) >= 0;
406 private unsafe bool EndsWith(string source
, string suffix
, CompareOptions options
)
408 Debug
.Assert(!GlobalizationMode
.Invariant
);
410 Debug
.Assert(!string.IsNullOrEmpty(source
));
411 Debug
.Assert(!string.IsNullOrEmpty(suffix
));
412 Debug
.Assert((options
& (CompareOptions
.Ordinal
| CompareOptions
.OrdinalIgnoreCase
)) == 0);
414 return FindString(FIND_ENDSWITH
| (uint)GetNativeCompareFlags(options
), source
, 0, source
.Length
,
415 suffix
, 0, suffix
.Length
, null) >= 0;
418 private unsafe bool EndsWith(ReadOnlySpan
<char> source
, ReadOnlySpan
<char> suffix
, CompareOptions options
)
420 Debug
.Assert(!GlobalizationMode
.Invariant
);
422 Debug
.Assert(!source
.IsEmpty
);
423 Debug
.Assert(!suffix
.IsEmpty
);
424 Debug
.Assert((options
& (CompareOptions
.Ordinal
| CompareOptions
.OrdinalIgnoreCase
)) == 0);
426 return FindString(FIND_ENDSWITH
| (uint)GetNativeCompareFlags(options
), source
, suffix
, null) >= 0;
430 private const uint LCMAP_SORTKEY
= 0x00000400;
431 private const uint LCMAP_HASH
= 0x00040000;
433 private const int FIND_STARTSWITH
= 0x00100000;
434 private const int FIND_ENDSWITH
= 0x00200000;
435 private const int FIND_FROMSTART
= 0x00400000;
436 private const int FIND_FROMEND
= 0x00800000;
438 // TODO: Instead of this method could we just have upstack code call LastIndexOfOrdinal with ignoreCase = false?
439 private static unsafe int FastLastIndexOfString(string source
, string target
, int startIndex
, int sourceCount
, int targetCount
)
443 int sourceStartIndex
= startIndex
- sourceCount
+ 1;
445 fixed (char* pSource
= source
, spTarget
= target
)
447 char* spSubSource
= pSource
+ sourceStartIndex
;
449 int endPattern
= sourceCount
- targetCount
;
453 Debug
.Assert(target
.Length
>= 1);
454 char patternChar0
= spTarget
[0];
455 for (int ctrSrc
= endPattern
; ctrSrc
>= 0; ctrSrc
--)
457 if (spSubSource
[ctrSrc
] != patternChar0
)
461 for (ctrPat
= 1; ctrPat
< targetCount
; ctrPat
++)
463 if (spSubSource
[ctrSrc
+ ctrPat
] != spTarget
[ctrPat
])
466 if (ctrPat
== targetCount
)
475 retValue
+= startIndex
- sourceCount
+ 1;
482 private unsafe SortKey
CreateSortKey(string source
, CompareOptions options
)
484 Debug
.Assert(!GlobalizationMode
.Invariant
);
486 if (source
== null) { throw new ArgumentNullException(nameof(source)); }
488 if ((options
& ValidSortkeyCtorMaskOffFlags
) != 0)
490 throw new ArgumentException(SR
.Argument_InvalidFlag
, nameof(options
));
494 if (source
.Length
== 0)
496 keyData
= Array
.Empty
<byte>();
500 uint flags
= LCMAP_SORTKEY
| (uint)GetNativeCompareFlags(options
);
502 fixed (char *pSource
= source
)
504 int sortKeyLength
= Interop
.Kernel32
.LCMapStringEx(_sortHandle
!= IntPtr
.Zero
? null : _sortName
,
506 pSource
, source
.Length
,
508 null, null, _sortHandle
);
509 if (sortKeyLength
== 0)
511 throw new ArgumentException(SR
.Arg_ExternalException
);
514 keyData
= new byte[sortKeyLength
];
516 fixed (byte* pBytes
= keyData
)
518 if (Interop
.Kernel32
.LCMapStringEx(_sortHandle
!= IntPtr
.Zero
? null : _sortName
,
520 pSource
, source
.Length
,
521 pBytes
, keyData
.Length
,
522 null, null, _sortHandle
) != sortKeyLength
)
524 throw new ArgumentException(SR
.Arg_ExternalException
);
530 return new SortKey(Name
, source
, options
, keyData
);
533 private static unsafe bool IsSortable(char* text
, int length
)
535 Debug
.Assert(!GlobalizationMode
.Invariant
);
536 Debug
.Assert(text
!= null);
538 return Interop
.Kernel32
.IsNLSDefinedString(Interop
.Kernel32
.COMPARE_STRING
, 0, IntPtr
.Zero
, text
, length
);
541 private const int COMPARE_OPTIONS_ORDINAL
= 0x40000000; // Ordinal
542 private const int NORM_IGNORECASE
= 0x00000001; // Ignores case. (use LINGUISTIC_IGNORECASE instead)
543 private const int NORM_IGNOREKANATYPE
= 0x00010000; // Does not differentiate between Hiragana and Katakana characters. Corresponding Hiragana and Katakana will compare as equal.
544 private const int NORM_IGNORENONSPACE
= 0x00000002; // Ignores nonspacing. This flag also removes Japanese accent characters. (use LINGUISTIC_IGNOREDIACRITIC instead)
545 private const int NORM_IGNORESYMBOLS
= 0x00000004; // Ignores symbols.
546 private const int NORM_IGNOREWIDTH
= 0x00020000; // Does not differentiate between a single-byte character and the same character as a double-byte character.
547 private const int NORM_LINGUISTIC_CASING
= 0x08000000; // use linguistic rules for casing
548 private const int SORT_STRINGSORT
= 0x00001000; // Treats punctuation the same as symbols.
550 private static int GetNativeCompareFlags(CompareOptions options
)
552 // Use "linguistic casing" by default (load the culture's casing exception tables)
553 int nativeCompareFlags
= NORM_LINGUISTIC_CASING
;
555 if ((options
& CompareOptions
.IgnoreCase
) != 0) { nativeCompareFlags |= NORM_IGNORECASE; }
556 if ((options
& CompareOptions
.IgnoreKanaType
) != 0) { nativeCompareFlags |= NORM_IGNOREKANATYPE; }
557 if ((options
& CompareOptions
.IgnoreNonSpace
) != 0) { nativeCompareFlags |= NORM_IGNORENONSPACE; }
558 if ((options
& CompareOptions
.IgnoreSymbols
) != 0) { nativeCompareFlags |= NORM_IGNORESYMBOLS; }
559 if ((options
& CompareOptions
.IgnoreWidth
) != 0) { nativeCompareFlags |= NORM_IGNOREWIDTH; }
560 if ((options
& CompareOptions
.StringSort
) != 0) { nativeCompareFlags |= SORT_STRINGSORT; }
562 // TODO: Can we try for GetNativeCompareFlags to never
563 // take Ordinal or OrdinalIgnoreCase. This value is not part of Win32, we just handle it special
565 // Suffix & Prefix shouldn't use this, make sure to turn off the NORM_LINGUISTIC_CASING flag
566 if (options
== CompareOptions
.Ordinal
) { nativeCompareFlags = COMPARE_OPTIONS_ORDINAL; }
568 Debug
.Assert(((options
& ~
(CompareOptions
.IgnoreCase
|
569 CompareOptions
.IgnoreKanaType
|
570 CompareOptions
.IgnoreNonSpace
|
571 CompareOptions
.IgnoreSymbols
|
572 CompareOptions
.IgnoreWidth
|
573 CompareOptions
.StringSort
)) == 0) ||
574 (options
== CompareOptions
.Ordinal
), "[CompareInfo.GetNativeCompareFlags]Expected all flags to be handled");
576 return nativeCompareFlags
;
579 private unsafe SortVersion
GetSortVersion()
581 Debug
.Assert(!GlobalizationMode
.Invariant
);
583 Interop
.Kernel32
.NlsVersionInfoEx nlsVersion
= new Interop
.Kernel32
.NlsVersionInfoEx();
584 nlsVersion
.dwNLSVersionInfoSize
= sizeof(Interop
.Kernel32
.NlsVersionInfoEx
);
585 Interop
.Kernel32
.GetNLSVersionEx(Interop
.Kernel32
.COMPARE_STRING
, _sortName
, &nlsVersion
);
586 return new SortVersion(
587 nlsVersion
.dwNLSVersion
,
588 nlsVersion
.dwEffectiveId
== 0 ? LCID
: nlsVersion
.dwEffectiveId
,
589 nlsVersion
.guidCustomVersion
);