PHP optimize nested loop - php

I have an array of matches, each match has an ID and an array of users (from 2 to 4), each user is uniquely identified by it's own user_id. Example of the array:
array (size=2)
0 =>
array (size=2)
'id' => int 11268
'users' =>
array (size=2)
0 =>
array (size=1)
'user_id' => int 960781
1 =>
array (size=1)
'user_id' => int 960786
1 =>
array (size=2)
'id' => int 11267
'users' =>
array (size=2)
0 =>
array (size=1)
'user_id' => int 960783
1 =>
array (size=1)
'user_id' => int 902177
Now I want to add users details to the above array, so I do a query on the DB and I have this: (the row with id=n contains the details of user with user_id=n)
if ($res = $stmt->get_result()) { // it gets user details
while($row=$res->fetch_assoc()) {
foreach ($matches as &$match) {
foreach ($match['users'] as &$user) {
if($user['user_id']==$row['id']) {
$user['details']=$row;
}
}
}
}
}
This is working fine, but it's not the best way, because for each row I walk all the array. Do you have any idea how can I optimize this?
Thanks very much

You can simplify the problem by indexing the user arrays by userid. The code becomes a bit more complex now, but the computational complexity class is lower. If the new solution is really faster depends on the actual sizes of the individual arrays, so you'd have to measure both solutions with real-life-production-data to see what the fastest solution actually is.
<?php
function index_array_by($array, $key) {
$result = array();
foreach($array as &$value) {
$new_key = $value[$key];
$result[$new_key] = &$value;
}
return $result;
}
foreach($matches as &$match) {
$match['users'] = index_array_by($match['users'], "user_id");
}
if ($res = $stmt->get_result()) { // it gets user details
while($row=$res->fetch_assoc()) {
foreach ($matches as &$match) {
$user_id = $row['id'];
$match['users'][$user_id]['details'] = $row;
}
}
}
?>

I found this solution that require to scan the array only once, but I guess it uses more memory since I'm saving the rows into an array:
//Save the results in an array to later add to the matches array
if ($res = $stmt->get_result()) { //second get: it gets user details
while($row=$res->fetch_assoc()) {
$rows[$row['id']]=$row;
}
}
mysqli_free_result($res);
//Add the user details to the matches array
foreach ($matches as &$match) {
foreach ($match['users'] as &$user) {
$user['details']=$rows[$user['user_id']];
}
}

Related

How to check if arrays values match while preserving its order in PHP

