Skip to content

Instantly share code, notes, and snippets.

@rklec
Last active February 12, 2024 14:09
Show Gist options
  • Select an option

  • Save rklec/8a78ae5fb5e5bcea0dcd1920d1caa0a4 to your computer and use it in GitHub Desktop.

Select an option

Save rklec/8a78ae5fb5e5bcea0dcd1920d1caa0a4 to your computer and use it in GitHub Desktop.
StringExtensions -> TruncateInMiddle for Stackexchange
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);
}
}
[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