How to Efficently count user's rank by amout of likes? - php

I have an array, that looks like this,
$users = array(
0 => array(
'user_id' => 'user_1',
'likes' => 50,
),
1 => array(
'user_id' => 'user_2',
'likes' => 72
),
2 => array(
'user_id' => 'user_3',
'likes' => 678
)
);
All I want to do is to implement ratings system according to amount of likes, so that it would look like as:
#rank 1 - user_3
#rank 2 - user_2
#rank 3 - user_1
I ended up with,
$user_counter = new User_Counter();
$user_counter->setData($users);
echo $user_counter->fetchRankByLikes(678);
class User_Counter
{
protected $array;
public function setData(array $array)
{
$this->array = $this->sort($array);
}
protected function sort(array $array)
{
$result = array();
foreach ($array as $index => $_array) {
$result[] = $_array['likes'];
}
// This will reset indexes
sort($result, SORT_NUMERIC);
$result = array_reverse($result);
$return = array();
$count = 0;
foreach ($result as $index => $rank) {
$count++;
$return[$count] = $rank;
}
$return = array_unique($return);
return $return;
}
public function getAll()
{
return $this->array;
}
public function fetchRankByLikes($likes)
{
$data = array_flip($this->array);
return $data[$likes];
}
public function fetchLikesByRank($rank)
{
return $this->array[$rank];
}
}
My problem is that, this approach lies sometimes - for example it gives incorrect info when there are no likes at all ( === i.e all members have 0 likes) - in that case it gives first rank to them all.
Is there another efficient approach to count user's rating by amount of their likes?
Or what am I doing wrong in my computations?
Thanks.

If you're looking for efficiency, I would look at the usort() function, native to PHP:
http://us1.php.net/manual/en/function.usort.php
What usort does is take an array and iteratively walk through it, providing an outside function with 2 items from it's input at a time. It then waits for the function to return either 1, -1 or 0 and determines the following truths:
-1 means left < right
+1 means left > right
0 means both parameters are equal
Here's a practical example:
$users = array(
0 => array(
'user_id' => 'user_1',
'likes' => 50,
),
1 => array(
'user_id' => 'user_2',
'likes' => 72
),
2 => array(
'user_id' => 'user_3',
'likes' => 678
)
);
usort($users, "sortLikesAscending");
function sortLikesAscending($a, $b) {
if ($a['likes'] > $b['likes']) {
return 1;
} elseif ($a['likes'] < $b['likes']) {
return -1;
} else {
return 0;
}
}
Hope that helps!
EDIT:
If you want to implement this usort() methodology from inside your User_Counter class, then call usort like this:
// From somewhere inside User_Counter, assumes User_Counter::mySortingFunction() is defined
usort($this->array, array(&$this, "mySortingFunction"));
The function callback is passed as an array with two entries: a &reference to an object containing the function and the function name as a string.

Related

Fastest return TRUE if all values of a column of a multidimensional array is_numeric

Are there faster methods to check the existence of a number (not null) in one
column of a multidimensional array in php (for instance, number9)?
Attempt:
The if statement below seems to be working okay.
$arr=Array(
[0] => Array
(
[date] => 2019-01-16
[number1] => 20.4
[number2] => 20.54
[number3] => 19.71
[number4] => 19.73
[number5] => 70849266
[number6] => 70849266
[number7] => -0.65
[number8] => -3.189
[number9] => 20.0902
[string1] => Jan 16
[number10] => 0.047796070100903
)
.
.
.
[21] => Array
(
[date] => 2019-01-17
[number1 => 19.49
[number2] => 20.51
[number3] => 19.02
[number4] => 20.25
[number5] => 85018421
[number6] => 85018421
[number7] => 0.52
[number8] => 2.636
[number9] => 19.7988
[string1] => Jan 17
[number10] => 0.075411577270313
)
);
function isNumeric($var){
if (is_numeric($var)){
return true;
} else {
return false;
}
}
if((array_walk(array_column($arr, "number8"), 'isNumeric'))!=1)
Here are my ideas.
First is to just filter the array for numeric only values and compare to the original:
function with_array_filter($arr) {
return $arr == array_filter($arr, 'is_numeric');
}
The second example uses casting to a float and then back to string, keep in mind that this is not going to be accurate for very big numbers, however seems to be the fastest (if you care about that at all):
function is_numeric_array_with_cast($arr) {
foreach ($arr as $b) {
if ($b != (string)(float)$b) {
return false;
}
}
return true;
}
However probably the simplest solution is to just foreach the array inside a function and return early:
function is_numeric_array_with_func($arr) {
foreach ($arr as $b) {
if (!is_numeric($b)) {
return false;
}
}
return true;
}
Benchmarked with an array of 20 elements over 100000 iterations on PHP 7.2:
$falseArray = [
'1',
2.5,
-10,
32,
11.0,
2.5,
100101221,
345,
-10,
'-10',
'+10',
'10',
12,
PHP_INT_MAX,
PHP_INT_MAX + 1.4e5,
'-10',
null,
'a',
'5',
5
];
Matt Fryer
Time: 4.8473789691925
is_numeric_array_with_func
Time: 4.0416791439056
is_numeric_array_with_cast
Time: 3.2953300476074
with_array_filter
Time: 3.99729180336
AS I said in the comments:
The if statement below seems to be working okay
However, given the code you posed I doubt that: lets look at it.
function isNumeric($var){ ... }
if(array_walk(array_column($arr, "number8"), 'isNumberic'))!=1
The first and most obvious things are these 2
isNumberic vs isNumeric, which is a fatal undefined function error (spelling/typo).
)!=1 then this is outside of the actual condition, or put another way if(...){ !=1 }
Let's assume those are just typos in the question. Even if your code was free of the 2 "defects" I mentioned above you would still have this problem, array_walk works by reference and simply returns true (always). Pass by reference updates the "Original" variable without returning a copy of it (in the case of array_walk)
http://php.net/manual/en/function.array-walk.php
Return Values
Returns TRUE.
Which of course just makes your condition pass no matter what. So you should always test both the passing and the failing of the condition (As I did by placing some "canned" bad data in there). This way I know for 100% sure exactly how my code behaves.
Others have posted how to correct this, but not what you did wrong. But just for the sake of completeness I will post an answer anyway.
$arr = array (
0 =>
array (
'date' => '2019-01-16',
'number1' => 20.4,
'number2' => 20.54,
'number3' => 19.71,
'number4' => 19.73,
'number5' => 70849266,
'number6' => 70849266,
'number7' => -0.65,
'number8' => -3.189,
'number9' => 20.0902,
'string1' => 'Jan16',
'number10' => 0.047796070100903
),
array (
'date' => '2019-01-16',
'number1' => 20.4,
'number2' => 20.54,
'number3' => 19.71,
'number4' => 19.73,
'number5' => 70849266,
'number6' => 70849266,
'number7' => -0.65,
'number8' => 'foo',#intentially not number
'number9' => 20.0902,
'string1' => 'Jan16',
'number10' => 0.047796070100903
),
);
$col = array_column($arr, "number8");
$col1 = array_filter($col, 'is_numeric');
if($col != $col1){
echo "not the same\n";
}
Output:
not the same
Sandbox
I should mention, there is no "need" to count these, as PHP can compare complex objects for equality. As we are comparing the same "root" array ($col in this example) with itself after (possibly) removing some elements, if no elements were removed then both arrays should be not only the same length but also "Identical".
Also if you want to do it in one line (inside the condition) you can do this:
if( ($col = array_column($arr, "number8")) && $col != array_filter($col, 'is_numeric')){
echo "not the same\n";
}
It's a bit harder to read, and pay attention to $col = array_column assignment.
Use a foreach loop:
$bool = true;
foreach ($arr as $row)
{
if (!is_numeric($row['number8']))
{
$bool = false;
break;
}
}
Thanks a million everyone, for your great answers!
On my PC, I tried your four functions in PHP 5.5.38 with ~5000 iterations and total times are:
"is_numeric_array_with_cast total time is 0.44153618812561"
"with_array_filter total time is 0.21628260612488"
"is_numeric_array_with_func total time is 0.14269280433655"
"is_numeric_matt_fryer total time is 0.155033826828"
$t1=$t2=$t3=$t4=0;
foreach($arrs as $k=>$arr){
$s1=microtime(true);
is_numeric_array_with_cast(array_column($arr, "number8"));
$e1=microtime(true)-$s1;
$t1=$t1+$e1;
$s2=microtime(true);
with_array_filter(array_column($arr, "number8"));
$e2=microtime(true)-$s2;
$t2=$t2+$e2;
$s3=microtime(true);
is_numeric_array_with_func(array_column($arr, "number8"));
$e3=microtime(true)-$s3;
$t3=$t3+$e3;
$s4=microtime(true);
is_numeric_matt_fryer(array_column($arr, "number8"),"number8");
$e4=microtime(true)-$s4;
$t4=$t4+$e4;
}
function is_numeric_array_with_cast($arr) {
foreach ($arr as $b) {
if ($b != (string)(float)$b) {
return false;
}
}
return true;
}
function with_array_filter($arr) {
return $arr == array_filter($arr, 'is_numeric');
}
function is_numeric_array_with_func($arr) {
foreach ($arr as $b) {
if (!is_numeric($b)) {
return false;
}
}
return true;
}
function is_numeric_matt_fryer($arr,$str){
$bool = true;
foreach ($arr as $row)
{
if (!is_numeric($row[$str]))
{
$bool = false;
}
}
return $bool;
}

php find max in 2 dimension array but return another element('id') of the array