What I'm trying to do is preserve the order of an array, while determining if each of the numbers match numbers that were pulled from the database. What I have is an array of numbers ($ids), and a non empty array of rows ($rows)
$images = array();
$ids = array(
0 => '41',
1 => '42',
2 => '43',
3 => '44'
);
// database example
$rows = array(
0 => array(
'name' => 'John Doe',
'id' => '42'
),
1 => array(
'name' => 'Jane Doe',
'id' => '43'
),
);
$i = 0;
foreach ($rows as $row) {
// This works, but it doesn't preserve the order of the $ids array
if (in_array($row['id'], $ids[$i])) {
// add $id to a new array
// or use the same $id array
$images[$i]['name'] = $row['name'];
$images[$i]['id'] = $row['id'];
$i++;
}
}
Any help would be appreciated.
Since id is unique in the rows from your database, you can index the $rows array by id to make it easier to look up values there. You can do this with array_column like this:
$rows = array_column($rows, null, 'id');
Or add them to $rows using id as index as you fetch them from the db like this:
while ($row = //whatever query result/prepared statement fetch you're using) {
$rows[$row['id']] = $row;
}
Then, instead of iterating $rows, you can iterate $ids. This will ensure that the resulting $images array will be in the same order as $ids.
foreach ($ids as $id) {
if (isset($rows[$id])) {
$images[] = [
'name' => $rows[$id]['name'],
'id' => $rows[$id]['id']
];
}
}
Another thing that might make this easier (unless you're using the non-matching rows for something else), would be to use the $ids array as a parameter to a WHERE id IN clause in the query that's selecting $rows, so you won't have to do that filtering in PHP. Here's a Q&A that shows how to do that with PDO, for example: PHP - Using PDO with IN clause array.
You could iterate over ids array instead. It will be something like this:
foreach($ids as $id) {
$data = find($id, $rows);
if ($data === null) {
continue;
}
$images[] = [
'name' => $data['name'],
'id' => $data['id']
];
}
function find($id, $rows) {
foreach($rows as $row) {
if($row['id'] == $id) {
return $row;
}
}
return null;
}
Totally agree with #Don't Panic, you should fetch only the data you'll use if possible.

Group subarrays by one column, make comma-separated values from other column within groups

I have a array that looks like this:
$array = [
["444", "0081"],
["449", "0081"],
["451", "0081"],
["455", "2100"],
["469", "2100"]
];
I need to group as a new array that looks like:
array (
0 =>
array (
0 => '444,449,451',
1 => '0081',
),
1 =>
array (
0 => '455,469',
1 => '2100',
),
)
I'd tried many scripts, but with no success.
function _group_by($array, $key) {
$return = array();
foreach($array as $val) {
$return[$val[$key]][] = $val;
}
return $return;
}
$newArray = _group_by($array, 1); // (NO SUCCESS)
There should be more elegant solutions, but simplest one I can think of would be this.
// The data you have pasted in the question
$data = [];
$groups = [];
// Go through the entire array $data
foreach($data as $item){
// If the key doesn't exist in the new array yet, add it
if(!array_key_exists($item[1], $groups)){
$groups[$item[1]] = [];
}
// Add the value to the array
$groups[$item[1]][] = $item[0];
}
// Create an array for the data with the structure you requested
$structured = [];
foreach($groups as $group => $values){
// With the array built in the last loop, implode it with a comma
// Also add the 'key' from the last array to it ($group)
$structured[] = [implode(',', $values), $group];
}
I haven't tested this but something similar should do the trick. This simply goes through the given array and collects all entries in a structurized manner (so $groups variable will contain an array entry for each group sharing a key, and the key will correspond to the 2nd item in each item within the given array). From there it's just about restructuring it to get the format you have requested.
http://php.net/manual/en/control-structures.foreach.php
Writing two loops is too much effort for this task. Use isset() with temporary keys applied to your output array as you iterate. When finished grouping the data, reindex the output with array_values().
Code (Demo)
$array = [
["444", "0081"],
["449", "0081"],
["451", "0081"],
["455", "2100"],
["469", "2100"]
];
foreach ($array as $row) {
if (!isset($result[$row[1]])) {
$result[$row[1]] = $row; // first occurrence of group, save whole row
} else {
$result[$row[1]][0] .= ',' . $row[0]; // not first occurrence, concat first element in group
}
}
var_export(array_values($result));
Or avoid the temporary associative arrays in the result array by using an array of references. (Demo)
$result = [];
foreach ($array as $row) {
if (!isset($ref[$row[1]])) {
$ref[$row[1]] = $row;
$result[] = &$ref[$row[1]];
} else {
$ref[$row[1]][0] .= ',' . $row[0];
}
}
var_export($result);
Or use array_reduce() to enjoy a functional-style technique. (Demo)
var_export(
array_values(
array_reduce(
$array,
function($result, $row) {
if (!isset($result[$row[1]])) {
$result[$row[1]] = $row;
} else {
$result[$row[1]][0] .= ',' . $row[0];
}
return $result;
}
)
)
);
All will output:
array (
0 =>
array (
0 => '444,449,451',
1 => '0081',
),
1 =>
array (
0 => '455,469',
1 => '2100',
),
)

Get key of a 3 dimensional array

Below is dump of how my array looks like. There is inner array called officers and I would want to loop through it and check if there is officer of a specific name and if so I would want to get the index key of the outer array.
'edges' =>
array (size=59)
0 =>
array (size=3)
'source' => int 0
'target' => int 12
'officers' =>
array (size=1)
0 => string 'PARKER, Thomas, Sir' (length=19)
1 =>
array (size=3)
'source' => int 0
'target' => int 19
'officers' =>
array (size=1)
0 => string 'STEVENS, Anne' (length=13)
So if I checked for STEVENS, Anne I would want to get key 1.
Here is code I found in a different question it works with 2d arrays but not with 3d array.
function array_search_inner ($array, $attr, $val, $strict = FALSE) {
// Error is input array is not an array
if (!is_array($array)) return FALSE;
// Loop the array
foreach ($array as $key => $inner) {
// Error if inner item is not an array (you may want to remove this line)
if (!is_array($inner)) return FALSE;
// Skip entries where search key is not present
if (!isset($inner[$attr])) continue;
if ($strict) {
// Strict typing
if ($inner[$attr] === $val) return $key;
} else {
// Loose typing
if ($inner[$attr] == $val) return $key;
}
}
// We didn't find it
return NULL;
}
Since there can be several index keys that fit the condition, it is reasonable to implement the function as a generator:
function getOfficerIndexKey($data, $officerName) {
foreach ($data['edges'] as $key => $value) {
in_array($officerName, $value['officers']) && (yield $key);
}
}
Now you can iterate over all found values:
foreach (getOfficerIndexKey($data, 'STEVENS, Anne') as $indexKey) {
// Do something
}
As well as just get the first found one:
getOfficerIndexKey($data, 'STEVENS, Anne')->current();

path to array value deep multidimensional array

I have an array where I need the path (keys) to a given value. I've written a function for it but I can't get it working, the array can have an infinite depth to be controlled in another function, I haven't decided on a limit yet but the depth is variable, it actually works up to a point but the depth may be 30-40-50 deep so I need it to work that way, this function is really just for value identification, the numbers are templates, the tags are tags in the templates, each tag has a single template associated with it, all template names are unique and tags names aren't because they are only associated with a single template, id values are separated by a dash (illegal in template and tag ids), where template ids are only numeric and tag ids are wrapped with squigglies and a dollar sign like so {$TAG}, don't have to worry about duplicate values because infinite loops are forbidden (template can't link to itself or a template that links to itself) plus ids are given only to templates where template names are unique. The id of the template named 4 in the given array would be 0-{$CONTENT}-2-{$PARAGRAPH}-4, where I lose it is my function won't go beyond this depth, thanks for the help
array (size=1)
0 =>
array (size=4)
'{$TITLE}' => null
'{$NAME}' => null
'{$FRIENDS}' =>
array (size=1)
1 =>
array (size=2)
'{$friend}' => null
'{$friends[$i]}' => null
'{$CONTENT}' =>
array (size=1)
2 =>
'{$HEADING}' =>
array (size=1)
3 =>
array (size=0)
empty
'{$PARAGRAPH}' =>
array (size=1)
4 =>
array (size=1)
'{$AnotherParagraph}' => null
here is my function, I've added the depth variable just for testing purposes, $this->family is the array given above, calling the function: $id = $this->get_id(4);
public function get_id($member, $family=null, $id=null, $depth=0) {
if (empty($this->family)) {
return false;
}
if (is_null($family)) {
$family = $this->family;
}
foreach ($family as $parent => $tag_child) {
if ($member === $parent) {
return $member;
}
foreach ($tag_child as $tag => $child) {
if (is_null($child) || empty($child)) {
continue;
}
$childkey = key($child);
if ($member === $childkey) {
$id .= '-'.$parent.'-'.$tag.'-'.$member;
$id = ltrim($id, '-');
return $id;
}
$family = $child;
if (!is_null($id) && !empty($id)) {
$id_array = explode('-', $id);
foreach ($id_array as $id_value) {
if ($id_value !== $childkey) {
$new_id_array[] = $id_value;
}else{
break;
}
}
$id = implode('-', $new_id_array);
}
if ($parent === 0) {
$id = $parent.'-'.$tag.'-'.$childkey;
}else{
$id .= '-'.$tag.'-'.$childkey;
}
$id = ltrim($id, '-');
$depth++;
$id = $this->get_id($member, $family, $id, $depth);
}
}
}
Use a recursive function. Here's one I wrote for this EXACT purpose.
public function recurseArray($array, $builtKey = "") {
$values = array();
foreach ($array as $key => $value) {
if (is_array($value)) {
if (!empty($builtKey)) {
$values = array_merge($values, recurseArray($value, $builtKey.".".$key));
} else {
$values = array_merge($values, recurseArray($value, $key));
}
} else {
if (!empty($builtKey)) {
$values[$builtKey.".".$key] = $value;
} else {
$values[$key] = $value;
}
}
}
return $values;
}
This results in a flat array that transforms this:
array(
"key" => array(
"of" => array(
"many" => array(
"depths" => "value"
)
)
),
"key2" => "value"
);
Into this:
array(
"key.of.many.depths" => "value",
"key2" => "value"
);

