simple way in PHP to split path string into breadth-first array - php

Using PHP 5.2, I'm trying to parse an arbitrary number of path/directory strings into an array, such that they can be processed breadth-first. This is to allow me to script a sparse checkout from a Subversion repository, telescoping the indicated paths. All the paths on the same level have to be specified in the same svn update --depth empty statement.
I get the desired output, but I wonder if there's a cleaner way to do this. (And, yes, I know there are changes needed for efficiency.)
EDIT I modified the original post to handle cases of multiple children in the same parent. My revised code is
$main = array(
'a/b/c1/',
'a/b/c2/d/e1',
'a/b/c2/d/e2',
'A/B/',
'alpha/beta/gamma/delta/epsilon'
);
$splits = array();
$max = 0;
for ($i=0; $i<count($main); $i++) {
$splits[$i] = explode(DIRECTORY_SEPARATOR, trim($main[$i], DIRECTORY_SEPARATOR));
if (count($splits[$i]) > $max) {
$max = count($splits[$i]);
}
}
for ($i=0; $i<$max; $i++) {
$levels[$i] = array();
for ($path=0; $path<count($splits); $path++) {
if (array_key_exists($i, $splits[$path])) {
$levels[$i][] = implode(DIRECTORY_SEPARATOR, array_slice($splits[$path], 0, $i+1));
}
}
$levels[$i] = array_unique($levels[$i]);
sort($levels[$i]); // just to reset indices
}
This changes my output structure to the following, which both provides unique directories at each level and retains sibling nodes.
Array
(
[0] => Array
(
[0] => A
[1] => a
[2] => alpha
)
[1] => Array
(
[0] => A/B
[1] => a/b
[2] => alpha/beta
)
[2] => Array
(
[0] => a/b/c1
[1] => a/b/c2
[2] => alpha/beta/gamma
)
[3] => Array
(
[0] => a/b/c2/d
[1] => alpha/beta/gamma/delta
)
[4] => Array
(
[0] => a/b/c2/d/e1
[1] => a/b/c2/d/e2
[2] => alpha/beta/gamma/delta/epsilon
)
)
In my code, I then iterate over the final $levels array. Unfortunately, this still requires two iterations: one for depth empty and one for depth infinity, but I'm sure that could be worked out.
$count = count($levels);
for ($i=0; $i<$count; $i++) {
echo '<p>', 'svn update --set-depth empty ', implode(' ', $levels[$i]), "</p>\n";
}
$count = count($main);
for ($i=0; $i<$count; $i++) {
echo '<p>', 'svn update --set-depth infinity ', $main[$i], "</p>\n";
}

$levels=array();
$depth=0;
$i=0;
foreach ($main as $m) {
$m=explode('/',$m);
while (sizeof($m)<$depth) $m[]=null;
$d=0;
foreach ($m as $mm) {
if ($d>$depth) {
if (!$mm) break;
$depth=$d;
$levels[$d]=array();
for ($j=0;$j<=$i;$j++) $levels[$d][$j]=null;
}
$levels[$d][$i]=$mm;
$d++;
}
$i++;
}
looks like a good alternative with only one traversion of the array. In short you don't use one pass to decide on the depth, but if you encounter a deeper entry, you just fill the relevant places in the array retroactively with nulls.
$depth has the depth-1 after the loop.
Edit:
This does yet handle a case of multiple children in the same parent, but I am unsure if it does so the way you want

Here's my implementation (it got easier with your latest request):
$levels = array();
for ($i=0; $i<count($main); $i++) {
$splits = explode(DIRECTORY_SEPARATOR, trim($main[$i], DIRECTORY_SEPARATOR));
$current = array();
/* Load every subpath in an array*/
for($j=0; $j<count($splits); $j++) {
$current[$j . "hack"] = implode("/", array_slice($splits, 0, $j+1));
}
$levels = array_merge_recursive($levels, $current);
}
/* Removes duplicates and resets indices */
array_walk($levels, function(&$l, $i) { $l = array_unique($l); sort($l); });
What makes this implementation simple is that I handle each path separately. I only have one loop, and join the results with array_merge_recursive. For example, with "a/b" and "a/c", my code :
creates array(0 => array("a"), 1 => array("a/b")) and array(0 => array("a"), 1 => array("a/c"))
joins them with array_merge_recursive which gives array(0 => array("a", "a"), 1 => array("a/b", "a/c"))
removes unique values with array_unique
resets the indices with sort
Note: I need to use $j + "hack", otherwise array_merge_recursive won't merge the values as expected (try it for yourself).

