Collection Framework - need to cache result from the PHP method - php

Need help/clarification with caching results from the PHP method that takes a string and returns the number of unique characters in the string (method below ⬇️ done and works)
function uniqueCharacters($str): int
{
$counter = [];
$splitted = str_split($str);
foreach($splitted as $s) {
if(!isset($counter[$s]))
$counter[$s] = 0;
$counter[$s]++;
}
$unique = array_keys(array_filter($counter, function($c) {
return $c == 1;
}));
return count($unique);
}
It is expected that a string with the same character sequence may be passed several times to the method. Since the counting operation can be time-consuming, the method should cache the results, so that when the method is given a string previously encountered, it will simply retrieve the stored result. Use collections and maps where appropriate.
Tried a solution from https://stackoverflow.com/questions/3540403/caching-function-results-in-php but that one is not what was asked from the task...

Just keep a static cache in your function and only do the work if encountering the string for the first time. The static keyword will ensure that the variable's contents are not lost between subsequent function calls.
Because you only have a single string as an incoming parameter, you don't need to serialize the data to form a string. When declaring string keys in an array, those values will not be corrupted. However if your values -- to become keys -- are floats, booleans, null, arrays, objects, resources, (and probably a few other types that I'm not thinking of) then you'd better convert it to some form of string to be safe. In other words, you can safely use my snippet if your incoming parameter is string or int typed (not a mix of int and strings).
Code: (Demo)
function countUniqueCharacters(string $str): int
{
static $cache = [];
if (!key_exists($str, $cache)) {
echo PHP_EOL . "*** doing the work on $str ***";
$cache[$str] = count(
array_filter(
count_chars($str, 1),
fn($count) => $count === 1
)
);
}
return $cache[$str];
}
$tests = ['seven', 'eight', 'nine', 'seven', 'eight'];
foreach ($tests as $test) {
echo PHP_EOL . "$test : " . countUniqueCharacters($test);
}
Output:
*** doing the work on seven ***
seven : 3
*** doing the work on eight ***
eight : 5
*** doing the work on nine ***
nine : 2
seven : 3
eight : 5
Because your return value is an int (cannot be null), you can simplify the above using the null coalescing operator like this: (Demo)
function countUniqueCharacters(string $str): int
{
static $cache = [];
return $cache[$str] ??= count(
array_filter(
count_chars($str, 1),
fn($count) => $count === 1
)
);
}
Relevant links:
PHP function that caches its response by re-writing itself after first call
How to cache results of a function in laravel
Is there a better way to cache a property in PHP?

Related

Test if array of the form [x_1, x_2, x_3 ...] [duplicate]

