From 1546581f4753a08be17767405a423a2154d619b4 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 1 Nov 2026 17:43:11 +0100 Subject: [PATCH] Update StringTruncationInCSharp to .NET 10, fix regex truncation of multi-line strings, add emoji-safe truncation Retarget both projects to net10.0 and update BenchmarkDotNet and the xUnit test packages. The regex method now uses RegexOptions.Singleline so strings with line breaks are truncated. Add TruncateWithSurrogateCheck and TruncateWithTextElements, benchmark the text element method, return results from every benchmark method, pass args to BenchmarkRunner, and add 10 tests. --- .../StringTruncationInCSharp/Program.cs | 2 +- .../StringTruncationInCSharp/StringHelper.cs | 37 ++++++++- .../StringHelperBenchmark.cs | 30 ++++--- .../StringTruncationInCSharp.csproj | 4 +- .../StringTruncationInCSharpTests.csproj | 10 +-- .../StringTruncationUnitTest.cs | 81 +++++++++++++++++++ 6 files changed, 142 insertions(+), 22 deletions(-) diff --git a/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/Program.cs b/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/Program.cs index 77cb8ee750..db218dbec4 100644 --- a/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/Program.cs +++ b/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/Program.cs @@ -1,4 +1,4 @@ using BenchmarkDotNet.Running; using StringTruncationInCSharp; -BenchmarkRunner.Run(); \ No newline at end of file +BenchmarkRunner.Run(args: args); \ No newline at end of file diff --git a/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/StringHelper.cs b/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/StringHelper.cs index 522bb49348..fc21440fa3 100644 --- a/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/StringHelper.cs +++ b/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/StringHelper.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Globalization; +using System.Text; using System.Text.RegularExpressions; namespace StringTruncationInCSharp; @@ -48,7 +49,7 @@ public static string TruncateWithRegularExpressions(string originalString, int m return TruncateString(originalString, maxLength, UsingRegularExpressions); static string UsingRegularExpressions(string str, int length) => - Regex.Replace(str, $"^(.{{0,{length}}}).*$", "$1"); + Regex.Replace(str, $"^(.{{0,{length}}}).*$", "$1", RegexOptions.Singleline); } public static string TruncateWithSpan(string originalString, int maxLength) @@ -58,6 +59,38 @@ public static string TruncateWithSpan(string originalString, int maxLength) static string UsingSpan(string str, int length) => str.AsSpan()[..length].ToString(); } + public static string TruncateWithSurrogateCheck(string originalString, int maxLength) + { + return TruncateString(originalString, maxLength, UsingSurrogateCheck); + + static string UsingSurrogateCheck(string str, int length) => + char.IsHighSurrogate(str[length - 1]) ? str[..(length - 1)] : str[..length]; + } + + public static string TruncateWithTextElements(string originalString, int maxLength) + { + return TruncateString(originalString, maxLength, UsingTextElements); + + static string UsingTextElements(string str, int length) + { + var end = 0; + + while (end < length) + { + var next = StringInfo.GetNextTextElementLength(str, end); + + if (end + next > length) + { + break; + } + + end += next; + } + + return str[..end]; + } + } + private static string TruncateString(string originalString, int maxLength, Func specificTruncateMethod) { diff --git a/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/StringHelperBenchmark.cs b/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/StringHelperBenchmark.cs index e8e8c7b9b2..982b24f49c 100644 --- a/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/StringHelperBenchmark.cs +++ b/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/StringHelperBenchmark.cs @@ -14,38 +14,44 @@ public class StringHelperBenchmark public int MaxLength; [Benchmark] - public void TruncateWithSubstring() + public string TruncateWithSubstring() { - StringHelper.TruncateWithSubstring(_originalString, MaxLength); + return StringHelper.TruncateWithSubstring(_originalString, MaxLength); } [Benchmark] - public void TruncateWithForLoopStringBuilder() + public string TruncateWithForLoopStringBuilder() { - StringHelper.TruncateWithForLoopStringBuilder(_originalString, MaxLength); + return StringHelper.TruncateWithForLoopStringBuilder(_originalString, MaxLength); } [Benchmark] - public void TruncateWithRegularExpressions() + public string TruncateWithRegularExpressions() { - StringHelper.TruncateWithRegularExpressions(_originalString, MaxLength); + return StringHelper.TruncateWithRegularExpressions(_originalString, MaxLength); } [Benchmark] - public void TruncateWithRemove() + public string TruncateWithRemove() { - StringHelper.TruncateWithRemove(_originalString, MaxLength); + return StringHelper.TruncateWithRemove(_originalString, MaxLength); } [Benchmark] - public void TruncateWithLINQ() + public string TruncateWithLINQ() { - StringHelper.TruncateWithLINQ(_originalString, MaxLength); + return StringHelper.TruncateWithLINQ(_originalString, MaxLength); } [Benchmark] - public void TruncateWithSpan() + public string TruncateWithSpan() { - StringHelper.TruncateWithSpan(_originalString, MaxLength); + return StringHelper.TruncateWithSpan(_originalString, MaxLength); + } + + [Benchmark] + public string TruncateWithTextElements() + { + return StringHelper.TruncateWithTextElements(_originalString, MaxLength); } } \ No newline at end of file diff --git a/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/StringTruncationInCSharp.csproj b/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/StringTruncationInCSharp.csproj index a0c610ccf1..62be4f867f 100644 --- a/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/StringTruncationInCSharp.csproj +++ b/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharp/StringTruncationInCSharp.csproj @@ -2,13 +2,13 @@ Exe - net8.0 + net10.0 enable enable - + diff --git a/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharpTests/StringTruncationInCSharpTests.csproj b/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharpTests/StringTruncationInCSharpTests.csproj index ea72a381cf..d31ba488a5 100644 --- a/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharpTests/StringTruncationInCSharpTests.csproj +++ b/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharpTests/StringTruncationInCSharpTests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -10,13 +10,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharpTests/StringTruncationUnitTest.cs b/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharpTests/StringTruncationUnitTest.cs index 0b31a769fe..8f43769fe3 100644 --- a/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharpTests/StringTruncationUnitTest.cs +++ b/strings-csharp/StringTruncationInCSharp/StringTruncationInCSharpTests/StringTruncationUnitTest.cs @@ -261,4 +261,85 @@ public void GivenANonPositiveLength_WhenTruncatingAString_ThenUseSpanMethodAndRe Assert.Equal(string.Empty, truncatedString); } + + [Fact] + public void GivenAMultiLineString_WhenTruncatingAString_ThenUseRegularExpressionsMethodToTruncateTheString() + { + var truncatedString = StringHelper.TruncateWithRegularExpressions("First line\nSecond line", 5); + + Assert.Equal("First", truncatedString); + } + + [Fact] + public void GivenATrailingNewLine_WhenTruncatingAString_ThenUseRegularExpressionsMethodToTruncateTheString() + { + var truncatedString = StringHelper.TruncateWithRegularExpressions("Hello world\n", 5); + + Assert.Equal("Hello", truncatedString); + } + + + [Fact] + public void WhenTruncatingAString_ThenUseTextElementsMethodToTruncateTheString() + { + var truncatedString = StringHelper.TruncateWithTextElements(OriginalString, MaxLength); + + Assert.Equal(ExpectedTruncatedString, truncatedString); + } + + [Fact] + public void GivenAZeroLength_WhenTruncatingAString_ThenUseTextElementsMethodAndReturnAnEmptyString() + { + var truncatedString = StringHelper.TruncateWithTextElements(OriginalString, 0); + + Assert.Equal(string.Empty, truncatedString); + } + + [Fact] + public void GivenALengthGreaterThanTheStringLength_WhenTruncatingAString_ThenUseTextElementsMethodWithoutTruncatingTheString() + { + var truncatedString = StringHelper.TruncateWithTextElements(OriginalString, OriginalString.Length + 10); + + Assert.Equal(OriginalString, truncatedString); + } + + [Fact] + public void GivenAnEmojiAtTheCut_WhenTruncatingWithSubstring_ThenTheResultEndsWithAHalfSurrogatePair() + { + var truncatedString = StringHelper.TruncateWithSubstring("Thumbs up \U0001F44D", 11); + + Assert.True(char.IsHighSurrogate(truncatedString[^1])); + } + + [Fact] + public void GivenAnEmojiAtTheCut_WhenTruncatingWithTextElements_ThenTheWholeEmojiIsDropped() + { + var truncatedString = StringHelper.TruncateWithTextElements("Thumbs up \U0001F44D", 11); + + Assert.Equal("Thumbs up ", truncatedString); + } + + [Fact] + public void GivenAJoinedEmojiAtTheCut_WhenTruncatingWithTextElements_ThenTheWholeEmojiIsDropped() + { + var truncatedString = StringHelper.TruncateWithTextElements("Family \U0001F468\u200D\U0001F469\u200D\U0001F467", 12); + + Assert.Equal("Family ", truncatedString); + } + + [Fact] + public void GivenAnEmojiAtTheCut_WhenTruncatingWithSurrogateCheck_ThenTheWholeEmojiIsDropped() + { + var truncatedString = StringHelper.TruncateWithSurrogateCheck("Thumbs up \U0001F44D", 11); + + Assert.Equal("Thumbs up ", truncatedString); + } + + [Fact] + public void WhenTruncatingAString_ThenUseSurrogateCheckMethodToTruncateTheString() + { + var truncatedString = StringHelper.TruncateWithSurrogateCheck(OriginalString, MaxLength); + + Assert.Equal(ExpectedTruncatedString, truncatedString); + } } \ No newline at end of file