Related

inserting different array value as a key in current array?

I'm trying to build an array structure to get the data easier.
Maybe somebody can help me?
how can I insert the value as key value from another array into it?
$pers = [
'2019/Herbert',
'2019/Wolfgang',
'2020/Doris',
'2020/Musti',
];
multidimensional array representing filepath
function parse_paths_of_files($array) {
rsort($array);
$result = array();
foreach ($array as $item) {
$parts = explode('/', $item);
$current = &$result;
include 'parentdir/'.$item.'/data.php';
//echo $article['date']; // example: 2020-05-06
for ($i = 1, $max = count($parts); $i < $max; $i++) {
if (!isset($current[$parts[$i - 1]])) {
$current[$parts[$i - 1]] = array();
}
$current = &$current[$parts[$i - 1]];
}
$last = end($parts);
if (!isset($current[$last]) && $last) {
// Don't add a folder name as an element if that folder has items
$current[] = end($parts);
}
}
return $result;
}
echo '<pre>'; print_r(parse_paths_of_files($pers)); echo '</pre>';
The result with automatic indexing:
Array
( // by rsort($array);
[2020] => Array
(
[0] => Herbert
[1] => Wolfgang
)
[2019] => Array
(
[0] => Doris
[1] => Musti
)
)
I would like to use the included date (2020-05-06) as a key:
Array
(
// by rsort($array);
[2020] => Array
( // by rsort(???);
[2020-12-06] => Herbert
[2020-10-09] => Wolfgang
[2020-05-19] => Andy
)
[2019] => Array
(
[2019-12-22] => Doris
[2019-10-02] => Musti
[2019-01-21] => Alex
[2019-01-20] => Felix
)
)
Thanks for your answers, since I am a beginner and do not really understand everything, it is not easy for me to formulate or prepare the questions correctly. Sort for that! Greetings from Vienna!
Assuming that $artilce['date'] is defined in the included file, this builds the proper structure. You might want to check for the date and if not there set to some other value. Also, keep in mind that if more than one article has the same date then only the one appearing last in the $pers array will be in the result:
function parse_paths_of_files($paths, &$array=array()) {
foreach($paths as $path) {
include "parentdir/$path/data.php";
$path = explode('/', $path);
$value = array_pop($path);
$temp =& $array;
foreach($path as $key) {
$temp =& $temp[$key];
}
$temp[$article['date']] = $value;
}
}
parse_paths_of_files($pers, $result);
Built off of my answer to something similar How to access and manipulate multi-dimensional array by key names / path?.
After all of that, with the same caveats, I think this change in your existing code would work:
$current[$article['date']] = end($parts);

Create new array inside a multidimensional array, step up name each cycle

Question:
How can I iterate below so it checks existence of key "round_1", next script run it should check existense of key "round_2", etc. Everytime it would encounter that the key is missing it should create the key.
It is working with "round_1" as expected.
<?php
// Create array skeleton.
$array_skeleton = array_fill(1, 3, "");
print_r($array_skeleton);
// Populate the skeleton with random numbers, values [1 to 6].
foreach($array_skeleton as $key => $value) {
$populated_array[$key] = random_int(1, 6);
};
print_r($populated_array);
// Create empty array for purpose to become multidimensional array.
$scorecard = [];
// Check if [round_1] is missing, if so create [round_1] and populate it.
if(!array_key_exists("round_1", $scorecard)) {
echo "round_1 is missing, creating it";
$scorecard["round_1"] = $populated_array;
}
print_r($scorecard);
Outcome works fine as expected, after first script run:
(
[round_1] => Array
(
[1] => 3
[2] => 4
[3] => 1
)
)
Expected outcome, after second script run:
Note! It is correct that the values would be different per each round since they are randomly created.
(
[round_1] => Array
(
[1] => 3
[2] => 4
[3] => 1
)
[round_2] => Array
(
[1] => 1
[2] => 4
[3] => 2
)
)
I think your entire code can be simplify:
first define function for create array with random number:
function createRandomNumberArray($numOfElem, $maxRange) {
for ($i = 0; $i < $numOfElem; $i++)
$res[] = random_int(1, $maxRange);
return $res;
}
Second, assuming your keys are made of "round_#INT#" pattern you can do
$biggest = max(array_map(function ($e) {$p = explode("_", $e); return $p[1];}, array_keys($arr)));
And now do:
$newKey = "round_" . ($biggest + 1);
$scorecard[$newKey] = createRandomNumberArray(3,6);
Reference: array-map, explode, max, random-int

