PHP Break String on Spaces using a Maximum Line Length

Posted: 2013/02/26 in Development, PHP

Background:

I need to break a paragraph into lines that are no longer than 60 characters for a particular, text-only email template.  I want the lines to break at/on spaces (because just breaking at the 60th character would probably cut words in half, randomly… not very professional!).

Solution, conceptually:

I need to go 60 characters ‘down’ (into) the string, and then come ‘back’ to the closest space.   Cut the string there, put the ‘first’ part (before the cut) into an array, and then repeat on what’s left-over until the there are no more left-overs that are more than 60 characters long.

Easy solution… strrpos():

There is a great function in PHP labeled strrpos() that searches for a string (needle) inside another string (haystack), but starting at the end of the haystack.  It takes two, obvious parameters ‘haystack’ and ‘needle’.  But it can also take a third parameter: ‘offset’.  Speaking to this ‘offset’, the manual page states “If specified, search will start this number of characters counted from the beginning of the string.”  Awesome!  I can do the following, then:

$first_line_break = strrpos($haystack, ' ', 60);

Nope. Here’s an example that if you run, you’ll see that this just doesn’t work:

$x = implode('', range('a','z')); // create a string of English letters a thru z
$x .= $x . $x . $x;  // quadruple it (it is now 104 characters long: 26 letters  *  4 times)
// Now let's find the second 'b' which should be at position 27
//  (position 25 = first 'z', 'b' = 2 characters after that)
//  So I'll start with an offset of '54' which gets me at the end of the second alphabet,
//    thereby searching 'back' to the letter 'b' should find the 2nd 'b'
echo "The second 'b' is at position: " . strrpos($x, 'b', 54) . "\n";
// output = ... position: 79

Next step: Check helpful, user-contributed notes and sure enough, either this documentation doesn’t make sense to me and some others or (eek) there’s a bit of a bug here.

Working solution:

My solution, then, is to do the following:

  1. Pull first 60 characters off of the beginning of the (haystack) string
  2. Use strrpos() without the ‘offset’ parameter to find the position of the last space
  3. Put from position 0 to the last-space into a temporary array
  4. Chop the current string (haystack) at the point of the last-space
  5. Repeat until the (haystack) string is less than 60 characters long – then just add the remaining less-than-60-characters to the temporary array

Here is the code in a somewhat verbose manner:

<?php
$max = (int) 60;
$string = 'I need this string to be broken apart so that no line is never more than $max characters long.';
$string .= '  However, I only want this string "broken" in/at/on the spaces.';
$in_pieces = array();

while (strlen($string) > $max) {
    // location of last space in the first $max characters
    $substring = substr($string, 0, $max);
    // pull from position 0 to $substring position
    $in_pieces[] = trim( substr( $substring, 0, strrpos($substring, ' ') ) );
    // $string (haystack) now = haystack with out the first $substring characters
    $string = substr($string, strrpos($substring, ' '));
    // UPDATE (2013 Oct 16) - if remaining string has no spaces AND has a string length
    //  greater than $max, then this will loop infinitely!  So instead, just have to bail-out (boo!)
    if (strlen($string) > $max) break;
}
$in_pieces[] = trim($string); // final bits o' text
echo "\n\n" . var_export($in_pieces, TRUE) . "\n\n";

Cracked myself up when I first did this because I forgot the last bit o’ text!  And I also forgot to trim() everything.

Updated: 2013 Mar 11 @ 9:53pm UTC-7 — forgot the ‘strrpos() function in last line of while() loop.  That is why I had extra parentheses in that line (thank you, ‘jesse’, for pointing that out!

Updated: 2013 Oct 16 @ 8:52am UTC-6 — added a break to the while-loop for when the remaining $string has not more spaces AND the length of $string is greater than the max-length ($max).  This was causing an infinite loop because, in this situation, the strlen($string) will always > $max but $string but $string will never get any smaller.  Sadly, found this out on a Live/Production site.  DOH!

Updated: 2014 Jan 29 @ 8:24am UTC-6 — updated the comments (I originally had ” // greater than, then will loop infinitely! So instead,…” – greater than WHAT!?! Oh, greater than $max!)

Advertisements
Comments
  1. jesse says:

    Thanks. I really didn’t want to have to figure this out myself. Really clever. btw you have an extra ‘)’ on your ‘$string =’ in your while statement. the var_export wasn’t all that needed for me and I just created a new variable = $in_peices[0]; Thank you so much.

  2. Good catch, jesse. It’s not that there isn’t an extra close-parenthesis — there was a missing function!! I forgot the strrpos() function – which would then use the close-parenthesis.
    – also added a semicolon to the end of the first $string=”…..” line. Thought I tested this before publishing!!

    David

  3. Good work David! I guess you missed sth in the last line for breaking the loop: Right now the temp array only holds maximum 2 lines with the second one including all the remaining string. To solve that you could use sth like

    if (strlen($string) > $max && strrpos($string, ‘ ‘) == -1) break;

    to get an array of lines, each with max length.

    Cheers, Stefan

  4. Sorry, to much JS in the last days… strrpos($string, ‘ ‘) return false, not -1 if it fails 🙂 Anyway there was another issue that made my server running out of memory so I re-wrote your function some bit:

    function cutLines($string, $max = 20){

    $in_pieces = array();

    while(strlen($string) > $max){

    $substring = substr($string, 0, $max);

    if( !strrpos($substring, ‘ ‘) ) break; //This one is new

    $cut = strrpos($substring, ‘ ‘);
    $in_pieces[] = trim( substr( $substring, 0, $cut ) );
    $string = substr($string, $cut);

    };

    $in_pieces[] = trim($string); // final bits of text

    //return array of lines
    return $in_pieces;

    }

    • Thanks Stefan! The strings I’ve been ‘breaking’ have been pretty small – as in, they would never come close to having my PHP instance run out of memory.

      I do like the new line you have – the ‘logic’ is much more obvious with the way you have written it.

      Perhaps since I only use smaller incoming $strings and, thus, am not thinking in terms of maximum RAM… I’m curious as to how your solution prevents the server from running out of memory (compared to the code I have posted) – can you explain?

      Please know, I am not in any way defending my code nor doubting you (as sometimes code-based bloggers can be a bit defensive!) – what I’m saying is that I don’t understand due to my lack of experience, knowledge, etc. But I’d like to know!

      Thanks for taking the time to post replies AND solutions!!!

      David

      • Hey David,

        i dont really know but i guess it ended up in an endless loop within the while. Maybe this was a special reason because i tried to break Newsfeeds down to lines so there was a lot of work. I was just walking through your code and tried to optimize it a bit:

        1. One less call of strrpos($substring, ‘ ‘) by storing the position in a var
        2. Moved the break statement to the beginning and comparing the substring, not string. otherwise (in your case) you would have the same statement to break as in your while-loop. Correct me if i missed something but

        while (strlen($string) > $max) {
        ….
        if (strlen($string) > $max) break;
        }

        will never loop a second time, right?

        Cheers, Stefan 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s