12

I'd like to be able to pass an array to a function and have the function behave differently depending on whether it's a "list" style array or a "hash" style array. E.g.:

myfunc(array("One", "Two", "Three")); // works
myfunc(array(1=>"One", 2=>"Two", 3=>"Three")); also works, but understands it's a hash

Might output something like:

One, Two, Three
1=One, 2=Two, 3=Three

ie: the function does something differently when it "detects" it's being passed a hash rather than an array. Can you tell I'm coming from a Perl background where %hashes are different references from @arrays?

I believe my example is significant because we can't just test to see whether the key is numeric, because you could very well be using numeric keys in your hash.

I'm specifically looking to avoid having to use the messier construct of myfunc(array(array(1=>"One"), array(2=>"Two"), array(3=>"Three")))

koregan
  • 10,054
  • 4
  • 23
  • 36
Tom Auger
  • 19,421
  • 22
  • 81
  • 104
  • 2
    Looking for something like this? http://stackoverflow.com/questions/173400/php-arrays-a-good-way-to-check-if-an-array-is-associative-or-sequential – Jeff Lambert May 13 '11 at 19:30
  • Interesting question. PHP unfortunately does not distinguish `array('a','b','c')` from `array(0=>'a',1=>'b',2=>'c')`... – Tadeck May 13 '11 at 19:31
  • 1
    PHP will always store numeric keys `array("1" => "1")` as integers. Can't detect that. You can only probe for continually growing keys to differentiate true lists from indexed arrays. – mario May 13 '11 at 19:35
  • Correct. Unfortunately it does not distinguish 'simple' arrays and those with numeric strings as keys (see my answer). – Tadeck May 13 '11 at 19:42
  • Duplicates http://stackoverflow.com/q/173400/287948 – Peter Krauss Sep 05 '15 at 19:41

6 Answers6

39

Pulled right out of the kohana framework.

public static function is_assoc(array $array)
{
    // Keys of the array
    $keys = array_keys($array);

    // If the array keys of the keys match the keys, then the array must
    // not be associative (e.g. the keys array looked like {0:0, 1:1...}).
    return array_keys($keys) !== $keys;
}
16

This benchmark gives 3 methods.

Here's a summary, sorted from fastest to slowest. For more informations, read the complete benchmark here.

1. Using array_values()

function($array) {
    return (array_values($array) !== $array);
}

2. Using array_keys()

function($array){
    $array = array_keys($array); return ($array !== array_keys($array));
}

3. Using array_filter()

function($array){
    return count(array_filter(array_keys($array), 'is_string')) > 0;
}
Hussard
  • 656
  • 7
  • 14
  • It would be good to annotate this with the actual benchmark results in case the link goes stale. It's very important to note that the `array_filter` approach is orders of magnitude __slower__ than the others. – Tom Auger Mar 04 '15 at 21:42
  • `[0 => 'a', 2 => 'b', 3=>'c']` returns true for the first case. – ssi-anik Jul 09 '20 at 19:29
7

PHP treats all arrays as hashes, technically, so there is not an exact way to do this. Your best bet would be the following I believe:

if (array_keys($array) === range(0, count($array) - 1)) {
   //it is a hash
}
Explosion Pills
  • 188,624
  • 52
  • 326
  • 405
  • This is the only solution that was posted so I'll accept it, despite its inherent limitation of not accounting for non-sequential array indices. This question: http://stackoverflow.com/questions/173400/php-arrays-a-good-way-to-check-if-an-array-is-associative-or-sequential has a lot more solutions, none of which are perfect. – Tom Auger Jun 15 '11 at 13:27
0

No, PHP does not differentiate arrays where the keys are numeric strings from the arrays where the keys are integers in cases like the following:

$a = array("0"=>'a', "1"=>'b', "2"=>'c');
$b = array(0=>'a', 1=>'b', 2=>'c');

var_dump(array_keys($a), array_keys($b));

It outputs:

array(3) {
    [0]=> int(0) [1]=> int(1) [2]=> int(2)
}
array(3) {
    [0]=> int(0) [1]=> int(1) [2]=> int(2)
}

(above formatted for readability)

Tadeck
  • 132,510
  • 28
  • 152
  • 198
0

Being a little frustrated, trying to write a function to address all combinations, an idea clicked in my mind: parse json_encode result.

When a json string contains a curly brace, then it must contain an object!

