4
\$\begingroup\$

I've got a string that has values that are delimited by comma's, like so:

$var = '1,23,45,123,145,200';

I'd like to get just the first value, so what I do is create an array from it and get the first element:

$first = current(explode(',', $var));

Fine enough. But this string can sometimes contain perhaps hundreds of values. Exploding it into an array and only using the first one seems kind of a waste. Is there a smarter alternative which is also more performant/less wasteful? I'm thinking some sort of regex or trimming, but I'm guessing that could be actually slower...

\$\endgroup\$
1
  • \$\begingroup\$ +1 for not ignoring your gut feeling, and being reluctant to tackle this using regex. It's proof of sentient activity, some people lack \$\endgroup\$ Oct 30, 2013 at 11:11

3 Answers 3

8
\$\begingroup\$

UPDATE:
A more complete benchmark script:

$start = $first = $str = null;//create vars, don't benchmark this
//time preg_match
$start = microtime(true);
$first = $str = implode(',', range(213,9999));
if (preg_match('/^[^,]+/', $str, $match))
{
    $first = $match[0];
}
echo $first, PHP_EOL, microtime(true) - $start, ' time taken<br/>', PHP_EOL;
//time str* functions
$start = microtime(true);
$first = $str = implode(',', range(213,9999));
$first = substr($str, 0, strpos($str, ','));
echo $first, PHP_EOL, microtime(true) - $start, ' time taken<br/>', PHP_EOL;
//now explode + current
$first = null;
$start = microtime(true);
$str = implode(',', range(213, 9999));
$first = current(explode(',', $str));
echo $first, PHP_EOL, microtime(true) - $start, ' time taken';

The result varried a little, but after 100 runs, the averages amounted to:

#1 substr+strpos: ~.0022ms as 1//base for speed
#2 preg_match: ~.0041 as ~2//about twice as slow as #1
#3 explode: ~.00789 as ~4//about 4 times <=> #1, twice as slow <=> regex

You're absolutely right, exploding a string, constructing an array to get just the first value is a waste of resources, and it is not the fastest way to get what you want.
Some might run to regex for help, and chances are that, in your case that will be faster. But nothing I can think of will beat the speed of PHP's string functions (which are very close to the C string functions). I'd do this:

$first = substr($var, 0, strpos($var, ','));

If the comma isn't present (say $var = '123'), then your current approach will assign 123 to $first. To preserve this behaviour, I'd go for:

$first = strpos($var, ',') === false ? $var : substr($var, 0, strpos($var, ','));

This is to say: if strpos returns false, then there is no comma at all, so assign the entire string to $first, else get everything in front of the first comma.

For completeness sake (and after some initial bench-marking), using preg_match did indeed prove to be faster than using explode with large strings ($var = implode(',', range(1, 9999));), when using this code:

$first = $var = implode(',', range(1,9999));
if (preg_match('/^[^,]*/',$var, $match))
{
    $first = $match[0];
}

But honestly, I wouldn't use regex in this case.

In the interest of fairness, and to to clarify how I found the regex to be faster:

$start = microtime(true);
$first = $str = implode(',', range(213,9999));
if (preg_match('/^[^,]+/', $str, $match))
{
    $first = $match[0];
}
echo $first, PHP_EOL, $str, PHP_EOL, microtime(true) - $start, ' time taken';
$first = null;
$start = microtime(true);
$str = implode(',', range(213, 9999));
$first = current(explode(',', $str));
echo $first, PHP_EOL, microtime(true) - $start, ' time taken';
\$\endgroup\$
6
  • \$\begingroup\$ Thanks, I did some benchmarking myself and found that both string and regex solutions are a lot faster than exploding. They are about the same when using this exact code, when losing the ternary notation for the string solution, I found it was actually faster than the regex by about 20% (because ternary uses copy-on-write). So I think I'll use that. As you said: nothing beats the speed of PHP's string functions. \$\endgroup\$
    – kasimir
    Oct 30, 2013 at 11:01
  • \$\begingroup\$ @kasimir: I've edited my answer some more, adding my benchmark code, and its results (over 100 runs). I found the string functions to be twice as fast as regex. Though I did run it on a VM, and didn't check how I had configured PHP (it's been ages, and still running 5.3). But if my answer answered your question, would you mind awfully accepting (and or upvoting) it? \$\endgroup\$ Oct 30, 2013 at 11:09
  • \$\begingroup\$ Don't worry... Ok, so string function is definitely the fastest, great! Also, the regex solution is the 'ugliest' in my opinion, kind of obscuring what you are doing. \$\endgroup\$
    – kasimir
    Oct 30, 2013 at 13:15
  • 1
    \$\begingroup\$ Along the same lines: did you know $count = substr_count($var, ',') + 1; is a lot faster than $count = count(explode(',', $var));? codepad.org/KGqtWbxO \$\endgroup\$
    – kasimir
    Oct 30, 2013 at 13:29
  • \$\begingroup\$ @Kasimir: It ought to be... I would've been surprized if it wasn't. Iterating through a char[] comparing j += char[i] == 44 ? 1 : 0 each time just has to be faster than iterating through that string, copying every chunk of data that is not a comma to a new array, only to count the chunks just cannot be as fast, because both operations start by doing the same thing, the difference is the copying, which isn't done in the first case. Good of you to check, though. I'd +1 you again for not taking assumptions for granted, but actually bother checking those things. A commendable attitude \$\endgroup\$ Oct 30, 2013 at 13:59
2
\$\begingroup\$

I am not familiar with php syntax but I hope you could do this

 $var = "1,23,45,123,145,200";                                                                                                                                                                                                                                                                                                                                                                                                                     
 $first_word = substr($var, 0, strpos($var, ','));
\$\endgroup\$
2
\$\begingroup\$

I am shocked that the first three approaches that came to mind didn't even get considered/mentioned/tested!

In reverse order of my preference...

Least robust because only works on integers and poorly handles an empty string, casting string as integer: (Demo)

$tests = ['1,23,45,123,145,200', '345,999,0,1', '0', '0,2', '-1,-122', '', '1.5,2.9'];

foreach ($tests as $test) {
    var_export((int)$test);  
    echo "\n";
}
// 1
// 345
// 0
// 0
// -1
// 0
// 1

strstr() with the before_needle parameter: (Demo)

// same tests
$before_comma = strstr($test, ',', true);
var_export($before_comma === false ? $test : $before_comma);
// '1'
// '345'
// '0'
// '0'
// '-1'
// ''
// '1.5'

explode() with a limit parameter: (Demo)

// same tests
var_export(explode(',', $test, 2)[0]);
// '1'
// '345'
// '0'
// '0'
// '-1'
// ''
// '1.5'

While I don't like the idea of creating an array to extract a string value of the first element, it is a single call solution. Setting an element limit means that function isn't asked to do heaps of unnecessary labor.

I am a big fan of strstr() but it must involve a conditional to properly handle comma-less strings.

If your comma-separated string is never empty and only contains integers, I would strongly recommend the (int) approach, surely that is fastest.

As much as I love regex, I would not entertain the use of a preg_ call -- not even for a second.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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