Creating an associative array from one-dimension array

Was not really sure on what question's title should be here...
Sample .csv:
tennis,soccer,sports
car,plane,things
jeans,shirt,things
My final, ideal, outcome should be an array that looks like this:
Array
(
[sports] => Array
(
[0] => tennis
[1] => soccer
)
[things] => Array
(
[0] => car
[1] => plane
[2] => jeans
[3] => shirt
)
)
Here is my most recent attempt to achieve the outcome above (after many tries):
<?php
$f_name = 'test.csv';
// Stores all csv data
$csv_data = array_map('str_getcsv', file($f_name));
$c = count($csv_data);
$tmp = array();
$data_for_email = array();
for ($i = 0; $i < $c; $i++) {
// Remove last element and make it a key
$le = array_pop($csv_data[$i]);
$tmp[$le] = $csv_data[$i];
$data_for_email = array_merge_recursive($data_for_email, $tmp); // MEMORY ERROR
}
print_r($data_for_email);
?>
This is what I get as a result:
Array
(
[sports] => Array
(
[0] => tennis
[1] => soccer
[2] => tennis
[3] => soccer
[4] => tennis
[5] => soccer
)
[things] => Array
(
[0] => car
[1] => plane
[2] => jeans
[3] => shirt
)
)
As you can see, I get duplicates of .csv's line 1 in [sports] array.
More detailed description of my requirement:
Each line has 3 fields.
3rd field becomes a key in a new associative array.
Two remaining fields (1st and 2nd) become values for that key.
Because multiple lines may (and do) contain identical 3rd field (while combination of 1st and 2nd fields are always different), I need to then merge all these duplicate keys' values into 1.
P.S. I could parse that array (to remove duplicate values) afterwards, but the real .csv file is large and it becomes too slow to process it, and I receive the following error at the line which I marked with // MEMORY ERROR:
Fatal Error: Allowed Memory Size of 134217728 Bytes Exhausted
I tried increasing the memory limit but I'd prefer to avoid this if possible.
Should be a little easier. No need for array_merge_recursive:
foreach($csv_data as $row) {
$key = array_pop($row);
if(!isset($data_for_email[$key])) {
$data_for_email[$key] = [];
}
$data_for_email[$key] = array_merge($data_for_email[$key], $row);
}
More memory efficient would be:
Not reading the whole file in memory. fgetcsv reads one line at a time
Avoiding a recursive merge
Code:
$handle = fopen($f_name, 'r');
if (!$handle) {
// Your error-handling
die("Couldn't open file");
}
$data_for_email = array();
while($csvLine = fgetcsv($handle)) {
// Remove last element and make it a key
$le = array_pop($csvLine);
if (isset($data_for_email[$le])) {
$data_for_email[$le] = array_merge($data_for_email[$le], $csvLine);
} else {
$data_for_email[$le] = $csvLine;
}
}
fclose($handle);
You just need to initialize $tmp in every loop which will resolve your problem. Check below code:
for ($i = 0; $i < $c; $i++) {
// Remove last element and make it a key
$le = array_pop($csv_data[$i]);
$tmp = []; //Reset here
$tmp[$le] = $csv_data[$i];
$data_for_email = array_merge_recursive($data_for_email, $tmp); // MEMORY ERROR
}
Hope it helps you.
Use the name for the key to get a unique list. It is cheaper than merge if there is a lot of data.:
$handle = fopen('test.csv', 'r');
$res = [];
while ($data = fgetcsv($handle)) {
list($first, $second, $type) = $data;
$res[$type] = ($res[$type] ?? []);
array_map(function($e)use(&$res, $type) {
$res[$type][$e] = $e;
}, [$first, $second]);
}
output:
Array
(
[sports] => Array
(
[tennis] => tennis
[soccer] => soccer
)
[things] => Array
(
[car] => car
[plane] => plane
[jeans] => jeans
[shirt] => shirt
)
)
i made something, too, but now the others were faster. :D
I've made it oop, it doesn't quite come out what you wanted but maybe it helps you further.
I have not come any further now, unfortunately, wanted to show it to you anyway :)
Here is your index.php ( or whatever the file is called. )
<?php
include "Data.php";
$f_name = 'in.csv';
// Stores all csv data
$csv_data = array_map('str_getcsv', file($f_name));
$c = count($csv_data);
$tmp = array();
$data_for_email = array();
foreach ($csv_data as $data){
$key = array_pop($data);
array_push($data_for_email,new Data($data,$key));
}
foreach ($data_for_email as $data){
array_push($tmp,$data->getValue());
}
foreach ($tmp as $value){
print_r($value);
echo "<br>";
}
and here the class Data:
<?php
class Data
{
private $value = [];
public function __construct($data, $key)
{
$this->value[$key]=$data;
}
/**
* #return array
*/
public function getValue()
{
return $this->value;
}
}
as output you bekome something like that:
Array ( [sports] => Array ( [0] => tennis [1] => soccer ) )
Array ( [things] => Array ( [0] => car [1] => plane ) )
Array ( [things] => Array ( [0] => jeans [1] => shirt ) )
ps:
surely there is another function that summarizes the same keys, but somehow i don't find anything now...
I hope it helps :)

