Last active
February 12, 2024 14:09
-
-
Save rklec/8a78ae5fb5e5bcea0dcd1920d1caa0a4 to your computer and use it in GitHub Desktop.
StringExtensions -> TruncateInMiddle for Stackexchange
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| /// <summary> | |
| /// Common extensions for manipulating strings. | |
| /// </summary> | |
| public static class StringExtensions | |
| { | |
| /// <summary> | |
| /// This truncates/abbreviates the string and places the separator as a user-facing indicator <i>in the middle</i> of that string. | |
| /// </summary> | |
| /// <remarks>For example, "1234567890abcdef" gets truncated as "12…def" if you have a limit of 6 characters.</remarks> | |
| /// <param name="input">the string to truncate</param> | |
| /// <param name="maxLength">the maximum length of the resulting string</param> | |
| /// <param name="separator">optionally, the separator to use, by default the ellipsis …</param> | |
| /// <returns>The truncated string, if necessary.</returns> | |
| /// <exception cref="ArgumentException">if the input parameters are invalid</exception> | |
| public static string TruncateInMiddle(this string input, int maxLength, string separator = "…") | |
| { | |
| if (input is null) | |
| throw new ArgumentNullException(nameof(input)); | |
| if (string.IsNullOrWhiteSpace(separator)) | |
| throw new ArgumentNullException(nameof(separator)); | |
| if (separator.Length > maxLength) | |
| throw new ArgumentOutOfRangeException(nameof(separator), | |
| $"Separator \"{separator}\" (length: {separator.Length}) must _NOT_ be larger than the maxLength {maxLength}." | |
| ); | |
| if (maxLength < separator.Length * 2) | |
| throw new ArgumentOutOfRangeException(nameof(maxLength), | |
| $"Length limit {maxLength} must be larger than double of length of the separation string \"{separator}\" (length: {separator.Length})x2 for proper display." | |
| ); | |
| if (input.Length <= maxLength) | |
| return input; | |
| var inputSpan = input.AsSpan(); | |
| var prefixEnd = maxLength / 2 - separator.Length; | |
| var prefix = inputSpan[..prefixEnd]; | |
| var remainingLength = (int)Math.Ceiling(maxLength / 2d); | |
| var suffixStart = input.Length - remainingLength; | |
| var suffix = inputSpan[suffixStart..]; | |
| return string.Concat(prefix, separator, suffix); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [TestOf(typeof(StringExtensions))] | |
| public class StringExtensionsTest | |
| { | |
| public class TruncateInMiddle : StringExtensionsTest | |
| { | |
| [TestCase(null)] | |
| public void ThrowIfInputIsNull(string? input) | |
| { | |
| // Arrange | |
| var separator = TestContext.CurrentContext.Random.GetString(100); | |
| // Act | |
| #pragma warning disable CS8604 // Possible null reference argument. | |
| var action = () => input.TruncateInMiddle(1000, separator); | |
| #pragma warning restore CS8604 // Possible null reference argument. | |
| // Assert | |
| action.Should().Throw<ArgumentNullException>() | |
| .WithMessage($"*(Parameter '{nameof(input)}')"); | |
| } | |
| [TestCase(null)] | |
| [TestCase("")] | |
| public void ThrowIfSeparatorIsNullOrEmpty(string? separator) | |
| { | |
| // Act | |
| #pragma warning disable CS8604 // Possible null reference argument. | |
| var action = () => "doesNotMatter".TruncateInMiddle(100, separator); | |
| #pragma warning restore CS8604 // Possible null reference argument. | |
| // Assert | |
| action.Should().Throw<ArgumentNullException>() | |
| .WithMessage($"*(Parameter '{nameof(separator)}')"); | |
| } | |
| [TestCase(0, 1)] | |
| [TestCase(1, 2)] | |
| [TestCase(2, 3)] | |
| [TestCase(3, 4)] | |
| [TestCase(4, 5)] | |
| [TestCase(1, 5)] | |
| public void ThrowsIfSeparatorIsTooLong(int maxLength, int separatorLength) | |
| { | |
| // Arrange | |
| var separator = TestContext.CurrentContext.Random.GetString(separatorLength); | |
| // Act | |
| var action = () => "doesNotMatter".TruncateInMiddle(maxLength, separator); | |
| // Assert | |
| action.Should().ThrowExactly<ArgumentOutOfRangeException>() | |
| .WithMessage("*(Parameter 'separator')") | |
| .WithMessage("*Separator * must _NOT_ be larger than * maxLength*") | |
| .WithMessage($"*{maxLength}*") | |
| .WithMessage($"*\"{separator}\"*") | |
| .WithMessage($"*(length: {separatorLength}*"); | |
| } | |
| [Test] | |
| public void ThrowsForTooSmallMaxLengths([Range(4, 5)] int maxLength) | |
| { | |
| // Act | |
| var action = () => "doesNotMatter".TruncateInMiddle(maxLength, "..."); | |
| // Assert | |
| action.Should().ThrowExactly<ArgumentOutOfRangeException>() | |
| .WithMessage($"*(Parameter '{nameof(maxLength)}')") | |
| .WithMessage("*Length limit * must be larger than double of length of the separation string*") | |
| .WithMessage($"*{maxLength}*") | |
| .WithMessage("*\"...\"*") | |
| .WithMessage("*(length: 3)x2*"); | |
| } | |
| [TestCase("", 10, "..", ExpectedResult = "")] | |
| [TestCase("abcde", 10, "..", ExpectedResult = "abcde")] | |
| public string ReturnsSpecialStringsSmallerThanLimitUnchanged(string inputString, | |
| int maxLength, | |
| string separator) | |
| { | |
| // Act | |
| var outputString = inputString.TruncateInMiddle(maxLength, separator); | |
| Debug.WriteLine(outputString); | |
| // Assert | |
| outputString.Should().NotContain(separator); | |
| return outputString; | |
| } | |
| [TestCase("abcde", 4, "..", ExpectedResult = "..de")] | |
| [TestCase("123456789", 5, "..", ExpectedResult = "..789")] | |
| [TestCase("1234567890abcdef", 6, "...", ExpectedResult = "...def")] | |
| [TestCase("1234567890abcdef", 6, "…", ExpectedResult = "12…def")] | |
| public string TruncatesMinimalExamplesCorrectly(string inputString, int maxLength, string separator) | |
| { | |
| // Act | |
| var outputString = inputString.TruncateInMiddle(maxLength, separator); | |
| Debug.WriteLine(outputString); | |
| // Assert | |
| outputString.Should().HaveLength(maxLength) | |
| .And.Contain(separator); | |
| return outputString; | |
| } | |
| [Test] | |
| public void AppliesMaxLength([Random(100, 150, 5)] int randomLength, [Random(2, 100, 5)] int maxLength) | |
| { | |
| // Arrange | |
| var inputString = TestContext.CurrentContext.Random.GetString(randomLength); | |
| Debug.WriteLine(inputString); | |
| // Act | |
| var outputString = inputString.TruncateInMiddle(maxLength); | |
| Debug.WriteLine(outputString); | |
| // Assert | |
| outputString.Should().HaveLength(maxLength) | |
| .And.Contain("…"); | |
| } | |
| [Test] | |
| public void ReturnsStringsSmallerThanLimitUnchanged([Random(2, 100, 5)] int randomLength, | |
| [Random(100, 150, 5)] int maxLength) | |
| { | |
| // Arrange | |
| var inputString = TestContext.CurrentContext.Random.GetString(randomLength); | |
| Debug.WriteLine(inputString); | |
| // Act | |
| var outputString = inputString.TruncateInMiddle(maxLength); | |
| Debug.WriteLine(outputString); | |
| // Assert | |
| outputString.Should().Be(inputString); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment