I have an array of data like this:
dataArray = [
'index1' => 'value1',
'index2' => 'value2',
'index3' => '[
'index1' => 'value1',
'index2' => [
'index1' => 'value1',
],
'index3' => 'value3',
]
]
The dimension of the array is unknown.
I have another array that defines what values from dataArray I need to print:
maskArray = ['index2', 'index3' => [ 'index1', 'index2' => [ 'index1' ] ]]
I need to output an array that match the fields from maskArray and dataArray, so in this case the output should be:
result = [
'index2' => 'value2',
'index3' => [
'index1' => 'value1'
'index2' => [
'index1' => 'value1'
]
]
]
In this scenario the maskArray is 3 levels deep, but it could be n levels deep.
If you don't know how deep the array could be nested, a recursive solution is probably appropriate. Maybe there are better ones, but I think this is quite short and readable:
function recursiveFilter($data, $whiteList)
{
$results = [];
foreach ($data as $key => $value) {
// if the current key is on the whitelist, either as value
// or as a key (it's a key if it's nested, otherwise a value)
if(in_array($key, $whiteList) or array_key_exists($key, $whiteList))
{
// if the current value is an array and the whitelist
// has filters for that array, then call this function
// again with just the relevant portion of data and filters,
// otherwise just grab the whole value as it is.
$results[$key] = is_array($value) && isset($whiteList[$key])
? recursiveFilter($value, $whiteList[$key])
: $value;
}
}
return $results;
}
Here's a working example: http://codepad.viper-7.com/16JIJf
Related
I have an array with two items, which are also arrays themselves: product and countries.
There are cases in which the countries array is the same for more than one product, like basic and pro in the example below.
Given this array:
$array = [
[
'product' => [
'value' => 'basic',
'label' => 'Basic'
],
'countries' => [
'Japan', // these
'Korea' // two...
],
],
[
'product' => [
'value' => 'pro',
'label' => 'Pro'
],
'countries' => [
'Japan', // ...and these two
'Korea' // are identical...
],
],
[
'product' => [
'value' => 'expert',
'label' => 'Expert'
],
'countries' => [
'Japan',
'France'
],
]
];
I would like to create new arrays grouped by countries, more precisely,
this is the result I'm after:
$array = [
[
'product' => [
[
'value' => 'basic',
'label' => 'Basic'
],
[
'value' => 'pro',
'label' => 'Pro'
]
],
'countries' => [
'Japan', // ...so they are now one single array
'Korea' // as the two products 'basic' and 'pro' have been grouped
],
],
[
'product' => [
'value' => 'expert',
'label' => 'Expert'
],
'countries' => [
'Japan',
'France'
],
]
];
As you can see in the second snippet, what I'm trying to do is to group basic and pro together in the same array, since they both share the exact same countries (Korea and Japan).
I've been trying for days to play around with this code, but it only seems to work if product and countries are strings rather than arrays:
$grouped = array();
foreach ($array as $element) {
$grouped[$element['countries']][] = $element;
}
var_dump($grouped);
This might be what you want
$productsByCountrySet = [];
foreach ($array as $product) {
$countries = $product['countries'];
sort($countries);
$countrySet = implode('/', $countries);
if (isset($productsByCountrySet[$countrySet])) {
$productsByCountrySet[$countrySet]['product'][] = $product['product'];
} else {
$productsByCountrySet[$countrySet] = [
'product' => [$product['product']],
'countries' => $countries,
];
}
}
$products = [];
foreach ($productsByCountrySet as $p) {
if (count($p['product']) == 1) {
$p['product'] = $p['product'][0];
}
$products[] = $p;
}
print_r($products);
It produces the output you're aiming for. It assumes that the order of countries is not significant (ie ['Japan', 'Korea'] is the same as ['Korea', 'Japan'])
It works by turning your countries array into a string (['Japan', 'Korea'] becomes 'Japan/Korea'), then uses that as a unique key for the entries. It builds up the desired output array by first assembling the unique key (I called it 'country set') and then checking if it has already been seen. If it has, the product is appended, if not, a new item is added to the output array.
The final section handles the case where there is only one product for a country set. We loop and catch this state, modifying the output accordingly.
I personally would not build the result structure that you are seeking because it would make the array processing code more convoluted, but hey, it's your project.
You need to establish consistent, first-level string keys in your result array so that you can determine if a set of countries has been encountered before.
If never encountered, save the full row data to the group.
If encountered, specifically, for a second time, you need to restructure the group's data (this is the elseif() logic).
If encountered more than twice, you can safely push the product's row data as a new child of the deeper structure.
Code: (Demo)
$result = [];
foreach ($array as $row) {
sort($row['countries']);
$compositeKey = implode('_', $row['countries']);
if (!isset($result[$compositeKey])) {
$result[$compositeKey] = $row;
} elseif (isset($result[$compositeKey]['product']['value'])) {
$result[$compositeKey]['product'] = [
$result[$compositeKey]['product'],
$row['product']
];
} else {
$result[$compositeKey]['product'][] = $row['product'];
}
}
echo json_encode(array_values($result), JSON_PRETTY_PRINT);
This general approach is efficient and direct because it only makes one pass over the array of data.
See my related answer: Group array row on one column and form subarrays of varying depth/structure
<?php
$array = [
[
'product' => [
'value' => 'basic',
'label' => 'Basic'
],
'countries' => [
'Japan', // these
'Korea' // two...
],
],
[
'product' => [
'value' => 'pro',
'label' => 'Pro'
],
'countries' => [
'Japan', // ...and these two
'Korea' // are identical...
],
],
[
'product' => [
'value' => 'expert',
'label' => 'Expert'
],
'countries' => [
'Japan',
'France'
],
]
];
// print(serialize($array));
$newarr = [];
//Here I am sorting the countries so that it can be compared and making a new array
foreach ($array as $key) {
$new = $key['countries'];
sort($key['countries']);
sort($key['product']);
$newarr[] = $key;
}
$result = [];
foreach($newarr as $key => $value) {
//Genetraing a unique key for each array type so that it can be compared
$ckey = md5(serialize($value['countries']));
$pkey = md5(serialize($value['product']));
//In the new array, the unique Countries key is used to generate a new array which will contain the product & countries
$result[$ckey]['product'][$pkey] = $value['product'];
//Product key is used to reduce redunant entires in product array
$result[$ckey]['countries'] = $value['countries'];
//This new loop is used to compare other arrays and group them together
foreach($newarr as $key2 => $value2) {
if($key != $key2 && $value['countries'] == $value2['countries']) {
$result[$ckey]['product'][$pkey] = $value2['product'];
}
}
}
print_r($result);
And the output is
Array
(
[00a9d5d0be04135916148f84706a2073] => Array
(
[product] => Array
(
[1c24c036cffc896aebf291da101ff88d] => Array
(
[0] => Pro
[1] => pro
)
[712ef34513bad5c6dd490337c22a5807] => Array
(
[0] => Basic
[1] => basic
)
)
[countries] => Array
(
[0] => Japan
[1] => Korea
)
)
[ae57f65be4cd65148d6f4ed3def12c8f] => Array
(
[product] => Array
(
[be5b95a64169e073ed0b6a72dfb79a83] => Array
(
[0] => Expert
[1] => expert
)
)
[countries] => Array
(
[0] => France
[1] => Japan
)
)
)
This way is little hacky and not the fastest solution but a working one. Since you don't have to do complex array operations it has unique keys rather than an index, that makes the process easy.l
Please read the code comments, I said how it works.
I have a solution in which I create a new array for the result called $newArray and I put the first element from $array into it. I then loop through each element in $array (except for the first one which I exclude using its key). For each element in $array, I loop through each element in $newArray. If both country names are present in $newArray, I just add the product array to $newArray. If there is no element in $newArray with both countries from the $array element being considered then I add the full $array element to $newArray. It does give your required array given your input array.
I had to change the way the product array appears in $newArray which explains the second and third lines of code below.
The & in &$subNewArr has the effect that $subNewArr is 'passed by reference' which means that it can be altered by the code where it is being used (see https://www.php.net/manual/en/language.types.array.php).
$newArray = [$array[0]];
$newArray[0]['product'] = [];
$newArray[0]['product'][] = $array[0]['product'];
foreach($array as $key => $subArr){
if($key > 0){
foreach($newArray as &$subNewArr){
if(
in_array($subArr['countries'][0], $subNewArr['countries']) &&
in_array($subArr['countries'][1], $subNewArr['countries'])
){
array_push($subNewArr['product'], $subArr['product']);
continue 2;
}
}
$newArray[] = $subArr;
}
}
I would like to merge two arrays to compare old vs new values. For example, $arr1 is old values $arr2 is new values.
In case when the data is deleted $arr2 is an empty array. Example:
$arr1 = [
"databases" => [
0 => [
"id" => 1
"name" => "DB1"
"slug" => "db1"
"url" => "https://www.db1.org"
]
]
];
$arr2 = [];
For this my expected output after merge is
$merged = [
"databases" => [
0 => [
"id" => [
'old' => 1,
'new' => null
],
"name" => [
'old' => "DB1",
'new' => null
],
"slug" => [
'old' => "db1",
'new' => null
],
"url" => [
'old' => "https://www.db1.org",
'new' => null
],
]
]
];
if arr2 is different then the values should be present in the new field instead of null.
For example:
$arr1 = [
"databases" => [
0 => [
"id" => 1
"name" => "DB1"
"slug" => "db1"
"url" => "https://www.db1.org"
]
]
];
$arr2 = [
"databases" => [
0 => [
"id" => 5
"name" => "DB2"
"slug" => "db2"
"url" => "https://www.db2.com"
]
]
];
expected output:
$merged = [
"databases" => [
0 => [
"id" => [
'old' => 1,
'new' => 5
],
"name" => [
'old' => "DB1",
'new' => "DB2"
],
"slug" => [
'old' => "db1",
'new' => "db2"
],
"url" => [
'old' => "https://www.db1.org",
'new' => "https://www.db2.com"
],
]
]
];
Case 3 is when $arr1 is empty but $arr2 is populated:
$arr1 = [];
$arr2 = [
"databases" => [
0 => [
"id" => 1
"name" => "DB1"
"slug" => "db1"
"url" => "https://www.db1.org"
]
]
];
and the expected output is:
$merged = [
"databases" => [
0 => [
"id" => [
'old' => null,
'new' => 1
],
"name" => [
'old' => null,
'new' => "DB1"
],
"slug" => [
'old' => null,
'new' => "db1"
],
"url" => [
'old' => null,
'new' => "https://www.db1.org"
],
]
]
];
The inbuilt php functions cannot format the data in old vs new format so was wondering how to go about this? Any solutions/suggestions would be appreciated.
Update
Here is what I had tried before:
I had tried simple array_merge_recursive but it does not store the source array. So if you have $arr1 key not there, the final merged array will only have one value.
I tried some more recursive functions late in the night but failed so in essence didn't have anything to show for what I had tried. However, this morning, I came up with the solution and have posted it as an answer in case anyone needs to use it.
Interesting question, as long as a (non-empty) array on one side means to traverse into it and any skalar or null is a terminating node (while if any of old or new being an array would enforce traversing deeper so dropping the other non-array value):
It works by mapping both old and new on one array recursively and when the decision is to make to traverse to offer null values in case a keyed member is not available while iterating over the super set of the keys of both while null would represent no keys:
$keys = array_unique(array_merge(array_keys($old ?? []), array_keys($new ?? [])));
$merged = [];
foreach ($keys as $key) {
$merged['old'] = $old[$key] ?? null;
$merged['new'] = $new[$key] ?? null;
}
This then can be applied recursively, for which I found it is easier to handle both $old and $new as ['old' => $old, 'new' => $new] for that as then the same structure can be recursively merged:
function old_and_new(array $old = null, array $new = null): array
{
$pair = get_defined_vars();
$map =
static fn(callable $map, array $arrays): array => in_array(true, array_map('is_array', $arrays), true)
&& ($parameter = array_combine($k = array_keys($arrays), $k))
&& ($keys = array_keys(array_flip(array_merge(...array_values(array_map('array_keys', array_filter($arrays, 'is_array'))))))
)
? array_map(
static fn($key) => $map($map, array_map(static fn($p) => $arrays[$p][$key] ?? null, $parameter)),
array_combine($keys, $keys)
)
: $arrays;
return $map($map, $pair);
}
print_r(old_and_new(new: $arr2));
Online demo: https://3v4l.org/4KdLs#v8.0.9
The inner technically works with more than two arrays, e.g. three. And it "moves" the array keys upwards, similar to a transpose operation. Which btw. there is a similar question (but only similar, for the interesting part in context of your question it is not answered and my answer here doesn't apply there directly):
Transposing multidimensional arrays in PHP
After reviewing my own code here is the solution I came up with. I am posting it here in case someone else needs a solution for this:
/**
* Function to merge old and new values to create one array with all entries
*
* #param array $old
* #param array $new
* #return void
*/
function recursiveMergeOldNew($old, $new) {
$merged = array();
$array_keys = array_keys($old) + array_keys($new);
if($array_keys) {
foreach($array_keys as $key) {
$oldChildArray = [];
$newChildArray = [];
if(isset($old[$key])) {
if(!is_array($old[$key])) {
$merged[$key]['old'] = $old[$key];
} else {
$oldChildArray = $old[$key];
}
} else {
$merged[$key]['old'] = null;
}
if(isset($new[$key])) {
if( !is_array($new[$key])) {
$merged[$key]['new'] = $new[$key];
} else {
$newChildArray = $new[$key];
}
} else {
$merged[$key]['new'] = null;
}
if($oldChildArray || $newChildArray) {
$merged[$key] = recursiveMergeOldNew($oldChildArray, $newChildArray);
}
}
}
return $merged;
}
Note - this solution needs testing.
My array looks some what like this (is has multiple sub arrays)
[
'reportId' => '20210623-da1ece3f'
'creationDate' => '2021-06-23 19:50:15'
'dueDate' => '2021-06-24 19:50:15'
'data' => [
'vehicleDetails' => [
'chassisNumber' => 'xxxxx-xxxxxx'
'make' => 'Honda'
'model' => 'City'
'manufactureDate' => '2000'
'body' => 'xxx-xxxxx'
'engine' => 'xxx-xxx'
'drive' => 'FF'
'transmission' => 'MT'
]
]
i'm trying to rename all keys from chassisNumber to chassis_number. This is what i've done so far
function changeArrayKeys($array)
{
if(!is_array($array)) return $array;
$tempArray = array();
foreach ($array as $key=>$value){
if(is_array($value)){
$value = changeArrayKeys($value);
}else{
$key = strtolower(trim(preg_replace('/([A-Z])/', '_$1', $key)));
}
$tempArray[$key]=$value;
}
return $tempArray;
}
print_r(changeArrayKeys($data)); die;
This code kind of works. it just doesn't replace the sub keys. like here
'data' => [
'vehicleDetails' => [ ]
]
but inside vehicleDetails[] it replace properly. Any idea what im missing here? or is there a better and more efficient way to do this instead of recursively ?
This looks pretty obvious to me:
if(is_array($value)){
$value = changeArrayKeys($value);
}else{
$key = strtolower(trim(preg_replace('/([A-Z])/', '_$1', $key)));
}
If the $value is an array, you do a recursive call, but you do not change the key of that nested array in any way. It could help to move the $key manipulation out of the else branch
Let's say I have an array like:
$array = [
'value1',
71,
'stringKey' => 'value2',
3 => 'value3',
4 => 'value4',
64 => 'value5',
'value6',
];
I want to loop through the entries and do a different thing depending on whether the entry has a key "manually set" (e.g., 64 => 'value5'), or it's just a value with a "sequential" key (e.g., 'value1' - which is actually 0 => 'value1').
foreach ($array as $key => $value) {
if (/* $key has not been "manually set" (i.e., is "sequential") */) {
$result[$value] = 'default';
} else {
$result[$key] = $value;
}
}
So my resulting array would be:
[
'value1' => 'default',
71 => 'default',
'stringKey' => 'value2',
3 => 'value3',
4 => 'value4',
64 => 'value5',
'value6' => 'default',
]
I have been trying with array_keys(), checking if is_numeric($key), but nothing works for all the entries above.
I'm starting to think this is actually impossible...
You could do something like this:
$i = 0;
foreach($array as $key => $value) {
if ($key == $i) {
$result[$value] = 'default';
} else {
$result[$key] = $value;
}
if (is_integer($key) && $key >= $i) $i = (int)$key + 1;
}
However, it will treat 4 => 'value4' differently from what you had indicated, because the input array would be exactly the same if you would have put just 'value4'. There is no way to determine from the array whether you had explicitly mentioned the key 4 or omitted the key.
So the above code treats this key/value pair in the same way that it treats the last key/value pair.
The output is:
array (
'value1' => 'default',
71 => 'default',
'stringKey' => 'value2',
3 => 'value3',
'value4' => 'default',
64 => 'value5',
'value6' => 'default',
)
I have a multidimensional array with variable number of levels of data. That is, I can't be sure how many iterations it will take to reach the Goal level, which is an array. Something like this:
[
'key1' = 'value1',
'key2' = 'value2',
'key3' = [
'key4' => [
'key5' => 'value3'
],
'key6' => 'value4'
],
'key7' => [
'Goal' => [
'value5',
'value6',
'value7'
]
],
'key8' => 'value8'],
'key9' => [
'Goal' => [
'value9',
'Foo',
'value10'
]
]
]
I've tried both array_walk_recursive and ArrayIterator, but neither seems to quite get me where I need to be.
I need to go through each element of the array, and if the key is Goal examine the value (eg. the array that Goal holds) and see if that array contains the value Foo.
If Foo is found in the array, I need to add a new value (in addition to Foo-- so call it Bar) to the array and then continue, since there may be more Goals in the parent array.
Is there a way to "stop" the iterator when we find a Goal, without iterating further, and then do the array_search operation?
Edit: Trying somethings along these lines--
$iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($array));
foreach($iterator as $key => $value)
{
if($key == 'Goal')
{
if (is_array($value)) {
if(array_search('Foo', $value)) {
$value[] = 'Bar';
}
}
}
}
Not entirely sure if this is what you want to achieve but here's a solution which adds Bar to arrays nested in the Goal key:
$array = [
'key1' => 'value1',
'key2' => 'value2',
'key3' => [
'key4' => [
'key5' => 'value3',
],
'key6' => 'value4',
],
'key7' => [
'Goal' => [
'value5',
'value6',
'value7',
],
],
'key8' => 'value8',
'key9' => [
'Goal' => [
'value9',
'Foo',
'value10',
],
],
];
function iterate(array $data, $goal = false)
{
foreach ($data as $key => &$value) {
if (is_array($value)) {
$value = iterate($value, $key === 'Goal');
} elseif (is_string($value)) {
if (($value === 'Foo') && $goal) {
$data[] = 'Bar';
return $data;
}
}
}
return $data;
}
var_export(iterate($array));
The code generates the following output:
array (
'key1' => 'value1',
'key2' => 'value2',
'key3' =>
array (
'key4' =>
array (
'key5' => 'value3',
),
'key6' => 'value4',
),
'key7' =>
array (
'Goal' =>
array (
0 => 'value5',
1 => 'value6',
2 => 'value7',
),
),
'key8' => 'value8',
'key9' =>
array (
'Goal' =>
array (
0 => 'value9',
1 => 'Foo',
2 => 'value10',
3 => 'Bar',
),
),
)
Iterators in my opinion would be weird to use in these kind of arrays... I would do it with something like this:
/*
Usage:
$wasFound = checkArray( "Goal", "Foo", $the_array);
if ( $wasFound ) echo "Key and Value pair found in the array!";
else { /* not found */ }
*/
function checkArray( $key_to_find, $value_to_find, $my_var, $last_key = NULL ) {
$found = FALSE;
if ( $last_key == $key_to_find && $my_var == $value_to_find )
return TRUE;
if ( $my_var == NULL )
return FALSE;
if ( is_array( $my_var ) ) {
foreach ( $my_var AS $key => $value )
{
if ( $found ) {
/* Do something else if needed when found */
break;
}
$found = checkArray( $key_to_find, $value_to_find, $value, $key );
}
}
return $found;
}
I agree that recursion is the way to do this. The problem with using array_walk_recursive is that you will not be able to see the Goal key, because as per the PHP documentation,
Any key that holds an array will not be passed to the function.
I am not really sure whether using a RecursiveIteratorIterator would be better than just writing a recursive function for it. A function for something like this should be fairly simple.
function addBar(&$array) {
foreach ($array as $key => &$value) {
if ($key === 'Goal') {
if(array_search('Foo', $value)) {
$value[] = 'Bar';
}
} elseif (is_array($value)) {
addBar($value);
}
}
}
This function takes a reference to your array, so it will update your actual array rather than creating a copy with Bar added to each Goal.