2
\$\begingroup\$

I need to optimize the below code, as much as possible in:

  • using less resources as possible
  • being as fast as possible

What the code does is simple, it splits the string by words and only if a word is equal or bigger than 40 characters it will add spaces every X characters to the word that was too big:

<?php

$text = "more text here 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000.1% more text here!";

foreach (explode(" ", $text) as $word)
{
    if (strlen($word) >= 40)
    {
        $word = wordwrap($word, 8, " ", true);
    }
    $result[] = $word;
}
$data = implode(" ", $result);
echo $data;

Live DEMO

Is there anything that could be improved in this already simple code?

\$\endgroup\$
2
\$\begingroup\$

I would think a better approach might be to identify location of spaces in string and look for cases where there is index position > 40 between these spaces, and then to insert the space into the string as necessary.

This would certainly be a memory-optimized way of working with this as you could potentially only need to hold the subject string (with any inserted separators) in memory, along with a few helper variables.

Your current approach would hold all the following in memory:

  • $text - the main subject string
  • temporary storage of explode results
  • $word - a substring duplication of content already stored in $text)
  • $result - an array containing duplicated content from $text + inserts
  • $data - a duplicate copy of content in $result just joined into a string on the separator.

So, there is a lot of duplication of storage in your solution.

I am guessing there is also increased operational complexity around the explode() and implode() operations that could be avoided.

I would also consider generalizing this into a function. I have given some sample code below for how this might look:

function chunk_substring(
    $subject_string,
    $separator = ' ',
    $max_substring_length = 40,
    $chunk_size = 8
) {
    // not shown - validate input as non-zero length strings or positive integers
    // as appropriate. May also want to check logic around chunk size
    // needing to be less than max substring length and such

    // loop through index positions of separator in target string
    $previous_index = 0;
    $continue = true;
    $separator_length = strlen($separator);
    while ($continue) {
        $next_index = strpos($subject_string, $separator, $previous_index);
        if($next_index === false) {
            // separator not found in remainder of string
            $continue = false;
            continue;
        }
        if($next_index === $previous_index) {
            // we had a separator match at beginning of string
            // or consecutive separators
            // let's increment $previous_index by separator length
            $previous_index += $separator_length ;
            continue;
        }
        if(($next_index - $previous_index) >= $max_substring_length) {
            // we have a "long" substring
            // let's start inserting separators into it
            $idx = $previous_index + $chunk_size;          
            while ($idx < $next_index) {
                // insert $separator at $idx
                $subject_string = substr_replace(
                    $subject_string,
                    $separator,
                    $idx,
                    0
                );

                // update $next_index to account for length of newly added
                // segment to string
                $next_index += $separator_length;

                // increase $idx by $chunk_size + $separator_length
                $idx = $idx + $chunk_size + $separator_length;
            }
        }
        // We are done evaluating / modifying this string segment
        // Set $previous_index to $next_index value in preparation for
        // next iteration
        $previous_index = $next_index;              
    }

    // return the string
    return $subject_string;
}

This approach would hold the following in memory:

  • $subject_string - would potentially grow as separators are inserted. You could also potentially have the function pass this by reference if you desire to have this function directly modify the external variable that is passed in
  • $separator - small string
  • Various low memory footprint integer variables for configuration and index storage.

Much less duplication in memory in this case to where overall memory utilization would scale (more or less) 1:1 with size of input string.

You might take a look at the accepted answer for the StackOverflow post I have linked below which deals with the related question of using explode() vs. string tokenization, which is, in essence, what I am proposing. You might consider running a similar test to find out for your self what works best for your use cases.

https://stackoverflow.com/questions/5861826/exploding-an-array-within-a-foreach-loop-parameter

\$\endgroup\$
  • \$\begingroup\$ Hi, thanks for the detailed reply, I will look into it some time today, need to digest all that information and perform some tests. \$\endgroup\$ – Prix Sep 15 '16 at 9:45
  • \$\begingroup\$ It partially works, but apparently it only catches the first long word, see eval.in/643179 \$\endgroup\$ – Prix Sep 16 '16 at 1:53
  • \$\begingroup\$ I am not able to reproduce. I made one change in answer about to have comparison be >= instead of > for 40 to match your requirements. Here are some some sandbox tests I put together using this code - looks like it is working correctly unless I mis-understand the problem statement - sandbox.onlinephpfunctions.com/code/… \$\endgroup\$ – Mike Brant Sep 16 '16 at 17:25
  • \$\begingroup\$ @Prix I did also fix one logic error in incrementing $idx withii my answer as well. Even with this bug, I still was not seeing the problem you mention. \$\endgroup\$ – Mike Brant Sep 16 '16 at 17:31
  • \$\begingroup\$ I have no idea why, but the same place u posted it gives the same issue sandbox.onlinephpfunctions.com/code/… \$\endgroup\$ – Prix Sep 16 '16 at 22:46
2
\$\begingroup\$

Normally I take the time to study Mike Brant's answers (because they are so frequently educational to me), but this time it was tl;dr.

If this was my project, I'd specifically/exclusively target the "long" strings with regex to reduce overall handling in the task. My pattern uses \S{40,} to target only 40 or more non-white-space characters as your method does. If you wish to be more specific about valid characters (perhaps exclude punctuation), you might use a character class or negated character class, but I can't make a call on that give your posted information.

Yes, regex is notoriously less efficient than simple string functions, but the substring targeting advantage in using it in this case may likely tip the scale.

Code: (Demo)

function chunkify($m) {
    return wordwrap($m[0], 8, " ", true);
}

$text = "Thisis39charactersandshouldstaytogether Thisisover39characterssoitmustbechunked!";
echo preg_replace_callback('/\S{40,}/', 'chunkify', $text);

Like your method would produce, this is the output:

Thisis39charactersandshouldstaytogether Thisisov er39char actersso itmustbe chunked!

*notice the difference if you change \S to \w.

Depending on the length and content of your input string, this will be a better approach because you don't have to explode on every space, process each element, and implode. This approach seem very clean and direct to me.

If you like one-liners, here is the equivalent:

echo preg_replace_callback('/\S{40,}/', function($m) { return wordwrap($m[0], 8, " ", true); }, $text);

If you want variable control:

$min_length=40;
$chunk_size=8;
echo preg_replace_callback('/\S{'.$min_length.',}/', function($m) use ($chunk_size) { return wordwrap($m[0], $chunk_size, " ", true); }, $text);

Please do some realist benchmarks with the input strings that may hit your code and let me know how this fairs (I'm curious).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.