[javascript] Shorten string without cutting words in JavaScript

I'm not very good with string manipulation in JavaScript, and I was wondering how you would go about shortening a string without cutting any word off. I know how to use substring, but not indexOf or anything really well.

Say I had the following string:

text = "this is a long string I cant display"

I want to trim it down to 10 characters, but if it doesn't end with a space, finish the word. I don't want the string variable to look like this:

"this is a long string I cant dis"

I want it to finish the word until a space occurs.

This question is related to javascript string substring

The answer is


If I understand correctly, you want to shorten a string to a certain length (e.g. shorten "The quick brown fox jumps over the lazy dog" to, say, 6 characters without cutting off any word).

If this is the case, you can try something like the following:

var yourString = "The quick brown fox jumps over the lazy dog"; //replace with your string.
var maxLength = 6 // maximum number of characters to extract

//Trim and re-trim only when necessary (prevent re-trim when string is shorted than maxLength, it causes last word cut) 
if(yourString.length > trimmedString.length){
    //trim the string to the maximum length
    var trimmedString = yourString.substr(0, maxLength);

    //re-trim if we are in the middle of a word and 
    trimmedString = trimmedString.substr(0, Math.min(trimmedString.length, trimmedString.lastIndexOf(" ")))
}

Here's a one-line version with a few useful properties:

  1. Handles any form of space matched by the \s regex
  2. Performs independent of input length (anything past the max length is not scanned)
  3. Performs independent of output length (scans backward from max length and doesn't split/join the string)
s.length > maxLen ? s.substring(0, s.substring(0, maxLen + 1).search(/\s+\S*$/)) : s

'Pasta with tomato and spinach'

if you do not want to cut the word in half

first iteration:

acc:0 / acc +cur.length = 5 / newTitle = ['Pasta'];

second iteration:

acc:5 / acc + cur.length = 9 / newTitle = ['Pasta', 'with'];

third iteration:

acc:9 / acc + cur.length = 15 / newTitle = ['Pasta', 'with', 'tomato'];

fourth iteration:

acc:15 / acc + cur.length = 18(limit bound) / newTitle = ['Pasta', 'with', 'tomato'];

const limitRecipeTitle = (title, limit=17)=>{
    const newTitle = [];
    if(title.length>limit){
        title.split(' ').reduce((acc, cur)=>{
            if(acc+cur.length <= limit){
                newTitle.push(cur);
            }
            return acc+cur.length;
        },0);
    }

    return `${newTitle.join(' ')} ...`
}

output: Pasta with tomato ...


Based on NT3RP answer which does not handle some corner cases, I've made this code. It guarantees to not return a text with a size > maxLength event an ellipsis ... was added at the end.

This also handle some corner cases like a text which have a single word being > maxLength

shorten: function(text,maxLength,options) {
    if ( text.length <= maxLength ) {
        return text;
    }
    if ( !options ) options = {};
    var defaultOptions = {
        // By default we add an ellipsis at the end
        suffix: true,
        suffixString: " ...",
        // By default we preserve word boundaries
        preserveWordBoundaries: true,
        wordSeparator: " "
    };
    $.extend(options, defaultOptions);
    // Compute suffix to use (eventually add an ellipsis)
    var suffix = "";
    if ( text.length > maxLength && options.suffix) {
        suffix = options.suffixString;
    }

    // Compute the index at which we have to cut the text
    var maxTextLength = maxLength - suffix.length;
    var cutIndex;
    if ( options.preserveWordBoundaries ) {
        // We use +1 because the extra char is either a space or will be cut anyway
        // This permits to avoid removing an extra word when there's a space at the maxTextLength index
        var lastWordSeparatorIndex = text.lastIndexOf(options.wordSeparator, maxTextLength+1);
        // We include 0 because if have a "very long first word" (size > maxLength), we still don't want to cut it
        // But just display "...". But in this case the user should probably use preserveWordBoundaries:false...
        cutIndex = lastWordSeparatorIndex > 0 ? lastWordSeparatorIndex : maxTextLength;
    } else {
        cutIndex = maxTextLength;
    }

    var newText = text.substr(0,cutIndex);
    return newText + suffix;
}

I guess you can easily remove the jquery dependency if this bothers you.


If you (already) using a lodash library, there is a function called truncate which can be used for trimming the string.

Based on the example on the docs page

_.truncate('hi-diddly-ho there, neighborino', {
  'length': 24,
  'separator': ' '
});
// => 'hi-diddly-ho there,...'

Here's yet another piece of code that truncates along punctuation marks (was looking for this and Google found this question here). Had to come up with a solution on my own, so this is what I hacked in 15 minutes. Finds all occurrences of . ! ? and truncates at any position of these that's < than len

function pos(str, char) {
    let pos = 0
    const ret = []
    while ( (pos = str.indexOf(char, pos + 1)) != -1) {
        ret.push(pos)
    }
    return ret
}

function truncate(str, len) {
    if (str.length < len)
        return str

    const allPos = [  ...pos(str, '!'), ...pos(str, '.'), ...pos(str, '?')].sort( (a,b) => a-b )
    if (allPos.length === 0) {
        return str.substr(0, len)
    }

    for(let i = 0; i < allPos.length; i++) {
        if (allPos[i] > len) {
            return str.substr(0, allPos[i-1] + 1)
        }
    }
}

module.exports = truncate

You can trim spaces with this:

var trimmedString = flabbyString.replace(/^\s*(.*)\s*$/, '$1');

Here is a solution in one line.

_x000D_
_x000D_
text = "this is a long string I cant display"_x000D_
_x000D_
function shorten(text,max) {_x000D_
    return text && text.length > max ? text.slice(0,max).split(' ').slice(0, -1).join(' ') : text_x000D_
}_x000D_
_x000D_
_x000D_
console.log(shorten(text,10));
_x000D_
_x000D_
_x000D_


Updated from @NT3RP I found that if the string happens to hit a space first time around it will end up deleting that word making your string one word shorter than it can be. So I just threw in an if else statement to check that the maxLength doesn't fall on a space.

codepen.io

var yourString = "The quick brown fox jumps over the lazy dog"; //replace with your string.
var maxLength = 15 // maximum number of characters to extract

if (yourString[maxLength] !== " ") {

//trim the string to the maximum length
var trimmedString = yourString.substr(0, maxLength);

alert(trimmedString)

//re-trim if we are in the middle of a word
trimmedString = trimmedString.substr(0, Math.min(trimmedString.length, trimmedString.lastIndexOf(" ")))
}

else {
  var trimmedString = yourString.substr(0, maxLength);
}

alert(trimmedString)

With boundary conditions like empty sentence and very long first word. Also, it uses no language specific string api/library.

_x000D_
_x000D_
function solution(message, k) {_x000D_
    if(!message){_x000D_
        return ""; //when message is empty_x000D_
    }_x000D_
    const messageWords = message.split(" ");_x000D_
    let result = messageWords[0];_x000D_
    if(result.length>k){_x000D_
        return ""; //when length of first word itself is greater that k_x000D_
    }_x000D_
    for(let i = 1; i<messageWords.length; i++){_x000D_
        let next = result + " " + messageWords[i];_x000D_
_x000D_
        if(next.length<=k){_x000D_
            result = next;_x000D_
        }else{_x000D_
            break;_x000D_
        }_x000D_
    }_x000D_
    return result;_x000D_
}_x000D_
_x000D_
console.log(solution("this is a long string i cant display", 10));
_x000D_
_x000D_
_x000D_


Everyone seems to forget that indexOf takes two arguments- the string to match, and the character index to start looking from. You can break the string at the first space after 10 characters.

function cutString(s, n){
    var cut= s.indexOf(' ', n);
    if(cut== -1) return s;
    return s.substring(0, cut)
}
var s= "this is a long string i cant display";
cutString(s, 10)

/*  returned value: (String)
this is a long
*/

I am kind of surprised that for a simple problem like this there are so many answers that are difficult to read and some, including the chosen one, do not work .

I usually want the result string to be at most maxLen characters. I also use this same function to shorten the slugs in URLs.

str.lastIndexOf(searchValue[, fromIndex]) takes a second parameter that is the index at which to start searching backwards in the string making things efficient and simple.

// Shorten a string to less than maxLen characters without truncating words.
function shorten(str, maxLen, separator = ' ') {
  if (str.length <= maxLen) return str;
  return str.substr(0, str.lastIndexOf(separator, maxLen));
}

This is a sample output:

for (var i = 0; i < 50; i += 3) 
  console.log(i, shorten("The quick brown fox jumps over the lazy dog", i));

 0 ""
 3 "The"
 6 "The"
 9 "The quick"
12 "The quick"
15 "The quick brown"
18 "The quick brown"
21 "The quick brown fox"
24 "The quick brown fox"
27 "The quick brown fox jumps"
30 "The quick brown fox jumps over"
33 "The quick brown fox jumps over"
36 "The quick brown fox jumps over the"
39 "The quick brown fox jumps over the lazy"
42 "The quick brown fox jumps over the lazy"
45 "The quick brown fox jumps over the lazy dog"
48 "The quick brown fox jumps over the lazy dog"

And for the slug:

for (var i = 0; i < 50; i += 10) 
  console.log(i, shorten("the-quick-brown-fox-jumps-over-the-lazy-dog", i, '-'));

 0 ""
10 "the-quick"
20 "the-quick-brown-fox"
30 "the-quick-brown-fox-jumps-over"
40 "the-quick-brown-fox-jumps-over-the-lazy"

For what it's worth I wrote this to truncate to word boundary without leaving punctuation or whitespace at the end of the string:

_x000D_
_x000D_
function truncateStringToWord(str, length, addEllipsis)_x000D_
{_x000D_
    if(str.length <= length)_x000D_
    {_x000D_
        // provided string already short enough_x000D_
        return(str);_x000D_
    }_x000D_
_x000D_
    // cut string down but keep 1 extra character so we can check if a non-word character exists beyond the boundary_x000D_
    str = str.substr(0, length+1);_x000D_
_x000D_
    // cut any non-whitespace characters off the end of the string_x000D_
    if (/[^\s]+$/.test(str))_x000D_
    {_x000D_
        str = str.replace(/[^\s]+$/, "");_x000D_
    }_x000D_
_x000D_
    // cut any remaining non-word characters_x000D_
    str = str.replace(/[^\w]+$/, "");_x000D_
_x000D_
    var ellipsis = addEllipsis && str.length > 0 ? '&hellip;' : '';_x000D_
_x000D_
    return(str + ellipsis);_x000D_
}_x000D_
_x000D_
var testString = "hi stack overflow, how are you? Spare";_x000D_
var i = testString.length;_x000D_
_x000D_
document.write('<strong>Without ellipsis:</strong><br>');_x000D_
_x000D_
while(i > 0)_x000D_
{_x000D_
  document.write(i+': "'+ truncateStringToWord(testString, i) +'"<br>');_x000D_
  i--;_x000D_
}_x000D_
_x000D_
document.write('<strong>With ellipsis:</strong><br>');_x000D_
_x000D_
i = testString.length;_x000D_
while(i > 0)_x000D_
{_x000D_
  document.write(i+': "'+ truncateStringToWord(testString, i, true) +'"<br>');_x000D_
  i--;_x000D_
}
_x000D_
_x000D_
_x000D_


I'm late to the party, but here's a small and easy solution I came up with to return an amount of words.

It's not directly related to your requirement of characters, but it serves the same outcome that I believe you were after.

function truncateWords(sentence, amount, tail) {
  const words = sentence.split(' ');

  if (amount >= words.length) {
    return sentence;
  }

  const truncated = words.slice(0, amount);
  return `${truncated.join(' ')}${tail}`;
}

const sentence = 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.';

console.log(truncateWords(sentence, 10, '...'));

See the working example here: https://jsfiddle.net/bx7rojgL/


I took a different approach. While I needed a similar result I wanted to keep my return value less than the specified length.

function wordTrim(value, length, overflowSuffix) {
    value = value.trim();
    if (value.length <= length) return value;
    var strAry = value.split(' ');
    var retString = strAry[0];
    for (var i = 1; i < strAry.length; i++) {
        if (retString.length >= length || retString.length + strAry[i].length + 1 > length) break;
        retString += " " + strAry[i];
    }
    return retString + (overflowSuffix || '');
}

Edit I refactored it a bit here: JSFiddle Example. It rejoins the original array instead of concatenating.

function wordTrim(value, length, overflowSuffix) {
    if (value.length <= length) return value;
    var strAry = value.split(' ');
    var retLen = strAry[0].length;
    for (var i = 1; i < strAry.length; i++) {
        if(retLen == length || retLen + strAry[i].length + 1 > length) break;
        retLen+= strAry[i].length + 1
    }
    return strAry.slice(0,i).join(' ') + (overflowSuffix || '');
}

shorten(str, maxLen, appendix, separator = ' ') {
if (str.length <= maxLen) return str;
let strNope = str.substr(0, str.lastIndexOf(separator, maxLen));
return (strNope += appendix);

}

var s= "this is a long string and I cant explain all"; shorten(s, 10, '...')

/* "this is .." */


There are lots of ways to do it, but a regular expression is a useful one line method:

"this is a longish string of text".replace(/^(.{11}[^\s]*).*/, "$1"); 
//"this is a longish"

This expressions returns the first 11 (any) characters plus any subsequent non-space characters.

Example script:

<pre>
<script>
var t = "this is a longish string of text";

document.write("1:   " + t.replace(/^(.{1}[^\s]*).*/, "$1") + "\n");
document.write("2:   " + t.replace(/^(.{2}[^\s]*).*/, "$1") + "\n");
document.write("5:   " + t.replace(/^(.{5}[^\s]*).*/, "$1") + "\n");
document.write("11:  " + t.replace(/^(.{11}[^\s]*).*/, "$1") + "\n");
document.write("20:  " + t.replace(/^(.{20}[^\s]*).*/, "$1") + "\n");
document.write("100: " + t.replace(/^(.{100}[^\s]*).*/, "$1") + "\n");
</script>

Output:

1:   this
2:   this
5:   this is
11:  this is a longish
20:  this is a longish string
100: this is a longish string of text

we can do this easily by using truncate function of lodash

_.truncate('hi-diddly-ho there, neighborino');
// => 'hi-diddly-ho there, neighbo...'

_.truncate('hi-diddly-ho there, neighborino', {
  'length': 24,
  'separator': ' '
 });
// => 'hi-diddly-ho there,...'

go on Lodash Documentation for more clearence.


This excludes the final word instead of including it.

function smartTrim(str, length, delim, appendix) {
    if (str.length <= length) return str;

    var trimmedStr = str.substr(0, length+delim.length);

    var lastDelimIndex = trimmedStr.lastIndexOf(delim);
    if (lastDelimIndex >= 0) trimmedStr = trimmedStr.substr(0, lastDelimIndex);

    if (trimmedStr) trimmedStr += appendix;
    return trimmedStr;
}

Usage:

smartTrim(yourString, 11, ' ', ' ...')
"The quick ..."

I came late for this but I think this function makes exactly what OP requests. You can easily change the SENTENCE and the LIMIT values for different results.

_x000D_
_x000D_
function breakSentence(word, limit) {_x000D_
  const queue = word.split(' ');_x000D_
  const list = [];_x000D_
_x000D_
  while (queue.length) {_x000D_
    const word = queue.shift();_x000D_
_x000D_
    if (word.length >= limit) {_x000D_
      list.push(word)_x000D_
    }_x000D_
    else {_x000D_
      let words = word;_x000D_
_x000D_
      while (true) {_x000D_
        if (!queue.length ||_x000D_
            words.length > limit ||_x000D_
            words.length + queue[0].length + 1 > limit) {_x000D_
          break;_x000D_
        }_x000D_
_x000D_
        words += ' ' + queue.shift();_x000D_
      }_x000D_
_x000D_
      list.push(words);_x000D_
    }_x000D_
  }_x000D_
_x000D_
  return list;_x000D_
}_x000D_
_x000D_
const SENTENCE = 'the quick brown fox jumped over the lazy dog';_x000D_
const LIMIT = 11;_x000D_
_x000D_
// get result_x000D_
const words = breakSentence(SENTENCE, LIMIT);_x000D_
_x000D_
// transform the string so the result is easier to understand_x000D_
const wordsWithLengths = words.map((item) => {_x000D_
  return `[${item}] has a length of - ${item.length}`;_x000D_
});_x000D_
_x000D_
console.log(wordsWithLengths);
_x000D_
_x000D_
_x000D_

The output of this snippet is where the LIMIT is 11 is:

[ '[the quick] has a length of - 9',
  '[brown fox] has a length of - 9',
  '[jumped over] has a length of - 11',
  '[the lazy] has a length of - 8',
  '[dog] has a length of - 3' ]

function shorten(str,n) {
  return (str.match(RegExp(".{"+n+"}\\S*"))||[str])[0];
}

shorten("Hello World", 3); // "Hello"

_x000D_
_x000D_
// SHORTEN STRING TO WHOLE WORDS_x000D_
function shorten(s,l) {_x000D_
  return (s.match(new RegExp(".{"+l+"}\\S*"))||[s])[0];_x000D_
}_x000D_
_x000D_
console.log( shorten("The quick brown fox jumps over the lazy dog", 6) ); // "The quick"
_x000D_
_x000D_
_x000D_


Didn't find the voted solutions satisfactory. So I wrote something thats is kind of generic and works both first and last part of your text (something like substr but for words). Also you can set if you'd like the spaces to be left out in the char-count.

    function chopTxtMinMax(txt, firstChar, lastChar=0){
        var wordsArr = txt.split(" ");
        var newWordsArr = [];

        var totalIteratedChars = 0;
        var inclSpacesCount = true;

        for(var wordIndx in wordsArr){
            totalIteratedChars += wordsArr[wordIndx].length + (inclSpacesCount ? 1 : 0);
            if(totalIteratedChars >= firstChar && (totalIteratedChars <= lastChar || lastChar==0)){
                newWordsArr.push(wordsArr[wordIndx]);
            }
        }

        txt = newWordsArr.join(" ");
        return txt;
    }

Lodash has a function specifically written for this: _.truncate

const truncate = _.truncate
const str = 'The quick brown fox jumps over the lazy dog'

truncate(str, {
  length: 30, // maximum 30 characters
  separator: /,?\.* +/ // separate by spaces, including preceding commas and periods
})

// 'The quick brown fox jumps...'

You can use truncate one-liner below:

_x000D_
_x000D_
const text = "The string that I want to truncate!";_x000D_
_x000D_
const truncate = (str, len) => str.substring(0, (str + ' ').lastIndexOf(' ', len));_x000D_
_x000D_
console.log(truncate(text, 14));
_x000D_
_x000D_
_x000D_


Typescript, and with ellipses :)