Create 2-element rows from flat array where the every second row value is also the first value of the next row

$arrayinput = array("a", "b", "c", "d", "e");
How can I achieve the following output....
output:
Array
(
[0] => Array
(
[0] => a
[1] => b
)
[1] => Array
(
[0] => b
[1] => c
)
[2] => Array
(
[0] => c
[1] => d
)
[3] => Array
(
[0] => d
[1] => e
)
[4] => Array
(
[0] => e
)
)
You can use this, live demo here.
<?php
$arrayinput = array("a","b","c","d","e");
$array = [];
foreach($arrayinput as $v)
{
$arr = [];
$arr[] = $v;
if($next = next($arrayinput))
$arr[] = $next;
$array[] = $arr;
}
print_r($array);
live example here: http://sandbox.onlinephpfunctions.com/code/4de9dda457de92abdee6b4aec83b3ccff680334e
$arrayinput = array("a","b","c","d","e");
$result = [];
for ($x = 0; $x < count($arrayinput); $x+=2 ) {
$tmp = [];
$tmp[] = $arrayinput[$x];
if ($x+1 < count($arrayinput)) $tmp[] = $arrayinput[$x+1];
$result[] = $tmp;
}
var_dump($result);
By declaring single-use reference variables, you don't need to call next() -- which is not falsey-safe and there is a warning in the php manual. You also won't need to keep track of the previous index or make iterated calls of count().
For all iterations except for the first one (because there is no previous value), push the current value into the previous subarray as the second element. After pushing the second value, "disconnect" the reference variable so that the same process can be repeated on subsequent iterations. This is how I'd do it in my own project.
Code: (Demo)
$result = [];
foreach (range('a', 'e') as $value) {
if ($result) {
$row[] = $value;
unset($row);
}
$row = [$value];
$result[] = &$row;
}
var_export($result);
If you always want to have 2 elements in each subarray and you want to pad with null on the final iteration, you could use a transposing technique and send a copy of the input array (less the first element) as an additional argument. This is a much more concise technique (one-liner) (Demo)
var_export(array_map(null, $array, array_slice($array, 1)));

How to reverse PHP array in partitions