here is my array sample $data[][];
Array( [0] => Array ( [id] => 1349
[rating1] => 1.9378838029981E-7
[rating2] => 1.1801796607774 )
[1] => Array ( [id] => 1350
[rating1] => 5.5499981876923E-7
[rating2] => 1.5121329727308 )
[2] => Array ( [id] => 1377
[rating1] => 0.00023952225410117
[rating2] => 2.1947077830236 )
[3] => Array ( [id] => 1378
[rating1] => 0.00022982302863634
[rating2] => 2.2135588326622 )
[4] => Array ( [id] => 1379
[rating1] => 0.00026272979843585
[rating2] => 2.2388295595073 )
[5] => Array ( [id] => 1380
[rating1] => 0.0002788640872546
[rating2] => 2.1815325502993 )
)
I want to find max($data[][rating?]) but return $data[id][max_rating?] i.e. id associated with the max value.
Finding the max was easy for one particular, say rating1, I used array_reduce as follows (this is inspired from this SO ):
$max = array_reduce($data, function($a, $b) {
return $a > $b['rating1'] ? $a : $b['rating1'];
});
Now I have two questions :
1. How can I extend above array_reduce to include rating2 ? I have other ratingX as well.
2. I do not want the max value, rather the $data[][id] associated with the max.
I am not so much concerned about Q1, but the second one is important as I don't want to search through the array again to get associated $data[][id].
One line of thought is to use array_map instead of array_reduce, but I couldn't come up with a version which will pass on both [id] and [rating?]. Also, there are complications when I try to max() multiple rating? at one shot, as each rating will have different max, which in turn associates with different [id].
EDIT : Just to be clear, I want all the respective ids associated with respective max of each rating?
assuming your array is unsorted you have to loop through it at least once (either manually or using builtin functions). i'd use the following code:
$array = array(
array( 'id' => 1349, 'sudhi_rating1' => 1.9378838029981E-7, 'sudhi_rating2' => 1.1801796607774 ),
array( /* … */ ),
/* … */
);
$idx_max = 0;
foreach($array as $idx => $item) {
if($item['sudhi_rating1'] > $array[$idx_max]['sudhi_rating1'])
$idx_max = $idx;
}
echo "Rating 1 has max value at id ", htmlspecialchars($array[$idx_max]['id']);
you can extend the code to check multiple ratings at once (make $idx_max an array itself and add more ifs):
$idx_max = array (
'sudhi_rating1' => 0,
'sudhi_rating2' => 0,
/* … */ );
foreach($array as $idx => $item) {
foreach($idx_max as $rating => &$max) {
if($item[$rating] > $array[$max][$rating])
$max = $idx;
}
}
foreach($idx_max as $rating => $max)
echo 'Max value for ', htmlspecialchars($rating), ' has id ', htmlspeciachars($array[$max]['id']);
$max = array_reduce($data, function($a, $b) {
if (is_null($a)) return $b;
return max($a['rating1'],$a['rating2'])>max($b['rating1'],$b['rating2']) ? $a : $b;
});
Result: no Entries $max= NULL otherwise $max['id'] is the id with the max rating
Alternatively this generic code
$max = array_reduce($data, function($a, $b) {
if (is_null($a)) return $b;
return maxRating($a)>maxRating($b) ? $a : $b;
});
function maxRating($row){
return (max(array_intersect_key($row,array_flip(array_filter(array_keys($row),function ($item) { return strstr($item,'rating')!==FALSE;})))));
}
Will find for all ratings of the form rating?
EDIT -- The code was trying to answer Q1 here is the answer for just Q2
$max = array_reduce($data, function($a, $b) {
if (is_null($a)) return $b;
return $a['rating1']>$b['rating1'] ? $a : $b;
});
EDIT2 -- This is a generic solution for any number of rating? columns
$ratingKeys=array_filter(array_keys($data[0]),function ($item) { return strstr($item,'rating')!==FALSE;});
$max = array_reduce($data,function($a,$b) use (&$ratingKeys) {
if (is_null($a)) {
$a=array();
foreach($ratingKeys as $key) {
$a[$key]=$b[$key];
$a[$key.'_id'] = $b['id'];
}
return $a;
}
foreach($ratingKeys as $key) {
if ($a[$key]<$b[$key]) {
$a[$key]=$b[$key];
$a[$key.'_id']=$b['id'];
}
}
return $a;
});
This code results in
array(4) {
["rating1"]=> float(0.0002788640872546)
["rating1_id"]=> int(1380)
["rating2"]=> float(2.2388295595073)
["rating2_id"]=> int(1379)
}
EDIT 3 -- If you change the format of the input array to use id as the array key, you can massively simplify
$max=array_reduce(array_keys($data),function ($a,$b) use (&$data) {
if (is_null($a)) $a=array();
foreach(array_keys($data[$b]) as $item) {
if (!isset($a[$item]) {
$a[$item] = $b;
} else {
if ($data[$a[$item]][$item]) < $data[$b][$item]) $a[$item]=$b;
}
return $a;
}
});
This code results in
array(2) {
["rating1"]=> int(1380)
["rating2"]=> int(1379)
}

Sort an array, placing children beneath parents

