67

I am trying to truncate some long text in C#, but I don't want my string to be cut off part way through a word. Does anyone have a function that I can use to truncate my string at the end of a word?

E.g:

"This was a long string..."

Not:

"This was a long st..."
bluish
  • 26,356
  • 27
  • 122
  • 180
TimS
  • 5,922
  • 6
  • 35
  • 55

10 Answers10

99

Try the following. It is pretty rudimentary. Just finds the first space starting at the desired length.

public static string TruncateAtWord(this string value, int length) {
    if (value == null || value.Length < length || value.IndexOf(" ", length) == -1)
        return value;

    return value.Substring(0, value.IndexOf(" ", length));
}
Annie
  • 3,090
  • 9
  • 36
  • 74
David
  • 19,389
  • 12
  • 63
  • 87
  • 16
    Perfect! And not a regex in sight :) – TimS Oct 23 '09 at 14:41
  • 6
    It might make sense to find the first space BEFORE the desired length? Otherwise, you have to guess at what the desired length? – mlsteeves Oct 23 '09 at 14:46
  • 2
    Also should be -1 for not using regex ;) – Goran Oct 23 '09 at 14:49
  • @mlseeves Good point but I'm not too worried in this instance, as this is just a vanity function, so there is no fixed length cut off. – TimS Oct 23 '09 at 14:56
  • 1
    The `@string` usage is uncalled-for: it's unnecessary and confusing in this instance. The parameter could as easily have been named `str`. If not for that, I would have upvoted this answer. – LBushkin Oct 23 '09 at 14:57
  • 2
    I don't agree on the use of the abbreviation "str" - I think abbreviations always make code less readable. +1 for the comment to use "value" instead. – Jon Rea Dec 16 '13 at 11:58
  • This answer is better than other answers, just needs one change tho, instead of `IndexOf` you should use `LastIndexOf`, otherwise the method's output could be longer than the `length` – Mehdi Dehghani Aug 06 '21 at 12:46
88

Thanks for your answer Dave. I've tweaked the function a bit and this is what I'm using ... unless there are any more comments ;)

public static string TruncateAtWord(this string input, int length)
{
    if (input == null || input.Length < length)
        return input;
    int iNextSpace = input.LastIndexOf(" ", length, StringComparison.Ordinal);
    return string.Format("{0}…", input.Substring(0, (iNextSpace > 0) ? iNextSpace : length).Trim());
}
Dan
  • 12,808
  • 7
  • 45
  • 54
TimS
  • 5,922
  • 6
  • 35
  • 55
  • 3
    Further to this, I am also now calling another string utility function from within this one, which strips out any HTML tags (using RegEx). This minimises the risk of broken HTML as a result of truncation, as all string will be in plain text. – TimS Oct 27 '09 at 12:17
  • 7
    Note that this method looks for the first space *AFTER* the specified length value, almost always causing the resulting string to be longer than the value. To find the last space prior to length, simply substitute `input.LastIndexOf(" ", length)` when calculating `iNextSpace`. – CBono Feb 14 '10 at 17:02
  • 3
    +100 for CBono's comment - this needs to be before! In the eventofareallylongwordlikethisoneyouwillhaveaverylongstringthatisfarbeyondyourdesiredlength! – Jason Jun 23 '10 at 16:30
  • 1
    Note also that the ellipsis (three periods) appended to the end of the truncated string will push the string over the maximum length in certain cases. – dthrasher Nov 22 '10 at 14:56
  • 7
    There is a ellipses char that you could add instead. '…' – Örjan Jämte Jul 08 '11 at 09:38
  • 2
    In line 5 I would suggest to use: `int iNextSpace = input.LastIndexOf(" ", length, System.StringComparison.Ordinal);` for language specific characters – Goran Žuri Dec 04 '12 at 20:50
  • as @GoranŽuri has recommended ( and Resharper also gave me the suggestion / warning) I added `StringComparison.Ordinal` in line 5 and also tested it to make sure it works as expected. – Shiva May 15 '17 at 01:15
5

My contribution:

public static string TruncateAtWord(string text, int maxCharacters, string trailingStringIfTextCut = "&hellip;")
{
    if (text == null || (text = text.Trim()).Length <= maxCharacters) 
      return text;

    int trailLength = trailingStringIfTextCut.StartsWith("&") ? 1 
                                                              : trailingStringIfTextCut.Length; 
    maxCharacters = maxCharacters - trailLength >= 0 ? maxCharacters - trailLength 
                                                     : 0;
    int pos = text.LastIndexOf(" ", maxCharacters);
    if (pos >= 0)
        return text.Substring(0, pos) + trailingStringIfTextCut;

    return string.Empty;
}

This is what I use in my projects, with optional trailing. Text will never exceed the maxCharacters + trailing text length.

bluish
  • 26,356
  • 27
  • 122
  • 180
Contra
  • 2,754
  • 4
  • 20
  • 18
4

If you are using windows forms, in the Graphics.DrawString method, there is an option in StringFormat to specify if the string should be truncated, if it does not fit into the area specified. This will handle adding the ellipsis as necessary.

http://msdn.microsoft.com/en-us/library/system.drawing.stringtrimming.aspx

mlsteeves
  • 1,251
  • 2
  • 16
  • 20