Of course, after reading the solutions here, mine is a bit funny... Anyway, I want to share it with the community, just to present an attempt to solve the problem from another prospective (more "visual").


function isAssociative(array $arr): bool
    {
        // consider empty, and [0, 1, 2, ...] sequential
        if(empty($arr) || array_is_list($arr)) {
            return false;
        }

        // first scenario:
        // [  1  => [*any*] ]
        // [ 'a' => [*any*] ]
        foreach ($arr as $key => $value) {
            if(is_array($value)) {
                return true;
            }
        }

         // second scenario: read the json string
        $jsonNest = json_encode($arr, JSON_THROW_ON_ERROR);

        return str_contains($jsonNest, '{'); // {} assoc, [] sequential
    }

NOTES

php@8.1 is required, check out the gist on github containing the unit test of this method + Polyfills (php>=7.3).

I've tested also Hussard's posted solutions, A & B are passing all tests, C fails to recognize: {"1":0,"2":1}.

BENCHMARKS

Here json parsing is ~200 ms behind B, but still 1.7 seconds faster than solution C!

What do you think about this version? Improvements are welcome!

funder7
  • 1,622
  • 17
  • 30
  • `empty($arr)` can be safely replaced with the function-less equivalent `!$arr`. – mickmackusa Feb 11 '22 at 12:08
  • does it cover empty string, and variable without a value cases? – funder7 Feb 15 '22 at 10:58
  • 1
    `empty($v)` and `!$v` will both return `true` on a string with no length. They both perform a loosely typed `false` check. – mickmackusa Feb 15 '22 at 11:05
  • Interesting, thank you! I thought that `empty()` was the most safe way to do this kind of check. – funder7 Feb 15 '22 at 23:05
  • `!empty()` or `isset()` should ONLY be used if you need to check that the variable exists AND you want the secondary check that they provide. `!empty()` checks that the variable exists and is not "falsey". `isset()` checks that the variable exists and is not null. There are many pages on Stack Overflow that explain the intricacies in excruciating detail. – mickmackusa Feb 15 '22 at 23:18
  • By the way `str_contains()` is a very easy check to monkeywrench. `$arr = ['one', 'two{'];` Is it indexed? Yes. Does it contain a opening curly brace? Yes. It breaks your script. Perhaps just check the first byte of the string instead of the whole string. – mickmackusa Feb 15 '22 at 23:20
  • Why does `in_array($value)` have any bearing on whether an array is associative/sequential? I don't understanding your intention. I'd recommend something like: https://stackoverflow.com/a/35858728/2943403 – mickmackusa Feb 15 '22 at 23:28
  • it's related on how `foreach` works: even if the array is sequential, it will always have a `key` (numeric when sequential), and a value. Anyway probably you're right: I considered as sequential, any array of "simple" values, like `[1, 2, 3]` or `["a", "b", "c"]`, but `$value` may be a list of arrays, and still be considered as sequential. I must test this scenario! I think that the condition should be updated to `if(is_string($key) && is_array($value))` – funder7 Feb 21 '22 at 20:57
0

My solution is to get keys of an array like below and check that if the key is not integer:

private function is_hash($array) {
    foreach($array as $key => $value) {
        return ! is_int($key);
    }
    return false;
}

It is wrong to get array_keys of a hash array like below:

array_keys(array(
       "abc" => "gfb",
       "bdc" => "dbc"
       )
);

will output:

array(
       0 => "abc",
       1 => "bdc"
)

So, it is not a good idea to compare it with a range of numbers as mentioned in top rated answer. It will always say that it is a hash array if you try to compare keys with a range.

GO.exe
  • 646
  • 7
  • 13
  • Walk me through your function, because there's something I'm missing. You have a foreach loop, but you return on the very first iteration of that loop! What's the point of the loop if you're just returning whether the first key is an integer? Perhaps you meant to set a flag to true and then in our loop if it IS an integer, set the flag to false? – Tom Auger Dec 20 '11 at 14:24
  • otherwise you always get an integer value as an array key thats why! – GO.exe Dec 27 '11 at 07:42
  • This is closer to the real answer IMHO. Tom Auger has a point, though. I think it would be more like: private function is_hash($array) { foreach(array_keys($array) as $key) { if (is_int($key)) { return false; } } return true; } – Garvin Feb 27 '17 at 20:45