Recursively replace placeholder substrings based on related row data - php

I have this array:
$data = [
["id" => 1, "data" => "data 1"],
["id" => 2, "data" => "data <4>"],
["id" => 3, "data" => "data 3"],
["id" => 4, "data" => "<3>"]
];
I want to produce a new array. The resulting array should be:
[
["id" => 1, "data" => "data 1"],
["id" => 2, "data" => "data data 3"],
["id" => 3, "data" => "data 3"],
["id" => 4, "data" => "data 3"]
]
The idea is that each time there is <number> in the data attribute then this value should be replaced by the data attribute in the element which has the same id. In the example above, the last element is: ["id" => 4, "data" => "<3>"]
So we replace <3> with data 3 since it is what is stored in the data attribute of element with id => 3.
I have already created a function that works with the above array:
public function refProcess($data, &$newData, $i, &$tmpData){
$dataLength = count($data);
if($i>=$dataLength){
return;
}
for(;$i<$dataLength;$i++){
if(is_null($tmpData)){
$tmpData = ['id'=> $data[$i]['id'], 'data'=>null];
}
if(strpos($data[$i]['data'],"[")!==false){
$parsed = $this->getInbetweenStrings("<", ">", $data[$i]['data']);
if(count($parsed)){
foreach($parsed as $occurance){
foreach($data as $key => $dataValue){
if($dataValue['id']==$occurance){
if(strpos($dataValue['data'], "<")!==false){
$this->refProcess($data, $newData, $key, $tmpData);
$tmpData=null;
}
else{
$tmpDataAtt = str_replace("<".$occurance.">", $dataValue['data'], $data[$i]['data']);
$tmpData['data'] = $tmpDataAtt;
$newData [] = $tmpData;
$tmpData = null;
break;
}
}
}
}
}
}
else{
$tmpData['data'] = $data[$i]['data'];
$newData [] = $tmpData;
$tmpData = null;
}
}//foreach
}
//returns an array contains strings existing between $start and $end. Multiple occurance
public function getInbetweenStrings($start, $end, $str){
$matches = array();
$regex = "/$start([a-zA-Z0-9_]*)$end/";
preg_match_all($regex, $str, $matches);
return $matches[1];
}
It works fine until I add another element to the array:
$data = [
["id" => 1, "data" => "data 1"],
["id" => 2, "data" => "data <4>"],
["id" => 3, "data" => "data 3"],
["id" => 4, "data" => "<3>"]
["id" => 5, "data" => "<2>"]
];
Element with id:5 the function goes into an endless loop. What am I missing?
The code can be tested at https://onlinephp.io/c/0da5d

Your code is too clumsy to begin with. I would rather suggest a much simpler approach.
Index your array with id value as it's key with array_column. This way, we can access any index with id of a particular value in O(1) time. This also gives you an advantage for the value of the id to be anything and not necessarily being symmetric with your subarray index key.
Capture the ID using regex. If there is a match, recurse again, else, our search ends here. Return it's data value to the parent call and you are done.
Snippet:
<?php
$data = array_column($data, null, 'id');
foreach($data as $id => $d){
getLeafNodeValue($id, $data);
}
function getLeafNodeValue($id, &$data){
$matches = [];
if(preg_match('/<(\d+)>/', $data[$id]['data'], $matches) === 1){
$data[$id]['data'] = preg_replace('/<(\d+)>/', getLeafNodeValue($matches[1], $data), $data[$id]['data']);
}
return $data[$id]['data'];
}
print_r($data);
Online Demo

