Skip to content
30 changes: 30 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,33 @@ public void Truncate_Test()
"hello".Truncate(5).Should().Be("hello");
"hello".Truncate(3).Should().Be("hel");
}

[Fact]
public void TruncateBytes_Test()
{
// ASCII-only (1 byte per char with UTF-8)
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated
"hello".TruncateBytes(10, Encoding.UTF8).Should().Be("hello");
"hello".TruncateBytes(5, Encoding.UTF8).Should().Be("hello");
"hello".TruncateBytes(3, Encoding.UTF8).Should().Be("hel");
"hello".TruncateBytes(0, Encoding.UTF8).Should().Be("");

// Multi-byte characters (é = 2 bytes in UTF-8)
"héllo".TruncateBytes(6, Encoding.UTF8).Should().Be("héllo");
"héllo".TruncateBytes(5, Encoding.UTF8).Should().Be("héll");
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated
"héllo".TruncateBytes(4, Encoding.UTF8).Should().Be("hél");
"héllo".TruncateBytes(3, Encoding.UTF8).Should().Be("hé");
"héllo".TruncateBytes(2, Encoding.UTF8).Should().Be("h");
"héllo".TruncateBytes(1, Encoding.UTF8).Should().Be("h");

// Emoji (𝄞 = 4 bytes in UTF-8, encoded as a surrogate pair in C#)
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated
"a𝄞b".TruncateBytes(5, Encoding.UTF8).Should().Be("a𝄞");
"a𝄞b".TruncateBytes(4, Encoding.UTF8).Should().Be("a");
"a𝄞b".TruncateBytes(1, Encoding.UTF8).Should().Be("a");

// Default encoding is UTF-8
"héllo".TruncateBytes(4, null).Should().Be("hél");
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated

// Non-UTF-8 encoding
"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).

}
}
32 changes: 32 additions & 0 deletions PowerKit/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,5 +171,37 @@ 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.
var enc = encoding ?? Encoding.UTF8;
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated

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

var chars = str.ToCharArray();
var lo = 0;
var hi = chars.Length;
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated

while (lo < hi)
{
var mid = lo + (hi - lo + 1) / 2;
if (enc.GetByteCount(chars, 0, mid) <= byteCount)
lo = mid;
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated
else
hi = 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 (lo > 0 && char.IsHighSurrogate(chars[lo - 1]))
lo--;

return str[..lo];
}
}
}