Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,99 @@ public string String

public int LengthInTextElements => Indexes?.Length ?? 0;

/// <summary>
/// Returns the number of text elements (extended grapheme clusters) in the given span.
/// </summary>
/// <remarks>
/// A grapheme cluster is a sequence of one or more Unicode code points that should be treated as a single unit.
/// </remarks>
/// <param name="str">The input span to analyze.</param>
/// <returns>The number of text elements within <paramref name="str"/>.</returns>
public static int GetLengthInTextElements(ReadOnlySpan<char> str)
{
int count = 0;
while (!str.IsEmpty)
{
str = str.Slice(GetNextTextElementLength(str));
count++;
}
return count;
}

/// <summary>
/// Retrieves a range covering a substring of text elements from the given span,
/// or <see langword="null"/> if the requested range extends beyond the end of <paramref name="str"/>.
/// </summary>
/// <remarks>
/// A grapheme cluster is a sequence of one or more Unicode code points that should be treated as a single unit.
/// </remarks>
/// <param name="str">The input span to analyze.</param>
/// <param name="startingTextElement">The zero-based text element index at which the substring begins.</param>
/// <param name="lengthInTextElements">The number of text elements to include in the substring.</param>
/// <returns>
/// A <see cref="Range"/> representing the char offsets within <paramref name="str"/> that correspond to the
/// requested text elements, or <see langword="null"/> if the requested range extends beyond the end
/// of <paramref name="str"/>.
/// </returns>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="startingTextElement"/> or <paramref name="lengthInTextElements"/> is negative.
/// </exception>
public static Range? GetRangeByTextElements(ReadOnlySpan<char> str, int startingTextElement, int lengthInTextElements)
{
ArgumentOutOfRangeException.ThrowIfNegative(startingTextElement);
ArgumentOutOfRangeException.ThrowIfNegative(lengthInTextElements);

int startOffset = 0;

for (int i = 0; i < startingTextElement; i++)
{
if (str.IsEmpty)
{
return null;
}
int len = GetNextTextElementLength(str);
str = str.Slice(len);
startOffset += len;
}

int endOffset = startOffset;

for (int i = 0; i < lengthInTextElements; i++)
{
if (str.IsEmpty)
{
return null;
}
int len = GetNextTextElementLength(str);
str = str.Slice(len);
endOffset += len;
}

return startOffset..endOffset;
}

/// <summary>
/// Retrieves a range covering a substring of text elements from the instance string,
/// or <see langword="null"/> if the requested range extends beyond the end of <see cref="String"/>.
/// </summary>
/// <remarks>
/// A grapheme cluster is a sequence of one or more Unicode code points that should be treated as a single unit.
/// </remarks>
/// <param name="startingTextElement">The zero-based text element index at which the substring begins.</param>
/// <param name="lengthInTextElements">The number of text elements to include in the substring.</param>
/// <returns>
/// A <see cref="Range"/> representing the char offsets within <see cref="String"/> that correspond to the
/// requested text elements, or <see langword="null"/> if the requested range extends beyond the end
/// of <see cref="String"/>.
/// </returns>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="startingTextElement"/> or <paramref name="lengthInTextElements"/> is negative.
/// </exception>
public Range? RangeByTextElements(int startingTextElement, int lengthInTextElements)
{
return GetRangeByTextElements(String, startingTextElement, lengthInTextElements);
}

public string SubstringByTextElements(int startingTextElement)
{
return SubstringByTextElements(startingTextElement, (Indexes?.Length ?? 0) - startingTextElement);
Expand Down
3 changes: 3 additions & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9882,14 +9882,17 @@ public StringInfo(string value) { }
public string String { get { throw null; } set { } }
public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? value) { throw null; }
public override int GetHashCode() { throw null; }
public static int GetLengthInTextElements(System.ReadOnlySpan<char> str) { throw null; }
public static string GetNextTextElement(string str) { throw null; }
public static string GetNextTextElement(string str, int index) { throw null; }
public static int GetNextTextElementLength(System.ReadOnlySpan<char> str) { throw null; }
public static int GetNextTextElementLength(string str) { throw null; }
public static int GetNextTextElementLength(string str, int index) { throw null; }
public static System.Range? GetRangeByTextElements(System.ReadOnlySpan<char> str, int startingTextElement, int lengthInTextElements) { throw null; }
public static System.Globalization.TextElementEnumerator GetTextElementEnumerator(string str) { throw null; }
public static System.Globalization.TextElementEnumerator GetTextElementEnumerator(string str, int index) { throw null; }
public static int[] ParseCombiningCharacters(string str) { throw null; }
public System.Range? RangeByTextElements(int startingTextElement, int lengthInTextElements) { throw null; }
public string SubstringByTextElements(int startingTextElement) { throw null; }
public string SubstringByTextElements(int startingTextElement, int lengthInTextElements) { throw null; }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,5 +346,113 @@ public void ParseCombiningCharacters_Null_ThrowsArgumentNullException()
{
AssertExtensions.Throws<ArgumentNullException>("str", () => StringInfo.ParseCombiningCharacters(null)); // Str is null
}

