PHP - Large Array merging by key content - php

I'm currently working on a PHP project in which I need to merge 5 large arrays of arrays (around 16000 entries each) by a specific key value. With this I mean each array includes about 16000 entries, each being an array with key => value format, and I intend to merge all these arrays where the value for a given key matches. For example:
I have:
arr1=[[id=>1,name=>john,surname=>doe],[id=>2,name=>katy,surname=>johnson]];
arr2=[[id=>2,age=>23,adress=>something][id=>1,age=>43,adress=>something else]];
arr3=[[id=>1,employee_number=1234],[id=>3,employee_number=2345],[id=>2,emplyee_number=>6523]];
And plan to obtain:
arr=[[id=>1,name=>john,surname=>doe,age=43,adress=>something else],[id=>2,name=>katy,surname=>johnson,age=>23,adress=>something]];
My first attempt was to loop through every array using for loops and join the arrays when the given value for the id key was matched, but this proved highly inefficient for such a big number of entries (16000 for each array). Can someone suggest any better options to accomplish the given task?
Edit:
Even though the example has generic arrays names (arr1,arr2) the code I tried to implement was this, with the referred 5 arrays and looping through each to compare and tried to merge by the sku key value.
for($i=0;$i < sizeof($received_products); $i++){
for($j=0; $j < sizeof($received_descriptions); $j++){
for($k=0; $k < sizeof($received_multimedia); $k++){
for($l=0; $l < sizeof($received_stocks); $l++){
for($m=0; $m < sizeof($received_prices); $m++){
if(strcmp(strcmp($received_products[$i]['sku'],$received_descriptions[$j]['sku'])&&strcmp($received_multimedia[$k]['sku'],$received_stocks[$l]['sku']),$received_prices[$m]['sku'])==0){
$all_products[$i]=array_merge($received_products[$i],$received_descriptions[$j],$received_multimedia[$k],$received_stocks[$l],$received_prices[$m]);
}
}
}
}
}
}

You should be able to do this with two nested loops:
<?php
$arr1 = [['id' => 1, 'name' => 'john', 'surname' => 'doe'], ['id' => 2, 'name' => 'katy', 'surname' => 'johnson']];
$arr2 = [['id' => 2, 'age' => 23, 'address' => 'something'], ['id' => 1, 'age' => 43, 'address' => 'something else']];
$arr3 = [['id' => 1, 'employee_number' => 1234], ['id' => 3, 'employee_number' => 2345], ['id' => 2, 'emplyee_number' => 6523]];
$buffer = [];
// For each of the records sets...
foreach ([$arr1, $arr2, $arr3] as $currRecordSet)
{
// Loop through the records...
foreach ($currRecordSet as $currRecord)
{
// If we do not have an entry in our buffer for ths ID yet, create one
$currKey = '_' . $currRecord['id'];
if (!array_key_exists($currKey, $buffer))
{
$buffer[$currKey] = [];
}
// Merge this record with the values in the buffer
$buffer[$currKey] = array_merge($buffer[$currKey], $currRecord);
}
}
// Get the bare array of combined entries without our ugly buffer keys
$combinedRecords = array_values($buffer);
print_r($combinedRecords);

Related

Best way to iterate array in a random order, without changing the order of the original array

I have an array of M data items, and I'd like to update N of them.
I'd like it to be a random N out of the M that get their data updated. I'd like to retain the original array order, so not shuffling the original array.
What's the best was to do this?
I could do it like below, but I was wondering if there's a more elegant way to do it?
// example data:
$data = [
['h' => 5],
['h' => 3],
['h' => 15],
['h' => 10],
['h' => 0],
['h' => 2],
];
// array of M keys
$keys = array_keys($data);
// say I want to update 2 of them at random
$n = 2;
// randomise keys
shuffle($keys);
// and take the first N
$keys = array_slice($keys, 0, $n)
// then update those N items
for ($i = 0; $i < $n; ++$i) {
$data[$keys[$i]] = updateThisItem($data[$keys[$i]]);
}