export const sliceByWord = (phrase: string, length: number, skipEllipses?: boolean): string => {
  if (phrase.length < length) return phrase
  else {
    let trimmed = phrase.slice(0, length)
    trimmed = trimmed.slice(0, Math.min(trimmed.length, trimmed.lastIndexOf(' ')))
    return skipEllipses ? trimmed : trimmed + '…'
  }
}

Examples related to javascript

need to add a class to an element How to make a variable accessible outside a function? Hide Signs that Meteor.js was Used How to create a showdown.js markdown extension Please help me convert this script to a simple image slider Highlight Anchor Links when user manually scrolls? Summing radio input values How to execute an action before close metro app WinJS javascript, for loop defines a dynamic variable name Getting all files in directory with ajax

Examples related to string

How to split a string in two and store it in a field String method cannot be found in a main class method Kotlin - How to correctly concatenate a String Replacing a character from a certain index Remove quotes from String in Python Detect whether a Python string is a number or a letter How does String substring work in Swift How does String.Index work in Swift swift 3.0 Data to String? How to parse JSON string in Typescript

Examples related to substring

Go test string contains substring How does String substring work in Swift Delete the last two characters of the String Split String by delimiter position using oracle SQL How do I check if a string contains another string in Swift? Python: Find a substring in a string and returning the index of the substring bash, extract string before a colon SQL SELECT everything after a certain character Finding second occurrence of a substring in a string in Java Select query to remove non-numeric characters