Loop through a multi dimensional array checking a specific key for repeat values in PHP

I have a multi dimensional array that I have got from a database and I want to check this array for duplicate data and store it in another array of duplicates. my code is as follows
//create temp array
$tmp = array();
foreach ($matchingarray as $nameKey => $match) {
// loop through and stoe the contents of that array to another so i can compare
$tmp[] = $match;
}
// create an array to store duplicates
$duplicatesArray = array();
// if the temp array is not empty then loop through both arrays
if (! empty($tmp)) {
foreach ($tmp as $key => $tmpvalue) {
foreach ($matchingarray as $key => $match) {
// if a key name is the same in both arrays then add it tothe duplicates array
if ($tmpvalue['name'] == $match['name']) {
$duplicatesArray = $match;
}
}
}
}
//count how many are duplicates
$dups = count($duplicatesArray);
What I would like to know is this the right logic?
I will take where Igoel left off
there is 1 error and also 1 suggest that i will make.
Error:
you cannot reuse $key twice in the foreach because they will override.
Suggestion as what Igoel stated: your best bet for duplicate effectively is to use sql. SQL is faster at processing than looping through arrays. Don't forget you need to load the data into memory and thats costly.
Try this way
<?php
static $cnt = array();
$min = 1;
$coll = array(
'dep1' => array(
'fy' => array('john', 'johnny', 'victor'),
'sy' => array('david', 'arthur'),
'ty' => array('sam', 'joe', 'victor')
),
'dep2' => array(
'fy' => array('natalie', 'linda', 'molly'),
'sy' => array('katie', 'helen', 'sam', 'ravi', 'vipul'),
'ty' => array('sharon', 'julia', 'maddy')
)
);
function recursive_search(&$v, $k){
global $cnt;
$cnt[] = $v;
}
array_walk_recursive($coll, 'recursive_search');
$newNumbers = array_filter(
array_count_values($cnt),
function ($value) use($min) {
return ($value > $min);
}
);
echo "Values > 1 are repeated \n";
print_r(array_count_values($cnt));
echo "Values repeted\n";
print_r($newNumbers);
DEMO

Categories