So I have an array of items in php, some may be linked to others via a parent_id key. I'm looking to sort this array so that any items whose parent is in this array ends up positioned right below the parent.
example: (actual array has many more keys)
some_array[0]['id'] = 15001;
some_array[0]['parent_id'] = 14899;
some_array[1]['id'] = 14723;
some_array[1]['parent_id'] = 0; //parent_id of 0 means item has no parent of its own
some_array[2]['id'] = 14899;
some_array[2]['parent_id'] = 0;
some_array[3]['id'] = 15000;
some_array[3][parent_id'] = 14723;
I'd like to sort these so they end up in this order:
some_array[0]['id'] = 14723;
some_array[1]['id'] = 15000;
some_array[2]['id'] = 14899;
some_array[3]['id'] = 15001;
ie. items are just below their parents.
Thanks in advance!
My shorter version of mattwang's answer:
/**
* sort parents before children
*
* #param array $objects input objects with attributes 'id' and 'parent'
* #param array $result (optional, reference) internal
* #param integer $parent (optional) internal
* #param integer $depth (optional) internal
* #return array output
*/
function parent_sort(array $objects, array &$result=array(), $parent=0, $depth=0) {
foreach ($objects as $key => $object) {
if ($object->parent == $parent) {
$object->depth = $depth;
array_push($result, $object);
unset($objects[$key]);
parent_sort($objects, $result, $object->id, $depth + 1);
}
}
return $result;
}
Only actual difference is that it sorts an array of objects instead of an array of arrays.
I doubt that you guys are still looking for a real answer to this, but it might help out others with the same problem. Below is a recursive function to resort an array placing children beneath parents.
$initial = array(
array(
'name' => 'People',
'ID' => 2,
'parent' => 0
),
array(
'name' => 'Paul',
'ID' => 4,
'parent' => 2
),
array(
'name' => 'Liz',
'ID' => 5,
'parent' => 2
),
array(
'name' => 'Comus',
'ID' => 6,
'parent' => 3
),
array(
'name' => 'Mai',
'ID' => 7,
'parent' => 2
),
array(
'name' => 'Titus',
'ID' => 8,
'parent' => 3
),
array(
'name' => 'Adult',
'ID' => 9,
'parent' => 6
),
array(
'name' => 'Puppy',
'ID' => 10,
'parent' => 8
),
array(
'name' => 'Programmers',
'ID' => 11,
'parent' => 4
) ,
array(
'name' => 'Animals',
'ID' => 3,
'parent' => 0
)
);
/*---------------------------------
function parentChildSort_r
$idField = The item's ID identifier (required)
$parentField = The item's parent identifier (required)
$els = The array (required)
$parentID = The parent ID for which to sort (internal)
$result = The result set (internal)
$depth = The depth (internal)
----------------------------------*/
function parentChildSort_r($idField, $parentField, $els, $parentID = 0, &$result = array(), &$depth = 0){
foreach ($els as $key => $value):
if ($value[$parentField] == $parentID){
$value['depth'] = $depth;
array_push($result, $value);
unset($els[$key]);
$oldParent = $parentID;
$parentID = $value[$idField];
$depth++;
parentChildSort_r($idField,$parentField, $els, $parentID, $result, $depth);
$parentID = $oldParent;
$depth--;
}
endforeach;
return $result;
}
$result = parentChildSort_r('ID','parent',$initial);
print '<pre>';
print_r($result);
print '</pre>';
It's a wind down method that removes elements from the original array and places them into result set in the proper order. I made it somewhat generic for you, so it just needs you to tell it what your 'ID' field and 'parent' fields are called. Top level items are required to have a parent_id (however you name it) of 0.
You can use usort to sort by a user defined function:
function cmp($a, $b)
{
if ( $a['id'] == $b['id'] ) {
return 0;
} else if ( $a['parent_id'] ) {
if ( $a['parent_id'] == $b['parent_id'] ) {
return ( $a['id'] < $b['id'] ? -1 : 1 );
} else {
return ( $a['parent_id'] >= $b['id'] ? 1 : -1 );
}
} else if ( $b['parent_id'] ) {
return ( $b['parent_id'] >= $a['id'] ? -1 : 1);
} else {
return ( $a['id'] < $b['id'] ? -1 : 1 );
}
}
usort($some_array, "cmp");
Note: this will only work with a tree that is one level deep (meaning no children of children). For more complex trees you probably want to sort the data into a graph and then flatten it.
Edit: fixed to edit a case where $b has a parent but $a does not.
Just use usort() function and compare two different elements of the 'big array' in a way you need. This becomes then a question about 'how do I really decide which element is before which element?'.
The simple usort won't work if you want to support more than one layer of children. There's simply no way to know how two arbitrary elements compare without other information.
I didn't think about it much, so perhaps it doesn't work. But here's a sorting class:
class TopSort
{
private $sorted, $unsorted;
private $history;
public function sort(array $unsorted)
{
$this->sorted = array();
$this->unsorted = $unsorted;
$this->history = array();
usort($this->unsorted, function($a, $b)
{
return $b['id'] - $a['id'];
});
foreach ($this->unsorted as $i => $a)
if ($a['parent_id'] == 0) $this->visit($i);
return array_reverse($this->sorted);
}
private function visit($i)
{
if (!array_key_exists($i, $this->history))
{
$this->history[$i] = true;
foreach ($this->unsorted as $j => $a)
if ($a['parent_id'] == $this->unsorted[$i]['id']) $this->visit($j);
$this->sorted[] = $this->unsorted[$i];
}
}
}
$sorter = new TopSort();
$some_array = $sorter->sort($some_array);
The idea here is to first sort in reverse by id. Then build up a new array by inserting the deepest elements (those with no children) first. Since we initially sorted the array by reverse id, it should mean the entire thing is upside down. After reversing the array, it should be exactly like you want. (Of course one could unshift items onto the array to avoid the reverse operation, but that might be slower...)
And this is very unoptimized as it iterates over the entire array many, many times. With a little rework, it wouldn't need to do that.
Here's an alternative class that is more optimized:
class TopSort
{
private $sorted;
public function sort(array $nodes)
{
$this->sorted = array();
# sort by id
usort($nodes, function($a, $b) {
return $a['id'] - $b['id'];
});
# build tree
$p = array(0 => array());
foreach($nodes as $n)
{
$pid = $n['parent_id'];
$id = $n['id'];
if (!isset($p[$pid]))
$p[$pid] = array('child' => array());
if (isset($p[$id]))
$child = &$p[$id]['child'];
else
$child = array();
$p[$id] = $n;
$p[$id]['child'] = &$child;
unset($child);
$p[$pid]['child'][] = &$p[$id];
}
$nodes = $p['0']['child'];
unset($p);
# flatten array
foreach ($nodes as $node)
$this->flatten($node);
return $this->sorted;
}
private function flatten(array $node)
{
$children = $node['child'];
unset($node['child']);
$this->sorted[] = $node;
foreach ($children as $node)
$this->flatten($node);
}
}
$sorter = new TopSort();
$sorted = $sorter->sort($some_array);
It's a three step approach:
Sort by id (usort)
Build nested array structure.
Flatten array in pre-order.
By virtue of presorting by id, each group of children should be sorted correctly.

Create array of hierarchical directories in PHP

I have the following code (I know that this code is not optimized but it's not for discussion):
function select_categories($cat_id)
{
$this->db = ORM::factory('category')
->where('parent', '=', $cat_id)
->find_all();
foreach ($this->db as $num => $category)
{
if($category->parent == 0)
{
$this->tmp[$category->parent][$category->id] = array();
}
else {
$this->tmp[$category->parent][$category->id] = array();
}
$this->select_categories($category->id);
}
return $this->tmp;
}
Function returns this array:
array(3) (
0 => array(2) (
1 => array(0)
2 => array(0)
)
2 => array(1) (
3 => array(0)
)
3 => array(2) (
4 => array(0)
5 => array(0)
)
)
But how should I change the code
else {
$this->tmp[$category->parent][$category->id] = array();
// ^^^^^^^^^^^^^^^^^^^^^^ (this bit)
}
To merge array[3] to array[2][3] for example (because array[3] is a subdirectory of array[2] and array[2] is a subdirectory of array[0][2]), so, I need to make this (when I don't know the level of subdirectories):
array (
0 => array (
1 => array
2 => array (
3 => array (
4 => array
5 => array
)
)
)
)
A long time ago I wrote some code to do this in PHP. It takes a list of entities (in your case, categories) and returns a structure where those entities are arranged in a tree. However, it uses associative arrays instead of objects; it assumes that the “parent” ID is stored in one of the associative array entries. I’m sure that you can adapt this to your needs.
function make_tree_structure ($nontree, $parent_field)
{
$parent_to_children = array();
$root_elements = array();
foreach ($nontree as $id => $elem) {
if (array_key_exists ($elem[$parent_field], $nontree))
$parent_to_children [ $elem[$parent_field] ][] = $id;
else
$root_elements[] = $id;
}
$result = array();
while (count ($root_elements)) {
$id = array_shift ($root_elements);
$result [ $id ] = make_tree_structure_recurse ($id, $parent_to_children, $nontree);
}
return $result;
}
function make_tree_structure_recurse ($id, &$parent_to_children, &$nontree)
{
$ret = $nontree [ $id ];
if (array_key_exists ($id, $parent_to_children)) {
$list_of_children = $parent_to_children [ $id ];
unset ($parent_to_children[$id]);
while (count ($list_of_children)) {
$child = array_shift ($list_of_children);
$ret['children'][$child] = make_tree_structure_recurse ($child, $parent_to_children, $nontree);
}
}
return $ret;
}
To see what this does, first try running it on a structure like this:
var $data = array (
0 => array('Name' => 'Kenny'),
1 => array('Name' => 'Lilo', 'Parent' => 0),
2 => array('Name' => 'Adrian', 'Parent' => 1)
3 => array('Name' => 'Mark', 'Parent' => 1)
);
var $tree = make_tree_structure($data, 'Parent');
If I’m not mistaken, you should get something like this out: (the “Parent” key would still be there, but I’m leaving it out for clarity)
array (
0 => array('Name' => 'Kenny', 'children' => array (
1 => array('Name' => 'Lilo', 'children' => array (
2 => array('Name' => 'Adrian')
3 => array('Name' => 'Mark')
)
)
)
Examine the code to see how it does this. Once you understand how this works, you can tweak it to work with your particular data.
Assuming you dont want any data/children tags in your array:
foreach ($this->db as $num => $category)
{
// save the data to the array
$this->tmp[$category->id] = array();
// save a reference to this item in the parent array
$this->tmp[$category->parent][$category->id] = &$this->tmp[$category->id];
$this->select_categories($category->id);
}
// the tree is at index $cat_id
return $this->tmp[$cat_id];
If you just need to retrieve the full tree out of the database, you can even simplify your query (get all records at once) and remove the recursive call in this function. You will need an extra check that will only set the $this->tmp[$catagory->id] when it does not exist and else it should merge the data with the existing data.

Replace keys in an array based on another lookup/mapping array

I have an associative array in the form key => value where key is a numerical value, however it is not a sequential numerical value. The key is actually an ID number and the value is a count. This is fine for most instances, however I want a function that gets the human-readable name of the array and uses that for the key, without changing the value.
I didn't see a function that does this, but I'm assuming I need to provide the old key and new key (both of which I have) and transform the array. Is there an efficient way of doing this?
$arr[$newkey] = $arr[$oldkey];
unset($arr[$oldkey]);
The way you would do this and preserve the ordering of the array is by putting the array keys into a separate array, find and replace the key in that array and then combine it back with the values.
Here is a function that does just that:
function change_key( $array, $old_key, $new_key ) {
if( ! array_key_exists( $old_key, $array ) )
return $array;
$keys = array_keys( $array );
$keys[ array_search( $old_key, $keys ) ] = $new_key;
return array_combine( $keys, $array );
}
if your array is built from a database query, you can change the key directly from the mysql statement:
instead of
"select ´id´ from ´tablename´..."
use something like:
"select ´id´ **as NEWNAME** from ´tablename´..."
The answer from KernelM is nice, but in order to avoid the issue raised by Greg in the comment (conflicting keys), using a new array would be safer
$newarr[$newkey] = $oldarr[$oldkey];
$oldarr=$newarr;
unset($newarr);
$array = [
'old1' => 1
'old2' => 2
];
$renameMap = [
'old1' => 'new1',
'old2' => 'new2'
];
$array = array_combine(array_map(function($el) use ($renameMap) {
return $renameMap[$el];
}, array_keys($array)), array_values($array));
/*
$array = [
'new1' => 1
'new2' => 2
];
*/
You could use a second associative array that maps human readable names to the id's. That would also provide a Many to 1 relationship. Then do something like this:
echo 'Widgets: ' . $data[$humanreadbleMapping['Widgets']];
If you want also the position of the new array key to be the same as the old one you can do this:
function change_array_key( $array, $old_key, $new_key) {
if(!is_array($array)){ print 'You must enter a array as a haystack!'; exit; }
if(!array_key_exists($old_key, $array)){
return $array;
}
$key_pos = array_search($old_key, array_keys($array));
$arr_before = array_slice($array, 0, $key_pos);
$arr_after = array_slice($array, $key_pos + 1);
$arr_renamed = array($new_key => $array[$old_key]);
return $arr_before + $arr_renamed + $arr_after;
}
Simple benchmark comparison of both solution.
Solution 1 Copy and remove (order lost, but way faster) https://stackoverflow.com/a/240676/1617857
<?php
$array = ['test' => 'value', ['etc...']];
$array['test2'] = $array['test'];
unset($array['test']);
Solution 2 Rename the key https://stackoverflow.com/a/21299719/1617857
<?php
$array = ['test' => 'value', ['etc...']];
$keys = array_keys( $array );
$keys[array_search('test', $keys, true)] = 'test2';
array_combine( $keys, $array );
Benchmark:
<?php
$array = ['test' => 'value', ['etc...']];
for ($i =0; $i < 100000000; $i++){
// Solution 1
}
for ($i =0; $i < 100000000; $i++){
// Solution 2
}
Results:
php solution1.php 6.33s user 0.02s system 99% cpu 6.356 total
php solution1.php 6.37s user 0.01s system 99% cpu 6.390 total
php solution2.php 12.14s user 0.01s system 99% cpu 12.164 total
php solution2.php 12.57s user 0.03s system 99% cpu 12.612 total
If your array is recursive you can use this function:
test this data:
$datos = array
(
'0' => array
(
'no' => 1,
'id_maquina' => 1,
'id_transaccion' => 1276316093,
'ultimo_cambio' => 'asdfsaf',
'fecha_ultimo_mantenimiento' => 1275804000,
'mecanico_ultimo_mantenimiento' =>'asdfas',
'fecha_ultima_reparacion' => 1275804000,
'mecanico_ultima_reparacion' => 'sadfasf',
'fecha_siguiente_mantenimiento' => 1275804000,
'fecha_ultima_falla' => 0,
'total_fallas' => 0,
),
'1' => array
(
'no' => 2,
'id_maquina' => 2,
'id_transaccion' => 1276494575,
'ultimo_cambio' => 'xx',
'fecha_ultimo_mantenimiento' => 1275372000,
'mecanico_ultimo_mantenimiento' => 'xx',
'fecha_ultima_reparacion' => 1275458400,
'mecanico_ultima_reparacion' => 'xx',
'fecha_siguiente_mantenimiento' => 1275372000,
'fecha_ultima_falla' => 0,
'total_fallas' => 0,
)
);
here is the function:
function changekeyname($array, $newkey, $oldkey)
{
foreach ($array as $key => $value)
{
if (is_array($value))
$array[$key] = changekeyname($value,$newkey,$oldkey);
else
{
$array[$newkey] = $array[$oldkey];
}
}
unset($array[$oldkey]);
return $array;
}
I like KernelM's solution, but I needed something that would handle potential key conflicts (where a new key may match an existing key). Here is what I came up with:
function swapKeys( &$arr, $origKey, $newKey, &$pendingKeys ) {
if( !isset( $arr[$newKey] ) ) {
$arr[$newKey] = $arr[$origKey];
unset( $arr[$origKey] );
if( isset( $pendingKeys[$origKey] ) ) {
// recursion to handle conflicting keys with conflicting keys
swapKeys( $arr, $pendingKeys[$origKey], $origKey, $pendingKeys );
unset( $pendingKeys[$origKey] );
}
} elseif( $newKey != $origKey ) {
$pendingKeys[$newKey] = $origKey;
}
}
You can then cycle through an array like this:
$myArray = array( '1970-01-01 00:00:01', '1970-01-01 00:01:00' );
$pendingKeys = array();
foreach( $myArray as $key => $myArrayValue ) {
// NOTE: strtotime( '1970-01-01 00:00:01' ) = 1 (a conflicting key)
$timestamp = strtotime( $myArrayValue );
swapKeys( $myArray, $key, $timestamp, $pendingKeys );
}
// RESULT: $myArray == array( 1=>'1970-01-01 00:00:01', 60=>'1970-01-01 00:01:00' )
Here is a helper function to achieve that:
/**
* Helper function to rename array keys.
*/
function _rename_arr_key($oldkey, $newkey, array &$arr) {
if (array_key_exists($oldkey, $arr)) {
$arr[$newkey] = $arr[$oldkey];
unset($arr[$oldkey]);
return TRUE;
} else {
return FALSE;
}
}
pretty based on #KernelM answer.
Usage:
_rename_arr_key('oldkey', 'newkey', $my_array);
It will return true on successful rename, otherwise false.
this code will help to change the oldkey to new one
$i = 0;
$keys_array=array("0"=>"one","1"=>"two");
$keys = array_keys($keys_array);
for($i=0;$i<count($keys);$i++) {
$keys_array[$keys_array[$i]]=$keys_array[$i];
unset($keys_array[$i]);
}
print_r($keys_array);
display like
$keys_array=array("one"=>"one","two"=>"two");
Easy stuff:
this function will accept the target $hash and $replacements is also a hash containing newkey=>oldkey associations.
This function will preserve original order, but could be problematic for very large (like above 10k records) arrays regarding performance & memory.
function keyRename(array $hash, array $replacements) {
$new=array();
foreach($hash as $k=>$v)
{
if($ok=array_search($k,$replacements))
$k=$ok;
$new[$k]=$v;
}
return $new;
}
this alternative function would do the same, with far better performance & memory usage, at the cost of losing original order (which should not be a problem since it is hashtable!)
function keyRename(array $hash, array $replacements) {
foreach($hash as $k=>$v)
if($ok=array_search($k,$replacements))
{
$hash[$ok]=$v;
unset($hash[$k]);
}
return $hash;
}
This page has been peppered with a wide interpretation of what is required because there is no minimal, verifiable example in the question body. Some answers are merely trying to solve the "title" without bothering to understand the question requirements.
The key is actually an ID number and the value is a count. This is
fine for most instances, however I want a function that gets the
human-readable name of the array and uses that for the key, without
changing the value.
PHP keys cannot be changed but they can be replaced -- this is why so many answers are advising the use of array_search() (a relatively poor performer) and unset().
Ultimately, you want to create a new array with names as keys relating to the original count. This is most efficiently done via a lookup array because searching for keys will always outperform searching for values.
Code: (Demo)
$idCounts = [
3 => 15,
7 => 12,
8 => 10,
9 => 4
];
$idNames = [
1 => 'Steve',
2 => 'Georgia',
3 => 'Elon',
4 => 'Fiona',
5 => 'Tim',
6 => 'Petra',
7 => 'Quentin',
8 => 'Raymond',
9 => 'Barb'
];
$result = [];
foreach ($idCounts as $id => $count) {
if (isset($idNames[$id])) {
$result[$idNames[$id]] = $count;
}
}
var_export($result);
Output:
array (
'Elon' => 15,
'Quentin' => 12,
'Raymond' => 10,
'Barb' => 4,
)
This technique maintains the original array order (in case the sorting matters), doesn't do any unnecessary iterating, and will be very swift because of isset().
If you want to replace several keys at once (preserving order):
/**
* Rename keys of an array
* #param array $array (asoc)
* #param array $replacement_keys (indexed)
* #return array
*/
function rename_keys($array, $replacement_keys) {
return array_combine($replacement_keys, array_values($array));
}
Usage:
$myarr = array("a" => 22, "b" => 144, "c" => 43);
$newkeys = array("x","y","z");
print_r(rename_keys($myarr, $newkeys));
//must return: array("x" => 22, "y" => 144, "z" => 43);
You can use this function based on array_walk:
function mapToIDs($array, $id_field_name = 'id')
{
$result = [];
array_walk($array,
function(&$value, $key) use (&$result, $id_field_name)
{
$result[$value[$id_field_name]] = $value;
}
);
return $result;
}
$arr = [0 => ['id' => 'one', 'fruit' => 'apple'], 1 => ['id' => 'two', 'fruit' => 'banana']];
print_r($arr);
print_r(mapToIDs($arr));
It gives:
Array(
[0] => Array(
[id] => one
[fruit] => apple
)
[1] => Array(
[id] => two
[fruit] => banana
)
)
Array(
[one] => Array(
[id] => one
[fruit] => apple
)
[two] => Array(
[id] => two
[fruit] => banana
)
)
This basic function handles swapping array keys and keeping the array in the original order...
public function keySwap(array $resource, array $keys)
{
$newResource = [];
foreach($resource as $k => $r){
if(array_key_exists($k,$keys)){
$newResource[$keys[$k]] = $r;
}else{
$newResource[$k] = $r;
}
}
return $newResource;
}
You could then loop through and swap all 'a' keys with 'z' for example...
$inputs = [
0 => ['a'=>'1','b'=>'2'],
1 => ['a'=>'3','b'=>'4']
]
$keySwap = ['a'=>'z'];
foreach($inputs as $k=>$i){
$inputs[$k] = $this->keySwap($i,$keySwap);
}
This function will rename an array key, keeping its position, by combining with index searching.
function renameArrKey($arr, $oldKey, $newKey){
if(!isset($arr[$oldKey])) return $arr; // Failsafe
$keys = array_keys($arr);
$keys[array_search($oldKey, $keys)] = $newKey;
$newArr = array_combine($keys, $arr);
return $newArr;
}
Usage:
$arr = renameArrKey($arr, 'old_key', 'new_key');
this works for renaming the first key:
$a = ['catine' => 'cat', 'canine' => 'dog'];
$tmpa['feline'] = $a['catine'];
unset($a['catine']);
$a = $tmpa + $a;
then, print_r($a) renders a repaired in-order array:
Array
(
[feline] => cat
[canine] => dog
)
this works for renaming an arbitrary key:
$a = ['canine' => 'dog', 'catine' => 'cat', 'porcine' => 'pig']
$af = array_flip($a)
$af['cat'] = 'feline';
$a = array_flip($af)
print_r($a)
Array
(
[canine] => dog
[feline] => cat
[porcine] => pig
)
a generalized function:
function renameKey($oldkey, $newkey, $array) {
$val = $array[$oldkey];
$tmp_A = array_flip($array);
$tmp_A[$val] = $newkey;
return array_flip($tmp_A);
}
There is an alternative way to change the key of an array element when working with a full array - without changing the order of the array.
It's simply to copy the array into a new array.
For instance, I was working with a mixed, multi-dimensional array that contained indexed and associative keys - and I wanted to replace the integer keys with their values, without breaking the order.
I did so by switching key/value for all numeric array entries - here: ['0'=>'foo']. Note that the order is intact.
<?php
$arr = [
'foo',
'bar'=>'alfa',
'baz'=>['a'=>'hello', 'b'=>'world'],
];
foreach($arr as $k=>$v) {
$kk = is_numeric($k) ? $v : $k;
$vv = is_numeric($k) ? null : $v;
$arr2[$kk] = $vv;
}
print_r($arr2);
Output:
Array (
[foo] =>
[bar] => alfa
[baz] => Array (
[a] => hello
[b] => world
)
)
best way is using reference, and not using unset (which make another step to clean memory)
$tab = ['two' => [] ];
solution:
$tab['newname'] = & $tab['two'];
you have one original and one reference with new name.
or if you don't want have two names in one value is good make another tab and foreach on reference
foreach($tab as $key=> & $value) {
if($key=='two') {
$newtab["newname"] = & $tab[$key];
} else {
$newtab[$key] = & $tab[$key];
}
}
Iterration is better on keys than clone all array, and cleaning old array if you have long data like 100 rows +++ etc..
One which preservers ordering that's simple to understand:
function rename_array_key(array $array, $old_key, $new_key) {
if (!array_key_exists($old_key, $array)) {
return $array;
}
$new_array = [];
foreach ($array as $key => $value) {
$new_key = $old_key === $key
? $new_key
: $key;
$new_array[$new_key] = $value;
}
return $new_array;
}
Here is an experiment (test)
Initial array (keys like 0,1,2)
$some_array[] = '6110';//
$some_array[] = '6111';//
$some_array[] = '6210';//
I must change key names to for example human_readable15, human_readable16, human_readable17
Something similar as already posted. During each loop i set necessary key name and remove corresponding key from the initial array.
For example, i inserted into mysql $some_array got lastInsertId and i need to send key-value pair back to jquery.
$first_id_of_inserted = 7;//lastInsertId
$last_loop_for_some_array = count($some_array);
for ($current_loop = 0; $current_loop < $last_loop_for_some_array ; $current_loop++) {
$some_array['human_readable'.($first_id_of_inserted + $current_loop)] = $some_array[$current_loop];//add new key for intial array
unset( $some_array[$current_loop] );//remove already renamed key from array
}
And here is the new array with renamed keys
echo '<pre>', print_r($some_array, true), '</pre>$some_array in '. basename(__FILE__, '.php'). '.php <br/>';
If instead of human_readable15, human_readable16, human_readable17 need something other. Then could create something like this
$arr_with_key_names[] = 'human_readable';
$arr_with_key_names[] = 'something_another';
$arr_with_key_names[] = 'and_something_else';
for ($current_loop = 0; $current_loop < $last_loop_for_some_array ; $current_loop++) {
$some_array[$arr_with_key_names[$current_loop]] = $some_array[$current_loop];//add new key for intial array
unset( $some_array[$current_loop] );//remove already renamed key from array
}
Hmm, I'm not test before, but I think this code working
function replace_array_key($data) {
$mapping = [
'old_key_1' => 'new_key_1',
'old_key_2' => 'new_key_2',
];
$data = json_encode($data);
foreach ($mapping as $needed => $replace) {
$data = str_replace('"'.$needed.'":', '"'.$replace.'":', $data);
}
return json_decode($data, true);
}
You can write simple function that applies the callback to the keys of the given array. Similar to array_map
<?php
function array_map_keys(callable $callback, array $array) {
return array_merge([], ...array_map(
function ($key, $value) use ($callback) { return [$callback($key) => $value]; },
array_keys($array),
$array
));
}
$array = ['a' => 1, 'b' => 'test', 'c' => ['x' => 1, 'y' => 2]];
$newArray = array_map_keys(function($key) { return 'new' . ucfirst($key); }, $array);
echo json_encode($array); // {"a":1,"b":"test","c":{"x":1,"y":2}}
echo json_encode($newArray); // {"newA":1,"newB":"test","newC":{"x":1,"y":2}}
Here is a gist https://gist.github.com/vardius/650367e15abfb58bcd72ca47eff096ca#file-array_map_keys-php.

Categories