You dont really do any recursive stuff, is a quit flat array structure you have.
UPDATE: This was wrong, keep it just for the related comments:
<?php
$data = [
["id" => 1, "data" => "data 1",],
["id" => 2, "data" => "data <4>",],
["id" => 3, "data" => "data 3",],
["id" => 4, "data" => "<3>",],
["id" => 5, "data" => "<2>",]
];
foreach ($data as &$set) {
$set['data'] = preg_replace_callback('#.*(\d+).*#', function ($m) {
return 'data ' . $m[1];
},$set['data']);
}
print_r($data);
UPDATE:
Here is now a working solution.
<?php
$data = [
["id" => 1, "data" => "data 1",],
["id" => 2, "data" => "data <4>",],
["id" => 3, "data" => "data 3",],
["id" => 4, "data" => "<3>",],
["id" => 5, "data" => "<2>",],
["id" => 6, "data" => "data <7>",],#loop to 7
["id" => 7, "data" => "<6>",],#loop to 6
["id" => 8, "data" => "<0>",],#dead link
["id" => 9, "data" => "<10>",],#multi level loop
["id" => 10, "data" => "data <11>",],#multi level loop
["id" => 11, "data" => "<9>",],#multi level loop
];
#just for testing: order can be ignored
shuffle($data);
#step 1 make keys easy to access
$prepared = [];
$tmp=[];
foreach ($data as $set) {
$prepared[$set['id']] = $set['data'];
$tmp[$set['id']] = $set;
}
$data=$tmp;
#setp 2 replace values
$final = [];
do {
foreach ($data as $k => &$set) {
$set['data'] = preg_replace_callback('#(.*)<(\d+)>#', function ($m) use ($prepared,$k) {
#check for dead links
if(!isset($prepared[$m[2]])){
return $m[1]." ?{$m[2]}?";
}
#check for loop refer
if(strpos($prepared[$m[2]],"<".$k.">")!==false){
return $m[1]." §{$m[2]}§";
}
return $m[1].$prepared[$m[2]];
}, $set['data']);
if (strpos($set['data'], '>') === false) {
$final[$k] = $set;
unset($data[$k]);
}
}
} while (count($data));
ksort($final);
print_r($final);
UPDATED:
Now checks for dead links or loops and marks them.
Added more prepare code, so order is now ignorable

Your code is way too complicated, there's an easier approach. Note, I reindex the initial data to the key:id -> value:array, to avoid all those loops for finding the entry for required id.
<?php
$data = [
[
"id"=>1,
"data"=>"data 1",
],
[
"id"=>2,
"data"=>"data <4>",
],
[
"id"=>3,
"data"=>"data 3",
],
[
"id"=>4,
"data"=>"<3>",
],
[
"id"=>5,
"data"=>"<2>",
]
];
function getValue( $arr, $id ){
// Required data contains placeholder, going deeper
if( preg_match("/<\d+>/", $arr[$id]['data']) ){
return preg_replace_callback("/<(?<source_id>\d+)>/", function( $matches ) use ($arr) {
return getValue( $arr, $matches['source_id'] );
}, $arr[$id]['data'] );
}
// Return raw value as-is
return $arr[$id]['data'];
}
// Reindexing to get convinient access
$indexed_data = [];
foreach( $data as $entry )
$indexed_data[ $entry['id'] ] = $entry;
// Resolving through recursive getValue function
foreach( $indexed_data as $id => &$entry )
$entry['data'] = getValue( $indexed_data, $id );
// Output in desired format
print_r( array_values( $indexed_data ) );
If there may be two entries with the same id, this approach not gonna work.

