I'm trying to implement my own serialization / var_dump style function in PHP. It seems impossible if there is the possibility of circular arrays (which there is).
In recent PHP versions, var_dump seems to detect circular arrays:
php > $a = array();
php > $a[] = &$a;
php > var_dump($a);
array(1) {
[0]=>
&array(1) {
[0]=>
*RECURSION*
}
}
How would I implement my own serialization type of method in PHP that can detect similarly? I can't just keep track of which arrays I've visited, because strict comparison of arrays in PHP returns true for different arrays that contain the same elements and comparing circular arrays causes a Fatal Error, anyways.
php > $b = array(1,2);
php > $c = array(1,2);
php > var_dump($b === $c);
bool(true)
php > $a = array();
php > $a[] = &$a;
php > var_dump($a === $a);
PHP Fatal error: Nesting level too deep - recursive dependency? in php shell code on line 1
I've looked for a way to find a unique id (pointer) for an array, but I can't find one. spl_object_hash only works on objects, not arrays. If I cast multiple different arrays to objects they all get the same spl_object_hash value (why?).
EDIT:
Calling print_r, var_dump, or serialize on each array and then using some mechanism to detect the presence of recursion as detected by those methods is an algorithmic complexity nightmare and will basically render any use too slow to be practical on large nested arrays.
ACCEPTED ANSWER:
I accepted the answer below that was the first to suggest temporarily altering the an array to see if it is indeed the same as another array. That answers the "how do I compare two arrays for identity?" from which recursion detection is trivial.
The isRecursiveArray(array) method below detects circular/recursive arrays. It keeps track of which arrays have been visited by temporarily adding an element containing a known object reference to the end of the array.
If you want help writing the serialization method, please update your topic question and provide a sample serialization format in your question.
function removeLastElementIfSame(array & $array, $reference) {
if(end($array) === $reference) {
unset($array[key($array)]);
}
}
function isRecursiveArrayIteration(array & $array, $reference) {
$last_element = end($array);
if($reference === $last_element) {
return true;
}
$array[] = $reference;
foreach($array as &$element) {
if(is_array($element)) {
if(isRecursiveArrayIteration($element, $reference)) {
removeLastElementIfSame($array, $reference);
return true;
}
}
}
removeLastElementIfSame($array, $reference);
return false;
}
function isRecursiveArray(array $array) {
$some_reference = new stdclass();
return isRecursiveArrayIteration($array, $some_reference);
}
$array = array('a','b','c');
var_dump(isRecursiveArray($array));
print_r($array);
$array = array('a','b','c');
$array[] = $array;
var_dump(isRecursiveArray($array));
print_r($array);
$array = array('a','b','c');
$array[] = &$array;
var_dump(isRecursiveArray($array));
print_r($array);
$array = array('a','b','c');
$array[] = &$array;
$array = array($array);
var_dump(isRecursiveArray($array));
print_r($array);
Funny method (I know it is stupid :)), but you can modify it and track the "path" to the recursive element. This is just an idea :) Based on the property of the serialized string, when recursion starts in will be the same as the string for the original array. As you can see - I tried it on many different variations and might be something is able to 'fool' it, but it 'detects' all listed recursions. And I did not try recursive arrays with objects.
$a = array('b1'=>'a1','b2'=>'a2','b4'=>'a3','b5'=>'R:1;}}}');
$a['a1'] = &$a;
$a['b6'] = &$a;
$a['b6'][] = array(1,2,&$a);
$b = serialize($a);
print_r($a);
function WalkArrayRecursive(&$array_name, &$temp){
if (is_array($array_name)){
foreach ($array_name as $k => &$v){
if (is_array($v)){
if (strpos($temp, preg_replace('#R:\d+;\}+$#', '',
serialize($v)))===0)
{
echo "\n Recursion detected at " . $k ."\n";
continue;
}
WalkArrayRecursive($v, $temp);
}
}
}
}
WalkArrayRecursive($a, $b);
regexp is for the situation when element with recursion is at the 'end' of the array. and, yes, this recursion is related to the whole array. It is possible to make recursion of the subelements, but it is too late for me to think about them. Somehow every element of the array should be checked for the recursion in its subelements. The same way, like above, through the output of the print_r function, or looking for specific record for recursion in serialized string (R:4;} something like this). And tracing should start from that element, comparing everything below by my script. All that is only if you want to detect where recursion starts, not just whether you have it or not.
ps: but the best thing should be, as I think, to write your own unserialize function from serailized string created by php itself.
My approach is to have a temp array that holds a copy of all objects that were already iterated. like this here:
// We use this to detect recursion.
global $recursion;
$recursion = [];
function dump( $data, $label, $level = 0 ) {
global $recursion;
// Some nice output for debugging/testing...
echo "\n";
echo str_repeat( " ", $level );
echo $label . " (" . gettype( $data ) . ") ";
// -- start of our recursion detection logic
if ( is_object( $data ) ) {
foreach ( $recursion as $done ) {
if ( $done === $data ) {
echo "*RECURSION*";
return;
}
}
// This is the key-line: Remember that we processed this item!
$recursion[] = $data;
}
// -- end of recursion check
if ( is_array( $data ) || is_object( $data ) ) {
foreach ( (array) $data as $key => $item ) {
dump( $item, $key, $level + 1 );
}
} else {
echo "= " . $data;
}
}
And here is some quick demo code to illustrate how it works:
$obj = new StdClass();
$obj->arr = [];
$obj->arr[] = 'Foo';
$obj->arr[] = $obj;
$obj->arr[] = 'Bar';
$obj->final = 12345;
$obj->a2 = $obj->arr;
dump( $obj, 'obj' );
This script will generate the following output:
obj (object)
arr (array)
0 (string) = Foo
1 (object) *RECURSION*
2 (string) = Bar
final (integer) = 12345
a2 (array)
0 (string) = Foo
1 (object) *RECURSION*
2 (string) = Bar
This is my approach. The key is to pass the array by reference to the recursive function simple_var_dump(), and use a tag (in this case "iterating_in_a_higher_level") to distinguish the arrays that are being iterated in a higher nesting level.
#!/usr/bin/php
<?php
function simple_var_dump(&$var, $depth = 0)
{
if (!is_array($var)) {
if (is_scalar($var)) {
return (string)$var;
} else {
return '?';
}
}
if (isset($var['__iterating_in_a_higher_level__'])) {
$r = 'array(' . (count($var)-1) . ')';
return $r . ' *RECURSION*';
}
$r = 'array(' . count($var) . ')';
$var['__iterating_in_a_higher_level__'] = true;
foreach ($var as $key => &$value) {
if ($key !== '__iterating_in_a_higher_level__') {
$r .= "\n" . str_repeat(' ', $depth + 1) . '[' . $key . '] => ' . simple_var_dump($value, $depth + 1);
}
}
unset($var['__iterating_in_a_higher_level__']);
return $r;
}
// example:
//
$a = [new stdClass(), &$a, 30, [40, [[&$a]]], [1, true, &$a], []];
echo simple_var_dump($a) . "\n";
Output:
array(6)
[0] => ?
[1] => array(6) *RECURSION*
[2] => 30
[3] => array(2)
[0] => 40
[1] => array(1)
[0] => array(1)
[0] => array(6) *RECURSION*
[4] => array(3)
[0] => 1
[1] => 1
[2] => array(6) *RECURSION*
[5] => array(0)
It's not elegant, but solves your problem (at least if you dont have someone using *RECURSION* as a value).
<?php
$a[] = &$a;
if(strpos(print_r($a,1),'*RECURSION*') !== FALSE) echo 1;
Related
Imagine you have a deep array like this:
<?php
$array = ['hello' => ['deep' => 'treasure']];
Then you had an array of the keys to access the string 'treasure'
['hello', 'deep'];
How do you delete the string treasure if you did not know the depth of the array till run time
Edit:
My apologises I've definitely not provided enough information for what I'm looking to achieve
Here is some code I've come up with which does what I need but uses unsafe eval (keep in mind the target destination could be an array so array_walk_recursive won't work)
function iterator_keys($iterator, $outer_data) {
$keys = array();
for ($i = 0; $i < $iterator->getDepth() + 1; $i++) {
$sub_iterator = $iterator->getSubIterator($i);
$keys[$i] = ($i == 0 && is_object($outer_data)
|| $i > 0 && is_object($last_iterator->current())) ?
'->{"' . $sub_iterator->key() . '"}' :
'["' . $sub_iterator->key() . '"]';
$last_iterator = $sub_iterator;
}
return $keys;
}
function recursive_filter($data, callable $selector_function, $iterator = NULL) {
$iterator = $iterator ?? new RecursiveIteratorIterator(
new RecursiveArrayIterator($data),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $key => $value) {
if ($selector_function($value, $key, $iterator)) {
eval('unset($data' . implode('', iterator_keys($iterator, $data)) . ');');
}
}
return $data;
}
The intention is to have a deep data structure a function that evalutes each node and if it matches a condition then remove it from the data structure in place, if this can be done without eval that would be amazing but so far I think PHP can't programmatically delete something that is more than one level deep
Hello I think what you want is somethings like this
<?php
$array = ['hello' => ['deep' => ['deep1' => 'treasure']]];
$keys = ["hello", "deep", "deep1"];
function remove_recursive(&$array, $keys, $level = 0)
{
if ($level >= count($keys)) {
return $array;
}
if (isset($array[$keys[$level]]) && $level == count($keys) - 1) {
unset($array[$keys[$level]]);
} elseif (isset($array[$keys[$level]])) {
$array[$keys[$level]] = remove_recursive($array[$keys[$level]], $keys, $level + 1);
}
return $array;
}
var_dump(remove_recursive($array, $keys));
Well you can try this really quick and dirty way that uses eval to achieve your goal:
$array = ['hello' => ['deep' => 'treasure']];
$keys = ['hello', 'deep'];
$expression = 'unset($array[\'' . implode('\'][\'', $keys) . '\']);';
eval($expression);
But maybe you can tell us more about your development and we can help you reorganize it somehow to avoid this problem at all.
This will set the target array element to null. Optionally you could use '':
$array = ['hello' => ['deep' => 'treasure']];
$path = ['hello', 'deep'];
$temp = &$array;
foreach($path as $key) {
$temp =& $temp[$key];
}
$temp = null;
print_r($array);
Yields:
Array
(
[hello] => Array
(
[deep] =>
)
)
I am trying to manually sort a PHP array without making use of ksort.
This is how my code looks at the moment:
function my_ksort(&$arg){
foreach($arg as $key1 => $value1){
foreach($arg as $key2 => $value2){
if($key1 > $key2){
$aux = $value2;
$arg[$key2] = $value1;
$arg[$key1] = $aux;
}
}
}
}
It doesn't sort, I can't figure out how to make it sort.
You could try this:
function my_ksort(&$arg)
{
$keys=array_keys($arg);
sort($keys);
foreach($keys as $key)
{
$val=$arg[$key];
unset($arg[$key]);
$arg[$key]=$val;
}
}
I'm sorting the keys separately and then deleting the elements one-by-one and appending them to the end, in ascending order.
I'm using another sorting function (sort()), but if you want to eliminate all available sorting functions from your emulation, sort() is much easier to emulate. In fact, #crypticous's algorithm does just that!
This function return array in ASC. Take in consideration that I'm using goto which is supported in (PHP 5 >= 5.3.0)
function ascending_array($array){
if (!is_array($array)){
$array = explode(",", $array);
}
$new = array();
$flag = true;
iter:
$array = array_values($array); // recount array values with new offsets
(isset($min["max"])) ? $min["value"] = $min["max"] : $min["value"] = $array[0];
$min["offset"] = 0;
for ($i=0;$i<count($array);$i++){
if ($array[$i] < $min["value"]){ // redefine min values each time if statement executed
$min["value"] = $array[$i];
$min["offset"] = $i;
}
if ($flag){ // execute only first time
if ($array[$i] > $min["value"]){ // define max value from array
$min["max"] = $array[$i];
}
$flag = false;
}
if ($i === (count($array)-1)){ // last array element
array_push($new,$min["value"]);
unset($array[$min["offset"]]);
}
}
if (count($array)!=0){
goto iter;
}
print_r($new);
}
$arr = array(50,25,98,45);
ascending_array($arr); // 25 45 50 98
PS. When I was studying php, I wrote this function and now remembered that I had it (that's why I really don't remember what I am doing in it, though fact is it's working properly and hopefully there are comments too), hope you'll enjoy :)
DEMO
I was checking some issue related to this post and i wanted to give my insight about it ! here's what i would have done to implement php's sort :
$array_res = array();
$array = array(50,25,98,45);
$i=0;
$temp = $array[0];
$key = array_search($temp, $array);
while ($i<count($array)-1){
$temp = $array[0];
for($n=0;$n<count($array) ;$n++)
{
if($array[$n]< $temp && $array[$n] != -1 )
{
$temp = $array[$n];
}
else{continue;}
}
//get the index for later deletion
$key = array_search($temp, $array);
array_push($array_res, $temp);
/// flag on those which were ordered
$array[$key] =-1;
$i++;
}
// lastly append the highest number
for($n=0;$n<count($array) ;$n++)
{
if ($array[$n] != -1)
array_push($array_res, $array[$n]);
}
// display the results
print_r($array_res);
This code will display : Array
(
[0] => 25
[1] => 45
[2] => 50
[3] => 98
)
Short and sweet
function custom_ksort($arg)
{
$keys = array_keys($arg);
sort($keys);
foreach($keys as $newV)
{
$newArr[$newV] = $arg[$newV];
}
return $newArr;
}
It looks like your issue is that you're changing "temporary" characters $key1 and $key2 but not the actual arrays. You have to change $arg, not just $key1 and $key2.
Try something like:
$arr = Array(3=>"a",7=>"b");
print_r( $arr );
foreach( $arr as $k=>$v ){
unset($arr[$k]);
$arr[$k+1] = $v;
}
print_r($arr);
I have a small task in php. I have a simple array.
Array
(
[0] => 50
[1] => 100
[2] => 150
)
Is there a php built in function which i can use so that it can return true or false, accordingly if array is already sorted or not, Or any other php script for this, no loops. I know it is easy with loops and conditions.
You can compare your input array with sorted one if they are equal.
$input = array(50, 100, 150);
$sorted = array_values($input);
sort($sorted);
if ( $input === $sorted ) {
// input array was already sorted
}
function arraySorted($array) {
$a = $array;
$b = $array;
sort($b);
if ($a == $b){
return true;
} else {
return false;
}
}
//test for [0],[3],[2]
$input = array(0 => 250,
3 => 100,
2 => 150);
var_dump($input);
echo "<br />";
//array(3) { [0]=> int(250) [3]=> int(100) [2]=> int(150) }
var_dump(arraySorted($input));
echo "<br />";
//bool(false)
//test for [0],[1],[2]
$input = array(0 => 250,
1 => 100,
2 => 150);
var_dump($input);
echo "<br />";
//array(3) { [0]=> int(250) [1]=> int(100) [2]=> int(150) }
var_dump(arraySorted($input));
echo "<br />";
//bool(false)
//test for [0],[3],[2] and asc values
$input = array(0 => 50,
1 => 100,
2 => 150);
var_dump($input);
echo "<br />";
//array(3) { [0]=> int(50) [1]=> int(100) [2]=> int(150) }
var_dump(arraySorted($input));
echo "<br />";
//bool(true)
Since PHP doesn't hold the state if an array is sorted or not it can't know. And the only other solution is to iterate over the array.
You could use array_reduce to compare each element to the next, and throw an exception if the array is unsorted.
Here, you can try with this code:
<?php
$sort = array(
0 => 50,
1 => 100,
2 => 150
);
$default = $sort;
sort($sort);
$flag = true;
foreach($sort as $key=>$value)
if($value!=$default[$key])
$flag = false;
if($flag)
echo "Already sorted";
else
echo "Not Already sorted";
?>
I'm quite surprised no one proposed to actually check input array itself.
Maybe it's my C++ background, but I just can't not to care about algorithm's complexity and frankly speaking, copying and sorting array is just silly.
namespace Util\Functions;
function compare($lhs, $rhs, $descendingOrder = false)
{
$result = 0;
if ($lhs < $rhs) {
$result = -1;
} else if ($lhs > $rhs) {
$result = 1;
}
if ($descendingOrder) {
$result *= -1;
}
return $result;
}
function isSorted(array $arr, callable $compareFunction = null)
{
$count = count($arr);
if ($count < 2){
return true;
}
if ($compareFunction === null) {
$compareFunction = 'Util\Functions\compare';
}
for ($i = 1; $i < $count; $i++) {
if ($compareFunction($arr[$i - 1], $arr[$i]) > 0) {
return false;
}
}
return true;
}
P.S. I know OP asked no loops but since there is no good solution, I decided to post code that anyone could just copy-paste to his project.
You can compare it with already sorted array, or you can control it via SQL query using order by if the array is coming from database, rest no inbuilt function in php to check this.
This will work regardless on keys:
$a = array(5 => 'aple', 3 => 'banana', 1 =>'citron');
$b = array(2 => 'orange', 1 => 'wine', 5 => 'apple');
echo arraySorted($a) ? "sorted" : 'not';
echo "\n";
echo arraySorted($b) ? "sorted" : 'not';
function arraySorted($array) {
$sorted = $vals = array_values($array);
sort($sorted);
return $sorted === $vals;
}
Arrays can't know weather they are sorted, because there are so many orderings. Just for numbers there can be ascending, descending, absolute ascending, absolute... You get the picture. However algorithm that check if array is sorted is independent of sort order.
What follow is isSorted function, that if given comparator will check if elements of array are sorted in that order. That way isSorted do not know what order is being tested, but instead delegate that to comparator.
Since question show numbers in specific order, I also provided comparator for ascending order based on php <.
$xs = [
50,
100,
150
];
// Here is example of comparator, it have to take 2 elements, and return boolean signaling weather relationship you want to test holds true
$comparator = function($current, $next) {
return $current < $next;
};
function isSorted($xs, $comparator){
$answer = true;
foreach ($xs as $key => $current) {
if(!isset($xs[$key + 1]))
continue;
$next = $xs[$key + 1];
$answer = $answer && $comparator($current, $next);
}
return $answer;
}
I see lots of answers that are performing a full array sort then comparing the whole original versus the whole sorted copy. This of course is not as efficient as it could be. To check if the array is sorted, you don't need to copy it or mutate it or sort it -- just iterate it and compare as you go. A script with an early break/return will do the absolute minimum work.
(I also see a couple of answers calling array_values(), but I can't understand why re-indexing is valuable.)
For instance: (Demo of a true and a false outcome)
$array = [50, 50, 100, 175]; // no break
$last = reset($array);
$isSorted = true;
foreach ($array as $value) {
if ($last > $value) {
$isSorted = false;
break;
}
$last = $value;
}
var_export($isSorted); //true
This will only ever make a complete run of your data if the array is completely sorted or if the final element is unsorted.
But really, if you are concerned with whether or not the array is sorted, you can probably just sort it and move on with your scripting.
If you need something that will perform an ultra fast evaluation because you are dealing with an insanely huge amount of data, then you probably want to employ a language outside of php.
This question already has answers here:
How to access and manipulate multi-dimensional array by key names / path?
(10 answers)
Closed 3 years ago.
I have string:
Main.Sub.SubOfSub
And some kind of data, may be a string:
SuperData
How I can transform it all to this array above?
Array
(
[Main] => Array
(
[Sub] => Array
(
[SubOfSub] => SuperData
)
)
)
Thanks for help,
PK
Given the values
$key = "Main.Sub.SubOfSub";
$target = array();
$value = "SuperData";
Here's some code I have lying around that does what you need¹:
$path = explode('.', $key);
$root = &$target;
while(count($path) > 1) {
$branch = array_shift($path);
if (!isset($root[$branch])) {
$root[$branch] = array();
}
$root = &$root[$branch];
}
$root[$path[0]] = $value;
See it in action.
¹ Actually it does slightly more than that: it can be trivially encapsulated inside a function, and it is configurable on all three input values (you can pass in an array with existing values, and it will expand it as necessary).
Like Jon suggested (and being asking feedback for in chat), a reference/variable alias is helpful here to traverse the dynamic stack of keys. So the only thing needed is to iterate over all subkeys and finally set the value:
$rv = &$target;
foreach(explode('.', $key) as $pk)
{
$rv = &$rv[$pk];
}
$rv = $value;
unset($rv);
The reference makes it possible to use a stack instead of recursion which is generally more lean. Additionally this code prevents to overwrite existing elements in the $target array. Full example:
$key = "Main.Sub.SubOfSub";
$target = array('Main' => array('Sub2' => 'Test'));
$value = "SuperData";
$rv = &$target;
foreach(explode('.', $key) as $pk)
{
$rv = &$rv[$pk];
}
$rv = $value;
unset($rv);
var_dump($target);
Output:
array(1) {
["Main"]=>
array(2) {
["Sub2"]=>
string(4) "Test"
["Sub"]=>
array(1) {
["SubOfSub"]=>
string(9) "SuperData"
}
}
}
Demo
Related Question(s):
dynamic array key additions
How to group elements of array?
What would be the fastest, most efficient way to implement a search method that will return an object with a qualifying id?
Sample object array:
$array = [
(object) ['id' => 'one', 'color' => 'white'],
(object) ['id' => 'two', 'color' => 'red'],
(object) ['id' => 'three', 'color' => 'blue']
];
What do I write inside of:
function findObjectById($id){
}
The desired result would return the object at $array[0] if I called:
$obj = findObjectById('one')
Otherwise, it would return false if I passed 'four' as the parameter.
You can iterate that objects:
function findObjectById($id){
$array = array( /* your array of objects */ );
foreach ( $array as $element ) {
if ( $id == $element->id ) {
return $element;
}
}
return false;
}
Edit:
Faster way is to have an array with keys equals to objects' ids (if unique);
Then you can build your function as follow:
function findObjectById($id){
$array = array( /* your array of objects with ids as keys */ );
if ( isset( $array[$id] ) ) {
return $array[$id];
}
return false;
}
It's an old question but for the canonical reference as it was missing in the pure form:
$obj = array_column($array, null, 'id')['one'] ?? false;
The false is per the questions requirement to return false. It represents the non-matching value, e.g. you can make it null for example as an alternative suggestion.
This works transparently since PHP 7.0. In case you (still) have an older version, there are user-space implementations of it that can be used as a drop-in replacement.
However array_column also means to copy a whole array. This might not be wanted.
Instead it could be used to index the array and then map over with array_flip:
$index = array_column($array, 'id');
$map = array_flip($index);
$obj = $array[$map['one'] ?? null] ?? false;
On the index the search problem might still be the same, the map just offers the index in the original array so there is a reference system.
Keep in mind thought that this might not be necessary as PHP has copy-on-write. So there might be less duplication as intentionally thought. So this is to show some options.
Another option is to go through the whole array and unless the object is already found, check for a match. One way to do this is with array_reduce:
$obj = array_reduce($array, static function ($carry, $item) {
return $carry === false && $item->id === 'one' ? $item : $carry;
}, false);
This variant again is with the returning false requirement for no-match.
It is a bit more straight forward with null:
$obj = array_reduce($array, static function ($carry, $item) {
return $carry ?? ($item->id === 'one' ? $item : $carry);
}, null);
And a different no-match requirement can then be added with $obj = ...) ?? false; for example.
Fully exposing to foreach within a function of its own even has the benefit to directly exit on match:
$result = null;
foreach ($array as $object) {
if ($object->id === 'one') {
$result = $object;
break;
}
}
unset($object);
$obj = $result ?? false;
This is effectively the original answer by hsz, which shows how universally it can be applied.
You can use the function array_search of php like this
$key=array_search("one", array_column(json_decode(json_encode($array),TRUE), 'color'));
var_dump($array[$key]);
i: is the index of item in array
1: is the property value looking for
$arr: Array looking inside
'ID': the property key
$i = array_search(1, array_column($arr, 'ID'));
$element = ($i !== false ? $arr[$i] : null);
Well, you would would have to loop through them and check compare the ID's unless your array is sorted (by ID) in which case you can implement a searching algorithm like binary search or something of that sort to make it quicker.
My suggestion would be to first sort the arrays using a sorting algorithm (binary sort, insertion sort or quick sort) if the array is not sorted already. Then you can implement a search algorithm which should improve performance and I think that's as good as it gets.
http://www.algolist.net/Algorithms/Binary_search
This is my absolute favorite algorithm for very quickly finding what I need in a very large array, quickly. It is a Binary Search Algorithm implementation I created and use extensively in my PHP code. It hands-down beats straight-forward iterative search routines. You can vary it a multitude of ways to fit your need, but the basic algorithm remains the same.
To use it (this variation), the array must be sorted, by the index you want to find, in lowest-to-highest order.
function quick_find(&$array, $property, $value_to_find, &$first_index) {
$l = 0;
$r = count($array) - 1;
$m = 0;
while ($l <= $r) {
$m = floor(($l + $r) / 2);
if ($array[$m]->{$property} < $value_to_find) {
$l = $m + 1;
} else if ($array[$m]->{$property} > $value_to_find) {
$r = $m - 1;
} else {
$first_index = $m;
return $array[$m];
}
}
return FALSE;
}
And to test it out:
/* Define a class to put into our array of objects */
class test_object {
public $index;
public $whatever_you_want;
public function __construct( $index_to_assign ) {
$this->index = $index_to_assign;
$this->whatever_you_want = rand(1, 10000000);
}
}
/* Initialize an empty array we will fill with our objects */
$my_array = array();
/* Get a random starting index to simulate data (possibly loaded from a database) */
$my_index = rand(1256, 30000);
/* Say we are needing to locate the record with this index */
$index_to_locate = $my_index + rand(200, 30234);
/*
* Fill "$my_array()" with ONE MILLION objects of type "test_object"
*
* 1,000,000 objects may take a little bit to generate. If you don't
* feel patient, you may lower the number!
*
*/
for ($i = 0; $i < 1000000; $i++) {
$searchable_object = new test_object($my_index); // Create the object
array_push($my_array, $searchable_object); // Add it to the "$my_array" array
$my_index++; /* Increment our unique index */
}
echo "Searching array of ".count($my_array)." objects for index: " . $index_to_locate ."\n\n";
$index_found = -1; // Variable into which the array-index at which our object was found will be placed upon return of the function.
$object = quick_find($my_array, "index", $index_to_locate, $index_found);
if ($object == NULL) {
echo "Index $index_to_locate was not contained in the array.\n";
} else {
echo "Object found at index $index_found!\n";
print_r($object);
}
echo "\n\n";
Now, a few notes:
You MAY use this to find non-unique indexes; the array MUST still be sorted in ascending order. Then, when it finds an element matching your criteria, you must walk the array backwards to find the first element, or forward to find the last. It will add a few "hops" to your search, but it will still most likely be faster than iterating a large array.
For STRING indexes, you can change the arithmetic comparisons (i.e. " > " and " < " ) in quick_find() to PHP's function "strcasecmp()". Just make sure the STRING indexes are sorted the same way (for the example implementation): Alphabetically and Ascending.
And if you want to have a version that can search arrays of objects sorted in EITHER ascending OR decending order:
function quick_find_a(&$array, $property, $value_to_find, &$first_index) {
$l = 0;
$r = count($array) - 1;
$m = 0;
while ($l <= $r) {
$m = floor(($l + $r) / 2);
if ($array[$m]->{$property} < $value_to_find) {
$l = $m + 1;
} else if ($array[$m]->{$property} > $value_to_find) {
$r = $m - 1;
} else {
$first_index = $m;
return $array[$m];
}
}
return FALSE;
}
function quick_find_d(&$array, $property, $value_to_find, &$first_index) {
$l = 0;
$r = count($array) - 1;
$m = 0;
while ($l <= $r) {
$m = floor(($l + $r) / 2);
if ($value_to_find > $array[$m]->{$property}) {
$r = $m - 1;
} else if ($value_to_find < $array[$m]->{$property}) {
$l = $m + 1;
} else {
$first_index = $m;
return $array[$m];
}
}
return FALSE;
}
function quick_find(&$array, $property, $value_to_find, &$first_index) {
if ($array[0]->{$property} < $array[count($array)-1]->{$property}) {
return quick_find_a($array, $property, $value_to_find, $first_index);
} else {
return quick_find_d($array, $property, $value_to_find, $first_index);
}
}
The thing with performance of data structures is not only how to get but mostly how to store my data.
If you are free to design your array, use an associative array:
$array['one']->id = 'one';
$array['one']->color = 'white';
$array['two']->id = 'two';
$array['two']->color = 'red';
$array['three']->id = 'three';
$array['three']->color = 'blue';
Finding is then the most cheap: $one = $array['one];
UPDATE:
If you cannot modify your array constitution, you could create a separate array which maps ids to indexes. Finding an object this way does not cost any time:
$map['one'] = 0;
$map['two'] = 1;
$map['three'] = 2;
...
getObjectById() then first lookups the index of the id within the original array and secondly returns the right object:
$index = $map[$id];
return $array[$index];
Something I like to do in these situations is to create a referential array, thus avoiding having to re-copy the object but having the power to use the reference to it like the object itself.
$array['one']->id = 'one';
$array['one']->color = 'white';
$array['two']->id = 'two';
$array['two']->color = 'red';
$array['three']->id = 'three';
$array['three']->color = 'blue';
Then we can create a simple referential array:
$ref = array();
foreach ( $array as $row )
$ref[$row->id] = &$array[$row->id];
Now we can simply test if an instance exists in the array and even use it like the original object if we wanted:
if ( isset( $ref['one'] ) )
echo $ref['one']->color;
would output:
white
If the id in question did not exist, the isset() would return false, so there's no need to iterate the original object over and over looking for a value...we just use PHP's isset() function and avoid using a separate function altogether.
Please note when using references that you want use the "&" with the original array and not the iterator, so using &$row would not give you what you want.
This is definitely not efficient, O(N). But it looks sexy:
$result = array_reduce($array, function ($found, $obj) use ($id) {
return $obj['id'] == $id ? $obj : $found;
}, null);
addendum:
I see hakre already posted something akin to this.
Here is what I use. Reusable functions that loop through an array of objects. The second one allows you to retrieve a single object directly out of all matches (the first one to match criteria).
function get_objects_where($match, $objects) {
if ($match == '' || !is_array($match)) return array ();
$wanted_objects = array ();
foreach ($objects as $object) {
$wanted = false;
foreach ($match as $k => $v) {
if (is_object($object) && isset($object->$k) && $object->$k == $v) {
$wanted = true;
} else {
$wanted = false;
break;
};
};
if ($wanted) $wanted_objects[] = $object;
};
return $wanted_objects;
};
function get_object_where($match, $objects) {
if ($match == '' || !is_array($match)) return (object) array ();
$wanted_objects = get_objects_where($match, $objects);
return count($wanted_objects) > 0 ? $wanted_objects[0] : (object) array ();
};
The easiest way:
function objectToArray($obj) {
return json_decode(json_encode($obj), true);
}