Better way to dynamically access values in nested array - php

I need a way to dynamically access values in a nested array using an index map. What i want to achieve is looping over an array with data and extract some values that can be in any level of the nesting and save it to a bi-dimensional array.
So far I've come up with the following code, which works quite well, but I was wondering if there is a more efficient way to do this.
<?php
// Sample data
$array = array();
$array[0]['code'] = "ABC123";
$array[0]['ship'] = array("name" => "Fortune", "code" => 'FA');
$array[0]['departure'] = array("port" => "Amsterdam", "code" => "AMS");
$array[0]['document'] = array("type" => "Passport", "data" => array("valid" => '2022-03-18', 'number' => 'AX123456') );
$array[1]['code'] = "QWERT67";
$array[1]['ship'] = array("name" => "Dream", "code" => 'DR');
$array[1]['departure'] = array("port" => "Barcelona", "code" => "BRC");
$array[1]['document'] = array("type" => "Passport", "data" => array("valid" => '2024-12-09', 'number' => 'DF908978') );
// map of indexes of $array I need in my final result array. The levels of the nested indexes is subdivided by ":"
$map = array("code", "ship:name", "departure:port", "document:type", "document:data:number");
$result = array();
// loop array for rows of data
foreach($array as $i => $row){
// loop map for indexes
foreach($map as $index){
// extract specific nested values from $row and save them in 2-dim array $result
$result[$i][$index] = xpath_array($index, $row);
}
}
// print out result
print_r($result);
// takes path to value in $array and returns given value
function xpath_array($xpath, $array){
$tmp = array();
// path is subdivded by ":"
$elems = explode(":", $xpath);
foreach($elems as $i => $elem){
// if first (or ony) iteration take root value from array and put it in $tmp
if($i == 0){
$tmp = $array[$elem];
}else{
// other iterations (if any) dig in deeper into the nested array until last item is reached
$tmp = $tmp[$elem];
}
}
// return found item (can be value or array)
return $tmp;
}
Any suggestion?

This was quite tricky for me, i used Recursive function, first we normalize array keys to obtain key as you want like this document:type, then we normalize array to obtain all at same level :
/**
* #param array $array
* #param string|null $key
*
* #return array
*/
function normalizeKey(array $array, ?string $key = ''): array
{
$result = [];
foreach ($array as $k => $v) {
$index = !empty($key) && !\is_numeric($key) ? $key.':'.$k : $k;
if (true === \is_array($v)) {
$result[$k] = normalizeKey($v, $index);
continue;
}
$result[$index] = $v;
}
return $result;
}
/**
* #param array $item
* #param int $level
*
* #return array
*/
function normalizeStructure(array $item, int $level = 0): array
{
foreach ($item as $k => $v) {
$level = isset($v['code']) ? 0 : $level;
if (true === \is_array($v) && 0 === $level) {
$item[$k] = normalizeStructure($v, ++$level);
continue;
}
if (true === \is_array($v) && 0 < $level) {
$item = \array_merge($item, normalizeStructure($v, ++$level));
unset($item[$k]);
continue;
}
}
return $item;
}
$data = normalizeStructure(normalizeKey($array));
I edited your data set to add more nests:
// Sample data
$array = array();
$array[0]['code'] = "ABC123";
$array[0]['ship'] = array("name" => "Fortune", "code" => 'FA');
$array[0]['departure'] = array("port" => "Amsterdam", "code" => "AMS");
$array[0]['document'] = array("type" => "Passport", "data" => array("valid" => '2022-03-18', 'number' => 'AX123456'));
$array[1]['code'] = "QWERT67";
$array[1]['ship'] = array("name" => "Dream", "code" => 'DR');
$array[1]['departure'] = array("port" => "Barcelona", "code" => "BRC");
$array[1]['document'] = array("type" => "Passport", "data" => array("valid" => '2024-12-09', 'number' => 'DF908978', 'check' => ['number' => '998', 'code' => 'itsWell', 'inception' => ['border' => 'finalInception']]));
With these data, you should finally receive this result:
/*
Array
(
[0] => Array
(
[code] => ABC123
[ship:name] => Fortune
[ship:code] => FA
[departure:port] => Amsterdam
[departure:code] => AMS
[document:type] => Passport
[document:data:valid] => 2022-03-18
[document:data:number] => AX123456
)
[1] => Array
(
[code] => QWERT67
[ship:name] => Dream
[ship:code] => DR
[departure:port] => Barcelona
[departure:code] => BRC
[document:type] => Passport
[document:data:valid] => 2024-12-09
[document:data:number] => DF908978
[document:data:check:number] => 998
[document:data:check:code] => itsWell
[document:data:check:inception:border] => finalInception
)
)
*/
Recursivity seems to be like Inception, everything is nested and you can lose your mind in 😆, mine was already lost in.

Related

Loop over associative array to generate separate section and header markup based on key value in PHP [duplicate]