I have a simple array that looks like this:
Array (
[0] => Array (
[id] => 8692
[name] => d
)
[1] => Array (
[id] => 8691
[name] => c
)
[2] => Array (
[id] => 8690
[name] => b
)
[3] => Array (
[id] => 8689
[name] => a
)
[4] => Array (
[id] => 8500
[name] => d
)
[5] => Array (
[id] => 8499
[name] => c
)
[6] => Array (
[id] => 8498
[name] => b
)
[7] => Array (
[id] => 8497
[name] => a
)
)
This array is quite long so I only included the first 4 items to give you an idea.
My problem is that I need the array to be in a format of
a,b,c,d,a,b,c,d
At the moment the format is like:
d,c,b,a,d,c,b,a
By this I mean the ['name'] value which is either a,b,c or d.
So I every 4 items in the array need to be reversed.
I have tried to achieve this but fail every time ending up with lots of for & while loops.
You can do it using array_chunk, array_merge and array_reverse:
$finalArray = array();
$arrays = array_chunk($myArray, 4);
foreach ($arrays as $array) {
$finalArray = array_merge($finalArray, array_reverse($array));
}
All the answers here, while perfectly valid, are pretty much on the order of O(n^2). So I figured I'd give you an O(n / 2), time complexity, solution as an alternative just in case you care about performance. The solution also uses only O(n + n + k) space complexity (in place swap).
Since the requirement is to reverse order of values, I'm ignoring keys and basing the solution on the constraint that the array is always 0-indexed.
To solve this problem, we can generalize the solution as a simple array reverse, which requires a simple O(n/2) operation with in-place swap. We can achieve this simply with two counters, $i starting from the beginning of the array, and $j starting at the end of the array. Thus, we can swap the values at $arr[$i] with that at $arr[$j] and then increment $i and decrement $j, at each step.
function reverseArray(Array $arr) {
for($i = 0, $j = count($arr); $i < $j; $i++, $j--) {
$tmp = $arr[$j];
$arr[$j] = $arr[$i];
$arr[$i] = $tmp;
}
return $arr;
}
Now, to apply the more specific solution of only reverse every group of 4 elements in the array, we just break up the array in partitions of 4 values, and only reverse each of those partitions at a time. Which just expands on the example above of reverseArray() by altering the starting and ending positions of the $i and $j counter to only reverse within each partition.
Thus we arrive the O(n / 2) solution here by just adding another loop for the partition size, and keep the inner loop from the earlier example.
function reverseArrayPartition(Array $arr, $partitionSize = 4) {
$end = count($arr);
// reverse only one partition at a time
for($start = 0; $start < $end; $start += $partitionSize ) {
$from = $start;
$to = $start + $partitionSize - 1;
for($i = $from, $j = $to; $i < $j; $i++, $j--) {
// swap the transposing values
$tmp = $arr[$j];
$arr[$j] = $arr[$i];
$arr[$i] = $tmp;
}
}
return $arr;
}
$arr = [4,3,2,1,4,3,2,1];
var_dump(reverseArrayPartition($arr)); // expected [1,2,3,4,1,2,3,4]
This will work with any array size at any $partitionSize so it's efficient if you're trying to do this on very large arrays.
You can iterate the array with a for and increment with 4 each time and keep the current offset in other variable and use array_slice to get the current slice of array and reverse order using array_reverse
I am thinking of something like this:
$step = 4;
$offset = 0;
$new_arr = []; //Here we create the new array.
for($i=0; $i<count($arr); $i+=$step) {
$part_of_array = array_slice($arr, $offset, $step);
$part_reverse = array_reverse($part_of_array);
$new_arr = array_merge($new_arr, $part_reverse);
$offset += $step;
}
print_r($new_arr); //Here will be the array as you expected.
All the content in the for can be simplify to:
$new_arr = array_merge($new_arr, array_reverse(array_slice($arr, $offset, $step)));
$offset += $step;
Not harcoding every 4, reverse based on char code of name value
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
/**
*/
$in = [
['name'=>'d','id'=>1]
, ['name'=>'c','id'=>12]
, ['name'=>'b','id'=>13]
, ['name'=>'a','id'=>14]
, ['name'=>'d','id'=>15]
, ['name'=>'c','id'=>16]
, ['name'=>'b','id'=>17]
, ['name'=>'a','id'=>18]
];
$last = PHP_INT_MAX;
$toReverse = [];
$out = [];
foreach ($in as $value) {
$p = ord($value['name']);
if ( $p < $last ) {
//echo 'ToReverse',var_export($value,true),"\n";
$toReverse[] = $value;
}
else {
$out = array_merge($out,array_reverse($toReverse));
//echo 'Join',var_export($out,true),"\n";
$toReverse = [$value];
}
$last = $p;
}
$out = array_merge($out,array_reverse($toReverse));
print_r($out);

Categories