String extension method that truncates at sentence punctuation 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 &lt;= 30)
                    continue;
                else 
                {
                    if (str.Length &gt; maxLength)
                    {
                        continue;
                    }
                    else
                    {
                        return str;
                    }
                }
            }
            else if (c == punc[1]) {

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

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

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

                if (str.Length &lt;= 30)
                    continue;
                else 
                {
                    if (str.Length &gt; maxLength)
                    {
                        continue;
                    }
                    else
                    {
                        return str;
                    }
                }
            }
        }


        var output = value.Length &lt;= 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);
    }
}
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 &lt;= 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.