3

This solution works too (takes first 10 words from myString):

String.Join(" ", myString.Split(' ').Take(10))
  • This is actually pretty neat. There are some scenarios it doesn't cater for (`word.` for example), but generally a nice readable approach. –  May 02 '18 at 11:03
3

I took your approach a little further:

public string TruncateAtWord(string value, int length)
{
    if (value == null || value.Trim().Length <= length)
        return value;

    int index = value.Trim().LastIndexOf(" ");

    while ((index + 3) > length)
        index = value.Substring(0, index).Trim().LastIndexOf(" ");

    if (index > 0)
        return value.Substring(0, index) + "...";

    return value.Substring(0, length - 3) + "...";
}

I'm using this to truncate tweets.

Douglas Ludlow
  • 10,754
  • 6
  • 30
  • 54
  • I would consider extracting the "..." into a constant, because if you decide to change it you have to update it in 4 places now (if you include the number 3) – Max Carroll Apr 01 '20 at 15:51
2

Taking into account more than just a blank space separator (e.g. words can be separated by periods followed by newlines, followed by tabs, etc.), and several other edge cases, here is an appropriate extension method:

    public static string GetMaxWords(this string input, int maxWords, string truncateWith = "...", string additionalSeparators = ",-_:")
    {
        int words = 1;
        bool IsSeparator(char c) => Char.IsSeparator(c) || additionalSeparators.Contains(c);

        IEnumerable<char> IterateChars()
        {
            yield return input[0];

            for (int i = 1; i < input.Length; i++)
            {
                if (IsSeparator(input[i]) && !IsSeparator(input[i - 1]))
                    if (words == maxWords)
                    {
                        foreach (char c in truncateWith)
                            yield return c;

                        break;
                    }
                    else
                        words++;

                yield return input[i];
            }
        }

        return !input.IsNullOrEmpty()
            ? new String(IterateChars().ToArray())
            : String.Empty;
    }
SysCafe
  • 21
  • 2
  • This is the only version that solved it for me, handling quotes... eg.. "User Updated 'Appointment Date' to 2020-10-28T12:00:00.000Z from 2020-10-28T04:00:00.000Z" – Dwain Browne Oct 24 '20 at 00:36
1

simplified, added trunking character option and made it an extension.

    public static string TruncateAtWord(this string value, int maxLength)
    {
        if (value == null || value.Trim().Length <= maxLength)
            return value;

        string ellipse = "...";
        char[] truncateChars = new char[] { ' ', ',' };
        int index = value.Trim().LastIndexOfAny(truncateChars);

        while ((index + ellipse.Length) > maxLength)
            index = value.Substring(0, index).Trim().LastIndexOfAny(truncateChars);

        if (index > 0)
            return value.Substring(0, index) + ellipse;

        return value.Substring(0, maxLength - ellipse.Length) + ellipse;
    }
Leon van Wyk
  • 679
  • 8
  • 7
  • this simply does not work as intended. Please at least do a sanity check before posting anything here. – avs099 Apr 02 '17 at 12:19
1

Heres what i came up with. This is to get the rest of the sentence also in chunks.

public static List<string> SplitTheSentenceAtWord(this string originalString, int length)
    {
        try
        {
            List<string> truncatedStrings = new List<string>();
            if (originalString == null || originalString.Trim().Length <= length)
            {
                truncatedStrings.Add(originalString);
                return truncatedStrings;
            }
            int index = originalString.Trim().LastIndexOf(" ");

            while ((index + 3) > length)
                index = originalString.Substring(0, index).Trim().LastIndexOf(" ");

            if (index > 0)
            {
                string retValue = originalString.Substring(0, index) + "...";
                truncatedStrings.Add(retValue);

                string shortWord2 = originalString;
                if (retValue.EndsWith("..."))
                {
                    shortWord2 = retValue.Replace("...", "");
                }
                shortWord2 = originalString.Substring(shortWord2.Length);

                if (shortWord2.Length > length) //truncate it further
                {
                    List<string> retValues = SplitTheSentenceAtWord(shortWord2.TrimStart(), length);
                    truncatedStrings.AddRange(retValues);
                }
                else
                {
                    truncatedStrings.Add(shortWord2.TrimStart());
                }
                return truncatedStrings;
            }
            var retVal_Last = originalString.Substring(0, length - 3);
            truncatedStrings.Add(retVal_Last + "...");
            if (originalString.Length > length)//truncate it further
            {
                string shortWord3 = originalString;
                if (originalString.EndsWith("..."))
                {
                    shortWord3 = originalString.Replace("...", "");
                }
                shortWord3 = originalString.Substring(retVal_Last.Length);
                List<string> retValues = SplitTheSentenceAtWord(shortWord3.TrimStart(), length);

                truncatedStrings.AddRange(retValues);
            }
            else
            {
                truncatedStrings.Add(retVal_Last + "...");
            }
            return truncatedStrings;
        }
        catch
        {
            return new List<string> { originalString };
        }
    }
-1

I use this

public string Truncate(string content, int length)
    {
        try
        {
            return content.Substring(0,content.IndexOf(" ",length)) + "...";
        }
        catch
        {
            return content;
        }
    }
Zhou Hai
  • 17
  • 2