Average each associative pair found in a 2d array

Consider this collection below:
$collection = [
[1 => 10.0, 2 => 20.0, 3 => 50.0, 4 => 80.0, 5 => 100.0],
[3 => 20.0, 5 => 20.0, 6 => 100.0, 7 => 10.0],
[1 => 30.0, 3 => 30.0, 5 => 10.0, 8 => 10.0]
];
Consider this theorical output based on the intersection of the Arrays contained into $collection, considering their array keys with respective values based on the average of the single values:
$output = Array ( 3 => 33.3333, 5 => 43.3333 );
Can this problem be resolved with a native PHP function like array_intersect_* in an elegant way?
If not, can you suggest me an elegant solution that doesn't necessarily need an outer ugly foreach?
Keep in mind that the number of arrays that need to be intersected is not fixed. It can be 2 input arrays as it can be 1000 input arrays.
Keys will be integers at all times, and Values will be floats or integers at all times.
In other words:
$collection = [
$arr1 = [ ... ];
$arr2 = [ ... ];
$arr3 = [ ... ];
...
$arrn = [ ... ];
];
$output = [ intersected and weighted array based (on comparison) on keys from $arr1 to $arrn, and (on values) from the value averages ];
Count the input array once.
$n = count($collection);
Compute the intersection of all the sub-arrays by key.
$intersection = array_intersect_key(...$collection);
// PHP5: $intersection = call_user_func_array('array_intersect_key', $input);
Build your result by averaging the column from the input array for each key from the intersection.
$output = [];
foreach ($intersection as $key => $value) {
$output[$key] = array_sum(array_column($collection, $key)) / $n;
}
If you really want to completely avoid foreach you can use array_map instead.
$output = array_map(function($key) use ($collection, $n) {
return array_sum(array_column($collection, $key)) / $n;
}, array_keys($intersection));
But in my opinion, this just adds unnecessary complexity.
Note: The values in $intersection will be single values from the first sub-array, but they don't really matter; they're disregarded when generating the output. If it bothers you to have a useless $value variable in the foreach, then you can do foreach (array_keys($intersection) as $key) instead, but I opted for avoiding an unnecessary function call.
Can this problem be resolved with a native PHP function like array_intersect_* in an elegant way?
Well, elegance is in the eye of the developer. If functional-style programming with no new globally-scoped variables equals elegance, then I have something tasty for you. Can a native array_intersect_*() call be leveraged in this task? You bet!
There's a big lack in PHP native functions on intersects - #Maurizio
I disagree. PHP has a broad suite of powerful, optimized, native array_intersect*() and array_diff*() functions. I believe that too few developers are well-acquainted with them all. I've even build a comprehensive demonstration of the different array_diff*() functions (which can be easily inverted to array_intersect*() for educational purposes).
Now, onto your task. First, the code, then the explanation.
Code: (Demo)
var_export(
array_reduce(
array_keys(
array_intersect_ukey(
...array_merge($collection, [fn($a, $b) => $a <=> $b])
)
),
fn($result, $k) => $result + [$k => array_sum(array_column($collection, $k)) / count($collection)],
[]
)
);
The first subtask is to isolate the keys which are present in every row. array_intersect_ukey() is very likely the best qualified tool. The easy part is the custom function -- just write the two parameters with the spaceship in between. The hard part is setting up the variable number of leading input parameters followed by the closure. For this, temporarily merge the closure as an array element onto the collection variable, then spread the parameters into the the native function.
The payload produced by #1 is an array consisting of the associative elements from the first row where the keys were represented in all rows ([3 => 50.0, 5 => 100.0]). To prepare the data for the next step, the keys must be converted to values -- array_keys() is ideal because the float value are of no further use.
Although there is an equal number of elements going into and returning in the final "averaging step", the final result must be a flat associative array -- so array_map() will not suffice. Instead, array_reduce() is better suited. With the collection variable accessible thanks to PHP7.4's arrow function syntax, array_column() can isolate the full column of data then the averaging result pushed as an associative element into the result array.
I guess it could be done like this:
<?php
$intersecting_arrays = Array (
0 => Array ( 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100 ),
1 => Array ( 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10 ),
2 => Array ( 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10 )
);
$temp = $intersecting_arrays[0];
for($i = 1; $i < count($intersecting_arrays); $i++) {
$temp = array_intersect_key($temp, $intersecting_arrays[$i]);
}
$result = Array();
foreach(array_keys($temp) as $key => $val) {
$value = 0;
foreach($intersecting_arrays as $val1) {
$value+= $val1[$val];
}
$result[$key] = $value / count($intersecting_arrays);
}
print_r($temp);
print_r($result);
https://3v4l.org/j8o75
In this manner it doesn't depend on how much arrays you have.
Here you get the intersection of keys in all arrays and then count an average using collected keys.
Ok, with an unknown number of input arrays, I would definitively go with two nested foreach loops to combine them first - getting an unknown number into array_merge_recursive or similar is going to be difficult.
$input = [
0 => [ 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100],
1 => [ 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10],
2 => [ 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10]
];
$combined = [];
foreach($input as $array) {
foreach($array as $key => $value) {
$combined[$key][] = $value;
}
}
$averages = array_map(function($item) {
return array_sum($item)/count($item);
}, $combined);
var_dump($averages);
https://3v4l.org/hmtj5
Note that this solution doesn't need to check for array vs single integer in the array_map callback, because unlike array_merge_recursive, $combined[$key][] inside the loops sees to it that even the keys with just one value will have that value in an array.
EDIT:
but keep in mind that not all the keys are going to be taken into account
Ah, ok, so you want averages only for those keys that occurred more than once. That can easily be fixed by filtering the combined array before using array_map on it:
$combined = array_filter($combined, function($v, $k) {
return count($v) != 1;
}, ARRAY_FILTER_USE_BOTH );
Integrated into above solution: https://3v4l.org/dn5ro
EDIT #2
[Andreas' comment] I think "one" should not be in output since it is not in all three arrays.
Ah, I see ... couldn't tell that was the actually desired result even from the example :-) Then my filtering has to be modified a little bit again, and take the number of input arrays into account:
$combined = array_filter($combined, function($v, $k) use($input) {
return count($v) == count($input);
}, ARRAY_FILTER_USE_BOTH );
https://3v4l.org/9H086
You can merge the arrays to one and use array_sum and count() to get the average.
$arr1 = Array ( 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100 );
$arr2 = Array ( 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10 );
$arr3 = Array ( 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10 );
$array = array_merge_recursive($arr1,$arr2,$arr3);
$key= "two";
If(is_array($array[$key])){
$avg = array_sum($array[$key])/count($array[$key]);
}Else{
$avg = $array[$key];
}
Echo $avg;
https://3v4l.org/pa3PH
Edit to follow $collection array.
Try this then. Use array column to grab the correct key and use array_sum and count to get the average.
$collection = array(
Array ( 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100 ),
Array ( 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10 ),
Array ( 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10 ));
$key= "three";
$array = array_column($collection, $key);
If(count($array) != 1){
$avg = array_sum($array)/count($array);
}Else{
$avg = $array[0];
}
Echo $avg;
https://3v4l.org/QPsiS
Final edit.
Here I loop through the first subarray and use array column to find all the matching keys.
If the count of keys is the same as the count of collection the key exsists in all subarrays and should be "saved".
$collection = array(
Array ( 'one' => 10, 'two' => 20, 'three' => 50, 'four' => 80, 'five' => 100 ),
Array ( 'three' => 20, 'five' => 20, 'six' => 100, 'seven' => 10 ),
Array ( 'one' => 30, 'three' => 30, 'five' => 10, 'eight' => 10 ));
Foreach($collection[0] as $key => $val){
$array = array_column($collection, $key);
If(count($array) == count($collection)){
$avg[$key] = array_sum($array)/count($array);
}
}
Var_dump($avg);
https://3v4l.org/LfktH

Merge arrays on one column, sum other column values in each group [duplicate]

This question already has answers here:
Group 2d array data by one column and sum other columns in each group (separately)
(4 answers)
Closed last month.
I have 2 arrays:
$array1 = [
['amount' => 21600.00, 'rows' => 2, 'student_id' => 1],
];
$array2 = [
['amount' => 541990.00, 'rows' => 512, 'student_id' => 1],
['amount' => 347480.00, 'rows' => 281, 'student_id' => 2],
['amount' => 507400.00, 'rows' => 214, 'student_id' => 3],
];
I want to merge both arrays based on the same student_id value. When a student_id is found in both arrays, I want to sum the two amount values and the two rows values.
Expected result:
array (
0 =>
array (
'amount' => 563590.0,
'rows' => 514,
'student_id' => 1,
),
1 =>
array (
'amount' => 347480.0,
'rows' => 281,
'student_id' => 2,
),
2 =>
array (
'amount' => 507400.0,
'rows' => 214,
'student_id' => 3,
),
)
I have tried using array_merge() and array_merge_recursive() but they didn't give me the expected result.
Here is simple code to solve your problem,
count($arr) > count($arr0) ? ($greater = $arr AND $smaller = $arr0) : ($smaller = $arr AND $greater = $arr0);
foreach ($greater as $key => &$value) {
foreach ($smaller as $key1 => &$value1) {
if($value['student_id'] == $value1['student_id']){
$value['amount'] +=$value1['amount'];
$value['rows'] +=$value1['rows'];
}
}
}
Check output here
The comment about combining the queries is probably the better approach.
SELECT student_id, sum(amount) as amount, sum(rows) as rows
from students
group by student_id;
However, if you can't do it via a query, PHP doesn't natively add array values together (it doesn't make sense)
Here's what you'd have to do
function arrayMergeMatch(&$a, &$b)
{
foreach ($a as $key => $value) {
if (array_key_exists($key, $b)) {
// there's a matching index in both a & b
$a[$key] = [
$a[$key]['amount'] += $b[$key]['amount'],
$a[$key]['rows'] += $b[$key]['rows'],
$a[$key]['student_id'],
];
}
}
}
$firstArray = $secondArray = [];
$firstArray[0] = [
'amount' => 21600.00,
'rows' => 2,
'student_id' => 1
];
$secondArray[0] = [
'amount' => 541990.00,
'rows' => 512,
'student_id' => 1,
];
$secondArray[1] = [
'amount' => 347480.00,
'rows' => 281,
'student_id' => 2,
];
$secondArray[2] = [
'amount' => 507400.00,
'rows' => 214,
'student_id' => 3,
];
$firstArrayLength = count($x);
$secondArrayLength = count($y);
$combinedArray = null;
if ($firstArrayLength > $secondArrayLength) {
// loop through x checking to see if there's a matching y
arrayMergeMatch($firstArray, $secondArray);
$combinedArray = $firstArray;
}
else {
// y is longer than or equal to x, so loop through y, looking for matching
// index in x
arrayMergeMatch($secondArray, $firstArray);
$combinedArray = $secondArray;
}
print_r($combinedArray);
Nested loops are not necessary. There will be several different styles, but the crux of the task is to create a result array that temporarily serves as a lookup array while you iterate the incoming data.
To do this, assign temporary keys using student_id values. When a student_id is encountered after it is found in the result array, just add the values to the stored related values in the group. When finished iterating, re-index the result with array_values().
Functional style: (Demo1) (Demo2 - likely slower)
var_export(
array_values(
array_reduce(
$array2,
function($result, $row) {
if (!isset($result[$row['student_id']])) {
$result[$row['student_id']] = $row;
} else {
$result[$row['student_id']]['amount'] += $row['amount'];
$result[$row['student_id']]['rows'] += $row['rows'];
}
return $result;
},
array_column($array1, null, 'student_id')
)
)
);
Language construct loop: (Demo1) (Demo2 - likely slower)
$result = array_column($array1, null, 'student_id');
foreach ($array2 as $row) {
if (!isset($result[$row['student_id']])) {
$result[$row['student_id']] = $row;
} else {
$result[$row['student_id']]['amount'] += $row['amount'];
$result[$row['student_id']]['rows'] += $row['rows'];
}
}
var_export(array_values($result));
All of the above snippets should be used instead of the other posted answers because they will never iterate more than m + n. Here's a critique of the other answers:
Rahul's answer is using a nested loop -- this means the number of iterations will be m*n (number of rows in array1 multiplied by number of rows in array2) -- this is very indirect/inefficient. Even if a break was used, it still wouldn't be as direct/efficient as my snippets.
Kearney's answer is completely ignoring the requirement to sort on student_id values (even after the $x and $y typos are fixed). This php-based answer is simply incorrect. Proof. Less importantly, $b does not need to be made modifiable by reference.

Merge the rows of two arrays (appending row data from one array to a row in another array)

I have two arrays populated from CodeIgniter query result sets (from calls of result_array()) and I need to merge the rows from the two arrays respectively/synchronously.
$array1 = [
['name' => 'John', 'course' => 'BSIT'],
['name' => 'Jane', 'course' => 'BSHRM'],
];
$array2 = [
['balance' => '1000', 'date' => '2013-05-01'],
['balance' => '2000', 'date' => '2013-05-07'],
];
How can I append the elements [balance], [date] from $array2 to $array1 so that the result looks like this:
[
[
'name' => 'John',
'course' => 'BSIT',
'balance' => '1000',
'date' => '2013-05-01'
],
[
'name' => 'Jane',
'course' => 'BSHRM',
'balance' => '2000',
'date' => '2013-05-07'
]
]
I have tried:
for($i = 0; $i<count($array1); $i++)
{
array_merge($array1[$i], $array2[$i]);
}
but I get an error that arguments are not array even if I do it like this:
for($i = 0; $i<count($array1); $i++)
{
array_merge(array($array1[$i]), array($array2[$i]));
}
Try setting the array_merge equal to something:
for($i = 0; $i<count($array1); $i++)
{
$array1[$i] = array_merge($array1[$i], $array2[$i]);
}
$merged_array = array_map(function($a, $b) {
return array_merge($a, $b);
}, $array1, $array2));
There is a simpler way to merge the rows of two or more arrays. Call array_map() with a callback of array_merge() and then list the arrays as final function parameters. array_map() will synchronously isolate one row at a time from the input arrays so that array_merge() can combine the two rows into one.
Code: (Demo)
var_export(
array_map('array_merge', $array1, $array2)
);
try using array_merge_recursive() function

PHP Slots, generating combinations

So I have an array of possible combinations:
$faces = array ('Bar', 'Cherry', 'Lemon', 'Seven', 'DoubleBar', 'TripleBar');
And an array of payouts
$payouts = array (
'Bar|Bar|Bar' => '5',
'Double Bar|Double Bar|Double Bar' => '10',
'Triple Bar|Triple Bar|Triple Bar' => '15',
'Cherry|Cherry|Cherry' => '20',
'Seven|Seven|Seven' => '70',
'Seven|Any|Any' => '70',
'Diamond|Diamond|Diamond' => '100',
);
And an array for every wheel (3 in total), how can I make weighted random combinations with Seven|Any|Any working properly?
I know I can just create two arrays 6^3 in size one representing the weights and the other array representing every possible combination and using something like this script, but isn't there a shorter, more efficient way?
function weighted_random_simple($values, $weights){
$count = count($values);
$i = 0;
$n = 0;
$num = mt_rand(0, array_sum($weights));
while($i < $count){
$n += $weights[$i];
if($n >= $num){
break;
}
$i++;
}
return $values[$i];
}
How about an array that correlates counts of possible results, rather than actual positions?
e.g. you do a run, get something like A/A/B, so you've got 2 A's, and look in the table
$payouts = array(
2 => array('Bar' => 10, 'Double Bar' => 20, etc...)
3 => array('Diamond' => 100, 'Bar' => 40, etc...)
);

Categories