とある開発で必要になったこともあり、任意の配列が連想配列なのか、通常の配列なのかを判定する関数を作ってみた。

基本的に配列が連想配列かどうかは配列キー(添え字)がすべて数値かどうかで判定できるので、キーだけ引っ張って来てそれらすべてが数値であるかを確認すればよいことになる。
早速、プロトタイプとして次のような関数を作ってみた。

function is_assoc( &$data ) {
  if (!is_array($data)) 
    return false;

  return !empty(array_diff( array_keys($data), range(0, count($data) - 1) ));
}

実行結果を見てみると、

$array_1 = array( 1, 4, 7, 11 );
var_dump(is_assoc($array_1)); // false => OK

$array_2 = array( 'a', 'a' => 'b', 'c', 'b' => 'd' );
var_dump(is_assoc($array_2)); // true => OK

想定していた結果を返してくれる。
では、次にパフォーマンスを見てみる。検証用に下記のようなコードを組んで、

$time_start = microtime(true);
$result = is_assoc($array);
$time_end = microtime(true);
$time = $time_end - $time_start;
printf('Result: %s / ProcTime: %f sec.', $result ? 'True' : 'False', $time);

次のような大きなサイズの配列を判定させてみた。

$array = range(1, 100000); // Result: False / ProcTime: 1.406080 sec.

$array = array_merge( [ 'a' => true ], range(1, 100000) ); // Result: True / ProcTime: 1.439082 sec.

$array = array_merge( range(1, 100000), [ 'a' => true ] ); // Result: True / ProcTime: 1.417081 sec.

う~ん、パフォーマンスが悪いな。
コード量は増えるけど、イテレータ方式に改めてみた。

function is_assoc( &$data ) {
  if (!is_array($data)) 
    return false;

  $keys = array_keys($data);
  $range = range(0, count($data) - 1);
  foreach ($keys as $i => $value) {
    if (!is_int($value) || $value !== $range[$i]) {
      return false;
    }
  }

  return true;
}

これでもう一回測定…

$array = range(1, 100000); // Result: False / ProcTime: 0.073004 sec.

$array = array_merge( [ 'a' => true ], range(1, 100000) ); // Result: True / ProcTime: 0.041002 sec.

$array = array_merge( range(1, 100000), [ 'a' => true ] ); // Result: True / ProcTime: 0.059004 sec.

処理時間が劇的に減った!

さらに、配列の参照渡しは不要なのでやめる。あと、わざわざ range() で比較配列作って数値添え字の連続性まで判定しなくても良いことに気づいた。むしろ、このままだと数値添え字が連続していないと連想配列として誤判定されてしまうので、その辺もあわせて修正した。
それではまた測定してみよう…

$array = range(1, 100000); // Result: False / ProcTime: 0.032002 sec.

$array = array_merge( [ 'a' => true ], range(1, 100000) ); // Result: True / ProcTime: 0.004000 sec.

$array = array_merge( range(1, 100000), [ 'a' => true ] ); // Result: True / ProcTime: 0.032002 sec.

$array = array( 'abc', 6 => 'nhj', '101' => [ 'a' ] ); // Result: False / ProcTime: 0.000000 sec.

おぉ~、かなり良いパフォーマンスになった。
しかも連続していない数値添え字の通常配列に対しても正常な判定を行ってくれる。

多次元配列への対応

さて、ここで多次元配列の扱いをどうしようか迷った。

多次元配列とは、配列が入れ子になっている配列のことで、キーが数値添え字であっても値が配列であれば多次元化する。そもそも、PHPではキーの添え字が数値だろうか文字列だろうが、配列が入れ子になった多次元配列だろうが配列の定義は一つであり、それを区別したければ利用する側でいかようにもしてくださいというポリシーだ。
多次元配列を連想配列と見なすかどうかは利用シーンによるわけで、そんな曖昧な区分けを関数側には持たせられない。そこは引数で区分けできるようにしておくのが無難だ。

つまり、もし一次元のキーが数値添え字である多次元配列を連想配列として取り扱いたい場合には、第二引数に true を指定すれば良いようにしてみた。指定しなければデフォルト値として false となり、多次元配列は連想配列に含めない建付けだ。

──というわけで、最終ソースは下記の通り。

/**
 * Whether the associative array or not
 *
 * @param array $data This argument should be expected an array
 * @param boolean $multidimensional True if a multidimensional array is inclusion into associative array, the default value is false
 * @return boolean
 */
function is_assoc( $data, $multidimensional=false ) {
  if (!is_array($data) || empty($data)) 
    return false;

  $has_array = false;
  foreach ($data as $key => $value) {
    if (is_array($value)) 
      $has_array = true;

    if (!is_int($key)) 
      return true;
  }

  return $multidimensional && $has_array ? true : false;

}