From 3741a6c114da152ad15515778ddda8a23f92bad0 Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Fri, 18 Jul 2025 12:12:28 +0200 Subject: [PATCH 1/3] Add string extension methods --- CHANGELOG.md | 1 + .../StringExtensionsTests.cs | 54 +++++++++++++++++++ Neolution.Utilities/StringExtensions.cs | 35 ++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 Neolution.Utilities.UnitTests/StringExtensionsTests.cs create mode 100644 Neolution.Utilities/StringExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 906959d..a6af494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,5 +12,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `FileSize` utility with method to display bytes in a human-readable way - `CultureInfo` extension method to get the language code of a culture - `IEnumerable` extension methods +- `String` extension methods - `IFormFile` extension method (AspNetCore package only) - `DbSet` extension methods for ISortableEntity interface (EntityFrameworkCore package only) diff --git a/Neolution.Utilities.UnitTests/StringExtensionsTests.cs b/Neolution.Utilities.UnitTests/StringExtensionsTests.cs new file mode 100644 index 0000000..72ebdb3 --- /dev/null +++ b/Neolution.Utilities.UnitTests/StringExtensionsTests.cs @@ -0,0 +1,54 @@ +namespace Neolution.Utilities.UnitTests; + +using Xunit; + +/// +/// Unit tests for . +/// +public class StringExtensionsTests +{ + /// + /// Truncate returns the expected result. + /// + /// The value. + /// The maximum length. + /// The expected. + [Theory] + [InlineData(null, 5, null)] + [InlineData("", 5, "")] + [InlineData("abc", 5, "abc")] + [InlineData("abcdef", 3, "abc")] + [InlineData("abcdef", 6, "abcdef")] + public void Truncate_ReturnsExpectedResult(string? value, int maxLength, string? expected) + { + // Act + var result = value.Truncate(maxLength); + + // Assert + Assert.Equal(expected, result); + } + + /// + /// Truncate with suffix returns the expected result. + /// + /// The value. + /// The maximum length. + /// The suffix. + /// The expected. + [Theory] + [InlineData(null, 5, "...", null)] + [InlineData("", 5, "...", "")] + [InlineData("abc", 5, "...", "abc")] + [InlineData("abcdef", 3, "...", "abc...")] + [InlineData("abcdef", 6, "...", "abcdef")] + [InlineData("abcdef", 4, "-", "abcd-")] + [InlineData("abcdef", 0, "!", "!")] + public void Truncate_WithSuffix_ReturnsExpectedResult(string? value, int maxLength, string suffix, string? expected) + { + // Act + var result = value.Truncate(maxLength, suffix); + + // Assert + Assert.Equal(expected, result); + } +} diff --git a/Neolution.Utilities/StringExtensions.cs b/Neolution.Utilities/StringExtensions.cs new file mode 100644 index 0000000..83df485 --- /dev/null +++ b/Neolution.Utilities/StringExtensions.cs @@ -0,0 +1,35 @@ +namespace Neolution.Utilities; + +/// +/// String extensions. +/// +public static class StringExtensions +{ + /// + /// Truncates the string to the specified maximum length. + /// + /// The value. + /// The maximum length. + /// The truncated string + public static string? Truncate(this string? value, int maxLength) + { + return Truncate(value, maxLength, string.Empty); + } + + /// + /// Truncates the string to the specified maximum length, appending a suffix if necessary. + /// + /// The value. + /// The maximum length. + /// The suffix to append if the string is truncated. + /// The truncated string + public static string? Truncate(this string? value, int maxLength, string suffix) + { + if (string.IsNullOrEmpty(value) || value.Length <= maxLength) + { + return value; + } + + return $"{value[..maxLength]}{suffix}"; + } +} From 44024e4799cc3676a977dfe5f1927d056323dcd0 Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Mon, 28 Jul 2025 17:24:00 +0200 Subject: [PATCH 2/3] move to correct location --- .../{ => Extensions}/StringExtensionsTests.cs | 3 ++- Neolution.Utilities/{ => Extensions}/StringExtensions.cs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) rename Neolution.Utilities.UnitTests/{ => Extensions}/StringExtensionsTests.cs (94%) rename Neolution.Utilities/{ => Extensions}/StringExtensions.cs (91%) diff --git a/Neolution.Utilities.UnitTests/StringExtensionsTests.cs b/Neolution.Utilities.UnitTests/Extensions/StringExtensionsTests.cs similarity index 94% rename from Neolution.Utilities.UnitTests/StringExtensionsTests.cs rename to Neolution.Utilities.UnitTests/Extensions/StringExtensionsTests.cs index 72ebdb3..fa54d6c 100644 --- a/Neolution.Utilities.UnitTests/StringExtensionsTests.cs +++ b/Neolution.Utilities.UnitTests/Extensions/StringExtensionsTests.cs @@ -1,5 +1,6 @@ -namespace Neolution.Utilities.UnitTests; +namespace Neolution.Utilities.UnitTests.Extensions; +using Neolution.Utilities.Extensions; using Xunit; /// diff --git a/Neolution.Utilities/StringExtensions.cs b/Neolution.Utilities/Extensions/StringExtensions.cs similarity index 91% rename from Neolution.Utilities/StringExtensions.cs rename to Neolution.Utilities/Extensions/StringExtensions.cs index 83df485..41965e7 100644 --- a/Neolution.Utilities/StringExtensions.cs +++ b/Neolution.Utilities/Extensions/StringExtensions.cs @@ -1,4 +1,4 @@ -namespace Neolution.Utilities; +namespace Neolution.Utilities.Extensions; /// /// String extensions. @@ -13,7 +13,7 @@ public static class StringExtensions /// The truncated string public static string? Truncate(this string? value, int maxLength) { - return Truncate(value, maxLength, string.Empty); + return value.Truncate(maxLength, string.Empty); } /// From 8c476d9034538b79a7ae7ff8e4020af319b9b5aa Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Tue, 7 Oct 2025 19:14:32 +0200 Subject: [PATCH 3/3] Fix --- .../Extensions/StringExtensionsTests.cs | 90 +++++++++++++++++-- .../Extensions/StringExtensions.cs | 17 +++- 2 files changed, 97 insertions(+), 10 deletions(-) diff --git a/Neolution.Utilities.UnitTests/Extensions/StringExtensionsTests.cs b/Neolution.Utilities.UnitTests/Extensions/StringExtensionsTests.cs index fa54d6c..3addce3 100644 --- a/Neolution.Utilities.UnitTests/Extensions/StringExtensionsTests.cs +++ b/Neolution.Utilities.UnitTests/Extensions/StringExtensionsTests.cs @@ -30,20 +30,22 @@ public void Truncate_ReturnsExpectedResult(string? value, int maxLength, string? } /// - /// Truncate with suffix returns the expected result. + /// Truncate with suffix returns the expected result when suffix length does not exceed maxLength. + /// maxLength represents the total length of the resulting string. /// /// The value. - /// The maximum length. + /// The maximum (final) length. /// The suffix. /// The expected. [Theory] [InlineData(null, 5, "...", null)] [InlineData("", 5, "...", "")] - [InlineData("abc", 5, "...", "abc")] - [InlineData("abcdef", 3, "...", "abc...")] - [InlineData("abcdef", 6, "...", "abcdef")] - [InlineData("abcdef", 4, "-", "abcd-")] - [InlineData("abcdef", 0, "!", "!")] + [InlineData("abc", 5, "...", "abc")] // no truncation because length <= maxLength + [InlineData("abcdef", 6, "...", "abcdef")] // no truncation + [InlineData("abcdef", 3, "...", "...")] // suffix length == maxLength => only suffix + [InlineData("abcdef", 4, "-", "abc-")] // keep 3 chars + '-' + [InlineData("abcdef", 4, "", "abcd")] // empty suffix behaves like plain truncate + [InlineData("abcdef", 0, "", "")] // zero length with empty suffix public void Truncate_WithSuffix_ReturnsExpectedResult(string? value, int maxLength, string suffix, string? expected) { // Act @@ -52,4 +54,78 @@ public void Truncate_WithSuffix_ReturnsExpectedResult(string? value, int maxLeng // Assert Assert.Equal(expected, result); } + + /// + /// Truncating with a negative maxLength throws an . + /// + [Fact] + public void Truncate_NegativeMaxLength_Throws() + { + Assert.Throws(() => "abc".Truncate(-1)); + Assert.Throws(() => "abc".Truncate(-5, "..")); + } + + /// + /// Truncating with a null suffix throws an . + /// + [Fact] + public void Truncate_NullSuffix_Throws() + { + Assert.Throws(() => "abc".Truncate(2, null!)); + } + + /// + /// Truncating with a suffix longer than maxLength throws an . + /// + [Fact] + public void Truncate_SuffixLongerThanMaxLength_Throws() + { + Assert.Throws(() => "abcdef".Truncate(3, "....")); + Assert.Throws(() => "abcdef".Truncate(0, ".")); + Assert.Throws(() => "abcdef".Truncate(2, "...")); + } + + /// + /// When the value length equals maxLength and a suffix is provided (shorter than maxLength), the value is returned unchanged. + /// + [Fact] + public void Truncate_ValueLengthEqualsMaxLength_WithSuffix_ReturnsUnchanged() + { + var value = "12345"; + var result = value.Truncate(5, "..."); + Assert.Equal("12345", result); + } + + /// + /// Result equals the suffix when suffix length == maxLength. + /// + [Fact] + public void Truncate_SuffixLengthEqualsMaxLength_ReturnsOnlySuffix() + { + var result = "abcdef".Truncate(3, "..."); + Assert.Equal("...", result); + } + + /// + /// Truncation works with Unicode surrogate pairs (keeping whole emoji, not splitting) when maxLength accommodates suffix + complete emoji. + /// + [Fact] + public void Truncate_UnicodeCharacters() + { + var value = "😀😃😄😁"; // 4 emoji (each surrogate pair) + var result = value.Truncate(5, "..."); // keep one emoji (2 code units) + '...' + Assert.Equal("😀...", result); + } + + /// + /// Truncation of a long string returns the expected prefix plus suffix with total length == maxLength. + /// + [Fact] + public void Truncate_LongString() + { + var value = new string('x', 1000); + var result = value.Truncate(10, "..."); // keep 7 + '...' + Assert.Equal(new string('x', 7) + "...", result); + Assert.Equal(10, result!.Length); + } } diff --git a/Neolution.Utilities/Extensions/StringExtensions.cs b/Neolution.Utilities/Extensions/StringExtensions.cs index 41965e7..7d4c5a6 100644 --- a/Neolution.Utilities/Extensions/StringExtensions.cs +++ b/Neolution.Utilities/Extensions/StringExtensions.cs @@ -21,15 +21,26 @@ public static class StringExtensions /// /// The value. /// The maximum length. - /// The suffix to append if the string is truncated. - /// The truncated string + /// The suffix to append if the string is truncated. The suffix length must not exceed . + /// The truncated string. + /// Thrown when is negative. + /// Thrown when is null. + /// Thrown when is longer than . public static string? Truncate(this string? value, int maxLength, string suffix) { + ArgumentOutOfRangeException.ThrowIfNegative(maxLength); + ArgumentNullException.ThrowIfNull(suffix); + + if (suffix.Length > maxLength) + { + throw new ArgumentException("Suffix length must not exceed maxLength.", nameof(suffix)); + } + if (string.IsNullOrEmpty(value) || value.Length <= maxLength) { return value; } - return $"{value[..maxLength]}{suffix}"; + return $"{value[..(maxLength - suffix.Length)]}{suffix}"; } }