public static IEnumerable<object[]> GetLengthInTextElements_TestData()
{
yield return new object[] { "", 0 };
yield return new object[] { "abc", 3 };
yield return new object[] { " ", 3 };
yield return new object[] { "\u4f00\u302a\ud800\udc00\u4f01", 3 }; // combining char + surrogate pair + single
yield return new object[] { "a\u0300", 1 }; // base + combining
yield return new object[] { "\uDBFF\uDFFFlk", 3 }; // surrogate pair + 2 chars
yield return new object[] { "\U0001F483\U0001F3FD", 1 }; // emoji with modifier
}

[Theory]
[MemberData(nameof(GetLengthInTextElements_TestData))]
public void GetLengthInTextElements(string str, int expected)
{
Assert.Equal(expected, StringInfo.GetLengthInTextElements(str.AsSpan()));
}

[Fact]
public void GetLengthInTextElements_MatchesInstanceProperty()
{
foreach (object[] data in Ctor_String_TestData())
{
string value = (string)data[0];
int expectedLength = (int)data[1];
Assert.Equal(expectedLength, StringInfo.GetLengthInTextElements(value.AsSpan()));
}
}

public static IEnumerable<object[]> GetRangeByTextElements_TestData()
{
// Simple ASCII
yield return new object[] { "abcde", 0, 3, (Range?)(0..3) };
yield return new object[] { "abcde", 2, 2, (Range?)(2..4) };
yield return new object[] { "abcde", 4, 1, (Range?)(4..5) };
yield return new object[] { "abcde", 0, 0, (Range?)(0..0) };
yield return new object[] { "abcde", 5, 0, (Range?)(5..5) };
// Surrogate pairs
yield return new object[] { "\uD800\uDC00\uD801\uDC01Left", 0, 2, (Range?)(0..4) };
yield return new object[] { "\uD800\uDC00\uD801\uDC01Left", 1, 3, (Range?)(2..6) };
// Combining characters
yield return new object[] { "a\u0300bc", 0, 2, (Range?)(0..3) }; // "a\u0300" + "b"
yield return new object[] { "a\u0300bc", 1, 1, (Range?)(2..3) }; // "b"
// Empty string
yield return new object[] { "", 0, 0, (Range?)(0..0) };
// Out of range (beyond end): returns null
yield return new object[] { "abc", 0, 4, (Range?)null };
yield return new object[] { "abc", 4, 0, (Range?)null };
yield return new object[] { "abc", 3, 1, (Range?)null };
yield return new object[] { "", 1, 0, (Range?)null };
}

[Theory]
[MemberData(nameof(GetRangeByTextElements_TestData))]
public void GetRangeByTextElements(string str, int startingTextElement, int lengthInTextElements, Range? expected)
{
Range? result = StringInfo.GetRangeByTextElements(str.AsSpan(), startingTextElement, lengthInTextElements);
Assert.Equal(expected, result);

if (result.HasValue)
{
string slice = str[result.Value];
Assert.Equal(lengthInTextElements, StringInfo.GetLengthInTextElements(slice.AsSpan()));
}
}

[Fact]
public void GetRangeByTextElements_NegativeArgs_ThrowsArgumentOutOfRangeException()
{
Assert.Throws<ArgumentOutOfRangeException>(() => StringInfo.GetRangeByTextElements("abc".AsSpan(), -1, 1));
Assert.Throws<ArgumentOutOfRangeException>(() => StringInfo.GetRangeByTextElements("abc".AsSpan(), 0, -1));
}

public static IEnumerable<object[]> RangeByTextElements_TestData()
{
yield return new object[] { "Simple Text", 7, 4, (Range?)(7..11) };
yield return new object[] { "Simple Text", 0, 6, (Range?)(0..6) };
yield return new object[] { "\uD800\uDC00\uD801\uDC01Left", 2, 2, (Range?)(4..6) };
yield return new object[] { "a\u0300bc", 0, 3, (Range?)(0..4) };
// Out of range (beyond end): returns null
yield return new object[] { "abc", 0, 4, (Range?)null };
yield return new object[] { "abc", 4, 0, (Range?)null };
yield return new object[] { "abc", 3, 1, (Range?)null };
}

[Theory]
[MemberData(nameof(RangeByTextElements_TestData))]
public void RangeByTextElements(string str, int startingTextElement, int lengthInTextElements, Range? expected)
{
StringInfo si = new StringInfo(str);
Range? result = si.RangeByTextElements(startingTextElement, lengthInTextElements);
Assert.Equal(expected, result);

if (result.HasValue)
{
string slice = str[result.Value];
Assert.Equal(lengthInTextElements, StringInfo.GetLengthInTextElements(slice.AsSpan()));
}
}

[Fact]
public void RangeByTextElements_NegativeArgs_ThrowsArgumentOutOfRangeException()
{
StringInfo si = new StringInfo("abc");
Assert.Throws<ArgumentOutOfRangeException>(() => si.RangeByTextElements(-1, 1));
Assert.Throws<ArgumentOutOfRangeException>(() => si.RangeByTextElements(0, -1));
}
}
}
Loading