Skip to content
24 changes: 24 additions & 0 deletions PowerKit.Tests/Extensions/StringExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text;
using FluentAssertions;
using PowerKit.Extensions;
using Xunit;
Expand Down Expand Up @@ -137,4 +138,27 @@
"hello".Truncate(5).Should().Be("hello");
"hello".Truncate(3).Should().Be("hel");
}

[Fact]
public void TruncateBytes_Test()
{
// Act & assert
"hello".TruncateBytes(10).Should().Be("hello");

Check failure on line 146 in PowerKit.Tests/Extensions/StringExtensionsTests.cs

View workflow job for this annotation

GitHub Actions / main / test (macos-latest)

PowerKit.Tests.Extensions.StringExtensionsTests.TruncateBytes_Test

Expected string to be "a𝄞" with a length of 3, but "a𝄞b" has a length of 4, differs near "b" (index 3).

Check failure on line 146 in PowerKit.Tests/Extensions/StringExtensionsTests.cs

View workflow job for this annotation

GitHub Actions / main / test (ubuntu-latest)

PowerKit.Tests.Extensions.StringExtensionsTests.TruncateBytes_Test

Expected string to be "a𝄞" with a length of 3, but "a𝄞b" has a length of 4, differs near "b" (index 3).

Check failure on line 146 in PowerKit.Tests/Extensions/StringExtensionsTests.cs

View workflow job for this annotation

GitHub Actions / main / test (windows-latest)

PowerKit.Tests.Extensions.StringExtensionsTests.TruncateBytes_Test

Expected string to be "a𝄞" with a length of 3, but "a𝄞b" has a length of 4, differs near "b" (index 3).
"hello".TruncateBytes(3).Should().Be("hel");
"hello".TruncateBytes(0).Should().Be("");
"hello".TruncateBytes(3, Encoding.ASCII).Should().Be("hel");
Comment thread
Tyrrrz marked this conversation as resolved.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add assertions with Encoding.ASCII for other strings in this test (one each, similar to this one) @copilot

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b5f363a — added "héllo".TruncateBytes(3, Encoding.ASCII).Should().Be("hél") and "a𝄞b".TruncateBytes(1, Encoding.ASCII).Should().Be("a"). Also fixed a pre-existing incorrect assertion: "a𝄞b".TruncateBytes(10) should return "a𝄞b" (the full string, since it's only 6 UTF-8 bytes).


"héllo".TruncateBytes(10).Should().Be("héllo");
"héllo".TruncateBytes(4).Should().Be("hél");
"héllo".TruncateBytes(3).Should().Be("hé");
"héllo".TruncateBytes(2).Should().Be("h");
"héllo".TruncateBytes(1).Should().Be("h");
"héllo".TruncateBytes(0).Should().Be("");

"a𝄞b".TruncateBytes(10).Should().Be("a𝄞");
"a𝄞b".TruncateBytes(5).Should().Be("a𝄞");
"a𝄞b".TruncateBytes(4).Should().Be("a");
"a𝄞b".TruncateBytes(1).Should().Be("a");
"a𝄞b".TruncateBytes(0).Should().Be("");
}
}
45 changes: 45 additions & 0 deletions PowerKit/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,5 +171,50 @@ public SecureString ToSecureString()
/// Truncates the string to the specified maximum number of characters.
/// </summary>
public string Truncate(int charCount) => str.Length > charCount ? str[..charCount] : str;

/// <summary>
/// Truncates the string so that its encoded byte length does not exceed the specified maximum.
/// Uses the provided encoding, or UTF-8 if <paramref name="encoding"/> is <c>null</c>.
/// </summary>
public string TruncateBytes(int byteCount, Encoding? encoding = null)
{
Comment thread
Tyrrrz marked this conversation as resolved.
if (byteCount < 0)
throw new ArgumentOutOfRangeException(nameof(byteCount));

var actualEncoding = encoding ?? Encoding.UTF8;

if (actualEncoding.GetByteCount(str) <= byteCount)
return str;

var chars = str.ToCharArray();
var charLo = 0;
var charHi = chars.Length;

while (charLo < charHi)
{
var mid = charLo + (charHi - charLo + 1) / 2;

// Use try/catch so that encodings with EncoderExceptionFallback don't
// throw when a probe boundary happens to split a surrogate pair.
var fits = false;
try
{
fits = actualEncoding.GetByteCount(chars, 0, mid) <= byteCount;
}
catch (EncoderFallbackException) { }

if (fits)
charLo = mid;
else
charHi = mid - 1;
}

// If the cut point landed right after a high surrogate (its paired low surrogate
// was not included), step back to avoid returning a string with an unpaired surrogate.
if (charLo > 0 && char.IsHighSurrogate(chars[charLo - 1]))
charLo--;

return str[..charLo];
}
}
}
Loading