Recursively rename caps in array keys to dash and lower case - php

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

Related

Group rows of data by subarray in column and create subsets with variable depth

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;
}
}

Search multidimensional array containing associative and indexed levels for a value then get a related element's value

I am trying to extract a value from an array by searching by another value.
I have the uri value and I require the playcount value that corresponds with the uri.
What is the best approach to traverse this multi-level array and return the desired data?
My current code:
$decoded = json_decode($response, true);
$trackids = 'spotify:track:'. $trackid .'';
$key = array_search($trackids, array_column($decoded, 'playcount'));
$result = $decoded[$key]['playcount'];
echo "Result: ";
echo $result;
I think it is incomplete and not sure how to proceed from there as it doesn't work.
The $decoded array:
$decoded = [
'success' => 1,
'data' => [
'uri' => 'spotify:album:3T4tUhGYeRNVUGevb0wThu',
'name' => 'รท (Deluxe)',
'cover' => [
'uri' => 'https://i.scdn.co/image/ab67616d00001e02ba5db46f4b838ef6027e6f96'
],
'year' => 2017,
'track_count' => 16,
'discs' => [
[
'number' => 1,
'name' => null,
'tracks' => [
[
'uri' => 'spotify:track:7oolFzHipTMg2nL7shhdz2',
'playcount' => 181540969,
'name' => 'Eraser',
'popularity' => 63,
'number' => 1,
'duration' => 227426,
'explicit' => null,
'playable' => 1,
'artists' => [
[
'name' => 'Ed Sheeran',
'uri' => 'spotify:artist:6eUKZXaKkcviH0Ku9w2n3V',
'image' => [
'uri' => 'https://i.scdn.co/image/ab6761610000517412a2ef08d00dd7451a6dbed6'
]
]
]
],
[
'uri' => 'spotify:track:6PCUP3dWmTjcTtXY02oFdT',
'playcount' => 966197832,
'name' => 'Castle on the Hill',
'popularity' => 79,
'number' => 2,
'duration' => 261153,
'explicit' => null,
'playable' => 1,
'artists' => [
[
'name' => 'Ed Sheeran',
'uri' => 'spotify:artist:6eUKZXaKkcviH0Ku9w2n3V',
'image' => [
'uri' => 'https://i.scdn.co/image/ab6761610000517412a2ef08d00dd7451a6dbed6'
]
]
]
]
]
]
]
]
];
$key = array_search($trackids, array_column($decoded, 'playcount')); in this line, you make two mistake. First, there is no column like playcount in $decode array. Second, you are searching with uri key, not playcount key. There is one more thing, discs key and track inside discs key, both are multidimensional array. So if you want to fetch the exact value, This query will be,
$decoded = array_map(function($x) {
return array_column($x, 'url');
}, array_column($decoded['data']['discs'], 'tracks'));
$decoded = call_user_func_array('array_merge', $decoded);
$key = array_search($trackids, $decoded);
You're on the right track, but you need to dig down deeper into the array structure to search for the tracks within each disc.
$decoded = json_decode($response, true);
$trackid = '7oolFzHipTMg2nL7shhdz2';
$trackidString = 'spotify:track:' . $trackid;
$playcount = null;
// Loop through the discs looking for the track
foreach ($decoded['data']['discs'] as $currDisc)
{
// If we find the track, get the playcount and break out of the loop
$key = array_search($trackidString, array_column($currDisc['tracks'], 'uri'));
if($key !== false)
{
$playcount = $currDisc['tracks'][$key]['playcount'];
break;
}
}
assert($playcount == 181540969, 'We should find the expected playcount value');
echo 'Result: ' . $playcount . PHP_EOL;
I see some inefficient usages of array functions for a task that is cleanest and most performantly performed with classic nested loops. When searching traversable structures for a solitary qualifying value, it is not best practice to use array functions because they do not permit "short-circuiting" or a break/return. In other words, array functions are a poor choice when you do not need to iterate the entire structure.
While traversing your array structure, whenever you encounter indexed keys on a given label, you should implement another foreach(). There are only 2 levels with indexes on your way to the targeted uri and playcount values, so only 2 foreach() loops are needed. When you find your targeted uri, extract the value then break your loop.
Code: (Demo)
$needle = 'spotify:track:7oolFzHipTMg2nL7shhdz2';
foreach ($decoded['data']['discs'] as $disc) {
foreach ($disc['tracks'] as $track) {
if ($track['uri'] === $needle) {
echo $track['playcount'];
break;
}
}
}

