2
\$\begingroup\$

I've received a request at work and the senior developer is away on vacation so I can't ask him. The request was to truncate lengthy article titles while maintaining readability. In other words, don't simply chop a word in half and add an ellipsis.

I wrote an extension method that looks for punctuation and establishes length requirements.

Bullet Points:

  • truncate at sentence endings ('.','!','?')
  • allow for variable maxLength
  • make sure title is at least 30 chars long (for the case where a title starts like "wow!")
  • when all else fails truncate at last space and add an ellipsis

Anyway, the method works the way I intended, however, the programming feels gross.

What is a more succinct way of writing this string extension method?

I've reproduced the code in a fiddle: .NET Fiddle

Extension method

public static class StringExtensions
{

    public static string TruncateAtFirstSentence(this string value, int maxLength)
    {

        char[] punc = new char[3];
        punc[0] = '.';
        punc[1] = '!';
        punc[2] = '?';

        // not the best programming, basically falls though when requirements
        // arent met. 
        foreach (char c in value) {

            if (c == punc[2]) {

                var str = value.Substring(0, value.IndexOf("?") + 1);

                if (str.Length <= 30)
                    continue;
                else 
                {
                    if (str.Length > maxLength)
                    {
                        continue;
                    }
                    else
                    {
                        return str;
                    }
                }
            }
            else if (c == punc[1]) {

                var str = value.Substring(0, value.IndexOf("!") + 1);

                if (str.Length <= 30)
                    continue;
                else 
                {
                    if (str.Length > maxLength)
                    {
                        continue;
                    }
                    else
                    {
                        return str;
                    }
                }
            }
            else if (c == punc[0]) {

                var str = value.Substring(0, value.IndexOf(".") + 1);

                if (str.Length <= 30)
                    continue;
                else 
                {
                    if (str.Length > maxLength)
                    {
                        continue;
                    }
                    else
                    {
                        return str;
                    }
                }
            }
        }


        var output = value.Length <= maxLength ? value : value.Substring(0, maxLength);
        output = output.Substring(0, output.LastIndexOf(' '));

        return output + "...";

    }

}

Main program

public class Program
{

    public static void Main()
    {

        string sentence = "This is a simple! question that ends with! a question mark. This is nearly the same. sentence that also ends with a period.";
        string longSentence = "This is a simple question that does not end with a question mark This is nearly the same sentence that ends with a period.";

        var output = StringExtensions.TruncateAtFirstSentence(sentence, 100);
        var output2 = StringExtensions.TruncateAtFirstSentence(longSentence, 100);


        Console.WriteLine("output 1: \n{0}\n", output);
        Console.WriteLine("output 2: \n{0}\n", output2);
    }
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

It's an interesting problem isn't it :)

Here's my extension method that does the same (stripped of some additional options)

public static string TruncateTo(
    this string input,
    int truncateLength,
    bool addEllipsis = true)
{
    // Check to see if we even need to bother truncating
    if (string.IsNullOrWhiteSpace(input) || input.Length <= truncateLength)
        return input;

    // copy the input
    var temp = input;

    // Define a list of characters that we can safely break on
    char[] anyOf = { '.', ',', ';', ':', '?', '!' };

    // LastIndexOfAny starts at the max position works backwards thru' the string
    var truncatePosition = temp.LastIndexOfAny(anyOf, truncateLength);

    // no appropriate place to truncate. 
    if (truncatePosition == -1)
    {
        // Return original string but you could fall back to splitting on last space
        return temp;
    }
    temp = temp.Substring(0, truncatePosition + (addEllipsis ? 0 : 1));
    return addEllipsis
        ? temp + "..."
        : temp;
}

I've added comments but I think the code is pretty clear. I've also modified the code a fair bit as we support quite a few different ways of deciding where to split.

You'll also notice that it's greedy - I.e. it tries to keep the string as long as possible.

\$\endgroup\$
5
  • \$\begingroup\$ very nice rob, much less verbose than mine, and I like the addition of your addEllipsis boolean... I'm going to mess with this for a bit and choose an answer this afternoon. \$\endgroup\$ Apr 2, 2015 at 15:32
  • \$\begingroup\$ also thanks to whomever voted this post up, I don't have privileges quite yet. I'll come back and upvote once I do. \$\endgroup\$ Apr 2, 2015 at 15:34
  • \$\begingroup\$ I had to select this as the answer, it does almost exactly what i needed. You could probably hear my facepalm from space the moment I realized how much unnecessary code could be eliminated by using LastIndexOfAny(). A few slight modifications and this code will be perfect. thanks @RobH \$\endgroup\$ Apr 2, 2015 at 15:48
  • 2
    \$\begingroup\$ Why do you copy the input, string are immuable \$\endgroup\$
    – Bruno
    Apr 2, 2015 at 15:52
  • \$\begingroup\$ @Bruno - I can't remember... It's probable that it's important in the fuller algorithm but don't have the code here now. \$\endgroup\$
    – RobH
    Apr 2, 2015 at 16:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.