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
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:
\s
regexs.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.
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.
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_
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.
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.
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_
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:
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 ? '…' : '';_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_
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.
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_
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"
// 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_
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:
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_
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 + '…'
}
}
Source: Stackoverflow.com