Question

I am trying to chop text to a fixed length, but I don't want to chop words at half, so I need to do something like this:

function fixedLength(str, len, bol) {
    var i, l = str.length, left = 0, right = l - 1, rtn, tmp;

    tmp = str.charAt(len);

    if (bol || tmp === " " || tmp === "") {
        rtn = str.substr(0, len);
    } else {
        tmp = len - 1;

        for (i = tmp; i > -1; i--) {
            if (str.charAt(i) === " ") {
                left = i;
                break;
            }
        }

        for (i = tmp; i < l; i++) {
            if (str.charAt(i) === " ") {
                right = i;
                break;
            }
        }

        rtn = str.substr(0, ((Math.abs(left - tmp) <= Math.abs(right - tmp)) ? left : right));
    }

    return rtn + "...";
}

But when I use it with this:

var str = "the quick brown fox jumped over the lazy dog";

for (var i = 0; i < 45; i++) {
    document.write("i:" + i + " - " + fixedLength(str, i) + "<br>");
}

Everyone seems to work correctly except in this line "i:43 - the quick brown fox jumped over the lazy do...", the word "dog" is chopped in half (Demo)

I can't find the flaw, every time I change something, I add a new bugs

Was it helpful?

Solution

Didn't check your code, but you could write your code more simple:

function fixedLength(str, len, bol) {
    while(!bol && str[len] && str[len] !== ' ') {
      len--;
    }
    return str.substr(0, len) + '...';
}

And the demo.

OTHER TIPS

In case you're interested, the bug in your original code was in the final assignment to the return value, where you compared tmp - left and tmp - right. The problem is that in the case of the last word in the string, "dog", right never gets re-assigned after its initial value is set to l - 1; and so your algorithm acts as if a space were found at index 44 when in fact there is a g there.

You could try to use regex to find the last space character.

function fixedLength(str, len, bol) {
    if(str.length <= len) {
       return str;  
    }

    var rtn = str.substr(0, len).match(/.* /);
    if(rtn == null) {
        rtn = "";
    } else {
        rtn = rtn + "...";
    }
    return rtn;
}

Demo here: http://jsfiddle.net/R8qMQ/2/

I also added a verification, if the input string is already in the max allowed length, simply return it. If there are no words that can be split, return an empty string instead of NULL.

I would go with regex since I could also add other characters in the future that I might consider as word delimiters (e.g. . or ; or maybe REGEX b - word delimiter).

rtn = str.substr(0, ((Math.abs(left - tmp + 1) <= Math.abs(right - tmp)) ? left : right));

I didn't debug it with a debugger but using your Demo was very helpful.

I tried refactoring your code in a very understandable manner:

function prettyCut(word, length) {
    //account for ellipses
    length -= 3;

    //break down into lengths
    var units = word.split(" ").map(function(word) {
        return word.length+1;
    });

    //map to accumulated sums
    var runningSum = 0;
    var sums = units.map(function(length, index) {
        return (runningSum += length)
    }); 

    //find difference from goal of each sum
    var differences = sums.map(function(sum) {
        return (length-sum)>0?(length-sum):100;
    });

    //minimize and return
    var closest = Math.min.apply(this, differences);
    return closest==0?word:word.substr(0, length-closest)+"...";
}
prettyCut("the quick brown fox jumped over the lazy dog", 45);
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top