PHP treats all arrays as associative, so there aren't any built in functions. Can anyone recommend a fairly efficient way to check if an array "is a list" (contains only numeric keys starting from 0)?
Basically, I want to be able to differentiate between this:
$sequentialArray = [
'apple', 'orange', 'tomato', 'carrot'
];
and this:
$assocArray = [
'fruit1' => 'apple',
'fruit2' => 'orange',
'veg1' => 'tomato',
'veg2' => 'carrot'
];
You have asked two questions that are not quite equivalent:
Firstly, how to determine whether an array has only numeric keys
Secondly, how to determine whether an array has sequential numeric keys, starting from 0
Consider which of these behaviours you actually need. (It may be that either will do for your purposes.)
The first question (simply checking that all keys are numeric) is answered well by Captain kurO.
For the second question (checking whether the array is zero-indexed and sequential), you can use the following function:
function isAssoc(array $arr)
{
if (array() === $arr) return false;
return array_keys($arr) !== range(0, count($arr) - 1);
}
var_dump(isAssoc(['a', 'b', 'c'])); // false
var_dump(isAssoc(["0" => 'a', "1" => 'b', "2" => 'c'])); // false
var_dump(isAssoc(["1" => 'a', "0" => 'b', "2" => 'c'])); // true
var_dump(isAssoc(["a" => 'a', "b" => 'b', "c" => 'c'])); // true
To merely check whether the array has non-integer keys (not whether the array is sequentially-indexed or zero-indexed):
function has_string_keys(array $array) {
return count(array_filter(array_keys($array), 'is_string')) > 0;
}
If there is at least one string key, $array will be regarded as an associative array.
Surely this is a better alternative.
<?php
$arr = array(1,2,3,4);
$isIndexed = array_values($arr) === $arr;
Many commenters in this question don't understand how arrays work in PHP. From the array documentation:
A key may be either an integer or a string. If a key is the standard representation of an integer, it will be interpreted as such (i.e. "8" will be interpreted as 8, while "08" will be interpreted as "08"). Floats in key are truncated to integer. The indexed and associative array types are the same type in PHP, which can both contain integer and string indices.
In other words, there is no such thing as an array key of "8" because it will always be (silently) converted to the integer 8. So trying to differentiate between integers and numeric strings is unnecessary.
If you want the most efficient way to check an array for non-integer keys without making a copy of part of the array (like array_keys() does) or all of it (like foreach does):
function keyedNext( &$arr, &$k){
$k = key($arr);
return next($arr);
}
for ($k = key(reset($my_array)); is_int($k); keyedNext($my_array,$k))
$onlyIntKeys = is_null($k);
This works because key() returns NULL when the current array position is invalid and NULL can never be a valid key (if you try to use NULL as an array key it gets silently converted to "").
As stated by the OP:
PHP treats all arrays as associative
it is not quite sensible (IMHO) to write a function that checks if an array is associative. So first thing first: what is a key in a PHP array?:
The key can either be an integer or a string.
That means there are 3 possible cases:
Case 1. all keys are numeric / integers.
Case 2. all keys are strings.
Case 3. some keys are strings, some keys are numeric / integers.
We can check each case with the following functions.
Case 1: all keys are numeric / integers.
Note: This function returns true for empty arrays too.
//! Check whether the input is an array whose keys are all integers.
/*!
\param[in] $InputArray (array) Input array.
\return (bool) \b true iff the input is an array whose keys are all integers.
*/
function IsArrayAllKeyInt($InputArray)
{
if(!is_array($InputArray))
{
return false;
}
if(count($InputArray) <= 0)
{
return true;
}
return array_unique(array_map("is_int", array_keys($InputArray))) === array(true);
}
Case 2: all keys are strings.
Note: This function returns true for empty arrays too.
//! Check whether the input is an array whose keys are all strings.
/*!
\param[in] $InputArray (array) Input array.
\return (bool) \b true iff the input is an array whose keys are all strings.
*/
function IsArrayAllKeyString($InputArray)
{
if(!is_array($InputArray))
{
return false;
}
if(count($InputArray) <= 0)
{
return true;
}
return array_unique(array_map("is_string", array_keys($InputArray))) === array(true);
}
Case 3. some keys are strings, some keys are numeric / integers.
Note: This function returns true for empty arrays too.
//! Check whether the input is an array with at least one key being an integer and at least one key being a string.
/*!
\param[in] $InputArray (array) Input array.
\return (bool) \b true iff the input is an array with at least one key being an integer and at least one key being a string.
*/
function IsArraySomeKeyIntAndSomeKeyString($InputArray)
{
if(!is_array($InputArray))
{
return false;
}
if(count($InputArray) <= 0)
{
return true;
}
return count(array_unique(array_map("is_string", array_keys($InputArray)))) >= 2;
}
It follows that:
If the value is not an array, all 3 functions return false.
If the value is an empty array, all 3 functions return true
(which is by definition, as in "the empty set is a subset of any set A because all its elements belong to A").
If the value is a non-empty array, exactly 1 function returns true.
Now, for an array to be a "genuine" array that we are all accustomed to, meaning:
Its keys are all numeric / integers.
Its keys are sequential (i.e. increasing by step 1).
Its keys start from zero.
We can check with the following function.
Case 3a. keys are numeric / integers, sequential, and zero-based.
Note: This function returns true for empty arrays too.
//! Check whether the input is an array whose keys are numeric, sequential, and zero-based.
/*!
\param[in] $InputArray (array) Input array.
\return (bool) \b true iff the input is an array whose keys are numeric, sequential, and zero-based.
*/
function IsArrayKeyNumericSequentialZeroBased($InputArray)
{
if(!is_array($InputArray))
{
return false;
}
if(count($InputArray) <= 0)
{
return true;
}
return array_keys($InputArray) === range(0, count($InputArray) - 1);
}
Caveats / Pitfalls (or, even more peculiar facts about array keys in PHP)
Integer keys
The keys for these arrays are integers:
array(0 => "b");
array(13 => "b");
array(-13 => "b"); // Negative integers are also integers.
array(0x1A => "b"); // Hexadecimal notation.
String keys
The keys for these arrays are strings:
array("fish and chips" => "b");
array("" => "b"); // An empty string is also a string.
array("stackoverflow_email#example.com" => "b"); // Strings may contain non-alphanumeric characters.
array("stack\t\"over\"\r\nflow's cool" => "b"); // Strings may contain special characters.
array('$tα€k↔øv∈rflöw⛄' => "b"); // Strings may contain all kinds of symbols.
array("functіon" => "b"); // You think this looks fine? Think again! (see https://stackoverflow.com/q/9246051/1402846)
array("ま말轉转ДŁ" => "b"); // How about Japanese/Korean/Chinese/Russian/Polish?
array("fi\x0sh" => "b"); // Strings may contain null characters.
array(file_get_contents("https://www.google.com/images/nav_logo114.png") => "b"); // Strings may even be binary!
Integer keys that look like strings
If you think the key in array("13" => "b") is a string, you are wrong. From the doc here:
Strings containing valid integers will be cast to the integer type. E.g. the key "8" will actually be stored under 8. On the other hand "08" will not be cast, as it isn't a valid decimal integer.
For example, the key for these arrays are integers:
array("13" => "b");
array("-13" => "b"); // Negative, ok.
But the key for these arrays are strings:
array("13." => "b");
array("+13" => "b"); // Positive, not ok.
array("-013" => "b");
array("0x1A" => "b"); // Not converted to integers even though it's a valid hexadecimal number.
array("013" => "b"); // Not converted to integers even though it's a valid octal number.
array("18446744073709551616" => "b"); // Not converted to integers as it can't fit into a 64-bit integer.
What's more, according to the doc,
The size of an integer is platform-dependent, although a maximum value of about two billion is the usual value (that's 32 bits signed). 64-bit platforms usually have a maximum value of about 9E18, except for Windows, which is always 32 bit. PHP does not support unsigned integers.
So the key for this array may or may not be an integer - it depends on your platform.
array("60000000000" => "b"); // Array key could be integer or string, it can fit into a 64-bit (but not 32-bit) integer.
Even worse, PHP tends to be buggy if the integer is near the 231 = 2,147,483,648 boundary (see bug 51430, bug 52899). For example, on my local environment (PHP 5.3.8 on XAMPP 1.7.7 on Windows 7), var_dump(array("2147483647" => "b")) gives
array(1) {
[2147483647]=>
string(1) "b"
}
but on this live demo on codepad (PHP 5.2.5), the same expression gives
array(1) {
["2147483647"]=>
string(1) "b"
}
So the key is an integer in one environment but a string in another, even though 2147483647 is a valid signed 32-bit integer.
PHP 8.1 adds a built-in function to determine whether an array is a list with those semantics, or not. the function is array_is_list:
$list = ["a", "b", "c"];
array_is_list($list); // true
$notAList = [1 => "a", 2 => "b", 3 => "c"];
array_is_list($notAList); // false
$alsoNotAList = ["a" => "a", "b" => "b", "c" => "c"];
array_is_list($alsoNotAList); // false
reference
Speed-wise:
function isAssoc($array)
{
return ($array !== array_values($array));
}
Memory-wise:
function isAssoc($array)
{
$array = array_keys($array); return ($array !== array_keys($array));
}
Actually the most efficient way is thus:
function is_assoc($array){
$keys = array_keys($array);
return $keys !== array_keys($keys);
}
This works because it compares the keys (which for a sequential array are always 0,1,2 etc) to the keys of the keys (which will always be 0,1,2 etc).
Laravel use this approach.
function checkAssoc($array){
return ctype_digit( implode('', array_keys($array) ) );
}
I've used both array_keys($obj) !== range(0, count($obj) - 1) and array_values($arr) !== $arr (which are duals of each other, although the second is cheaper than the first) but both fail for very large arrays.
This is because array_keys and array_values are both very costly operations (since they build a whole new array of size roughly that of the original).
The following function is more robust than the methods provided above:
function array_type( $obj ){
$last_key = -1;
$type = 'index';
foreach( $obj as $key => $val ){
if( !is_int( $key ) || $key < 0 ){
return 'assoc';
}
if( $key !== $last_key + 1 ){
$type = 'sparse';
}
$last_key = $key;
}
return $type;
}
Also note that if you don't care to differentiate sparse arrays from associative arrays you can simply return 'assoc' from both if blocks.
Finally, while this might seem much less "elegant" than a lot of "solutions" on this page, in practice it is vastly more efficient. Almost any associative array will be detected instantly. Only indexed arrays will get checked exhaustively, and the methods outlined above not only check indexed arrays exhaustively, they duplicate them.
Here is another simple but powerful logic ( which is also used by great Laravel framework in it's internal mechanism )
/**
* Determines if an array is associative.
* #param array $array
* #return bool
*/
function isAssoc(array $array)
{
$keys = array_keys($array);
return array_keys($keys) !== $keys;
}
I think the following two functions are the best way to go for checking 'if an array is associative or numeric'. Since 'numeric' could mean only numeric keys or only sequential numeric keys, two functions are listed below that check either condition:
function is_indexed_array(&$arr) {
for (reset($arr); is_int(key($arr)); next($arr));
return is_null(key($arr));
}
function is_sequential_array(&$arr, $base = 0) {
for (reset($arr), $base = (int) $base; key($arr) === $base++; next($arr));
return is_null(key($arr));
}
The first function checks if each key is an integer value. The second function checks if each key is an integer value and in addition checks if all keys are sequential starting at $base, which defaults to 0 and thus can be omitted if you do not need to specify another base value. key($my_array) returns null if the read pointer is moved past the end of the array, which is what ends the for loop and makes the statement after the for loop return true if all keys were integer. If not, the loop ends prematurely because a key is of type string, and the statement after the for loop will return false. The latter function in addition adds one to $base after each compare, to be able to check if the next key is of the correct value. The strict compare makes it also check if the key is of type integer. The $base = (int) $base part in the first section of the for loop can be left out when $base is omitted or if you make sure it is only called using an integer. But since I can't be sure for everybody, I left it in. The statement is executed only once, anyway. I think these are the most efficient solutions:
Memory wise: No copying of data or key ranges. Doing an array_values or array_keys may seem shorter (less code) but keep in mind what goes on in the background once you make that call. Yes there are more (visible) statements than in some other solutions, but that is not what counts, is it?
Time wise: Besides the fact that copying/extracting data and/or keys also takes time, this solution is more efficient than doing a foreach. Again a foreach may seem more efficient to some because it is shorter in notation, but in the background foreach also calls reset, key and next to do it's looping. But in addition it also calls valid to check the end condition, which is avoided here due to the combination with the integer check.
Remember that an array key can only be an integer or a string, and a strictly numeric string such as "1" (but not "01") will be translated into an integer. Which is what makes checking for an integer key the only needed operation besides counting if you want the array to be sequential. Naturally, if is_indexed_array returns false the array can be seen as associative. I say 'seen', because in fact they all are.
One way to approach this is to piggyback on json_encode, which already has its own internal method of differentiating between an associative array and an indexed array in order to output the correct JSON.
You can do this by checking to see if the first character returned after encoding is a { (associative array) or a [ (indexed array).
// Too short :)
function is_assoc($arr) {
ksort($arr);
return json_encode($arr)[0] === '{';
}
function array_is_assoc(array $a) {
$i = 0;
foreach ($a as $k => $v) {
if ($k !== $i++) {
return true;
}
}
return false;
}
Fast, concise, and memory efficient. No expensive comparisons, function calls or array copying.
This function can handle:
array with holes in index (e.g. 1,2,4,5,8,10)
array with "0x" keys: e.g. key '08' is associative while key '8' is sequential.
the idea is simple: if one of the keys is NOT an integer, it is associative array, otherwise it's sequential.
function is_asso($a){
foreach(array_keys($a) as $key) {if (!is_int($key)) return TRUE;}
return FALSE;
}
There are many answers already, but here is the method that Laravel relies on within its Arr class:
/**
* Determines if an array is associative.
*
* An array is "associative" if it doesn't have sequential numerical keys beginning with zero.
*
* #param array $array
* #return bool
*/
public static function isAssoc(array $array)
{
$keys = array_keys($array);
return array_keys($keys) !== $keys;
}
Source: https://github.com/laravel/framework/blob/5.4/src/Illuminate/Support/Arr.php
I noticed two popular approaches for this question: one using array_values() and other using key(). To find out which is faster, I wrote a small program:
$arrays = Array(
'Array #1' => Array(1, 2, 3, 54, 23, 212, 123, 1, 1),
'Array #2' => Array("Stack", 1.5, 20, Array(3.4)),
'Array #3' => Array(1 => 4, 2 => 2),
'Array #4' => Array(3.0, "2", 3000, "Stack", 5 => "4"),
'Array #5' => Array("3" => 4, "2" => 2),
'Array #6' => Array("0" => "One", 1.0 => "Two", 2 => "Three"),
'Array #7' => Array(3 => "asdf", 4 => "asdf"),
'Array #8' => Array("apple" => 1, "orange" => 2),
);
function is_indexed_array_1(Array &$arr) {
return $arr === array_values($arr);
}
function is_indexed_array_2(Array &$arr) {
for (reset($arr), $i = 0; key($arr) === $i++; next($arr))
;
return is_null(key($arr));
}
// Method #1
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
foreach ($arrays as $array) {
$dummy = is_indexed_array_1($array);
}
}
$end = microtime(true);
echo "Time taken with method #1 = ".round(($end-$start)*1000.0,3)."ms\n";
// Method #2
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
foreach ($arrays as $array) {
$dummy = is_indexed_array_2($array);
}
}
$end = microtime(true);
echo "Time taken with method #1 = ".round(($end-$start)*1000.0,3)."ms\n";
Output for the program on PHP 5.2 on CentOS is as follows:
Time taken with method #1 = 10.745ms
Time taken with method #2 = 18.239ms
Output on PHP 5.3 yielded similar results. Obviously using array_values() is much faster.
Most answers have sub-optimal time/space complexity or are changing semantics. So, here is another answer with the fastest and most functionally correct solution:
function is_sequential_array(Array &$a) {
$n = count($a);
for($i=0; $i<$n; $i++) {
if(!array_key_exists($i, $a)) {
return false;
}
}
return true;
}
This answer has following advantages over other answers:
Space complexity of O(1) (many answers here use O(n) space!)
Does not apply equality on keys (which is an unwanted and expensive operation)
Treats input array as immutable (many answers have implicitly created a copy by applying mutating functions)
Uses function array_key_exists instead of isset (remember, isset additionally checks for 'is not null', thereby changing semantics)
Worst-case time complexity is O(n) (many answers here have best-case time complexity of O(n))
After some local benchmarking, debugging, compiler probing, profiling, and abusing 3v4l.org to benchmark across more versions (yes, I got a warning to stop) and
comparing against every variation I could find...
I give you an organically derived best-average-worst-case scenario associative array test function that is at worst roughly as good as or better than all other average-case scenarios.
/**
* Tests if an array is an associative array.
*
* #param array $array An array to test.
* #return boolean True if the array is associative, otherwise false.
*/
function is_assoc(array &$arr) {
// don't try to check non-arrays or empty arrays
if (FALSE === is_array($arr) || 0 === ($l = count($arr))) {
return false;
}
// shortcut by guessing at the beginning
reset($arr);
if (key($arr) !== 0) {
return true;
}
// shortcut by guessing at the end
end($arr);
if (key($arr) !== $l-1) {
return true;
}
// rely on php to optimize test by reference or fast compare
return array_values($arr) !== $arr;
}
From https://3v4l.org/rkieX:
<?php
// array_values
function method_1(Array &$arr) {
return $arr === array_values($arr);
}
// method_2 was DQ; did not actually work
// array_keys
function method_3(Array &$arr) {
return array_keys($arr) === range(0, count($arr) - 1);
}
// foreach
function method_4(Array &$arr) {
$idx = 0;
foreach( $arr as $key => $val ){
if( $key !== $idx )
return FALSE;
++$idx;
}
return TRUE;
}
// guessing
function method_5(Array &$arr) {
global $METHOD_5_KEY;
$i = 0;
$l = count($arr)-1;
end($arr);
if ( key($arr) !== $l )
return FALSE;
reset($arr);
do {
if ( $i !== key($arr) )
return FALSE;
++$i;
next($arr);
} while ($i < $l);
return TRUE;
}
// naieve
function method_6(Array &$arr) {
$i = 0;
$l = count($arr);
do {
if ( NULL === #$arr[$i] )
return FALSE;
++$i;
} while ($i < $l);
return TRUE;
}
// deep reference reliance
function method_7(Array &$arr) {
return array_keys(array_values($arr)) === array_keys($arr);
}
// organic (guessing + array_values)
function method_8(Array &$arr) {
reset($arr);
if ( key($arr) !== 0 )
return FALSE;
end($arr);
if ( key($arr) !== count($arr)-1 )
return FALSE;
return array_values($arr) === $arr;
}
function benchmark(Array &$methods, Array &$target, $expected){
foreach($methods as $method){
$start = microtime(true);
for ($i = 0; $i < 2000; ++$i) {
//$dummy = call_user_func($method, $target);
if ( $method($target) !== $expected ) {
echo "Method $method is disqualified for returning an incorrect result.\n";
unset($methods[array_search($method,$methods,true)]);
$i = 0;
break;
}
}
if ( $i != 0 ) {
$end = microtime(true);
echo "Time taken with $method = ".round(($end-$start)*1000.0,3)."ms\n";
}
}
}
$true_targets = [
'Giant array' => range(0, 500),
'Tiny array' => range(0, 20),
];
$g = range(0,10);
unset($g[0]);
$false_targets = [
'Large array 1' => range(0, 100) + ['a'=>'a'] + range(101, 200),
'Large array 2' => ['a'=>'a'] + range(0, 200),
'Tiny array' => range(0, 10) + ['a'=>'a'] + range(11, 20),
'Gotcha array' => $g,
];
$methods = [
'method_1',
'method_3',
'method_4',
'method_5',
'method_6',
'method_7',
'method_8'
];
foreach($false_targets as $targetName => $target){
echo "==== Benchmark using $targetName expecing FALSE ====\n";
benchmark($methods, $target, false);
echo "\n";
}
foreach($true_targets as $targetName => $target){
echo "==== Benchmark using $targetName expecting TRUE ====\n";
benchmark($methods, $target, true);
echo "\n";
}
I know it's a bit pointless adding an answer to this huge queue, but here's a readable O(n) solution that doesn't require duplicating any values:
function isNumericArray($array) {
$count = count($array);
for ($i = 0; $i < $count; $i++) {
if (!isset($array[$i])) {
return FALSE;
}
}
return TRUE;
}
Rather than check the keys to see if they are all numeric, you iterate over the keys that would be there for a numeric array and make sure they exist.
By using xarray PHP extension
You can do this very fast (about 30+ times faster in PHP 5.6):
if (array_is_indexed($array)) { }
Or:
if (array_is_assoc($array)) { }
A lot of the solutions here are elegant and pretty, but don't scale well, and are memory intensive or CPU intensive. Most are creating 2 new data points in memory with this solution from both sides of the comparison. The larger the array the harder and longer the process and memory used, and you lose the benefit of short circuit evaluation. I Did some testing with a few different ideas. Trying to avoid array_key_exists as it is costly, and also avoiding creating new large datasets to compare. I feel this is a simple way to tell if an array is sequential.
public function is_sequential( $arr = [] ){
if( !is_array( $arr ) || empty( $arr ) ) return false;
$i = 0;
$total = count( $arr );
foreach( $arr as $key => $value ) if( $key !== $i++ ) return false;
return true;
}
You run a single count on the main array and store a single integer. You then loop through the array and check for an exact match while iterating the counter. You should have from 1 to count. If it fails it will short circuit out which gives you performance boost when it is false.
Originally I did this with a for loop and checking for isset( $arr[$i] ) but this will not detect null keys which requires array_key_exists, and as we know that is the worst function to use for speed.
Constantly updating variables via foreach to check along with the iterator never growing past it's integer size let's PHP use it's built in memory optimization, caching and garbage collection to keep you at very low resource usage.
Also, I will argue that using array_keys in a foreach is silly when you can simply run $key => $value and check the key. Why create the new data point? Once you abstract away the array keys you've consumed more memory immediately.
For those using Laravel:
Arr::isAssoc returns true if the given array is an associative array. An array is considered "associative" if it doesn't have sequential numerical keys beginning with zero:
use Illuminate\Support\Arr;
$isAssoc = Arr::isAssoc(['product' => ['name' => 'Desk', 'price' => 100]]);
// true
$isAssoc = Arr::isAssoc([1, 2, 3]);
// false
https://laravel.com/docs/8.x/helpers#method-array-isassoc
Here's the method I use:
function is_associative ( $a )
{
return in_array(false, array_map('is_numeric', array_keys($a)));
}
assert( true === is_associative(array(1, 2, 3, 4)) );
assert( false === is_associative(array('foo' => 'bar', 'bar' => 'baz')) );
assert( false === is_associative(array(1, 2, 3, 'foo' => 'bar')) );
Note that this doesn't account for special cases like:
$a = array( 1, 2, 3, 4 );
unset($a[1]);
assert( true === is_associative($a) );
Sorry, can't help you with that. It's also somewhat performant for decently sized arrays, as it doesn't make needless copies. It is these little things that makes Python and Ruby so much nicer to write in... :P
<?php
function is_list($array) {
return array_keys($array) === range(0, count($array) - 1);
}
function is_assoc($array) {
return count(array_filter(array_keys($array), 'is_string')) == count($array);
}
?>
Both of these examples, which scored the most points do not work correctly with arrays like $array = array('foo' => 'bar', 1)
This would work too (demo):
function array_has_numeric_keys_only(array $array)
{
try {
SplFixedArray::fromArray($array, true);
} catch (InvalidArgumentException $e) {
return false;
}
return true;
}
Please note that the main point of this answer is to inform you about the existence of SplFixedArray and not to encourage you to use Exceptions for these kinds of tests.
I think the definition of a scalar array will vary by application. That is, some applications will require a more strict sense of what qualifies as a scalar array, and some applications will require a more loose sense.
Below I present 3 methods of varying strictness.
<?php
/**
* Since PHP stores all arrays as associative internally, there is no proper
* definition of a scalar array.
*
* As such, developers are likely to have varying definitions of scalar array,
* based on their application needs.
*
* In this file, I present 3 increasingly strict methods of determining if an
* array is scalar.
*
* #author David Farrell <DavidPFarrell#gmail.com>
*/
/**
* isArrayWithOnlyIntKeys defines a scalar array as containing
* only integer keys.
*
* If you are explicitly setting integer keys on an array, you
* may need this function to determine scalar-ness.
*
* #param array $a
* #return boolean
*/
function isArrayWithOnlyIntKeys(array $a)
{
if (!is_array($a))
return false;
foreach ($a as $k => $v)
if (!is_int($k))
return false;
return true;
}
/**
* isArrayWithOnlyAscendingIntKeys defines a scalar array as
* containing only integer keys in ascending (but not necessarily
* sequential) order.
*
* If you are performing pushes, pops, and unsets on your array,
* you may need this function to determine scalar-ness.
*
* #param array $a
* #return boolean
*/
function isArrayWithOnlyAscendingIntKeys(array $a)
{
if (!is_array($a))
return false;
$prev = null;
foreach ($a as $k => $v)
{
if (!is_int($k) || (null !== $prev && $k <= $prev))
return false;
$prev = $k;
}
return true;
}
/**
* isArrayWithOnlyZeroBasedSequentialIntKeys defines a scalar array
* as containing only integer keys in sequential, ascending order,
* starting from 0.
*
* If you are only performing operations on your array that are
* guaranteed to either maintain consistent key values, or that
* re-base the keys for consistency, then you can use this function.
*
* #param array $a
* #return boolean
*/
function isArrayWithOnlyZeroBasedSequentialIntKeys(array $a)
{
if (!is_array($a))
return false;
$i = 0;
foreach ($a as $k => $v)
if ($i++ !== $k)
return false;
return true;
}
One more fast from source.
Fit encoding of json_encode (and bson_encode). So has javascript Array compliance.
function isSequential($value){
if(is_array($value) || ($value instanceof \Countable && $value instanceof \ArrayAccess)){
for ($i = count($value) - 1; $i >= 0; $i--) {
if (!isset($value[$i]) && !array_key_exists($i, $value)) {
return false;
}
}
return true;
} else {
throw new \InvalidArgumentException(
sprintf('Data type "%s" is not supported by method %s', gettype($value), __METHOD__)
);
}
}
Could this be the solution?
public static function isArrayAssociative(array $array) {
reset($array);
return !is_int(key($array));
}
The caveat is obviously that the array cursor is reset but I'd say probably the function is used before the array is even traversed or used.
My solution:
function isAssociative(array $array)
{
return array_keys(array_merge($array)) !== range(0, count($array) - 1);
}
array_merge on a single array will reindex all integer keys, but not other. For example:
array_merge([1 => 'One', 3 => 'Three', 'two' => 'Two', 6 => 'Six']);
// This will returns [0 => 'One', 1 => 'Three', 'two' => 'Two', 2 => 'Six']
So if a list (a non-associative array) is created ['a', 'b', 'c'] then a value is removed unset($a[1]) then array_merge is called, the list is reindexed starting from 0.

Generate predictable-suffled random array

SO,
The problem
It's well known about pseudo-random numbers. 'Pseudo' actually means, that, despite they are random (i.e. unpredictable) in general, they still will be same in sequence, in which same generator init value was used. For example, in PHP there's mt_srand() function to do that. Example:
mt_srand(1);
var_dump(mt_rand(), mt_rand(), mt_rand());
-no matter, how many time we'll launch our script: generated three numbers will always be same in sequence.
Now, my issue is how to do the same - but for shuffling array. I.e. I want to create a function, which will accept input array to shuffle and seed. Within same seed value shuffling must have consecutive same order. I.e. let we call that function shuffleWithSeed() - and then following should work for every script launch:
$input = ['foo', 'bar', 'baz'];
$test = shuffleWithSeed($input, 1000);//1000 is just some constant value
var_dump($test); //let it be ['bar', 'foo', 'baz']
$test = shuffleWithSeed($test, 1000);
var_dump($test); //let it be ['baz', 'foo', 'bar']
$test = shuffleWithSeed($test, 1000);
var_dump($test); //let it be ['baz', 'bar', 'foo']
//...
-i.e. no matter how many times we'll do shuffle for our array - I want for the next script launch ordering sequence will be always the same within one seed value.
My approach
I have in mind this algorithm:
Initialize random numbers generator with passed seed
Generate N random numbers, where N is the number of $input members
Sort numbers from step 2
Make corresponding numbers be dependent from $input keys.
I've implemented this in:
function shuffleWithSeed(array $input, $seed=null)
{
if(!isset($seed))
{
shuffle($input);
return $input;
}
if(!is_int($seed))
{
throw new InvalidArgumentException('Invalid seed value');
}
mt_srand($seed);
$random = [];
foreach($input as $key=>$value)
{
$random[$key] = mt_rand();
}
asort($random);
$random = array_combine(array_keys($random), array_values($input));
ksort($random);
return $random;
}
-now, also found Fisher-Yates algorithm - but not sure if it can work with pseudorandom numbers (i.e. with seed)
The question
As you can see, I'm doing two sorts in my function - first by values and second by keys.
Can this be done with one sort? Or without sort at all? Input array could be large, so I want to avoid this.
However, may be my algorithm is not well? If yes, what other options could be suggested?
Here's a copy and paste of a function I have implemented a while ago for exactly this purpose:
/**
* Shuffles an array in a repeatable manner, if the same $seed is provided.
*
* #param array &$items The array to be shuffled.
* #param integer $seed The result of the shuffle will be the same for the same input ($items and $seed). If not given, uses the current time as seed.
* #return void
*/
protected function seeded_shuffle(array &$items, $seed = false) {
$items = array_values($items);
mt_srand($seed ? $seed : time());
for ($i = count($items) - 1; $i > 0; $i--) {
$j = mt_rand(0, $i);
list($items[$i], $items[$j]) = array($items[$j], $items[$i]);
}
}
It implements a simple Fisher-Yates shuffle with a seeded random number generator.

PHP best way to MD5 multi-dimensional array?

What is the best way to generate an MD5 (or any other hash) of a multi-dimensional array?
I could easily write a loop which would traverse through each level of the array, concatenating each value into a string, and simply performing the MD5 on the string.
However, this seems cumbersome at best and I wondered if there was a funky function which would take a multi-dimensional array, and hash it.
(Copy-n-paste-able function at the bottom)
As mentioned prior, the following will work.
md5(serialize($array));
However, it's worth noting that (ironically) json_encode performs noticeably faster:
md5(json_encode($array));
In fact, the speed increase is two-fold here as (1) json_encode alone performs faster than serialize, and (2) json_encode produces a smaller string and therefore less for md5 to handle.
Edit: Here is evidence to support this claim:
<?php //this is the array I'm using -- it's multidimensional.
$array = unserialize('a:6:{i:0;a:0:{}i:1;a:3:{i:0;a:0:{}i:1;a:0:{}i:2;a:3:{i:0;a:0:{}i:1;a:0:{}i:2;a:0:{}}}i:2;s:5:"hello";i:3;a:2:{i:0;a:0:{}i:1;a:0:{}}i:4;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:0:{}}}}}}}i:5;a:5:{i:0;a:0:{}i:1;a:4:{i:0;a:0:{}i:1;a:0:{}i:2;a:3:{i:0;a:0:{}i:1;a:0:{}i:2;a:0:{}}i:3;a:6:{i:0;a:0:{}i:1;a:3:{i:0;a:0:{}i:1;a:0:{}i:2;a:3:{i:0;a:0:{}i:1;a:0:{}i:2;a:0:{}}}i:2;s:5:"hello";i:3;a:2:{i:0;a:0:{}i:1;a:0:{}}i:4;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:0:{}}}}}}}i:5;a:5:{i:0;a:0:{}i:1;a:3:{i:0;a:0:{}i:1;a:0:{}i:2;a:3:{i:0;a:0:{}i:1;a:0:{}i:2;a:0:{}}}i:2;s:5:"hello";i:3;a:2:{i:0;a:0:{}i:1;a:0:{}}i:4;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:0:{}}}}}}}}}}i:2;s:5:"hello";i:3;a:2:{i:0;a:0:{}i:1;a:0:{}}i:4;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:1:{i:0;a:0:{}}}}}}}}}');
//The serialize test
$b4_s = microtime(1);
for ($i=0;$i<10000;$i++) {
$serial = md5(serialize($array));
}
echo 'serialize() w/ md5() took: '.($sTime = microtime(1)-$b4_s).' sec<br/>';
//The json test
$b4_j = microtime(1);
for ($i=0;$i<10000;$i++) {
$serial = md5(json_encode($array));
}
echo 'json_encode() w/ md5() took: '.($jTime = microtime(1)-$b4_j).' sec<br/><br/>';
echo 'json_encode is <strong>'.( round(($sTime/$jTime)*100,1) ).'%</strong> faster with a difference of <strong>'.($sTime-$jTime).' seconds</strong>';
JSON_ENCODE is consistently over 250% (2.5x) faster (often over 300%) -- this is not a trivial difference. You may see the results of the test with this live script here:
http://nathanbrauer.com/playground/serialize-vs-json.php
http://nathanbrauer.com/playground/plain-text/serialize-vs-json.php
Now, one thing to note is array(1,2,3) will produce a different MD5 as array(3,2,1). If this is NOT what you want. Try the following code:
//Optionally make a copy of the array (if you want to preserve the original order)
$original = $array;
array_multisort($array);
$hash = md5(json_encode($array));
Edit: There's been some question as to whether reversing the order would produce the same results. So, I've done that (correctly) here:
http://nathanbrauer.com/playground/json-vs-serialize.php
http://nathanbrauer.com/playground/plain-text/json-vs-serialize.php
As you can see, the results are exactly the same. Here's the (corrected) test originally created by someone related to Drupal:
http://nathanjbrauer.com/playground/drupal-calculation.php
http://nathanjbrauer.com/playground/plain-text/drupal-calculation.php
And for good measure, here's a function/method you can copy and paste (tested in 5.3.3-1ubuntu9.5):
function array_md5(Array $array) {
//since we're inside a function (which uses a copied array, not
//a referenced array), you shouldn't need to copy the array
array_multisort($array);
return md5(json_encode($array));
}
md5(serialize($array));
I'm joining a very crowded party by answering, but there is an important consideration that none of the extant answers address. The value of json_encode() and serialize() both depend upon the order of elements in the array!
Here are the results of not sorting and sorting the arrays, on two arrays with identical values but added in a different order (code at bottom of post):
serialize()
1c4f1064ab79e4722f41ab5a8141b210
1ad0f2c7e690c8e3cd5c34f7c9b8573a
json_encode()
db7178ba34f9271bfca3a05c5dddf502
c9661c0852c2bd0e26ef7951b4ca9e6f
Sorted serialize()
1c4f1064ab79e4722f41ab5a8141b210
1c4f1064ab79e4722f41ab5a8141b210
Sorted json_encode()
db7178ba34f9271bfca3a05c5dddf502
db7178ba34f9271bfca3a05c5dddf502
Therefore, the two methods that I would recommend to hash an array would be:
// You will need to write your own deep_ksort(), or see
// my example below
md5( serialize(deep_ksort($array)) );
md5( json_encode(deep_ksort($array)) );
The choice of json_encode() or serialize() should be determined by testing on the type of data that you are using. By my own testing on purely textual and numerical data, if the code is not running a tight loop thousands of times then the difference is not even worth benchmarking. I personally use json_encode() for that type of data.
Here is the code used to generate the sorting test above:
$a = array();
$a['aa'] = array( 'aaa'=>'AAA', 'bbb'=>'ooo', 'qqq'=>'fff',);
$a['bb'] = array( 'aaa'=>'BBBB', 'iii'=>'dd',);
$b = array();
$b['aa'] = array( 'aaa'=>'AAA', 'qqq'=>'fff', 'bbb'=>'ooo',);
$b['bb'] = array( 'iii'=>'dd', 'aaa'=>'BBBB',);
echo " serialize()\n";
echo md5(serialize($a))."\n";
echo md5(serialize($b))."\n";
echo "\n json_encode()\n";
echo md5(json_encode($a))."\n";
echo md5(json_encode($b))."\n";
$a = deep_ksort($a);
$b = deep_ksort($b);
echo "\n Sorted serialize()\n";
echo md5(serialize($a))."\n";
echo md5(serialize($b))."\n";
echo "\n Sorted json_encode()\n";
echo md5(json_encode($a))."\n";
echo md5(json_encode($b))."\n";
My quick deep_ksort() implementation, fits this case but check it before using on your own projects:
/*
* Sort an array by keys, and additionall sort its array values by keys
*
* Does not try to sort an object, but does iterate its properties to
* sort arrays in properties
*/
function deep_ksort($input)
{
if ( !is_object($input) && !is_array($input) ) {
return $input;
}
foreach ( $input as $k=>$v ) {
if ( is_object($v) || is_array($v) ) {
$input[$k] = deep_ksort($v);
}
}
if ( is_array($input) ) {
ksort($input);
}
// Do not sort objects
return $input;
}
Answer is highly depends on data types of array values.
For big strings use:
md5(serialize($array));
For short strings and integers use:
md5(json_encode($array));
4 built-in PHP functions can transform array to string:
serialize(), json_encode(), var_export(), print_r().
Notice: json_encode() function slows down while processing associative arrays with strings as values. In this case consider to use serialize() function.
Test results for multi-dimensional array with md5-hashes (32 char) in keys and values:
Test name Repeats Result Performance
serialize 10000 0.761195 sec +0.00%
print_r 10000 1.669689 sec -119.35%
json_encode 10000 1.712214 sec -124.94%
var_export 10000 1.735023 sec -127.93%
Test result for numeric multi-dimensional array:
Test name Repeats Result Performance
json_encode 10000 1.040612 sec +0.00%
var_export 10000 1.753170 sec -68.47%
serialize 10000 1.947791 sec -87.18%
print_r 10000 9.084989 sec -773.04%
Associative array test source.
Numeric array test source.
Aside from Brock's excellent answer (+1), any decent hashing library allows you to update the hash in increments, so you should be able to update with each string sequentially, instead having to build up one giant string.
See: hash_update
md5(serialize($array));
Will work, but the hash will change depending on the order of the array (that might not matter though).
Note that serialize and json_encode act differently when it comes to numeric arrays where the keys don't start at 0, or associative arrays.
json_encode will store such arrays as an Object, so json_decode returns an Object, where unserialize will return an array with exact the same keys.
I think that this could be a good tip:
Class hasharray {
public function array_flat($in,$keys=array(),$out=array()){
foreach($in as $k => $v){
$keys[] = $k;
if(is_array($v)){
$out = $this->array_flat($v,$keys,$out);
}else{
$out[implode("/",$keys)] = $v;
}
array_pop($keys);
}
return $out;
}
public function array_hash($in){
$a = $this->array_flat($in);
ksort($a);
return md5(json_encode($a));
}
}
$h = new hasharray;
echo $h->array_hash($multi_dimensional_array);
Important note about serialize()
I don't recommend to use it as part of hashing function because it can return different result for the following examples. Check the example below:
Simple example:
$a = new \stdClass;
$a->test = 'sample';
$b = new \stdClass;
$b->one = $a;
$b->two = clone $a;
Produces
"O:8:"stdClass":2:{s:3:"one";O:8:"stdClass":1:{s:4:"test";s:6:"sample";}s:3:"two";O:8:"stdClass":1:{s:4:"test";s:6:"sample";}}"
But the following code:
<?php
$a = new \stdClass;
$a->test = 'sample';
$b = new \stdClass;
$b->one = $a;
$b->two = $a;
Output:
"O:8:"stdClass":2:{s:3:"one";O:8:"stdClass":1:{s:4:"test";s:6:"sample";}s:3:"two";r:2;}"
So instead of second object php just create link "r:2;" to the first instance. It's definitely good and correct way to serialize data, but it can lead to the issues with your hashing function.
// Convert nested arrays to a simple array
$array = array();
array_walk_recursive($input, function ($a) use (&$array) {
$array[] = $a;
});
sort($array);
$hash = md5(json_encode($array));
----
These arrays have the same hash:
$arr1 = array(0 => array(1, 2, 3), 1, 2);
$arr2 = array(0 => array(1, 3, 2), 1, 2);
I didn't see the solution so easily above so I wanted to contribute a simpler answer. For me, I was getting the same key until I used ksort (key sort):
Sorted first with Ksort, then performed sha1 on a json_encode:
ksort($array)
$hash = sha1(json_encode($array) //be mindful of UTF8
example:
$arr1 = array( 'dealer' => '100', 'direction' => 'ASC', 'dist' => '500', 'limit' => '1', 'zip' => '10601');
ksort($arr1);
$arr2 = array( 'direction' => 'ASC', 'limit' => '1', 'zip' => '10601', 'dealer' => '100', 'dist' => '5000');
ksort($arr2);
var_dump(sha1(json_encode($arr1)));
var_dump(sha1(json_encode($arr2)));
Output of altered arrays and hashes:
string(40) "502c2cbfbe62e47eb0fe96306ecb2e6c7e6d014c"
string(40) "b3319c58edadab3513832ceeb5d68bfce2fb3983"
there are several answers telling to use json_code,
but json_encode don't work fine with iso-8859-1 string, as soon as there is a special char, the string is cropped.
i would advice to use var_export :
md5(var_export($array, true))
not as slow as serialize, not as bugged as json_encode
Currently the most up-voted answer md5(serialize($array)); doesn't work well with objects.
Consider code:
$a = array(new \stdClass());
$b = array(new \stdClass());
Even though arrays are different (they contain different objects), they have same hash when using md5(serialize($array));. So your hash is useless!
To avoid that problem, you can replace objects with result of spl_object_hash() before serializing. You also should do it recursively if your array has multiple levels.
Code below also sorts arrays by keys, as dotancohen have suggested.
function replaceObjectsWithHashes(array $array)
{
foreach ($array as &$value) {
if (is_array($value)) {
$value = $this->replaceObjectsInArrayWithHashes($value);
} elseif (is_object($value)) {
$value = spl_object_hash($value);
}
}
ksort($array);
return $array;
}
Now you can use md5(serialize(replaceObjectsWithHashes($array))).
(Note that the array in PHP is value type. So replaceObjectsWithHashes function DO NOT change original array.)
in some case maybe it's better to use http_build_query to convert array to string :
md5( http_build_query( $array ) );

Searching an array of different strings inside a single string in PHP

I have an array of strings that I want to try and match to the end of a normal string. I'm not sure the best way to do this in PHP.
This is sorta what I am trying to do:
Example:
Input: abcde
Search array: er, wr, de
Match: de
My first thought was to write a loop that goes through the array and crafts a regular expression by adding "\b" on the end of each string and then check if it is found in the input string. While this would work it seems sorta inefficient to loop through the entire array. I've been told regular expressions are slow in PHP and don't want to implement something that will take me down the wrong path.
Is there a better way to see if one of the strings in my array occurs at the end of the input string?
The preg_filter() function looks like it might do the job but is for PHP 5.3+ and I am still sticking with 5.2.11 stable.
For something this simple, you don't need a regex. You can either loop over the array, and use strpos to see if the index is length(input) - length(test). If each entry in the search array is always of a constant length, you can also speed things up by chopping the end off the input, then comparing that to each item in the array.
You can't avoid going through the whole array, as in the worst general case, the item that matches will be at the end of the array. However, unless the array is huge, I wouldn't worry too much about performance - it will be much faster than you think.
Though compiling the regular expression takes some time I wouldn't dismiss using pcre so easily. Unless you find a compare function that takes several needles you need a loop for the needles and executing the loop + calling the compare function for each single needle takes time, too.
Let's take a test script that fetches all the function names from php.net and looks for certain endings. This was only an adhoc script but I suppose no matter which strcmp-ish function + loop you use it will be slower than the simple pcre pattern (in this case).
count($hs)=5549
pcre: 4.377925157547 s
substr_compare: 7.951938867569 s
identical results: bool(true)
This was the result when search for nine different patterns. If there were only two ('yadda', 'ge') both methods took the same time.
Feel free to criticize the test script (aren't there always errors in synthetic tests that are obvious for everyone but oneself? ;-) )
<?php
/* get the test data
All the function names from php.net
*/
$doc = new DOMDocument;
$doc->loadhtmlfile('http://docs.php.net/quickref.php');
$xpath = new DOMXPath($doc);
$hs = array();
foreach( $xpath->query('//a') as $a ) {
$hs[] = $a->textContent;
}
echo 'count($hs)=', count($hs), "\n";
// should find:
// ge, e.g. imagick_adaptiveblurimage
// ing, e.g. m_setblocking
// name, e.g. basename
// ions, e.g. assert_options
$ns = array('yadda', 'ge', 'foo', 'ing', 'bar', 'name', 'abcd', 'ions', 'baz');
sleep(1);
/* test 1: pcre */
$start = microtime(true);
for($run=0; $run<100; $run++) {
$matchesA = array();
$pattern = '/(?:' . join('|', $ns) . ')$/';
foreach($hs as $haystack) {
if ( preg_match($pattern, $haystack, $m) ) {
#$matchesA[$m[0]]+= 1;
}
}
}
echo "pcre: ", microtime(true)-$start, " s\n";
flush();
sleep(1);
/* test 2: loop + substr_compare */
$start = microtime(true);
for($run=0; $run<100; $run++) {
$matchesB = array();
foreach( $hs as $haystack ) {
$hlen = strlen($haystack);
foreach( $ns as $needle ) {
$nlen = strlen($needle);
if ( $hlen >= $nlen && 0===substr_compare($haystack, $needle, -$nlen) ) {
#$matchesB[$needle]+= 1;
}
}
}
}
echo "substr_compare: ", microtime(true)-$start, " s\n";
echo 'identical results: '; var_dump($matchesA===$matchesB);
I might approach this backwards;
if your string-ending list is fixed or varies rarely,
I would start by preprocessing it to make it easy to match against,
then grab the end of your string and see if it matches!
Sample code:
<?php
// Test whether string ends in predetermined list of suffixes
// Input: string to test
// Output: if matching suffix found, returns suffix as string, else boolean false
function findMatch($str) {
$matchTo = array(
2 => array( 'ge' => true, 'de' => true ),
3 => array( 'foo' => true, 'bar' => true, 'baz' => true ),
4 => array( 'abcd' => true, 'efgh' => true )
);
foreach($matchTo as $length => $list) {
$end = substr($str, -$length);
if (isset($list[$end]))
return $end;
}
return $false;
}
?>
This might be an overkill but you can try the following.
Create a hash for each entry of your search array and store them as keys in the array (that will be your lookup array).
Then go from the end of your input string one character at time (e, de,cde and etc) and compute a hash on a substring at each iteration. If a hash is in your lookup array, you have much.

How to check if PHP array is associative or sequential?

PHP treats all arrays as associative, so there aren't any built in functions. Can anyone recommend a fairly efficient way to check if an array "is a list" (contains only numeric keys starting from 0)?
Basically, I want to be able to differentiate between this:
$sequentialArray = [
'apple', 'orange', 'tomato', 'carrot'
];
and this:
$assocArray = [
'fruit1' => 'apple',
'fruit2' => 'orange',
'veg1' => 'tomato',
'veg2' => 'carrot'
];
You have asked two questions that are not quite equivalent:
Firstly, how to determine whether an array has only numeric keys
Secondly, how to determine whether an array has sequential numeric keys, starting from 0
Consider which of these behaviours you actually need. (It may be that either will do for your purposes.)
The first question (simply checking that all keys are numeric) is answered well by Captain kurO.
For the second question (checking whether the array is zero-indexed and sequential), you can use the following function:
function isAssoc(array $arr)
{
if (array() === $arr) return false;
return array_keys($arr) !== range(0, count($arr) - 1);
}
var_dump(isAssoc(['a', 'b', 'c'])); // false
var_dump(isAssoc(["0" => 'a', "1" => 'b', "2" => 'c'])); // false
var_dump(isAssoc(["1" => 'a', "0" => 'b', "2" => 'c'])); // true
var_dump(isAssoc(["a" => 'a', "b" => 'b', "c" => 'c'])); // true
To merely check whether the array has non-integer keys (not whether the array is sequentially-indexed or zero-indexed):
function has_string_keys(array $array) {
return count(array_filter(array_keys($array), 'is_string')) > 0;
}
If there is at least one string key, $array will be regarded as an associative array.
Surely this is a better alternative.
<?php
$arr = array(1,2,3,4);
$isIndexed = array_values($arr) === $arr;
Many commenters in this question don't understand how arrays work in PHP. From the array documentation:
A key may be either an integer or a string. If a key is the standard representation of an integer, it will be interpreted as such (i.e. "8" will be interpreted as 8, while "08" will be interpreted as "08"). Floats in key are truncated to integer. The indexed and associative array types are the same type in PHP, which can both contain integer and string indices.
In other words, there is no such thing as an array key of "8" because it will always be (silently) converted to the integer 8. So trying to differentiate between integers and numeric strings is unnecessary.
If you want the most efficient way to check an array for non-integer keys without making a copy of part of the array (like array_keys() does) or all of it (like foreach does):
function keyedNext( &$arr, &$k){
$k = key($arr);
return next($arr);
}
for ($k = key(reset($my_array)); is_int($k); keyedNext($my_array,$k))
$onlyIntKeys = is_null($k);
This works because key() returns NULL when the current array position is invalid and NULL can never be a valid key (if you try to use NULL as an array key it gets silently converted to "").
As stated by the OP:
PHP treats all arrays as associative
it is not quite sensible (IMHO) to write a function that checks if an array is associative. So first thing first: what is a key in a PHP array?:
The key can either be an integer or a string.
That means there are 3 possible cases:
Case 1. all keys are numeric / integers.
Case 2. all keys are strings.
Case 3. some keys are strings, some keys are numeric / integers.
We can check each case with the following functions.
Case 1: all keys are numeric / integers.
Note: This function returns true for empty arrays too.
//! Check whether the input is an array whose keys are all integers.
/*!
\param[in] $InputArray (array) Input array.
\return (bool) \b true iff the input is an array whose keys are all integers.
*/
function IsArrayAllKeyInt($InputArray)
{
if(!is_array($InputArray))
{
return false;
}
if(count($InputArray) <= 0)
{
return true;
}
return array_unique(array_map("is_int", array_keys($InputArray))) === array(true);
}
Case 2: all keys are strings.
Note: This function returns true for empty arrays too.
//! Check whether the input is an array whose keys are all strings.
/*!
\param[in] $InputArray (array) Input array.
\return (bool) \b true iff the input is an array whose keys are all strings.
*/
function IsArrayAllKeyString($InputArray)
{
if(!is_array($InputArray))
{
return false;
}
if(count($InputArray) <= 0)
{
return true;
}
return array_unique(array_map("is_string", array_keys($InputArray))) === array(true);
}
Case 3. some keys are strings, some keys are numeric / integers.
Note: This function returns true for empty arrays too.
//! Check whether the input is an array with at least one key being an integer and at least one key being a string.
/*!
\param[in] $InputArray (array) Input array.
\return (bool) \b true iff the input is an array with at least one key being an integer and at least one key being a string.
*/
function IsArraySomeKeyIntAndSomeKeyString($InputArray)
{
if(!is_array($InputArray))
{
return false;
}
if(count($InputArray) <= 0)
{
return true;
}
return count(array_unique(array_map("is_string", array_keys($InputArray)))) >= 2;
}
It follows that:
If the value is not an array, all 3 functions return false.
If the value is an empty array, all 3 functions return true
(which is by definition, as in "the empty set is a subset of any set A because all its elements belong to A").
If the value is a non-empty array, exactly 1 function returns true.
Now, for an array to be a "genuine" array that we are all accustomed to, meaning:
Its keys are all numeric / integers.
Its keys are sequential (i.e. increasing by step 1).
Its keys start from zero.
We can check with the following function.
Case 3a. keys are numeric / integers, sequential, and zero-based.
Note: This function returns true for empty arrays too.
//! Check whether the input is an array whose keys are numeric, sequential, and zero-based.
/*!
\param[in] $InputArray (array) Input array.
\return (bool) \b true iff the input is an array whose keys are numeric, sequential, and zero-based.
*/
function IsArrayKeyNumericSequentialZeroBased($InputArray)
{
if(!is_array($InputArray))
{
return false;
}
if(count($InputArray) <= 0)
{
return true;
}
return array_keys($InputArray) === range(0, count($InputArray) - 1);
}
Caveats / Pitfalls (or, even more peculiar facts about array keys in PHP)
Integer keys
The keys for these arrays are integers:
array(0 => "b");
array(13 => "b");
array(-13 => "b"); // Negative integers are also integers.
array(0x1A => "b"); // Hexadecimal notation.
String keys
The keys for these arrays are strings:
array("fish and chips" => "b");
array("" => "b"); // An empty string is also a string.
array("stackoverflow_email#example.com" => "b"); // Strings may contain non-alphanumeric characters.
array("stack\t\"over\"\r\nflow's cool" => "b"); // Strings may contain special characters.
array('$tα€k↔øv∈rflöw⛄' => "b"); // Strings may contain all kinds of symbols.
array("functіon" => "b"); // You think this looks fine? Think again! (see https://stackoverflow.com/q/9246051/1402846)
array("ま말轉转ДŁ" => "b"); // How about Japanese/Korean/Chinese/Russian/Polish?
array("fi\x0sh" => "b"); // Strings may contain null characters.
array(file_get_contents("https://www.google.com/images/nav_logo114.png") => "b"); // Strings may even be binary!
Integer keys that look like strings
If you think the key in array("13" => "b") is a string, you are wrong. From the doc here:
Strings containing valid integers will be cast to the integer type. E.g. the key "8" will actually be stored under 8. On the other hand "08" will not be cast, as it isn't a valid decimal integer.
For example, the key for these arrays are integers:
array("13" => "b");
array("-13" => "b"); // Negative, ok.
But the key for these arrays are strings:
array("13." => "b");
array("+13" => "b"); // Positive, not ok.
array("-013" => "b");
array("0x1A" => "b"); // Not converted to integers even though it's a valid hexadecimal number.
array("013" => "b"); // Not converted to integers even though it's a valid octal number.
array("18446744073709551616" => "b"); // Not converted to integers as it can't fit into a 64-bit integer.
What's more, according to the doc,
The size of an integer is platform-dependent, although a maximum value of about two billion is the usual value (that's 32 bits signed). 64-bit platforms usually have a maximum value of about 9E18, except for Windows, which is always 32 bit. PHP does not support unsigned integers.
So the key for this array may or may not be an integer - it depends on your platform.
array("60000000000" => "b"); // Array key could be integer or string, it can fit into a 64-bit (but not 32-bit) integer.
Even worse, PHP tends to be buggy if the integer is near the 231 = 2,147,483,648 boundary (see bug 51430, bug 52899). For example, on my local environment (PHP 5.3.8 on XAMPP 1.7.7 on Windows 7), var_dump(array("2147483647" => "b")) gives
array(1) {
[2147483647]=>
string(1) "b"
}
but on this live demo on codepad (PHP 5.2.5), the same expression gives
array(1) {
["2147483647"]=>
string(1) "b"
}
So the key is an integer in one environment but a string in another, even though 2147483647 is a valid signed 32-bit integer.
PHP 8.1 adds a built-in function to determine whether an array is a list with those semantics, or not. the function is array_is_list:
$list = ["a", "b", "c"];
array_is_list($list); // true
$notAList = [1 => "a", 2 => "b", 3 => "c"];
array_is_list($notAList); // false
$alsoNotAList = ["a" => "a", "b" => "b", "c" => "c"];
array_is_list($alsoNotAList); // false
reference
Speed-wise:
function isAssoc($array)
{
return ($array !== array_values($array));
}
Memory-wise:
function isAssoc($array)
{
$array = array_keys($array); return ($array !== array_keys($array));
}
Actually the most efficient way is thus:
function is_assoc($array){
$keys = array_keys($array);
return $keys !== array_keys($keys);
}
This works because it compares the keys (which for a sequential array are always 0,1,2 etc) to the keys of the keys (which will always be 0,1,2 etc).
Laravel use this approach.
function checkAssoc($array){
return ctype_digit( implode('', array_keys($array) ) );
}
I've used both array_keys($obj) !== range(0, count($obj) - 1) and array_values($arr) !== $arr (which are duals of each other, although the second is cheaper than the first) but both fail for very large arrays.
This is because array_keys and array_values are both very costly operations (since they build a whole new array of size roughly that of the original).
The following function is more robust than the methods provided above:
function array_type( $obj ){
$last_key = -1;
$type = 'index';
foreach( $obj as $key => $val ){
if( !is_int( $key ) || $key < 0 ){
return 'assoc';
}
if( $key !== $last_key + 1 ){
$type = 'sparse';
}
$last_key = $key;
}
return $type;
}
Also note that if you don't care to differentiate sparse arrays from associative arrays you can simply return 'assoc' from both if blocks.
Finally, while this might seem much less "elegant" than a lot of "solutions" on this page, in practice it is vastly more efficient. Almost any associative array will be detected instantly. Only indexed arrays will get checked exhaustively, and the methods outlined above not only check indexed arrays exhaustively, they duplicate them.
Here is another simple but powerful logic ( which is also used by great Laravel framework in it's internal mechanism )
/**
* Determines if an array is associative.
* #param array $array
* #return bool
*/
function isAssoc(array $array)
{
$keys = array_keys($array);
return array_keys($keys) !== $keys;
}
I think the following two functions are the best way to go for checking 'if an array is associative or numeric'. Since 'numeric' could mean only numeric keys or only sequential numeric keys, two functions are listed below that check either condition:
function is_indexed_array(&$arr) {
for (reset($arr); is_int(key($arr)); next($arr));
return is_null(key($arr));
}
function is_sequential_array(&$arr, $base = 0) {
for (reset($arr), $base = (int) $base; key($arr) === $base++; next($arr));
return is_null(key($arr));
}
The first function checks if each key is an integer value. The second function checks if each key is an integer value and in addition checks if all keys are sequential starting at $base, which defaults to 0 and thus can be omitted if you do not need to specify another base value. key($my_array) returns null if the read pointer is moved past the end of the array, which is what ends the for loop and makes the statement after the for loop return true if all keys were integer. If not, the loop ends prematurely because a key is of type string, and the statement after the for loop will return false. The latter function in addition adds one to $base after each compare, to be able to check if the next key is of the correct value. The strict compare makes it also check if the key is of type integer. The $base = (int) $base part in the first section of the for loop can be left out when $base is omitted or if you make sure it is only called using an integer. But since I can't be sure for everybody, I left it in. The statement is executed only once, anyway. I think these are the most efficient solutions:
Memory wise: No copying of data or key ranges. Doing an array_values or array_keys may seem shorter (less code) but keep in mind what goes on in the background once you make that call. Yes there are more (visible) statements than in some other solutions, but that is not what counts, is it?
Time wise: Besides the fact that copying/extracting data and/or keys also takes time, this solution is more efficient than doing a foreach. Again a foreach may seem more efficient to some because it is shorter in notation, but in the background foreach also calls reset, key and next to do it's looping. But in addition it also calls valid to check the end condition, which is avoided here due to the combination with the integer check.
Remember that an array key can only be an integer or a string, and a strictly numeric string such as "1" (but not "01") will be translated into an integer. Which is what makes checking for an integer key the only needed operation besides counting if you want the array to be sequential. Naturally, if is_indexed_array returns false the array can be seen as associative. I say 'seen', because in fact they all are.
One way to approach this is to piggyback on json_encode, which already has its own internal method of differentiating between an associative array and an indexed array in order to output the correct JSON.
You can do this by checking to see if the first character returned after encoding is a { (associative array) or a [ (indexed array).
// Too short :)
function is_assoc($arr) {
ksort($arr);
return json_encode($arr)[0] === '{';
}
function array_is_assoc(array $a) {
$i = 0;
foreach ($a as $k => $v) {
if ($k !== $i++) {
return true;
}
}
return false;
}
Fast, concise, and memory efficient. No expensive comparisons, function calls or array copying.
This function can handle:
array with holes in index (e.g. 1,2,4,5,8,10)
array with "0x" keys: e.g. key '08' is associative while key '8' is sequential.
the idea is simple: if one of the keys is NOT an integer, it is associative array, otherwise it's sequential.
function is_asso($a){
foreach(array_keys($a) as $key) {if (!is_int($key)) return TRUE;}
return FALSE;
}
There are many answers already, but here is the method that Laravel relies on within its Arr class:
/**
* Determines if an array is associative.
*
* An array is "associative" if it doesn't have sequential numerical keys beginning with zero.
*
* #param array $array
* #return bool
*/
public static function isAssoc(array $array)
{
$keys = array_keys($array);
return array_keys($keys) !== $keys;
}
Source: https://github.com/laravel/framework/blob/5.4/src/Illuminate/Support/Arr.php
I noticed two popular approaches for this question: one using array_values() and other using key(). To find out which is faster, I wrote a small program:
$arrays = Array(
'Array #1' => Array(1, 2, 3, 54, 23, 212, 123, 1, 1),
'Array #2' => Array("Stack", 1.5, 20, Array(3.4)),
'Array #3' => Array(1 => 4, 2 => 2),
'Array #4' => Array(3.0, "2", 3000, "Stack", 5 => "4"),
'Array #5' => Array("3" => 4, "2" => 2),
'Array #6' => Array("0" => "One", 1.0 => "Two", 2 => "Three"),
'Array #7' => Array(3 => "asdf", 4 => "asdf"),
'Array #8' => Array("apple" => 1, "orange" => 2),
);
function is_indexed_array_1(Array &$arr) {
return $arr === array_values($arr);
}
function is_indexed_array_2(Array &$arr) {
for (reset($arr), $i = 0; key($arr) === $i++; next($arr))
;
return is_null(key($arr));
}
// Method #1
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
foreach ($arrays as $array) {
$dummy = is_indexed_array_1($array);
}
}
$end = microtime(true);
echo "Time taken with method #1 = ".round(($end-$start)*1000.0,3)."ms\n";
// Method #2
$start = microtime(true);
for ($i = 0; $i < 1000; $i++) {
foreach ($arrays as $array) {
$dummy = is_indexed_array_2($array);
}
}
$end = microtime(true);
echo "Time taken with method #1 = ".round(($end-$start)*1000.0,3)."ms\n";
Output for the program on PHP 5.2 on CentOS is as follows:
Time taken with method #1 = 10.745ms
Time taken with method #2 = 18.239ms
Output on PHP 5.3 yielded similar results. Obviously using array_values() is much faster.
Most answers have sub-optimal time/space complexity or are changing semantics. So, here is another answer with the fastest and most functionally correct solution:
function is_sequential_array(Array &$a) {
$n = count($a);
for($i=0; $i<$n; $i++) {
if(!array_key_exists($i, $a)) {
return false;
}
}
return true;
}
This answer has following advantages over other answers:
Space complexity of O(1) (many answers here use O(n) space!)
Does not apply equality on keys (which is an unwanted and expensive operation)
Treats input array as immutable (many answers have implicitly created a copy by applying mutating functions)
Uses function array_key_exists instead of isset (remember, isset additionally checks for 'is not null', thereby changing semantics)
Worst-case time complexity is O(n) (many answers here have best-case time complexity of O(n))
After some local benchmarking, debugging, compiler probing, profiling, and abusing 3v4l.org to benchmark across more versions (yes, I got a warning to stop) and
comparing against every variation I could find...
I give you an organically derived best-average-worst-case scenario associative array test function that is at worst roughly as good as or better than all other average-case scenarios.
/**
* Tests if an array is an associative array.
*
* #param array $array An array to test.
* #return boolean True if the array is associative, otherwise false.
*/
function is_assoc(array &$arr) {
// don't try to check non-arrays or empty arrays
if (FALSE === is_array($arr) || 0 === ($l = count($arr))) {
return false;
}
// shortcut by guessing at the beginning
reset($arr);
if (key($arr) !== 0) {
return true;
}
// shortcut by guessing at the end
end($arr);
if (key($arr) !== $l-1) {
return true;
}
// rely on php to optimize test by reference or fast compare
return array_values($arr) !== $arr;
}
From https://3v4l.org/rkieX:
<?php
// array_values
function method_1(Array &$arr) {
return $arr === array_values($arr);
}
// method_2 was DQ; did not actually work
// array_keys
function method_3(Array &$arr) {
return array_keys($arr) === range(0, count($arr) - 1);
}
// foreach
function method_4(Array &$arr) {
$idx = 0;
foreach( $arr as $key => $val ){
if( $key !== $idx )
return FALSE;
++$idx;
}
return TRUE;
}
// guessing
function method_5(Array &$arr) {
global $METHOD_5_KEY;
$i = 0;
$l = count($arr)-1;
end($arr);
if ( key($arr) !== $l )
return FALSE;
reset($arr);
do {
if ( $i !== key($arr) )
return FALSE;
++$i;
next($arr);
} while ($i < $l);
return TRUE;
}
// naieve
function method_6(Array &$arr) {
$i = 0;
$l = count($arr);
do {
if ( NULL === #$arr[$i] )
return FALSE;
++$i;
} while ($i < $l);
return TRUE;
}
// deep reference reliance
function method_7(Array &$arr) {
return array_keys(array_values($arr)) === array_keys($arr);
}
// organic (guessing + array_values)
function method_8(Array &$arr) {
reset($arr);
if ( key($arr) !== 0 )
return FALSE;
end($arr);
if ( key($arr) !== count($arr)-1 )
return FALSE;
return array_values($arr) === $arr;
}
function benchmark(Array &$methods, Array &$target, $expected){
foreach($methods as $method){
$start = microtime(true);
for ($i = 0; $i < 2000; ++$i) {
//$dummy = call_user_func($method, $target);
if ( $method($target) !== $expected ) {
echo "Method $method is disqualified for returning an incorrect result.\n";
unset($methods[array_search($method,$methods,true)]);
$i = 0;
break;
}
}
if ( $i != 0 ) {
$end = microtime(true);
echo "Time taken with $method = ".round(($end-$start)*1000.0,3)."ms\n";
}
}
}
$true_targets = [
'Giant array' => range(0, 500),
'Tiny array' => range(0, 20),
];
$g = range(0,10);
unset($g[0]);
$false_targets = [
'Large array 1' => range(0, 100) + ['a'=>'a'] + range(101, 200),
'Large array 2' => ['a'=>'a'] + range(0, 200),
'Tiny array' => range(0, 10) + ['a'=>'a'] + range(11, 20),
'Gotcha array' => $g,
];
$methods = [
'method_1',
'method_3',
'method_4',
'method_5',
'method_6',
'method_7',
'method_8'
];
foreach($false_targets as $targetName => $target){
echo "==== Benchmark using $targetName expecing FALSE ====\n";
benchmark($methods, $target, false);
echo "\n";
}
foreach($true_targets as $targetName => $target){
echo "==== Benchmark using $targetName expecting TRUE ====\n";
benchmark($methods, $target, true);
echo "\n";
}
I know it's a bit pointless adding an answer to this huge queue, but here's a readable O(n) solution that doesn't require duplicating any values:
function isNumericArray($array) {
$count = count($array);
for ($i = 0; $i < $count; $i++) {
if (!isset($array[$i])) {
return FALSE;
}
}
return TRUE;
}
Rather than check the keys to see if they are all numeric, you iterate over the keys that would be there for a numeric array and make sure they exist.
By using xarray PHP extension
You can do this very fast (about 30+ times faster in PHP 5.6):
if (array_is_indexed($array)) { }
Or:
if (array_is_assoc($array)) { }
answers are already given but there's too much disinformation about performance.
I wrote this little benchmark script that shows that the foreach method is the fastest.
Disclaimer: following methods were copy-pasted from the other answers
<?php
function method_1(Array &$arr) {
return $arr === array_values($arr);
}
function method_2(Array &$arr) {
for (reset($arr), $i = 0; key($arr) !== $i++; next($arr));
return is_null(key($arr));
}
function method_3(Array &$arr) {
return array_keys($arr) === range(0, count($arr) - 1);
}
function method_4(Array &$arr) {
$idx = 0;
foreach( $arr as $key => $val ){
if( $key !== $idx )
return FALSE;
$idx++;
}
return TRUE;
}
function benchmark(Array $methods, Array &$target){
foreach($methods as $method){
$start = microtime(true);
for ($i = 0; $i < 1000; $i++)
$dummy = call_user_func($method, $target);
$end = microtime(true);
echo "Time taken with $method = ".round(($end-$start)*1000.0,3)."ms\n";
}
}
$targets = [
'Huge array' => range(0, 30000),
'Small array' => range(0, 1000),
];
$methods = [
'method_1',
'method_2',
'method_3',
'method_4',
];
foreach($targets as $targetName => $target){
echo "==== Benchmark using $targetName ====\n";
benchmark($methods, $target);
echo "\n";
}
results:
==== Benchmark using Huge array ====
Time taken with method_1 = 5504.632ms
Time taken with method_2 = 4509.445ms
Time taken with method_3 = 8614.883ms
Time taken with method_4 = 2720.934ms
==== Benchmark using Small array ====
Time taken with method_1 = 77.159ms
Time taken with method_2 = 130.03ms
Time taken with method_3 = 160.866ms
Time taken with method_4 = 69.946ms
A lot of the solutions here are elegant and pretty, but don't scale well, and are memory intensive or CPU intensive. Most are creating 2 new data points in memory with this solution from both sides of the comparison. The larger the array the harder and longer the process and memory used, and you lose the benefit of short circuit evaluation. I Did some testing with a few different ideas. Trying to avoid array_key_exists as it is costly, and also avoiding creating new large datasets to compare. I feel this is a simple way to tell if an array is sequential.
public function is_sequential( $arr = [] ){
if( !is_array( $arr ) || empty( $arr ) ) return false;
$i = 0;
$total = count( $arr );
foreach( $arr as $key => $value ) if( $key !== $i++ ) return false;
return true;
}
You run a single count on the main array and store a single integer. You then loop through the array and check for an exact match while iterating the counter. You should have from 1 to count. If it fails it will short circuit out which gives you performance boost when it is false.
Originally I did this with a for loop and checking for isset( $arr[$i] ) but this will not detect null keys which requires array_key_exists, and as we know that is the worst function to use for speed.
Constantly updating variables via foreach to check along with the iterator never growing past it's integer size let's PHP use it's built in memory optimization, caching and garbage collection to keep you at very low resource usage.
Also, I will argue that using array_keys in a foreach is silly when you can simply run $key => $value and check the key. Why create the new data point? Once you abstract away the array keys you've consumed more memory immediately.
For those using Laravel:
Arr::isAssoc returns true if the given array is an associative array. An array is considered "associative" if it doesn't have sequential numerical keys beginning with zero:
use Illuminate\Support\Arr;
$isAssoc = Arr::isAssoc(['product' => ['name' => 'Desk', 'price' => 100]]);
// true
$isAssoc = Arr::isAssoc([1, 2, 3]);
// false
https://laravel.com/docs/8.x/helpers#method-array-isassoc
Here's the method I use:
function is_associative ( $a )
{
return in_array(false, array_map('is_numeric', array_keys($a)));
}
assert( true === is_associative(array(1, 2, 3, 4)) );
assert( false === is_associative(array('foo' => 'bar', 'bar' => 'baz')) );
assert( false === is_associative(array(1, 2, 3, 'foo' => 'bar')) );
Note that this doesn't account for special cases like:
$a = array( 1, 2, 3, 4 );
unset($a[1]);
assert( true === is_associative($a) );
Sorry, can't help you with that. It's also somewhat performant for decently sized arrays, as it doesn't make needless copies. It is these little things that makes Python and Ruby so much nicer to write in... :P
<?php
function is_list($array) {
return array_keys($array) === range(0, count($array) - 1);
}
function is_assoc($array) {
return count(array_filter(array_keys($array), 'is_string')) == count($array);
}
?>
Both of these examples, which scored the most points do not work correctly with arrays like $array = array('foo' => 'bar', 1)
This would work too (demo):
function array_has_numeric_keys_only(array $array)
{
try {
SplFixedArray::fromArray($array, true);
} catch (InvalidArgumentException $e) {
return false;
}
return true;
}
Please note that the main point of this answer is to inform you about the existence of SplFixedArray and not to encourage you to use Exceptions for these kinds of tests.
I think the definition of a scalar array will vary by application. That is, some applications will require a more strict sense of what qualifies as a scalar array, and some applications will require a more loose sense.
Below I present 3 methods of varying strictness.
<?php
/**
* Since PHP stores all arrays as associative internally, there is no proper
* definition of a scalar array.
*
* As such, developers are likely to have varying definitions of scalar array,
* based on their application needs.
*
* In this file, I present 3 increasingly strict methods of determining if an
* array is scalar.
*
* #author David Farrell <DavidPFarrell#gmail.com>
*/
/**
* isArrayWithOnlyIntKeys defines a scalar array as containing
* only integer keys.
*
* If you are explicitly setting integer keys on an array, you
* may need this function to determine scalar-ness.
*
* #param array $a
* #return boolean
*/
function isArrayWithOnlyIntKeys(array $a)
{
if (!is_array($a))
return false;
foreach ($a as $k => $v)
if (!is_int($k))
return false;
return true;
}
/**
* isArrayWithOnlyAscendingIntKeys defines a scalar array as
* containing only integer keys in ascending (but not necessarily
* sequential) order.
*
* If you are performing pushes, pops, and unsets on your array,
* you may need this function to determine scalar-ness.
*
* #param array $a
* #return boolean
*/
function isArrayWithOnlyAscendingIntKeys(array $a)
{
if (!is_array($a))
return false;
$prev = null;
foreach ($a as $k => $v)
{
if (!is_int($k) || (null !== $prev && $k <= $prev))
return false;
$prev = $k;
}
return true;
}
/**
* isArrayWithOnlyZeroBasedSequentialIntKeys defines a scalar array
* as containing only integer keys in sequential, ascending order,
* starting from 0.
*
* If you are only performing operations on your array that are
* guaranteed to either maintain consistent key values, or that
* re-base the keys for consistency, then you can use this function.
*
* #param array $a
* #return boolean
*/
function isArrayWithOnlyZeroBasedSequentialIntKeys(array $a)
{
if (!is_array($a))
return false;
$i = 0;
foreach ($a as $k => $v)
if ($i++ !== $k)
return false;
return true;
}
One more fast from source.
Fit encoding of json_encode (and bson_encode). So has javascript Array compliance.
function isSequential($value){
if(is_array($value) || ($value instanceof \Countable && $value instanceof \ArrayAccess)){
for ($i = count($value) - 1; $i >= 0; $i--) {
if (!isset($value[$i]) && !array_key_exists($i, $value)) {
return false;
}
}
return true;
} else {
throw new \InvalidArgumentException(
sprintf('Data type "%s" is not supported by method %s', gettype($value), __METHOD__)
);
}
}
Could this be the solution?
public static function isArrayAssociative(array $array) {
reset($array);
return !is_int(key($array));
}
The caveat is obviously that the array cursor is reset but I'd say probably the function is used before the array is even traversed or used.

Categories