Here is a tidy recursive snippet that will modify your array by reference.
Loop over the array (I am choosing to use array de-structuring syntax to declare individual row variables.
If the needle is null or matches the row id, attempt the recursive replacement.
If the needle matches a row's id, return the value to the parent level, so that eventually the top level array is affected.
Code: (Demo)
function recurse(&$array, $needle = null) {
foreach ($array as ['id' => $id, 'data' => &$data]) {
if (($needle ?? $id) === $id) {
$data = preg_replace_callback(
'/<(\d+)>/',
fn($m) => recurse($array, (int) $m[1]),
$data
);
}
if ($needle === $id) {
return $data;
}
}
}
recurse($array);
var_export($array);

Related

Filter array of associative arrays with a dynamic associative array where all elements must be matched

I need to filter my indexed array of associative arrays using an associative that may have different combinations of elements and potentially a variable number of elements.
$users = [
[
"name" => "ali",
"age" => 22,
"score" => 12
],
[
"name" => "hasan",
"age" => 32,
"score" => 52
],
];
And
$filters = [
"name" => "ali",
"age" => 22
];
I need to filter the $users array dynamically depending on this $filters.
function filter($item)
{
// i dont how what should write here
}
$filtered_users = array_filter($users, 'filter');
To filter, you need to iterate over all the filters in the filterFunc callback function and check if the filter applies. If one does not apply return false immediatly, else return true:
<?php
$users = array(
0 => array(
"name" => "ali",
"age" => 22,
"score" => 12
),
1 => array(
"name" => "hasan",
"age" => 32,
"score" => 52
),
);
$filters = array(
"name" => "ali",
"age" => 22
);
function filterFunc($item) {
global $filters;
foreach ($filters as $key => $value) {
if ($item[$key] != $value) {
return false;
}
}
return true;
}
$filtered_users = array_filter($users, "filterFunc");
print_r($filtered_users);
?>
For a concise, fully functional-style approach, check that there is no remaining, unmatched element in the $filters array while comparing each row. This approach will not break even if keys in $filters do not exist in the $users rows. (Demo)
var_export(
array_filter(
$users,
fn($row) => !array_diff_assoc($filters, $row)
)
);
For best performance, use a conditionally breakable loop which will check each key-value pair in the filters array against each row. This assumes/requires that all keys nominated in filters will be guaranteed to exist in the $users array. (Demo)
var_export(
array_filter(
$users,
function($row) use($filters) {
foreach ($filters as $fkey => $fval) {
if ($row[$fkey] != $fval) {
return false;
}
}
return true;
}
)
);

Remove from multidimensional array if has seen id before in an basic array

I would like in php to stop duplicate messages by logging msgid to a text file using something like this file_put_contents("a.txt", implode(PHP_EOL, $array1), FILE_APPEND);
and then converting it back to an array using $array1 = file("a.txt"); I would also like to delete messages from the array if they are from a set name
I know how to convert json to an array $array1 = json_decode($json, true);
Json Reply from an api that I cannot control
{
"API": "Online",
"MSG": [
{
"info": {
"name": "example"
},
"msg": "example",
"msgid": "example"
},
{
"info": {
"name": "example"
},
"msg": "example",
"msgid": "example"
}
]
}
Hi use the following code, first test it out accordingly
$uniqueMessages = unique_multidim_array($messages,'msg');
Usage : Pass the key as the 2nd parameter for which you need to check the uniqueness of array.
<?php
/* Function to handle unique assocative array */
function unique_multidim_array($array, $key) {
/* temp array to hold unique array */
$temp_array = array();
/* array to hold */
$i = 0;
/* array to hold the key for unique array */
$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;
}
$messages = array(
0 => array(
'info' => array(
'name' => 'example'
),
'msg' => 'example',
'msgid' => 'example'
),
1 => array(
'info' => array(
'name' => 'example 1'
),
'msg' => 'example 1',
'msgid' => 'example 1'
),
3 => array(
'info' => array(
'name' => 'example'
),
'msg' => 'example',
'msgid' => 'example'
)
);
echo '<pre>';
echo '*****************BEFORE***********************<br/>';
var_dump($messages);
echo '*****************AFTER***********************<br/>';
$uniqueMessages = unique_multidim_array($messages,'msg');
var_dump($uniqueMessages);
This works for me this is an modded function click here for original function
function RemoveElementByArray($array, $key, $seen){
foreach($array as $subKey => $subArray){
if(in_array($subArray[$key], $seen)){
unset($array[$subKey]);
}
}
return $array;
}
Example:
$array = array(
array("id" => "1", "name" => "example1"),
array("id" => "2", "name" => "example2"),
array("id" => "3", "name" => "example3"));
$SeenArray = array("1", "2");
print_r(RemoveElementByArray($array, "id", $SeenArray));
Result:
Array
(
[2] => Array
(
[id] => 3
[name] => example3
)
)

Search for matching subarray in parent or child of a multidimensional array

I have the following multidimensional array to build a dynamic menu:
[
"3gnitjUdm6" => [
"name" => "Overview",
"slug" => "overview",
"priority" => 1,
"pages" => [
"i3OQlLqgqO" => [
"name" => "Dashboard",
"url" => "",
"priority" => 2,
"subpages" => [],
],
"izma1tvjGd" => [
"name" => "Settings",
"url" => "/settings",
"priority" => 4,
"subpages" => [],
]
]
],
"IcSujiIx9A" => [
"name" => "Content",
"slug" => "content",
"priority" => 5,
"pages" => [
"3KJdhtCRuI" => [
"name" => "Users",
"url" => "/users",
"priority" => 2,
"subpages" => [],
],
"M3zw9hq6rW" => [
"name" => "Pets",
"url" => "/pets",
"priority" => 4,
"subpages" => [],
],
],
],
]
Each section contains an array of pages, and each page can contain an array of subpages. I need to be able to search through this array to find the key of the section using a key and value pair.
private function _find_section($key, $value) {
foreach($this->menu as $section_key => $section) {
if(is_array($section[$key])) {
foreach($section[$key] as $sub_key => $sub) {
if($sub_key === $value) {
return $section_key;
}
}
} elseif(is_string($section[$key])) {
if($section[$key] === $value) {
return $section_key;
}
} else {
return false;
}
}
}
Running the following code:
_find_section('name', 'Content')
Always returns false.
function flatten(array $collection, array $nested_keys = []) {
$output = [];
foreach ($collection as $key => $value) {
foreach ($nested_keys as $nested_key) {
if (isset($value[$nested_key]) && is_array($value[$nested_key])) {
$output = array_merge($output, flatten($value[$nested_key], [$nested_key]));
}
}
$output[$key] = $value;
}
return $output;
}
function column(array $collection, string $key) {
return array_combine(
array_keys($collection),
array_map(function ($row) use ($key) { return $row[$key]; }, $collection)
);
}
function find_section(array $menu, string $key, string $value) {
$set = column(flatten($menu, ['pages', 'subpages']), $key);
return array_search($value, $set);
}
var_dump(find_section($menu, 'name', 'Dashboard')); // string(10) "i3OQlLqgqO"
You might want to try doing a recursive function instead and forego hardcoded foreach() loops, then you can easily search through many levels of your array using this method:
function recurseFind($array,$findK,$findV,$last=false)
{
if(is_object($array))
$array = (array) $array;
foreach($array as $key => $value) {
if($key == $findK && $value == $findV)
return $last;
if(is_array($value))
$doFind = recurseFind($value,$findK,$findV,$key);
if(!empty($doFind))
return $doFind;
}
}
print_r(recurseFind($arr,'name','Dashboard'));
Gives you:
i3OQlLqgqO

How to specifically choose a key and value to store in array PHP

I want to just store in result values from key 'code' instead of all including 'name' and 'id'
Here is the code:
<?php
$array = array(
array(
"name" => "a",
"code" => "416",
"id" => "a1"
),
array(
"name" => "a",
"code" => "522",
"id" => "a2"
),
array(
"name" => "b",
"code" => "580",
"id" => "b1"
)
);
$counts = array_count_values(
array_map(function (array $entry) { return $entry['name']; }, $array)
// or array_column($array, 'name') in PHP 5.5+
);
$uniqueNames = array_keys(
array_filter($counts, function ($count) { return $count == 1; })
);
$result = array_filter($array, function (array $entry) use ($uniqueNames) {
return in_array($entry['name'], $uniqueNames);
});
print_r($result);
Try this to see the result : http://3v4l.org/32JUL#v530
What I want is: store "code" : "580" only instead of all sets.
I improved this one, because there might be possibility to get more ['code'] values with unique ['name'] , this would not be in best fit here. I would apply a simple foreach to get desired result.
$array = array(
array(
"name" => "a",
"code" => "416",
"id" => "a1"
),
array(
"name" => "a",
"code" => "522",
"id" => "a2"
),
array(
"name" => "b",
"code" => "580",
"id" => "b1"
)
);
$counts = array_count_values(
array_map(function (array $entry) { return $entry['name']; }, $array)
// or array_column($array, 'name') in PHP 5.5+
);
$uniqueNames = array_keys(
array_filter($counts, function ($count) { return $count == 1; })
);
$result = array_filter($array, function (array $entry) use ($uniqueNames) {
return in_array($entry['name'], $uniqueNames);
});
foreach ($result as $res){
$uniqueCodes[] = $res['code'];
}
print_r($uniqueCodes);

Run throw array of associative array and find keys

I have two arrays that look like this:
(this one is ordered by value_max)
$max_values = [
["name" => "john", "id" => 5, "value_max" => 500],
["name" => "john", "id" => 3, "value_max" => 200],
...
];
$min_values = [
["name" => "john", "id" => 5, "value_min" => 100],
["name" => "john", "id" => 3, "value_min" => 150],
...
];
And I need to have a final array like this:
(This one stills need to be ordered by value_max, so I assume I could just overwrite the first array with the calculations done with the second)
$max_and_difference_values = [
["name" => "john", "id" => 5, "value_max" => 500, "difference_value" => 400],
["name" => "john", "id" => 3, "value_max" => 200, "difference_value" => 50 ],
...
];
My question is pretty straight: What is the best/effective way to run through both first arrays and build the last one. Assuming that the size of the arrays can be of around 150 elements.
To avoid looping through all arrays repeatedly, index one array by the field you want to merge on, i.e. the 'id' key:
$second = array_combine(array_map(function ($i) { return $i['id']; }, $second_array), $second_array);
Then looping through the other and comparing the values is pretty easy:
$third = array();
foreach ($first_array as $i) {
$third[] = $i + array('difference_value' => $i['value_max'] - $second[$i['id']]['value_min']);
}
If it's guaranteed that both arrays will have exactly matching keys, you don't even need the first step and just go by already existing keys.
This will sort your array. So you can sort the second array.
<?php
$min_values = array(
array("name" => "john", "id" => 5, "value_min" => 100),
array("name" => "john", "id" => 3, "value_min" => 150),
);
function aasort (&$array, $key) {
$sorter=array();
$ret=array();
reset($array);
foreach ($array as $ii => $va) {
$sorter[$ii]=$va[$key];
}
asort($sorter);
foreach ($sorter as $ii => $va) {
$ret[$ii]=$array[$ii];
}
$array=$ret;
}
aasort($min_values,"id");
echo "<pre>";
print_r($min_values);
echo "</pre>";
?>
And then you can use the logic what Alessandro Minoccheri mentioned.
$arr = array();
for ($i=0; $i<count($first_array);$i++){
$arr['name'] = $first_array[$i]['name'];
$arr['id'] = $first_array[$i]['id'];
$arr['value_max'] = $first_array[$i]['value_max'];
$arr['difference_value'] = $first_array[$i]['value_max']-$second[$i['id']]['value_max'] ;
}
As you have not shared how the second array with the minimum values is ordered (in itself and relative to the maximum values array), I'd say, index the minimum values by the id entry and then do the calculation in a second iteration. It should be fast enough, 150 elements is just "nothing":
$max_values = [
["name" => "john", "id" => 5, "value_max" => 500],
["name" => "john", "id" => 3, "value_max" => 200],
];
$min_values = [
["name" => "john", "id" => 5, "value_min" => 100],
["name" => "john", "id" => 3, "value_min" => 150],
];
$min_values_by_id = [];
foreach($min_values as $min) {
$min_values_by_id[$min['id']] = $min['value_min'];
}
$max_and_difference_values = $max_values;
foreach($max_and_difference_values as &$entry)
{
$entry['difference_value'] = $entry['value_max'] - $min_values_by_id[$entry['id']];
}
unset($entry);
print_r($max_and_difference_values);
This is just a straight forward example, nothing fancy. Demo
the following works for me. I'm not a PHP expert though, i.e. I'm not so familiar with cloning (note the empty clone array). Note that it only copies existing properties (if one array does contain a min_value and another does only contain a some other key)
In essence
function convertArr( $arr ) {
// Easy referencing without having to search through the arrays more than once for a matching id
$new_arr = array();
foreach( $arr as $array ) {
$new_arr[ $array['id'] ] = cloneArr( $array );
}
return $new_arr;
}
function merge( $arr1, $arr2 ) {
$convertedArr1 = convertArr( $arr1 );
$convertedArr2 = convertArr( $arr2 );
$arr = array();
// Based on the ordered array
foreach( $convertedArr1 as $array ) {
$id = $array['id'];
$tempArr = array();
foreach( $convertedArr1[ $id ] as $k => $v ) {
$tempArr[ $k ] = $v;
}
foreach( $convertedArr2[ $id ] as $k => $v ) {
$tempArr[ $k ] = $v;
}
array_push( $arr, $tempArr );
}
return $arr;
}
Full example:
<?php
//$arr1 = [ ["name" => "john", "id" => 5, "value_max" => 500], ["name" => "john", "id" => 3, "value_max" => 200] ];
//$arr2 = [ ["name" => "john", "id" => 5, "value_min" => 100], ["name" => "john", "id" => 3, "value_min" => 150] ];
$arr1 = array(
array( "name" => "john", "id" => 5, "value_max" => 500 ),
array( "name" => "john", "id" => 3, "value_max" => 200 )
);
$arr2 = array(
array( "name" => "john", "id" => 5, "value_min" => 100 ),
array( "name" => "john", "id" => 3, "value_min" => 150 )
);
function neatPrint( $arr ) {
echo "<pre>";
print_r( $arr );
echo "</pre>";
}
echo "<h2>Before</h2>";
neatPrint( $arr1 );
neatPrint( $arr2 );
echo "<hr/>";
function cloneArr( $old ) {
// I dunno how to properly clone
return $old;
}
function convertArr( $arr ) {
// Easy referencing without having to search through the arrays more than once for a matching id
$new_arr = array();
foreach( $arr as $array ) {
$new_arr[ $array['id'] ] = cloneArr( $array );
}
return $new_arr;
}
function merge( $arr1, $arr2 ) {
$convertedArr1 = convertArr( $arr1 );
$convertedArr2 = convertArr( $arr2 );
$arr = array();
// Based on the ordered array
foreach( $convertedArr1 as $array ) {
$id = $array['id'];
$tempArr = array();
neatPrint( $convertedArr1[ $id ] );
neatPrint( $convertedArr2[ $id ] );
foreach( $convertedArr1[ $id ] as $k => $v ) {
$tempArr[ $k ] = $v;
}
foreach( $convertedArr2[ $id ] as $k => $v ) {
$tempArr[ $k ] = $v;
}
array_push( $arr, $tempArr );
}
echo "<h2>Result</h2>";
return $arr;
}
echo "<h2>Loopthrough</h2>";
neatPrint( merge( $arr1, $arr2 ) );
?>
Let me know what you think :-)
if the element are in order try this code:
$arr = array();
for ($i=0; $i<count($first_array);$i++){
$arr['name'] = $first_array[$i]['name'];
$arr['id'] = $first_array[$i]['id'];
$arr['value_max'] = $first_array[$i]['value_max'];
$arr['difference_value'] = $first_array[$i]['value_max']-$second[$i['id']]['value_max'] ;
}

Categories