Recursive function not working when matching more than one item

An array with the following structure
[
'name' => :string
'size' => :string|int
]
must be recursively converted into an object anytime it encounters this structure in iteration.
So, I wrote these functions
// Converts an array into the instance of stdclass
function to_entity(array $file){
$object = new stdclass;
$object->name = $file['name'];
$object->size = $file['size'];
return $object;
}
// Converts required arrays into objects
function recursive_convert_to_object(array $files){
foreach ($files as $name => $file) {
// Recursive protection
if (!is_array($file)) {
continue;
}
foreach ($file as $key => $value) {
if (is_array($value)) {
// Recursive call
$files[$name] = recursive_convert_to_object($files[$name]);
} else {
$files[$name] = to_entity($file);
}
}
}
return $files;
}
Now, when providing an input like this:
$input = [
'translation' => [
1 => [
'user' => [
'name' => 'Tom',
'size' => 2
]
]
]
];
And calling it like this, it works as expected (i.e an ecountered item user is being converted to stdclass via to_entity() function:
print_r( recursive_convert_to_object($input) );
Now here's the problem:
if an input like this gets provded (i.e one item with the key 4 => [...] is added) it no longer works throwing E_NOTICE about undefined indexes both name and size.
$input = [
'translation' => [
1 => [
'user' => [
'name' => 'Tom',
'size' => 2
]
],
4 => [
'user' => [
'name' => 'Tom',
'size' => 5
]
],
]
];
Let me repeat, that the depth of the target array is unknown. So no matter what depth is, it must find and convert an array into the object.
So where's the logical issue inside that recursive function?
Not sure if this is the best method to solve the problem, but as you already have invested some time in it.
The issue is that once you have replaced some of the content in the array, you put an object back in the array. Then when you test if it's an array in
if (is_array($value)) {
which it isn't so you convert it into an entity, it's trying to convert the object again.
To stop this happening...
if ( !is_object($value)) {
$files[$name] = to_entity($file);
}

Select what fields to display in an array of arrays

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

Get path and value of all elements in nested associative array

Consider an associative array of arbitrary form and nesting depth, for example:
$someVar = array(
'name' => 'Dotan',
'age' => 35,
'children' => array(
0 => array(
'name' => 'Meirav',
'age' => 6,
),
1 => array(
'name' => 'Maayan',
'age' => 4,
)
),
'dogs' => array('Gili', 'Gipsy')
);
I would like to convert this to an associative array of paths and values:
$someVar = array(
'name' => 'Dotan',
'age' => 35,
'children/0/name' => 'Meirav',
'children/0/age' => 6,
'children/1/name' => 'Maayan',
'children/1/age' => 4,
'dogs/0' => 'Gili',
'dogs/1' => 'Gipsy'
);
I began writing a recursive function which for array elements would recurse and for non-array elements (int, floats, bools, and strings) return an array $return['path'] and $return['value']. This got sloppy quick! Is there a better way to do this in PHP? I would assume that callables and objects would not be passed in the array, though any solution which deals with that possibility would be best. Also, I am assuming that the input array would not have the / character in an element name, but accounting for that might be prudent! Note that the input array could be nested as deep as 8 or more levels deep!
Recursion is really the only way you'll be able to handle this, but here's a simple version to start with:
function nested_values($array, $path=""){
$output = array();
foreach($array as $key => $value) {
if(is_array($value)) {
$output = array_merge($output, nested_values($value, (!empty($path)) ? $path.$key."/" : $key."/"));
}
else $output[$path.$key] = $value;
}
return $output;
}
function getRecursive($path, $node) {
if (is_array($node)) {
$ret = '';
foreach($node as $key => $val)
$ret .= getRecursive($path.'.'.$key, $val);
return $ret;
}
return $path.' => '.$node."\n";
}
$r = getRecursive('', $someVar);
print_r($r);
All yours to place it in an array.

Categories