Assert.Equal(2, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Assert.Equal(5, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Assert.Equal(-1, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
if (value.Length == 0)
{
- int expectedIndex = s.Length > 0 ? s.Length - 1 : 0;
- int expectedStartIndex = startIndex == s.Length ? startIndex - 1 : startIndex;
+ int expectedStartIndex = startIndex;
if (s.Length == 0 && (startIndex == -1 || startIndex == 0))
- expectedStartIndex = (value.Length == 0) ? 0 : -1;
- Assert.Equal(expectedIndex, s.LastIndexOf(value, comparison));
+ expectedStartIndex = 0; // empty string occurs at beginning of search space
+ if (s.Length > 0 && startIndex < s.Length)
+ expectedStartIndex = startIndex + 1; // empty string occurs just after the last char included in the search space
+
+ Assert.Equal(s.Length, s.LastIndexOf(value, comparison));
Assert.Equal(expectedStartIndex, s.LastIndexOf(value, startIndex, comparison));
- Assert.Equal(expectedIndex, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
+ Assert.Equal(s.Length, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
return;
}
if (s.Length == 0)
{
+ // unit test shouldn't have passed a weightless string to this routine
+ Assert.NotEqual(value, string.Empty, StringComparer.FromComparison(comparison));
+
Assert.Equal(-1, s.LastIndexOf(value, comparison));
Assert.Equal(-1, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(-1, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
}
[Theory]
- [InlineData("foo", 2)]
- [InlineData("hello", 4)]
+ [InlineData("foo", 3)]
+ [InlineData("hello", 5)]
[InlineData("", 0)]
public static void LastIndexOf_EmptyString(string s, int expected)
{
string s1 = "0172377457778667789";
string s2 = string.Empty;
int index = s1.LastIndexOf(s2);
- Assert.Equal(s1.Length - 1, index);
+ Assert.Equal(s1.Length, index);
- // A zero-length value is always "found" at the start of the span.
+ // A zero-length value is always "found" at the end of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
- Assert.Equal(0, index);
+ Assert.Equal(span.Length, index);
}
[Fact]
int index = s1.LastIndexOf(s2);
Assert.Equal(2, index);
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
int index = s1.LastIndexOf(s2);
Assert.Equal(-1, index);
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
public static IEnumerable<object[]> LastIndexOf_TestData()
{
// Empty strings
- yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 2 };
+ yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 3 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, CompareOptions.None, 0 };
yield return new object[] { s_invariantCompare, "", "a", 0, 0, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "", "", -1, 0, CompareOptions.None, 0 };
yield return new object[] { s_invariantCompare, "Hello", "b", 5, 5, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 5, 0, CompareOptions.None, -1 };
- yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 4 };
- yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 4 };
+ yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 5 };
+ yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 5 };
// OrdinalIgnoreCase
yield return new object[] { s_invariantCompare, "Hello", "l", 4, 5, CompareOptions.OrdinalIgnoreCase, 3 };
// Use LastIndexOf(string, string, int, int, CompareOptions)
Assert.Equal(expected, compareInfo.LastIndexOf(source, value, startIndex, count, options));
+ // Fixup offsets so that we can call the span-based APIs.
+
+ ReadOnlySpan<char> sourceSpan;
+ int adjustmentFactor; // number of chars to add to retured index from span-based APIs
+
+ if (startIndex == source.Length - 1 && count == source.Length)
+ {
+ // This idiom means "read the whole span"
+ sourceSpan = source;
+ adjustmentFactor = 0;
+ }
+ else if (startIndex == source.Length)
+ {
+ // Account for possible off-by-one at the call site
+ sourceSpan = source.AsSpan()[^(Math.Max(0, count - 1))..];
+ adjustmentFactor = source.Length - sourceSpan.Length;
+ }
+ else
+ {
+ // Bump 'startIndex' by 1, then go back 'count' chars
+ sourceSpan = source.AsSpan()[..(startIndex + 1)][^count..];
+ adjustmentFactor = startIndex + 1 - count;
+ }
+
+ if (expected < 0) { adjustmentFactor = 0; } // don't modify "not found" (-1) return values
+
if ((compareInfo == s_invariantCompare) && ((options == CompareOptions.None) || (options == CompareOptions.IgnoreCase)))
{
StringComparison stringComparison = (options == CompareOptions.IgnoreCase) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
Assert.Equal(expected, source.LastIndexOf(value, startIndex, count, stringComparison));
// Use int MemoryExtensions.LastIndexOf(this ReadOnlySpan<char>, ReadOnlySpan<char>, StringComparison)
- // Filter differences betweeen string-based and Span-based LastIndexOf
- // - Empty value handling - https://github.com/dotnet/runtime/issues/13382
- // - Negative count
- if (value.Length == 0 || count < 0)
- return;
-
- if (startIndex == source.Length)
- {
- startIndex--;
- if (count > 0)
- count--;
- }
- int leftStartIndex = (startIndex - count + 1);
- Assert.Equal((expected == -1) ? -1 : (expected - leftStartIndex), source.AsSpan(leftStartIndex, count).LastIndexOf(value.AsSpan(), stringComparison));
+ Assert.Equal(expected - adjustmentFactor, sourceSpan.LastIndexOf(value.AsSpan(), stringComparison));
}
}
public static IEnumerable<object[]> IndexOf_TestData()
{
- yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 0 };
+ yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 1 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "Hello", "l", 0, 2, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 3, 3, 3 };
public static IEnumerable<object[]> IsSortable_TestData()
{
- yield return new object[] { "", false, false };
- yield return new object[] { "abcdefg", false, true };
- yield return new object[] { "\uD800\uDC00", true, true };
- yield return new object[] { "\uD800\uD800", true, false };
+ yield return new object[] { "", false };
+ yield return new object[] { "abcdefg", true };
+ yield return new object[] { "\uD800\uDC00", true };
+
+ // VS test runner for xunit doesn't handle ill-formed UTF-16 strings properly.
+ // We'll send this one through as an array to avoid U+FFFD substitution.
+
+ yield return new object[] { new char[] { '\uD800', '\uD800' }, false };
}
[Theory]
public void IndexOfTest(CompareInfo compareInfo, string source, string value, int startIndex, int indexOfExpected, int lastIndexOfExpected)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value, startIndex));
- if (value.Length > 0)
+ if (value.Length == 1)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value[0], startIndex));
}
Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value, startIndex));
- if (value.Length > 0)
+ if (value.Length == 1)
{
Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value[0], startIndex));
}
[Theory]
[MemberData(nameof(IsSortable_TestData))]
- public void IsSortableTest(string source, bool hasSurrogate, bool expected)
+ public void IsSortableTest(object sourceObj, bool expected)
{
+ string source = sourceObj as string ?? new string((char[])sourceObj);
Assert.Equal(expected, CompareInfo.IsSortable(source));
- bool charExpectedResults = hasSurrogate ? false : expected;
+ // If the string as a whole is sortable, then all chars which aren't standalone
+ // surrogate halves must also be sortable.
+
foreach (char c in source)
- Assert.Equal(charExpectedResults, CompareInfo.IsSortable(c));
+ Assert.Equal(expected && !char.IsSurrogate(c), CompareInfo.IsSortable(c));
}
[Fact]
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Buffers;
using System.Collections.Generic;
using System.Collections;
using System.Text;
public static IEnumerable<object[]> LastIndexOf_TestData()
{
// Empty strings
- yield return new object[] { "foo", "", 2, 3, CompareOptions.None, 2 };
+ yield return new object[] { "foo", "", 2, 3, CompareOptions.None, 3 };
yield return new object[] { "", "", 0, 0, CompareOptions.None, 0 };
yield return new object[] { "", "a", 0, 0, CompareOptions.None, -1 };
yield return new object[] { "", "", -1, 0, CompareOptions.None, 0 };
yield return new object[] { "Hello", "b", 5, 5, CompareOptions.None, -1 };
yield return new object[] { "Hello", "l", 5, 0, CompareOptions.None, -1 };
- yield return new object[] { "Hello", "", 5, 5, CompareOptions.None, 4 };
- yield return new object[] { "Hello", "", 5, 0, CompareOptions.None, 4 };
+ yield return new object[] { "Hello", "", 5, 5, CompareOptions.None, 5 };
+ yield return new object[] { "Hello", "", 5, 0, CompareOptions.None, 5 };
// OrdinalIgnoreCase
yield return new object[] { "Hello", "l", 4, 5, CompareOptions.OrdinalIgnoreCase, 3 };
yield return new object[] { "foo", "", CompareOptions.None, true };
yield return new object[] { "", "", CompareOptions.None, true };
+ // Early exit for empty values before 'options' is validated
+ yield return new object[] { "hello", "", (CompareOptions)(-1), true };
+
// Long strings
yield return new object[] { new string('a', 5555), "aaaaaaaaaaaaaaa", CompareOptions.None, true };
yield return new object[] { new string('a', 5555), new string('a', 5000), CompareOptions.None, true };
yield return new object[] { "foo", "", CompareOptions.None, true };
yield return new object[] { "", "", CompareOptions.None, true };
+ // Early exit for empty values before 'options' is validated
+ yield return new object[] { "hello", "", (CompareOptions)(-1), true };
+
// Long strings
yield return new object[] { new string('a', 5555), "aaaaaaaaaaaaaaa", CompareOptions.None, true };
yield return new object[] { new string('a', 5555), new string('a', 5000), CompareOptions.None, true };
{
Assert.Equal(result, CultureInfo.GetCultureInfo(cul).CompareInfo.IsPrefix(source, value, options));
Assert.Equal(result, source.StartsWith(value, GetStringComparison(options)));
- Assert.Equal(result, source.AsSpan().StartsWith(value.AsSpan(), GetStringComparison(options)));
+
+ // Span versions - using BoundedMemory to check for buffer overruns
+
+ using BoundedMemory<char> sourceBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(source);
+ sourceBoundedMemory.MakeReadonly();
+ ReadOnlySpan<char> sourceBoundedSpan = sourceBoundedMemory.Span;
+
+ using BoundedMemory<char> valueBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(value);
+ valueBoundedMemory.MakeReadonly();
+ ReadOnlySpan<char> valueBoundedSpan = valueBoundedMemory.Span;
+
+ Assert.Equal(result, sourceBoundedSpan.StartsWith(valueBoundedSpan, GetStringComparison(options)));
}
}
{
Assert.Equal(result, CultureInfo.GetCultureInfo(cul).CompareInfo.IsSuffix(source, value, options));
Assert.Equal(result, source.EndsWith(value, GetStringComparison(options)));
- Assert.Equal(result, source.AsSpan().EndsWith(value.AsSpan(), GetStringComparison(options)));
+
+ // Span versions - using BoundedMemory to check for buffer overruns
+
+ using BoundedMemory<char> sourceBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(source);
+ sourceBoundedMemory.MakeReadonly();
+ ReadOnlySpan<char> sourceBoundedSpan = sourceBoundedMemory.Span;
+
+ using BoundedMemory<char> valueBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(value);
+ valueBoundedMemory.MakeReadonly();
+ ReadOnlySpan<char> valueBoundedSpan = valueBoundedMemory.Span;
+
+ Assert.Equal(result, sourceBoundedSpan.EndsWith(valueBoundedSpan, GetStringComparison(options)));
}
}
+ [Theory]
+ [InlineData("", false)]
+ [InlineData('x', true)]
+ [InlineData('\ud800', true)] // standalone high surrogate
+ [InlineData("hello", true)]
+ public void TestIsSortable(object sourceObj, bool expectedResult)
+ {
+ if (sourceObj is string s)
+ {
+ Assert.Equal(expectedResult, CompareInfo.IsSortable(s));
+ }
+ else
+ {
+ Assert.Equal(expectedResult, CompareInfo.IsSortable((char)sourceObj));
+ }
+ }
[Theory]
[MemberData(nameof(Compare_TestData))]
foreach (string cul in s_cultureNames)
{
int res = CultureInfo.GetCultureInfo(cul).CompareInfo.Compare(source, value, options);
- if (res < 0) res = -1;
- if (res > 0) res = 1;
- Assert.Equal(result, res);
+ Assert.Equal(result, Math.Sign(res));
+
res = string.Compare(source, value, GetStringComparison(options));
- if (res < 0) res = -1;
- if (res > 0) res = 1;
- Assert.Equal(result, res);
+ Assert.Equal(result, Math.Sign(res));
+
+ // Span versions - using BoundedMemory to check for buffer overruns
+
+ using BoundedMemory<char> sourceBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(source);
+ sourceBoundedMemory.MakeReadonly();
+ ReadOnlySpan<char> sourceBoundedSpan = sourceBoundedMemory.Span;
+
+ using BoundedMemory<char> valueBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(value);
+ valueBoundedMemory.MakeReadonly();
+ ReadOnlySpan<char> valueBoundedSpan = valueBoundedMemory.Span;
+
+ res = sourceBoundedSpan.CompareTo(valueBoundedSpan, GetStringComparison(options));
+ Assert.Equal(result, Math.Sign(res));
}
}
[Fact]
public static void IndexOfSequenceLengthOneValue()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 2, 3, 4, 5 });
ReadOnlySpan<int> value = new ReadOnlySpan<int>(new int[] { 2 });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValueAtVeryEnd()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 2, 3, 4, 5 });
ReadOnlySpan<int> value = new ReadOnlySpan<int>(new int[] { 5 });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
ReadOnlySpan<int> value = new ReadOnlySpan<int>(new int[] { 5 });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValue_String()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<string> span = new ReadOnlySpan<string>(new string[] { "0", "1", "2", "3", "4", "5" });
ReadOnlySpan<string> value = new ReadOnlySpan<string>(new string[] { "2" });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValueAtVeryEnd_String()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<string> span = new ReadOnlySpan<string>(new string[] { "0", "1", "2", "3", "4", "5" });
ReadOnlySpan<string> value = new ReadOnlySpan<string>(new string[] { "5" });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_String()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<string> span = new ReadOnlySpan<string>(new string[] { "0", "1", "2", "3", "4", "5" }, 0, 5);
ReadOnlySpan<string> value = new ReadOnlySpan<string>(new string[] { "5" });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValue_Byte()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(new byte[] { 2 });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValueAtVeryEnd_Byte()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(new byte[] { 5 });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_Byte()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(new byte[] { 5 });
int index = span.IndexOf(value);
[Fact]
public static void LastIndexOfSequenceZeroLengthValue()
{
- // A zero-length value is always "found" at the start of the span.
+ // A zero-length value is always "found" at the end of the span.
ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 77, 2, 3, 77, 77, 4, 5, 77, 77, 77, 88, 6, 6, 77, 77, 88, 9 });
ReadOnlySpan<int> value = new ReadOnlySpan<int>(Array.Empty<int>());
int index = span.LastIndexOf(value);
- Assert.Equal(0, index);
+ Assert.Equal(span.Length, index);
}
[Fact]
[Fact]
public static void LastIndexOfSequenceLengthOneValue()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 2, 3, 4, 5 });
ReadOnlySpan<int> value = new ReadOnlySpan<int>(new int[] { 2 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueAtVeryEnd()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 2, 3, 4, 5 });
ReadOnlySpan<int> value = new ReadOnlySpan<int>(new int[] { 5 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueMultipleTimes()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 5, 3, 4, 5 });
ReadOnlySpan<int> value = new ReadOnlySpan<int>(new int[] { 5 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<int> span = new ReadOnlySpan<int>(new int[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
ReadOnlySpan<int> value = new ReadOnlySpan<int>(new int[] { 5 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceZeroLengthValue_String()
{
- // A zero-length value is always "found" at the start of the span.
+ // A zero-length value is always "found" at the end of the span.
ReadOnlySpan<string> span = new ReadOnlySpan<string>(new string[] { "0", "1", "77", "2", "3", "77", "77", "4", "5", "77", "77", "77", "88", "6", "6", "77", "77", "88", "9" });
ReadOnlySpan<string> value = new ReadOnlySpan<string>(Array.Empty<string>());
int index = span.LastIndexOf(value);
- Assert.Equal(0, index);
+ Assert.Equal(span.Length, index);
}
[Fact]
[Fact]
public static void LastIndexOfSequenceLengthOneValue_String()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<string> span = new ReadOnlySpan<string>(new string[] { "0", "1", "2", "3", "4", "5" });
ReadOnlySpan<string> value = new ReadOnlySpan<string>(new string[] { "2" });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_String()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<string> span = new ReadOnlySpan<string>(new string[] { "0", "1", "2", "3", "4", "5" });
ReadOnlySpan<string> value = new ReadOnlySpan<string>(new string[] { "5" });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_String()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<string> span = new ReadOnlySpan<string>(new string[] { "0", "1", "2", "3", "4", "5" }, 0, 5);
ReadOnlySpan<string> value = new ReadOnlySpan<string>(new string[] { "5" });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceZeroLengthValue_Byte()
{
- // A zero-length value is always "found" at the start of the span.
+ // A zero-length value is always "found" at the end of the span.
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 77, 2, 3, 77, 77, 4, 5, 77, 77, 77, 88, 6, 6, 77, 77, 88, 9 });
ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(Array.Empty<byte>());
int index = span.LastIndexOf(value);
- Assert.Equal(0, index);
+ Assert.Equal(span.Length, index);
}
[Fact]
[Fact]
public static void LastIndexOfSequenceLengthOneValue_Byte()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(new byte[] { 2 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_Byte()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(new byte[] { 5 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueMultipleTimes_Byte()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 5, 3, 4, 5 });
ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(new byte[] { 5 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_Byte()
{
- // A zero-length value is always "found" at the start of the span.
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(new byte[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
ReadOnlySpan<byte> value = new ReadOnlySpan<byte>(new byte[] { 5 });
int index = span.LastIndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValue()
{
- // A zero-length value is always "found" at the start of the span.
Span<int> span = new Span<int>(new int[] { 0, 1, 2, 3, 4, 5 });
Span<int> value = new Span<int>(new int[] { 2 });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValueAtVeryEnd()
{
- // A zero-length value is always "found" at the start of the span.
Span<int> span = new Span<int>(new int[] { 0, 1, 2, 3, 4, 5 });
Span<int> value = new Span<int>(new int[] { 5 });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd()
{
- // A zero-length value is always "found" at the start of the span.
Span<int> span = new Span<int>(new int[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
Span<int> value = new Span<int>(new int[] { 5 });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValue_String()
{
- // A zero-length value is always "found" at the start of the span.
Span<string> span = new Span<string>(new string[] { "0", "1", "2", "3", "4", "5" });
Span<string> value = new Span<string>(new string[] { "2" });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValueAtVeryEnd_String()
{
- // A zero-length value is always "found" at the start of the span.
Span<string> span = new Span<string>(new string[] { "0", "1", "2", "3", "4", "5" });
Span<string> value = new Span<string>(new string[] { "5" });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_String()
{
- // A zero-length value is always "found" at the start of the span.
Span<string> span = new Span<string>(new string[] { "0", "1", "2", "3", "4", "5" }, 0, 5);
Span<string> value = new Span<string>(new string[] { "5" });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValue_Byte()
{
- // A zero-length value is always "found" at the start of the span.
Span<byte> span = new Span<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
Span<byte> value = new Span<byte>(new byte[] { 2 });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValueAtVeryEnd_Byte()
{
- // A zero-length value is always "found" at the start of the span.
Span<byte> span = new Span<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
Span<byte> value = new Span<byte>(new byte[] { 5 });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_Byte()
{
- // A zero-length value is always "found" at the start of the span.
Span<byte> span = new Span<byte>(new byte[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
Span<byte> value = new Span<byte>(new byte[] { 5 });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValue_Char()
{
- // A zero-length value is always "found" at the start of the span.
Span<char> span = new Span<char>(new char[] { '0', '1', '2', '3', '4', '5' });
Span<char> value = new Span<char>(new char[] { '2' });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValueAtVeryEnd_Char()
{
- // A zero-length value is always "found" at the start of the span.
Span<char> span = new Span<char>(new char[] { '0', '1', '2', '3', '4', '5' });
Span<char> value = new Span<char>(new char[] { '5' });
int index = span.IndexOf(value);
[Fact]
public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
{
- // A zero-length value is always "found" at the start of the span.
Span<char> span = new Span<char>(new char[] { '0', '1', '2', '3', '4', '5' }, 0, 5);
Span<char> value = new Span<char>(new char[] { '5' });
int index = span.IndexOf(value);
[Fact]
public static void LastIndexOfSequenceZeroLengthValue()
{
- // A zero-length value is always "found" at the start of the span.
+ // A zero-length value is always "found" at the end of the span.
Span<int> span = new Span<int>(new int[] { 0, 1, 77, 2, 3, 77, 77, 4, 5, 77, 77, 77, 88, 6, 6, 77, 77, 88, 9 });
Span<int> value = new Span<int>(Array.Empty<int>());
int index = span.LastIndexOf(value);
- Assert.Equal(0, index);
+ Assert.Equal(span.Length, index);
}
[Fact]
[Fact]
public static void LastIndexOfSequenceLengthOneValue()
{
- // A zero-length value is always "found" at the start of the span.
Span<int> span = new Span<int>(new int[] { 0, 1, 2, 3, 4, 5 });
Span<int> value = new Span<int>(new int[] { 2 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueAtVeryEnd()
{
- // A zero-length value is always "found" at the start of the span.
Span<int> span = new Span<int>(new int[] { 0, 1, 2, 3, 4, 5 });
Span<int> value = new Span<int>(new int[] { 5 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueMultipleTimes()
{
- // A zero-length value is always "found" at the start of the span.
Span<int> span = new Span<int>(new int[] { 0, 1, 5, 3, 4, 5 });
Span<int> value = new Span<int>(new int[] { 5 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd()
{
- // A zero-length value is always "found" at the start of the span.
Span<int> span = new Span<int>(new int[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
Span<int> value = new Span<int>(new int[] { 5 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceZeroLengthValue_String()
{
- // A zero-length value is always "found" at the start of the span.
+ // A zero-length value is always "found" at the end of the span.
Span<string> span = new Span<string>(new string[] { "0", "1", "77", "2", "3", "77", "77", "4", "5", "77", "77", "77", "88", "6", "6", "77", "77", "88", "9" });
Span<string> value = new Span<string>(Array.Empty<string>());
int index = span.LastIndexOf(value);
- Assert.Equal(0, index);
+ Assert.Equal(span.Length, index);
}
[Fact]
[Fact]
public static void LastIndexOfSequenceLengthOneValue_String()
{
- // A zero-length value is always "found" at the start of the span.
Span<string> span = new Span<string>(new string[] { "0", "1", "2", "3", "4", "5" });
Span<string> value = new Span<string>(new string[] { "2" });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_String()
{
- // A zero-length value is always "found" at the start of the span.
Span<string> span = new Span<string>(new string[] { "0", "1", "2", "3", "4", "5" });
Span<string> value = new Span<string>(new string[] { "5" });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_String()
{
- // A zero-length value is always "found" at the start of the span.
Span<string> span = new Span<string>(new string[] { "0", "1", "2", "3", "4", "5" }, 0, 5);
Span<string> value = new Span<string>(new string[] { "5" });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceZeroLengthValue_Byte()
{
- // A zero-length value is always "found" at the start of the span.
+ // A zero-length value is always "found" at the end of the span.
Span<byte> span = new Span<byte>(new byte[] { 0, 1, 77, 2, 3, 77, 77, 4, 5, 77, 77, 77, 88, 6, 6, 77, 77, 88, 9 });
Span<byte> value = new Span<byte>(Array.Empty<byte>());
int index = span.LastIndexOf(value);
- Assert.Equal(0, index);
+ Assert.Equal(span.Length, index);
}
[Fact]
[Fact]
public static void LastIndexOfSequenceLengthOneValue_Byte()
{
- // A zero-length value is always "found" at the start of the span.
Span<byte> span = new Span<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
Span<byte> value = new Span<byte>(new byte[] { 2 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_Byte()
{
- // A zero-length value is always "found" at the start of the span.
Span<byte> span = new Span<byte>(new byte[] { 0, 1, 2, 3, 4, 5 });
Span<byte> value = new Span<byte>(new byte[] { 5 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueMultipleTimes_Byte()
{
- // A zero-length value is always "found" at the start of the span.
Span<byte> span = new Span<byte>(new byte[] { 0, 1, 5, 3, 4, 5 });
Span<byte> value = new Span<byte>(new byte[] { 5 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_Byte()
{
- // A zero-length value is always "found" at the start of the span.
Span<byte> span = new Span<byte>(new byte[] { 0, 1, 2, 3, 4, 5 }, 0, 5);
Span<byte> value = new Span<byte>(new byte[] { 5 });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceZeroLengthValue_Char()
{
- // A zero-length value is always "found" at the start of the span.
+ // A zero-length value is always "found" at the end of the span.
Span<char> span = new Span<char>(new char[] { '0', '1', '7', '2', '3', '7', '7', '4', '5', '7', '7', '7', '8', '6', '6', '7', '7', '8', '9' });
Span<char> value = new Span<char>(Array.Empty<char>());
int index = span.LastIndexOf(value);
- Assert.Equal(0, index);
+ Assert.Equal(span.Length, index);
}
[Fact]
[Fact]
public static void LastIndexOfSequenceLengthOneValue_Char()
{
- // A zero-length value is always "found" at the start of the span.
Span<char> span = new Span<char>(new char[] { '0', '1', '2', '3', '4', '5' });
Span<char> value = new Span<char>(new char[] { '2' });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_Char()
{
- // A zero-length value is always "found" at the start of the span.
Span<char> span = new Span<char>(new char[] { '0', '1', '2', '3', '4', '5' });
Span<char> value = new Span<char>(new char[] { '5' });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueMultipleTimes_Char()
{
- // A zero-length value is always "found" at the start of the span.
Span<char> span = new Span<char>(new char[] { '0', '1', '5', '3', '4', '5' });
Span<char> value = new Span<char>(new char[] { '5' });
int index = span.LastIndexOf(value);
[Fact]
public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
{
- // A zero-length value is always "found" at the start of the span.
Span<char> span = new Span<char>(new char[] { '0', '1', '2', '3', '4', '5' }, 0, 5);
Span<char> value = new Span<char>(new char[] { '5' });
int index = span.LastIndexOf(value);
{
{ new string[] { "1", null, "2" }, new string[] { "1", null, "2" }, 0},
{ new string[] { "1", null, "2" }, new string[] { null }, 1},
- { new string[] { "1", null, "2" }, (string[])null, 0},
+ { new string[] { "1", null, "2" }, (string[])null, 3},
{ new string[] { "1", "3", "1" }, new string[] { "1", null, "2" }, -1},
{ new string[] { "1", "3", "1" }, new string[] { "1" }, 2},
{ new string[] { "1", "3", "1" }, new string[] { null }, -1},
- { new string[] { "1", "3", "1" }, (string[])null, 0},
+ { new string[] { "1", "3", "1" }, (string[])null, 3},
{ null, new string[] { "1", null, "2" }, -1},
}
}
- internal static unsafe int InvariantLastIndexOf(string source, string value, int startIndex, int count, bool ignoreCase)
+ private static unsafe int InvariantLastIndexOf(string source, string value, int startIndex, int count, bool ignoreCase)
{
- Debug.Assert(source != null);
+ Debug.Assert(!string.IsNullOrEmpty(source));
Debug.Assert(value != null);
Debug.Assert(startIndex >= 0 && startIndex < source.Length);
if (valueCount == 0)
{
- return fromBeginning ? 0 : sourceCount - 1;
+ return fromBeginning ? 0 : sourceCount;
}
if (sourceCount < valueCount)
Debug.Assert(target != null);
Debug.Assert((options & CompareOptions.OrdinalIgnoreCase) == 0);
+ // startIndex points to the final char to include in the search space.
+ // empty target strings trivially occur at the end of the search space.
+
if (target.Length == 0)
{
- return startIndex;
+ return startIndex + 1;
}
if (options == CompareOptions.Ordinal)
{
Debug.Assert(!GlobalizationMode.Invariant);
+ Debug.Assert(!string.IsNullOrEmpty(source));
Debug.Assert(target != null);
Debug.Assert((options & CompareOptions.OrdinalIgnoreCase) == 0);
+ // startIndex points to the final char to include in the search space.
+ // empty target strings trivially occur at the end of the search space.
+
if (target.Length == 0)
- return startIndex;
+ return startIndex + 1;
if ((options & CompareOptions.Ordinal) != 0)
{
count--;
}
- // If we are looking for nothing, just return 0
+ // empty substrings trivially occur at the end of the search space
if (value.Length == 0 && count >= 0 && startIndex - count + 1 >= 0)
{
- return startIndex;
+ return startIndex + 1;
}
}
return LastIndexOfCore(source, value, startIndex, count, options);
}
- internal static int LastIndexOfOrdinal(string source, string value, int startIndex, int count, bool ignoreCase)
+ private static int LastIndexOfOrdinal(string source, string value, int startIndex, int count, bool ignoreCase)
{
- Debug.Assert(source != null);
+ Debug.Assert(!string.IsNullOrEmpty(source));
Debug.Assert(value != null);
if (GlobalizationMode.Invariant)
if (value.Length == 0)
{
- return Math.Max(0, startIndex - count + 1);
+ return startIndex + 1; // startIndex is the index of the last char to include in the search space
}
if (count == 0)
if (value.Length == 0)
{
- return 0;
+ return 0; // empty substring trivially occurs at every index (including start) of search space
}
if (span.Length == 0)
if (value.Length == 0)
{
- return span.Length > 0 ? span.Length - 1 : 0;
+ return span.Length; // empty substring trivially occurs at every index (including end) of search space
}
if (span.Length == 0)
if (value.Length == 0)
{
- return true;
+ return true; // the empty string is trivially a suffix of every other string
}
if (comparisonType >= StringComparison.Ordinal || GlobalizationMode.Invariant)
if (value.Length == 0)
{
- return true;
+ return true; // the empty string is trivially a prefix of every other string
}
if (comparisonType >= StringComparison.Ordinal || GlobalizationMode.Invariant)
Debug.Assert(valueLength >= 0);
if (valueLength == 0)
- return 0; // A zero-length sequence is always treated as "found" at the start of the search space.
+ return searchSpaceLength; // A zero-length sequence is always treated as "found" at the end of the search space.
byte valueHead = value;
ref byte valueTail = ref Unsafe.Add(ref value, 1);
Debug.Assert(valueLength >= 0);
if (valueLength == 0)
- return 0; // A zero-length sequence is always treated as "found" at the start of the search space.
+ return searchSpaceLength; // A zero-length sequence is always treated as "found" at the end of the search space.
T valueHead = value;
ref T valueTail = ref Unsafe.Add(ref value, 1);
return (CompareOptions)((int)comparisonType & (int)CompareOptions.IgnoreCase);
}
+
+ private static CompareOptions GetCompareOptionsFromOrdinalStringComparison(StringComparison comparisonType)
+ {
+ Debug.Assert(comparisonType == StringComparison.Ordinal || comparisonType == StringComparison.OrdinalIgnoreCase);
+
+ // StringComparison.Ordinal (0x04) --> CompareOptions.Ordinal (0x4000_0000)
+ // StringComparison.OrdinalIgnoreCase (0x05) -> CompareOptions.OrdinalIgnoreCase (0x1000_0000)
+
+ int ct = (int)comparisonType;
+ return (CompareOptions)((ct & -ct) << 28); // neg and shl
+ }
}
}
charMap[value & PROBABILISTICMAP_BLOCK_INDEX_MASK] |= 1u << (value >> PROBABILISTICMAP_BLOCK_INDEX_SHIFT);
}
+ /*
+ * IndexOf, LastIndexOf, Contains, StartsWith, and EndsWith
+ * ========================================================
+ *
+ * Given a search string 'searchString', a target string 'value' to locate within the search string, and a comparer
+ * 'comparer', the comparer will return a set S of tuples '(startPos, endPos)' for which the below expression
+ * returns true:
+ *
+ * >> bool result = searchString.Substring(startPos, endPos - startPos).Equals(value, comparer);
+ *
+ * If the set S is empty (i.e., there is no combination of values 'startPos' and 'endPos' which makes the
+ * above expression evaluate to true), then we say "'searchString' does not contain 'value'", and the expression
+ * "searchString.Contains(value, comparer)" should evaluate to false. If the set S is non-empty, then we say
+ * "'searchString' contains 'value'", and the expression "searchString.Contains(value, comparer)" should
+ * evaluate to true.
+ *
+ * Given a 'searchString', 'value', and 'comparer', the behavior of the IndexOf method is that it finds the
+ * smallest possible 'endPos' for which there exists any corresponding 'startPos' which makes the above
+ * expression evaluate to true, then it returns any 'startPos' within that subset. For example:
+ *
+ * let searchString = "<ZWJ><ZWJ>hihi" (where <ZWJ> = U+200D ZERO WIDTH JOINER, a weightless code point)
+ * let value = "hi"
+ * let comparer = a linguistic culture-invariant comparer (e.g., StringComparison.InvariantCulture)
+ * then S = { (0, 4), (1, 4), (2, 4), (4, 6) }
+ * so the expression "<ZWJ><ZWJ>hihi".IndexOf("hi", comparer) can evaluate to any of { 0, 1, 2 }.
+ *
+ * n.b. ordinal comparers (e.g., StringComparison.Ordinal and StringComparison.OrdinalIgnoreCase) do not
+ * exhibit this ambiguity, as any given 'startPos' or 'endPos' will appear at most exactly once across
+ * all entries from set S. With the above example, S = { (2, 4), (4, 6) }, so IndexOf = 2 unambiguously.
+ *
+ * There exists a relationship between IndexOf and StartsWith. If there exists in set S any entry with
+ * the tuple values (startPos = 0, endPos = <anything>), we say "'searchString' starts with 'value'", and
+ * the expression "searchString.StartsWith(value, comparer)" should evaluate to true. If there exists
+ * no such entry in set S, then we say "'searchString' does not start with 'value'", and the expression
+ * "searchString.StartsWith(value, comparer)" should evaluate to false.
+ *
+ * LastIndexOf and EndsWith have a similar relationship as IndexOf and StartsWith. The behavior of the
+ * LastIndexOf method is that it finds the largest possible 'endPos' for which there exists any corresponding
+ * 'startPos' which makes the expression evaluate to true, then it returns any 'startPos' within that
+ * subset. For example:
+ *
+ * let searchString = "hi<ZWJ><ZWJ>hi" (this is slightly modified from the earlier example)
+ * let value = "hi"
+ * let comparer = StringComparison.InvariantCulture
+ * then S = { (0, 2), (0, 3), (0, 4), (2, 6), (3, 6), (4, 6) }
+ * so the expression "hi<ZWJ><ZWJ>hi".LastIndexOf("hi", comparer) can evaluate to any of { 2, 3, 4 }.
+ *
+ * If there exists in set S any entry with the tuple values (startPos = <anything>, endPos = searchString.Length),
+ * we say "'searchString' ends with 'value'", and the expression "searchString.EndsWith(value, comparer)"
+ * should evaluate to true. If there exists no such entry in set S, then we say "'searchString' does not
+ * start with 'value'", and the expression "searchString.EndsWith(value, comparer)" should evaluate to false.
+ *
+ * There are overloads of IndexOf and LastIndexOf which take an offset and length in order to constrain the
+ * search space to a substring of the original search string.
+ *
+ * For LastIndexOf specifially, overloads which take a 'startIndex' and 'count' behave differently
+ * than their IndexOf counterparts. 'startIndex' is the index of the last char element that should
+ * be considered when performing the search. For example, if startIndex = 4, then the caller is
+ * indicating "when finding the match I want you to include the char element at index 4, but not
+ * any char elements past that point."
+ *
+ * idx = 0123456 ("abcdefg".Length = 7)
+ * So, if the search string is "abcdefg", startIndex = 5 and count = 3, then the search space will
+ * ~~~ be the substring "def", as highlighted to the left.
+ * Essentially: "the search space should be of length 3 chars and should end *just after* the char
+ * element at index 5."
+ *
+ * Since this behavior can introduce off-by-one errors in the boundary cases, we allow startIndex = -1
+ * with a zero-length 'searchString' (treated as equivalent to startIndex = 0), and we allow
+ * startIndex = searchString.Length (treated as equivalent to startIndex = searchString.Length - 1).
+ *
+ * Note also that this behavior can introduce errors when dealing with UTF-16 surrogate pairs.
+ * If the search string is the 3 chars "[BMP][HI][LO]", startIndex = 1 and count = 2, then the
+ * ~~~~~~~~~ search space wil be the substring "[BMP][ HI]".
+ * This means that the char [HI] is incorrectly seen as a standalone high surrogate, which could
+ * lead to incorrect matching behavior, or it could cause LastIndexOf to incorrectly report that
+ * a zero-weight character could appear between the [HI] and [LO] chars.
+ */
+
public int IndexOf(string value)
{
return IndexOf(value, StringComparison.CurrentCulture);
public int LastIndexOf(string value, int startIndex, int count, StringComparison comparisonType)
{
- if (value == null)
- throw new ArgumentNullException(nameof(value));
-
- // Special case for 0 length input strings
- if (this.Length == 0 && (startIndex == -1 || startIndex == 0))
- return (value.Length == 0) ? 0 : -1;
-
- // Now after handling empty strings, make sure we're not out of range
- if (startIndex < 0 || startIndex > this.Length)
- throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
-
- // Make sure that we allow startIndex == this.Length
- if (startIndex == this.Length)
- {
- startIndex--;
- if (count > 0)
- count--;
- }
-
- // 2nd half of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
- if (count < 0 || startIndex - count + 1 < 0)
- throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
-
- // If we are looking for nothing, just return startIndex
- if (value.Length == 0)
- return startIndex;
+ // Parameter checking will be done by CompareInfo.LastIndexOf.
switch (comparisonType)
{
case StringComparison.Ordinal:
case StringComparison.OrdinalIgnoreCase:
- return CompareInfo.LastIndexOfOrdinal(this, value, startIndex, count, GetCaseCompareOfComparisonCulture(comparisonType) != CompareOptions.None);
+ return CompareInfo.Invariant.LastIndexOf(this, value, startIndex, count, GetCompareOptionsFromOrdinalStringComparison(comparisonType));
default:
- throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
+ throw (value is null)
+ ? new ArgumentNullException(nameof(value))
+ : new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
}
}