I have the following array
Array
(
[0] => Array
(
[id] => 96
[shipping_no] => 212755-1
[part_no] => reterty
[description] => tyrfyt
[packaging_type] => PC
)
[1] => Array
(
[id] => 96
[shipping_no] => 212755-1
[part_no] => dftgtryh
[description] => dfhgfyh
[packaging_type] => PC
)
[2] => Array
(
[id] => 97
[shipping_no] => 212755-2
[part_no] => ZeoDark
[description] => s%c%s%c%s
[packaging_type] => PC
)
)
How can I group the array by id? Is there any native php functions are available to do this?
While this approach works, I want to do this using a foreach, since with the above I will get duplicate items, which I'm trying to avoid?
On the above example id have 2 items, so its need to be inside of the id
There is no native one, just use a loop.
$result = array();
foreach ($data as $element) {
$result[$element['id']][] = $element;
}
You can try the following:
$group = array();
foreach ( $array as $value ) {
$group[$value['id']][] = $value;
}
var_dump($group);
Output:
array
96 =>
array
0 =>
array
'id' => int 96
'shipping_no' => string '212755-1' (length=8)
'part_no' => string 'reterty' (length=7)
'description' => string 'tyrfyt' (length=6)
'packaging_type' => string 'PC' (length=2)
1 =>
array
'id' => int 96
'shipping_no' => string '212755-1' (length=8)
'part_no' => string 'dftgtryh' (length=8)
'description' => string 'dfhgfyh' (length=7)
'packaging_type' => string 'PC' (length=2)
97 =>
array
0 =>
array
'id' => int 97
'shipping_no' => string '212755-2' (length=8)
'part_no' => string 'ZeoDark' (length=7)
'description' => string 's%c%s%c%s' (length=9)
'packaging_type' => string 'PC' (length=2)
In a more functional programming style, you could use array_reduce
$groupedById = array_reduce($data, function (array $accumulator, array $element) {
$accumulator[$element['id']][] = $element;
return $accumulator;
}, []);
I just threw this together, inspired by .NET LINQ
<?php
// callable type hint may be "closure" type hint instead, depending on php version
function array_group_by(array $arr, callable $key_selector) {
$result = array();
foreach ($arr as $i) {
$key = call_user_func($key_selector, $i);
$result[$key][] = $i;
}
return $result;
}
$data = array(
array(1, "Andy", "PHP"),
array(1, "Andy", "C#"),
array(2, "Josh", "C#"),
array(2, "Josh", "ASP"),
array(1, "Andy", "SQL"),
array(3, "Steve", "SQL"),
);
$grouped = array_group_by($data, function($i){ return $i[0]; });
var_dump($grouped);
?>
And voila you get
array(3) {
[1]=>
array(3) {
[0]=>
array(3) {
[0]=>
int(1)
[1]=>
string(4) "Andy"
[2]=>
string(3) "PHP"
}
[1]=>
array(3) {
[0]=>
int(1)
[1]=>
string(4) "Andy"
[2]=>
string(2) "C#"
}
[2]=>
array(3) {
[0]=>
int(1)
[1]=>
string(4) "Andy"
[2]=>
string(3) "SQL"
}
}
[2]=>
array(2) {
[0]=>
array(3) {
[0]=>
int(2)
[1]=>
string(4) "Josh"
[2]=>
string(2) "C#"
}
[1]=>
array(3) {
[0]=>
int(2)
[1]=>
string(4) "Josh"
[2]=>
string(3) "ASP"
}
}
[3]=>
array(1) {
[0]=>
array(3) {
[0]=>
int(3)
[1]=>
string(5) "Steve"
[2]=>
string(3) "SQL"
}
}
}
Consume and cache the column value that you want to group by, then push the remaining data as a new subarray of the group you have created in the the result.
function array_group(array $data, $by_column)
{
$result = [];
foreach ($data as $item) {
$column = $item[$by_column];
unset($item[$by_column]);
$result[$column][] = $item;
}
return $result;
}
If you desire a Composer alternative with a full suite of tests, the array_group_by function achieves what you are looking for. Full disclosure: I am the author of said library.
$grouped = array_group_by($arr, 'id');
It also supports multi-level groupings, or even complex grouping through use of custom callback functions:
// Multilevel grouping
$grouped = array_group_by($arr, 'id', 'part_no');
// Grouping by a callback/callable function
$grouped = array_group_by($records, function ($row) {
return $row->city;
});
$arr = Data Araay;
$fldName = Group By Colum Name;
function array_group_by( $arr, $fldName) {
$groups = array();
foreach ($arr as $rec) {
$groups[$rec[$fldName]] = $rec;
}
return $groups;
}
function object_group_by( $obj, $fldName) {
$groups = array();
foreach ($obj as $rec) {
$groups[$rec->$fldName] = $rec;
}
return $groups;
}
$arr = array();
foreach($old_arr as $key => $item)
{
$arr[$item['id']][$key] = $item;
}
ksort($arr, SORT_NUMERIC);
for($i = 0 ; $i < count($arr) ; $i++ )
{
$tmpArr[$arr[$i]['id']] = $arr[$i]['id'];
}
$vmpArr = array_keys($tmpArr);
print_r($vmpArr);
Expanding on #baba's answer, which I like, but creates a more complex three level deep multi-dimensional (array(array(array))):
$group = array();
foreach ( $array as $value ) {
$group[$value['id']][] = $value;
}
// output only data from id 96
foreach ($group as $key=>$value) { //outer loop
foreach ($value as $k=>$v){ //inner loop
if($key==96){ //if outer loop is equal to 96 (could be variable)
for ($i=0;$i<count($k);$i++){ //iterate over the inner loop
printf($key.' has a part no. of '.$v['part_no'].' and shipping no. of '.$v['shipping_no'].'<br>');
}
}
}
}
Will output:
96 has a part no. of reterty and shipping number of 212755-1
96 has a part no. of dftgtryh and shipping number of 212755-1
It's trivial to do with LINQ, which is implemented in PHP in several libraries, including YaLinqo*. It allows performing SQL-like queries on arrays and objects. The groubBy function is designed specifically for grouping, you just need to specify the field you want to group by:
$grouped_array = from($array)->groupBy('$v["id"]')->toArray();
Where '$v["id"]' is a shorthand for function ($v) { return $v["id"]; } which this library supports.
The result will be exactly like in the accepted answer, just with less code.
* developed by me
1. GROUP BY one key
This function works as GROUP BY for array, but with one important limitation: Only one grouping "column" ($identifier) is possible.
function arrayUniqueByIdentifier(array $array, string $identifier)
{
$ids = array_column($array, $identifier);
$ids = array_unique($ids);
$array = array_filter($array,
function ($key, $value) use($ids) {
return in_array($value, array_keys($ids));
}, ARRAY_FILTER_USE_BOTH);
return $array;
}
2. Detecting the unique rows for a table (twodimensional array)
This function is for filtering "rows". If we say, a twodimensional array is a table, then its each element is a row. So, we can remove the duplicated rows with this function. Two rows (elements of the first dimension) are equal, if all their columns (elements of the second dimension) are equal. To the comparsion of "column" values applies: If a value is of a simple type, the value itself will be use on comparing; otherwise its type (array, object, resource, unknown type) will be used.
The strategy is simple: Make from the original array a shallow array, where the elements are imploded "columns" of the original array; then apply array_unique(...) on it; and as last use the detected IDs for filtering of the original array.
function arrayUniqueByRow(array $table = [], string $implodeSeparator)
{
$elementStrings = [];
foreach ($table as $row) {
// To avoid notices like "Array to string conversion".
$elementPreparedForImplode = array_map(
function ($field) {
$valueType = gettype($field);
$simpleTypes = ['boolean', 'integer', 'double', 'float', 'string', 'NULL'];
$field = in_array($valueType, $simpleTypes) ? $field : $valueType;
return $field;
}, $row
);
$elementStrings[] = implode($implodeSeparator, $elementPreparedForImplode);
}
$elementStringsUnique = array_unique($elementStrings);
$table = array_intersect_key($table, $elementStringsUnique);
return $table;
}
It's also possible to improve the comparing, detecting the "column" value's class, if its type is object.
The $implodeSeparator should be more or less complex, z.B. spl_object_hash($this).
3. Detecting the rows with unique identifier columns for a table (twodimensional array)
This solution relies on the 2nd one. Now the complete "row" doesn't need to be unique. Two "rows" (elements of the first dimension) are equal now, if all relevant "fields" (elements of the second dimension) of the one "row" are equal to the according "fields" (elements with the same key).
The "relevant" "fields" are the "fields" (elements of the second dimension), which have key, that equals to one of the elements of the passed "identifiers".
function arrayUniqueByMultipleIdentifiers(array $table, array $identifiers, string $implodeSeparator = null)
{
$arrayForMakingUniqueByRow = $removeArrayColumns($table, $identifiers, true);
$arrayUniqueByRow = $arrayUniqueByRow($arrayForMakingUniqueByRow, $implodeSeparator);
$arrayUniqueByMultipleIdentifiers = array_intersect_key($table, $arrayUniqueByRow);
return $arrayUniqueByMultipleIdentifiers;
}
function removeArrayColumns(array $table, array $columnNames, bool $isWhitelist = false)
{
foreach ($table as $rowKey => $row) {
if (is_array($row)) {
if ($isWhitelist) {
foreach ($row as $fieldName => $fieldValue) {
if (!in_array($fieldName, $columnNames)) {
unset($table[$rowKey][$fieldName]);
}
}
} else {
foreach ($row as $fieldName => $fieldValue) {
if (in_array($fieldName, $columnNames)) {
unset($table[$rowKey][$fieldName]);
}
}
}
}
}
return $table;
}
This should group an associative array
Ejm Group By Country
function getGroupedArray($array, $keyFieldsToGroup) {
$newArray = array();
foreach ($array as $record)
$newArray = getRecursiveArray($record, $keyFieldsToGroup, $newArray);
return $newArray;
}
function getRecursiveArray($itemArray, $keys, $newArray) {
if (count($keys) > 1)
$newArray[$itemArray[$keys[0]]] = getRecursiveArray($itemArray, array_splice($keys, 1), $newArray[$itemArray[$keys[0]]]);
else
$newArray[$itemArray[$keys[0]]][] = $itemArray;
return $newArray;
}
$countries = array(array('Country'=>'USA', 'State'=>'California'),
array('Country'=>'USA', 'State'=>'Alabama'),
array('Country'=>'BRA', 'State'=>'Sao Paulo'));
$grouped = getGroupedArray($countries, array('Country'));
Check indexed function from Nspl:
use function \nspl\a\indexed;
$grouped = indexed($data, 'id');
function array_group_by($arr, array $keys) {
if (!is_array($arr)) {
trigger_error('array_group_by(): The first argument should be an array', E_USER_ERROR);
}
if (count($keys)==0) {
trigger_error('array_group_by(): The Second argument Array can not be empty', E_USER_ERROR);
}
// Load the new array, splitting by the target key
$grouped = [];
foreach ($arr as $value) {
$grouped[$value[$keys[0]]][] = $value;
}
// Recursively build a nested grouping if more parameters are supplied
// Each grouped array value is grouped according to the next sequential key
if (count($keys) > 1) {
foreach ($grouped as $key => $value) {
$parms = array_merge([$value], [array_slice($keys, 1,count($keys))]);
$grouped[$key] = call_user_func_array('array_group_by', $parms);
}
}
return $grouped;
}
function groupeByPHP($array,$indexUnique,$assoGroup,$keepInOne){
$retour = array();
$id = $array[0][$indexUnique];
foreach ($keepInOne as $keep){
$retour[$id][$keep] = $array[0][$keep];
}
foreach ($assoGroup as $cle=>$arrayKey){
$arrayGrouped = array();
foreach ($array as $data){
if($data[$indexUnique] != $id){
$id = $data[$indexUnique];
foreach ($keepInOne as $keep){
$retour[$id][$keep] = $data[$keep];
}
}
foreach ($arrayKey as $val){
$arrayGrouped[$val] = $data[$val];
}
$retour[$id][$cle][] = $arrayGrouped;
$retour[$id][$cle] = array_unique($retour[$id][$cle],SORT_REGULAR);
}
}
return $retour;
}
Try this function
groupeByPHP($yourArray,'id',array('desc'=>array('part_no','packaging_type')),array('id','shipping_no'))
Recursive function grouping 2-dimensional array by keys from first to last
Input:
$arr = array(
'0' => array(
'key0' => 'value0',
'key1' => 'value1',
'key2' => 'value02',
),
'2' => array(
'key0' => 'value0',
'key1' => 'value1',
'key2' => 'value12',
),
'3' => array(
'key0' => 'value0',
'key1' => 'value3',
'key2' => 'value22',
),
);
$keys = array('key0', 'key1', 'key2');
Output:
$arr = array(
'value0' => array(
'value1 => array(
'value02' => null,
'value12' => null,
),
'value3' => 'value22',
),
);
Code:
function array_group_by_keys(&$arr, $keys) {
if (count($arr) < 2){
$arr = array_shift($arr[0]);
return;
}
foreach ($arr as $k => $item) {
$fvalue = array_shift($item);
$arr[$fvalue][] = $item;
unset($arr[$k]);
}
array_shift($keys);
foreach ($arr as &$sub_arr) {
array_group_by_keys($sub_arr, $keys);
}
}
How about multiple level grouping.
data:
$rows = [
['country'=>'Japan', 'city'=>'Tokyo', 'surname'=>'Miyazaki', 'name'=>'Hayao'],
['country'=>'France', 'city'=>'Paris', 'surname'=>'Godard', 'name'=>'Jean-Luc'],
['country'=>'France', 'city'=>'Lyon', 'surname'=>'Godard', 'name'=>'Marguerite'],
['country'=>'Japan', 'city'=>'Tokyo', 'surname'=>'Miyazaki', 'name'=>'Akira'],
['country'=>'Japan', 'city'=>'Nara', 'surname'=>'Kurosawa', 'name'=>'Akira'],
['country'=>'France', 'city'=>'Paris', 'surname'=>'Duras', 'name'=>'Marguerite'],
];
$groups = groupBy($rows, 'country', 'city', 'surname');
code:
function groupBy($rows, ...$keys)
{
if ($key = array_shift($keys)) {
$groups = array_reduce($rows, function ($groups, $row) use ($key) {
$group = is_object($row) ? $row->{$key} : $row[$key]; // object is available too.
$groups[$group][] = $row;
return $groups;
}, []);
if ($keys) {
foreach ($groups as $subKey=>$subRows) {
$groups[$subKey] = self::groupBy($subRows, ...$keys);
}
}
}
return $groups;
}
It's easy, you can group by any "key" in the array by using my function groupBy();
$data = [
[
"id" => 96,
"shipping_no" => "212755-1",
"part_no" => "reterty",
"description" => "tyrfyt",
"packaging_type" => "PC"
],
[
"id" => 96,
"shipping_no" => "212755-1",
"part_no" => "dftgtryh",
"description" => "dfhgfyh",
"packaging_type" => "PC"
],
[
"id" => 97,
"shipping_no" => "212755-2",
"part_no" => "ZeoDark",
"description" => "s%c%s%c%s",
"packaging_type" => "PC"
]
];
function groupBy($array, $key) {
$groupedData = [];
$data = [];
$_id = "";
for ($i=0; $i < count($array); $i++) {
$row = $array[$i];
if($row[$key] != $_id){
if(count($data) > 0){
$groupedData[] = $data;
}
$_id = $row[$key];
$data = [
$key => $_id
];
}
unset($row[$key]);
$data["data"][] = $row;
if($i == count($array) - 1){
$groupedData[] = $data;
}
}
return $groupedData;
}
print_r(groupBy($data, "id"));
The results will be:
Array
(
[0] => Array
(
[id] => 96
[data] => Array
(
[0] => Array
(
[shipping_no] => 212755-1
[part_no] => reterty
[description] => tyrfyt
[packaging_type] => PC
)
[1] => Array
(
[shipping_no] => 212755-1
[part_no] => dftgtryh
[description] => dfhgfyh
[packaging_type] => PC
)
)
)
[1] => Array
(
[id] => 97
[data] => Array
(
[0] => Array
(
[shipping_no] => 212755-2
[part_no] => ZeoDark
[description] => s%c%s%c%s
[packaging_type] => PC
)
)
)
)
If you change the "key" parameter, it should works without changes:
print_r(groupBy($data, "shipping_no"));
Array
(
[0] => Array
(
[shipping_no] => 212755-1
[data] => Array
(
[0] => Array
(
[id] => 96
[part_no] => reterty
[description] => tyrfyt
[packaging_type] => PC
)
[1] => Array
(
[id] => 96
[part_no] => dftgtryh
[description] => dfhgfyh
[packaging_type] => PC
)
)
)
[1] => Array
(
[shipping_no] => 212755-2
[data] => Array
(
[0] => Array
(
[id] => 97
[part_no] => ZeoDark
[description] => s%c%s%c%s
[packaging_type] => PC
)
)
)
)
What about array_combine() ?
Using array_combine() stores each row on the index of $groupByColumn, so we can use that $groupByColumn as keys. This returns the last row for every group (array_combine() overwrites the value when the key already exists - see https://www.php.net/manual/en/function.array-combine.php#111668). If you want to return the first or some specific row, you can play around with array_reverse() or usort() etc.
$result = array_combine(
array_column($source, $groupByColumn),
$source
);

php multidimensional array merge with same key and value

I've spent hours trying to find the answer to this question, but I'm struggling. I'm reasonably familiar with PHP and the various in-built functions, and can build a complex foreach() loop to do this, but I thought I'd ask to see if anyone has a smarter solution to my problem.
I have the following simplified example array with three "rows" (the real array is usually a lot bigger and more complex, but the issue is the same).
$rows[] = [
"widget_id" => "widget1",
"size" => "large",
"item" => [
"item_id" => "item1",
"shape" => "circle",
"paint" => [
"paint_id" => "paint1",
"colour" => "red",
]
]
];
# Exactly the same as above, except the "paint" child array is different
$rows[] = [
"widget_id" => "widget1",
"size" => "large",
"item" => [
"item_id" => "item1",
"shape" => "circle",
"paint" => [
"paint_id" => "paint2",
"colour" => "green",
]
]
];
# Same children ("item" and "paint") as the first row, but different parents ("widget_id" is different)
$rows[] = [
"widget_id" => "widget2",
"size" => "medium",
"item" => [
"item_id" => "item1",
"shape" => "circle",
"paint" => [
"paint_id" => "paint1",
"colour" => "red",
]
]
];
What I'm trying to get to is the following output:
[[
"widget_id" => "widget1",
"size" => "large",
"item" => [
"item_id" => "item1",
"shape" => "circle",
"paint" => [[
"paint_id" => "paint1",
"colour" => "red",
],[
"paint_id" => "paint2",
"colour" => "green",
]]
]
],[
"widget_id" => "widget2",
"size" => "medium",
"item" => [
"item_id" => "item1",
"shape" => "circle",
"paint" => [
"paint_id" => "paint1",
"colour" => "red",
]
]
]]
Basically, when two rows share the same key and values, merge them. When the key is the same, but the value is different, keep both values and put them in a numerical array under the key (sort of like how array_merge_recursive does it).
The challenge is that the values can themselves be arrays and there is an unknown number of levels. Is there a smart and effective way of doing this, or do I have to resort to a heavy duty foreach loop?
Thank you for browsing, hope there are some people more clever than me reading this!
I achieved to get the expected array structure with the following function, I hope comments are explicit on what's inside:
function complex_merge(array $arr): array
{
// Grouped items
$result = [];
$iterationKey = 0;
// Loop through every item
while (($element = array_shift($arr)) !== null) {
// Save scalar values as is
$scalarValues = array_filter($element, 'is_scalar');
// Save array values in an array
$arrayValues = array_map(fn(array $arrVal) => [$arrVal], array_filter($element, 'is_array'));
$arrayValuesKeys = array_keys($arrayValues);
$result[$iterationKey] = array_merge($scalarValues, $arrayValues);
// Compare with remaining items
for ($i = 0; $i < count($arr); $i++) {
$comparisonScalarValues = array_filter($arr[$i], 'is_scalar');
// Scalar values are same, add the array values to the containing arrays
if ($scalarValues === $comparisonScalarValues) {
$comparisonArrayValues = array_filter($arr[$i], 'is_array');
foreach ($arrayValuesKeys as $arrayKey) {
$result[$iterationKey][$arrayKey][] = $comparisonArrayValues[$arrayKey];
}
// Remove matching item
array_splice($arr, $i, 1);
$i--;
}
}
// Merge array values
foreach ($arrayValuesKeys as $arrayKey) {
$result[$iterationKey][$arrayKey] = complex_merge($result[$iterationKey][$arrayKey]);
// array key contains a single item, extract it
if (count($result[$iterationKey][$arrayKey]) === 1) {
$result[$iterationKey][$arrayKey] = $result[$iterationKey][$arrayKey][0];
}
}
// Increment result key
$iterationKey++;
}
return $result;
}
Just pass $rows to the function, quick checkup of the values:
echo '<pre>' . print_r(complex_merge($rows), true) . '</pre>';
/*
Displays:
Array
(
[0] => Array
(
[widget_id] => widget1
[size] => large
[item] => Array
(
[item_id] => item1
[shape] => circle
[paint] => Array
(
[0] => Array
(
[paint_id] => paint1
[colour] => red
)
[1] => Array
(
[paint_id] => paint2
[colour] => green
)
)
)
)
[1] => Array
(
[widget_id] => widget2
[size] => medium
[item] => Array
(
[item_id] => item1
[shape] => circle
[paint] => Array
(
[paint_id] => paint1
[colour] => red
)
)
)
)
*/
Here's my own attempt. I think I prefer AymDev's version though, a lot more succinct. I wonder which is faster.
class ComplexMerge{
/**
* Checks to see whether an array has sequential numerical keys (only),
* starting from 0 to n, where n is the array count minus one.
*
* #link https://codereview.stackexchange.com/questions/201/is-numeric-array-is-missing/204
*
* #param $arr
*
* #return bool
*/
private static function isNumericArray($arr)
{
if(!is_array($arr)){
return false;
}
return array_keys($arr) === range(0, (count($arr) - 1));
}
/**
* Given an array, separate out
* array values that themselves are arrays
* and those that are not.
*
* #param array $array
*
* #return array[]
*/
private static function separateOutArrayValues(array $array): array
{
$valuesThatAreArrays = [];
$valuesThatAreNotArrays = [];
foreach($array as $key => $val){
if(is_array($val)){
$valuesThatAreArrays[$key] = $val;
} else {
$valuesThatAreNotArrays[$key] = $val;
}
}
return [$valuesThatAreArrays, $valuesThatAreNotArrays];
}
/**
* Groups row keys together that have the same non-array values.
* If every row is already unique, returns NULL.
*
* #param $array
*
* #return array|null
*/
private static function groupRowKeysWithSameNonArrayValues($array): ?array
{
foreach($array as $key => $row){
# Separate out the values that are arrays and those that are not
[$a, $v] = self::separateOutArrayValues($row);
# Serialise the values that are not arrays and create a unique ID from them
$uniqueRowId = md5(serialize($v));
# Store all the original array keys under the unique ID
$deduplicatedArray[$uniqueRowId][] = $key;
}
# If every row is unique, there are no more rows to combine, and our work is done
if(!$a && count($array) == count($deduplicatedArray)){
return NULL;
}
return $deduplicatedArray;
}
private static function mergeRows(array $array): array
{
# Get the grouped row keys
if(!$groupedRowKeys = self::groupRowKeysWithSameNonArrayValues($array)){
//If there are no more rows to merge
return $array;
}
foreach($groupedRowKeys as $uniqueRowId => $keys){
foreach($keys as $id => $key){
# Separate out the values that are arrays and those that are not
[$valuesThatAreArrays, $valuesThatAreNotArrays] = self::separateOutArrayValues($array[$key]);
//We're using the key from the grouped row keys array, but using it on the original array
# If this is the first row from the group, throw in the non-array values
if(!$id){
$unique[$uniqueRowId] = $valuesThatAreNotArrays;
}
# For each of the values that are arrays include them back in
foreach($valuesThatAreArrays as $k => $childArray){
$unique[$uniqueRowId][$k][] = $childArray;
//Wrap them in a numerical key array so that only children and siblings are have the same parent-child relationship
}
}
}
# Go deeper
foreach($unique as $key => $val){
foreach($val as $k => $valuesThatAreNotArrays){
if(self::isNumericArray($valuesThatAreNotArrays)){
$unique[$key][$k] = self::mergeRows($unique[$key][$k]);
}
}
}
# No need to include the unique row IDs
return array_values($unique);
}
public static function normalise($array): ?array
{
$array = self::mergeRows($array);
return $array;
}
}
Usage:
$array = ComplexMerge::normalise($array);
Demo

ForEach loop inside associative array PHP

I have following foreach loop
$selectedids = "1255;1256;1257";
$selectedidsarr = explode(';', $selectedids);
$idstand = '1';
foreach ($selectedidsarr as $item) {
$output1 = $idstand++;
echo "<li>product_id_$output1 = $item,</li>";
}
I want to add the output of the above loop inside following associative array
$paramas = array(
'loginId' => $cred1,
'password' => $credpass1,
'orderId' => $orderid,
'offer' => $offerid,
'shipid' => $shipcharge
)
So that the final array will look like this;
$paramas = array(
'loginId' => $cred1,
'password' => $credpass1,
'orderId' => $orderid,
'offer' => $offerid,
'shipid' => $shipcharge,
'product1_id' => 1255,
'product2_id' => 1256,
'product3_id' => 1257,
)
I tried creating following solution but its not working for me
$selectedids = $boughtitem;
$selectedidsarr = explode(';', $selectedids);
$idstand = '1';
foreach ($selectedidsarr as $item) {
$idoutput1 = $idstand++;
$paramas [] = array (
'product$idoutput1_id' => $item,
);
}
Need advice.
You don't need to define a new array, just set the key of the current array to the value you want, in the form of $array[$key] = $value to get an array that looks like [$key=>$value], or in your case...
$paramas['product' . $idoutput1 . '_id'] = $item;

How to show duplicate data in multidimensional array using PHP

I am using Spout Excel reader to read Excel files from php code and saving into a multidimensional array in PHP variable,Array looks like this
$array = [
[
'id[0]' => 'BX-78',
'Name[0]' => 'XXX',
'Address[0]' => 'YUUSATD'
],
[
'id[1]' => 'BX-79',
'Name[1]' => 'YYY',
'Address[1]' => 'DHJSHDJGY'
],
[
'id[2]' => 'BX-80',
'Name[2]' => 'ZZZ',
'Address[2]' => 'DDSDSDA'
]
[
'id[3]' => 'BX-78',
'Name[3]' => 'AAA',
'Address[3]' => 'FSDSDS'
][
'id[4]' => 'BX-81',
'Name[4]' => 'XXX',
'Address[4]' => 'DSDSDSD'
]];
Now i want to show duplicate data from above array using two keys ['id'] and ['name'] if id repeats show as duplicate data,
If name repeats show that row as duplicate data if both are duplicate show as again duplicate row
Otherwise it is unique row.
I have tried using multidimensional array sorting but it is using only one key to match data in rows.
foreach ($arrExcelData as $v) {
if (isset($arrExcelData[$v[0]])) {
// found duplicate
continue;
}
// remember unique item
$arrExcelData3[$v[0]] = $v;
}
// if you need a zero-based array, otheriwse work with $_data
$arrExcelData2 = array_values($arrExcelData3);
Edited : Expected Output Result :
Matching Rows:
Id Name Address
-------------------------
BX-78 XXX YUUSATD
BX-78 AAA DDSDSDA
BX-81 XXX DSDSDSD`
If you want to list the duplicate values, I think the address of the second match should be FSDSDS as there is not item with name AAA and value DDSDSDA:
BX-78 AAA FSDSDS
If that is the case, what you could do is to first use a double foreach to mark the arrays that contain a duplicate id or name by for example adding a property named id and name except when the array is itself in the second loop.
After this loop, you can tell which arrays are the duplicate ones. Instead of using a corresponding index 0 as in id[0], I have used reset and next so it is not tied to these indexes.
To get the filtered result you could use array_reduce to check for the array keys and unset them.
For example:
foreach ($array as $index => $a) {
foreach ($array as $v) {
if ($v === $a) continue;
if (reset($v) === reset($a)) $array[$index]["id"] = "duplicate";
if (next($v) === next($a)) $array[$index]["name"] = "duplicate";
}
}
$array = array_reduce($array, function($carry, $item) {
if (array_key_exists("id", $item) || array_key_exists("name", $item)) {
unset($item["id"], $item["name"]);
$carry[] = $item;
}
return $carry;
}, []);
print_r($array);
Result
Array
(
[0] => Array
(
[id[0]] => BX-78
[Name[0]] => XXX
[Address[0]] => YUUSATD
)
[1] => Array
(
[id[3]] => BX-78
[Name[3]] => AAA
[Address[3]] => FSDSDS
)
[2] => Array
(
[id[4]] => BX-81
[Name[4]] => XXX
[Address[4]] => DSDSDSD
)
)
See a php demo
I've this very pragmatic approach:
$spout_output = [
[
'id[0]' => 'BX-78',
'Name[0]' => 'XXX',
'Address[0]' => 'YUUSATD'
],
[
'id[1]' => 'BX-79',
'Name[1]' => 'YYY',
'Address[1]' => 'DHJSHDJGY'
],
[
'id[2]' => 'BX-80',
'Name[2]' => 'ZZZ',
'Address[2]' => 'DDSDSDA'
],
[
'id[3]' => 'BX-78',
'Name[3]' => 'AAA',
'Address[3]' => 'FSDSDS'
],
[
'id[4]' => 'BX-81',
'Name[4]' => 'XXX',
'Address[4]' => 'DSDSDSD'
]];
// store id to row, and name to row mappings.
// id and name will be keys, value will be an array of indexes of the array $spout_output
$id_to_rows = array();
$name_to_rows = array();
$duplicate_ids = array();
$duplicate_names = array();
foreach($spout_output as $row => $data)
{
$key_id = 'id['.$row.']';
$key_name = 'Name['.$row.']';
if(!isset($data[$key_id]))
continue;
$value_id = $data[$key_id];
$value_name = $data[$key_name];
if(!isset($id_to_rows[$value_id]))
{
$id_to_rows[$value_id] = array();
}
else
{
if(!isset($duplicate_ids[$value_id]))
{
$duplicate_ids[$value_id] = $id_to_rows[$value_id];
}
$duplicate_ids[$value_id][] = $row;
}
if(!isset($name_to_rows[$value_name]))
{
$name_to_rows[$value_name] = array();
}
else
{
if(!isset($duplicate_names[$value_name]))
{
$duplicate_names[$value_name] = $name_to_rows[$value_name];
}
$duplicate_names[$value_name][] = $row;
}
$id_to_rows[$value_id][] = $row;
$name_to_rows[$value_name][] = $row;
}
echo 'Duplicates:';
echo '<br>';
$shown_rows = array();
foreach($duplicate_ids as $id => $rows)
{
foreach($rows as $nr)
{
echo $id . '|' . $spout_output[$nr]['Name['.$nr.']'] . '|' . $spout_output[$nr]['Address['.$nr.']'];
echo '<br>';
$shown_rows[] = $nr;
}
}
foreach($duplicate_names as $name => $rows)
{
foreach($rows as $nr)
{
// if already shown above, skip this row
if(in_array($nr, $shown_rows))
continue;
echo $spout_output[$nr]['id['.$nr.']'] . '|' . $spout_output[$nr]['Name['.$nr.']'] . '|' . $spout_output[$nr]['Address['.$nr.']'];
echo '<br>';
$shown_rows[] = $nr;
}
}
Outputs:
Duplicates:
BX-78|XXX|YUUSATD
BX-78|AAA|FSDSDS
BX-81|XXX|DSDSDSD
I think your 'wanted output' contains an error in the address?
Anyway, with my code above I think you'll have enough mapped data to produce the output you want.
You could do something like this:
$dupes = [];
$current = [];
foreach ($array as $index => $entry) {
$idKey = "id[$index]";
$nameKey = "Name[$index]";
if (array_key_exists($entry[$idKey], $current)) {
$dupes[] = [$entry, $current[$entry[$idKey]]];
}
elseif (array_key_exists($entry[$nameKey], $current)) {
$dupes[] = [$entry, $current[$entry[$nameKey]]];
}
else {
$current[$entry[$idKey]] = $current[$entry[$nameKey]] = $entry;
}
}
print_r($dupes);
Which results in an array containing each set of duplicates (array of arrays):
Array
(
[0] => Array
(
[0] => Array
(
[id[3]] => BX-78
[Name[3]] => AAA
[Address[3]] => FSDSDS
)
[1] => Array
(
[id[0]] => BX-78
[Name[0]] => XXX
[Address[0]] => YUUSATD
)
)
[1] => Array
(
[0] => Array
(
[id[4]] => BX-81
[Name[4]] => XXX
[Address[4]] => DSDSDSD
)
[1] => Array
(
[id[0]] => BX-78
[Name[0]] => XXX
[Address[0]] => YUUSATD
)
)
)
Demo here: https://3v4l.org/JAtNU
In case someone of you are searching unique values by key.
function unique_multidim_array($array, $key) {
$temp_array = array();
$i = 0;
$key_array = array();
foreach($array as $val) {
if (!in_array($val[$key], $key_array)) {
$key_array[$i] = $val[$key];
$temp_array[$i] = $val;
}
$i++;
}
return $temp_array;
}
This function just takes multidimensional array and key value of field you need.
Then takes value of given array one by one (smaller arrays).
Then traverses given array and looking if taken key-value pair matches with given key.
After that if taken key-value pair matches with given key function just inserts smaller array in temporary array (array with unique values).
Don't forget to increment indexes of arrays ($i).
Then return array you got (with unique values) after function ends work.

How to group by and sum a multi-dimensional array?

I have already seen this
stackoverflow page but it is not helping me.
I want to group by two columns and sum the values of a third column.
If the discount_id and dis_percent are the same then add the discount_value.
Here is my array:
$dis = [
[['Dis_id' => 'Dl-Dis1'], ['Dis_per' => '7.500'], ['Dis_val' => '192.75']],
[['Dis_id' => 'Dl-Dis2'], ['Dis_per' => '2.500'], ['Dis_val' => '97.88']],
[['Dis_id' => 'Dl-Dis1'], ['Dis_per' => '5.000'], ['Dis_val' => '39.90']],
[['Dis_id' => 'Dl-Dis2'], ['Dis_per' => '2.500'], ['Dis_val' => '99.90']]
];
The output that I need is:
D1-Dis1->7.5->192.75
D1-Dis1->5.0->39.9
D1-Dis2->2.5->197.78
My code looks like this:
$newarr = array();
$reverse_map = array();
foreach($dis as $idx => $entry) {
if (isset($reverse_map[$entry['Dis_id']])) {
// have we seen this name before? retrieve its original index value
$idx = $reverse_map[$entry['Dis_id']];
} else {
// nope, new name, so store its index value
$reverse_map[$entry['Dis_id']] = $idx;
}
// copy the 'constant' values
$newarr[$idx]['Dis_id'] = $entry['Dis_id'];
$newarr[$idx]['Dis_per'] = $entry['Dis_per'];
// sum the qtd_post values to whatever we previously stored.
foreach($entry['Dis_val'] as $x => $y) {
$newarr[$idx]['Dis_val'][$x] += $y;
}
}
This is the solution I've come up with based off of the understanding that your intended array structure was as so;
$dis = array(
array(
'Dis_id' => 'Dl-Dis1',
'Dis_per' => 7.500,
'Dis_val' => 192.75
),
...
);
It determines the solution by creating a multidimensional array where the first dimension is the Dis_id, and the second dimension is the Dis_per, and the value becomes the sum of the Dis_val;
$sums = array();
foreach ($dis as $entry) {
if (!isset($sums[$entry['Dis_id']])) {
$sums[$entry['Dis_id']] = array();
}
if (!isset($sums[$entry['Dis_id']]["{$entry['Dis_per']}"])) {
$sums[$entry['Dis_id']]["{$entry['Dis_per']}"] = 0;
}
$sums[$entry['Dis_id']]["{$entry['Dis_per']}"] += $entry['Dis_val'];
}
See this working example; https://eval.in/158661
As you iterate your input array, you will need to isolate the Dis_id and Dis_per values and use them as keys when storing your Dis_val.
If there no value present for a [Dis_id][Dis_per] element, then you can simply store the value. If there is a pre-existing value, you need to add the new value to the stored value. isset() is most efficient function to aid in the identification of new/existing elements in the result array.
Code: (Demo)
$dis = [
[['Dis_id' => 'Dl-Dis1'], ['Dis_per' => '7.500'], ['Dis_val' => '192.75']],
[['Dis_id' => 'Dl-Dis2'], ['Dis_per' => '2.500'], ['Dis_val' => '97.88']],
[['Dis_id' => 'Dl-Dis1'], ['Dis_per' => '5.000'], ['Dis_val' => '39.90']],
[['Dis_id' => 'Dl-Dis2'], ['Dis_per' => '2.500'], ['Dis_val' => '99.90']]
];
foreach ($dis as $group) {
$id = $group[0]['Dis_id'];
$per = $group[1]['Dis_per'];
if (!isset($result[$id][$per])) {
$result[$id][$per] = $group[2]['Dis_val'];
} else {
$result[$id][$per] += $group[2]['Dis_val'];
}
}
var_export($result);
Output:
array (
'Dl-Dis1' =>
array (
'7.500' => '192.75',
'5.000' => '39.90',
),
'Dl-Dis2' =>
array (
'2.500' => 197.78,
),
)

Categories