How to build a tree from an array in PHP knowing the rules that need to be followed to build the tree?
I don't have any particular place in the code where I can find what's really wrong
Anyway, the problems are probably related with the passing by reference I'm repeatedly using.
I did not use recursion because speed is really really important here. From the rules that the input array has (I have full control over them), this is possible, and faster, without recursion.
How it works:
As you transverse the array, each element's ['start_order'] is a bigger number than the previous ['start_order'].Every time that the next element's ['start_order'] is bigger than this element's ['start_order'] and the next element's ['end_order'] is smaller than this element's ['end_order'], then that element is a child of this element. After that step, I must find all the children of that element (the one I just found that it it this element's child).
Here's my code.
<?php
ksort($treeArray);
$tree = array();
$stack = array();
reset($treeArray);
$currentParent = &$treeArray[key($treeArray)];
$tree[] = &$treeArray[key($treeArray)];
while(next($treeArray) !== false){
if(current($treeArray)['start_order'] <= $currentParent['end_order']){
if(current($treeArray)['end_order'] <= $currentParent['end_order']){
// There's a child of the previous
// push the new parent
$stack[] = $currentParent;
if(!isset($currentParent['children'])){
$currentParent['children'] = array();
}
$currentParent['children'][] = &$treeArray[key($treeArray)];
$currentParent = current($treeArray);
}else{
// Supposed not to happen. Log the problem.
}
}else /* if(current($treeArray)['start_order'] > $currentParent['end_order']) */{
// Pop the child here, there are no more children.
if(($popedElement = array_pop($stack)) === NULL){
$tree[] = $currentParent;
}else{
$popedElement['children'][] = &$treeArray[key($treeArray)];
$stack[] = $popedElement;
$currentParent = current($treeArray);
}
}
}
?>
Example:
The input array of this can be something that structurally looks like this:
[1][child1][child1child1][child2][child2child1][child2child2][child2child3][2][child2child1]
which resolves into this tree:
[1]
[child1]
[child1child1]
[child2]
[child2child1]
[child2child2]
[child2child3]
[2]
[child2child1]
And don't forget that this order maintains. [child2child3] never appears before [child2child2], [2] never appears before [1]. In this analogy, is almost like when dealing with XML.
Found the problem here. The problem is related on how I treat the pass-by-reference while trying to solve this problem.
Here's the solution:
$tree[] = &$treeArray[key($treeArray)];
$currentParent = &$treeArray[key($treeArray)];
next($treeArray);
while(current($treeArray) !== false){
if(current($treeArray)['start_order']['start_position'] <= $currentParent['end_order']['end_order']){
if(current($treeArray)['end_order']['end_order'] <= $currentParent['end_order']['end_order']){
// There's a child of the previous
// push the new parent
$stack[] = &$currentParent;
$currentParent['children'][] = &$treeArray[key($treeArray)];
$currentParent = &$treeArray[key($treeArray)];
}else{
// Supposed not to happen. Log the problem.
}
next($treeArray);
}else /* if(current($treeArray)['start_order']['start_position'] > $currentParent['end_order']['end_order']) */{
// Close previous element here. There are no more children.
if(end($stack) === false){
$currentParent = &$treeArray[key($treeArray)];
$tree[] = &$treeArray[key($treeArray)];
next($treeArray);
}else{
$currentParent = &$stack[key($stack)];
unset($stack[key($stack)]);
}
}
}
The main problem was actually the pass-by-reference which is different in PHP than it is in C.
To solve this problem, I'm unable to use both push or pop php functions:
The push because the $var[] operator is faster and using that function.
The pop, because its return is a copy of what I had pushed before instead of the actual element I had pushed.
Additionally, all variable assignments have to be explicitly made by reference (using the & character), so using current() for assignment is out of the question, instead I need to use the key() function like I did.
I also had a small but important bug that forced me to change the "while" instruction. It now contains current() instead of next(). That's only because after the pop of the stack I mustn't move to the next element. I need to do another iteration before I move to the next one. This solves the bad nesting generated when there are multiple tags closing in the same place.
Please note that this code is not optimized for speed.
Related
The typical algorithm of search until found, in PHP and for some arrays where we can't use the language array search functions (I think), could be implemented in this way:
$found = false;
while (list($key, $alreadyRP) = each($this->relatedProducts) && !$found) {
if ($alreadyRP->getProduct()->getId() === $rp->getProduct()->getId()) {
$found = true;
}
}
if (!$found) {
// Do something here
}
Please, take it as pseudocode, I didn't executed it. What I like about it, is that it gracefully stops if it is found what we are looking for.
Now, due to the "each" function is deprecated, I have to code something like this:
$found = false;
foreach ($this->relatedProducts as $alreadyRP) {
if ($alreadyRP->getProduct()->getId() === $rp->getProduct()->getId()) {
$found = true;
break;
}
}
if (!$found) {
// Do something here
}
To put a "break" statement inside a "for" loop is ugly in structured programming. Yes, it is optional, but if we avoid it, the "foreach" will go through all the array, which is not the most efficient way.
Any idea to recover the efficiency and structure that "each" gives in this case?
Thank you.
The beauty of the each() method is in the eye of the beholder, but there are other reasons to prefer foreach, including this interesting bit of information from the RFC that led to the deprecation of each()
The each() function is inferior to foreach in pretty much every imaginable way, including being more than 10 times slower.
If the purpose of the method is to // Do something here if the $rp is not found in $this->relatedProducts, I think a more "beautiful" way to handle it would be to extract the search through related products into a separate method.
protected function inRelatedProducts($id) {
foreach ($this->relatedProducts as $alreadyRP) {
if ($alreadyRP->getProduct()->getId() === $id) {
return true;
}
}
return false;
}
Moving the related products search into a separate method is advantageous because
It separates that functionality from the original method so that it becomes reusable instead of being tied to whatever // Do something here does.
It simplifies the original method so it can focus on its main task
$id = $rp->getProduct()->getId();
if (!$this->inRelatedProducts($id)) {
// Do something here
}
It simplifies the search code because if it's contained in its own method, you can just return true; as soon as you find a match, so you won't need to break, or to keep track of a $found variable at all.
On the other hand, if this was my project I would be looking for a way to remove the need for this method by populating $this->relatedProducts so that it's indexed by ID (assuming ID is unique there) so the determination could be reduced to
$id = $rp->getProduct()->getId();
if (isset($this->relatedProducts[$id])) { ...
If you're looking for a rewrite that doesn't involve extra variables, you can replace the each call with calls to current and next:
do {
$found = (current($this->relatedProducts)->getProduct()->getId() === $rp->getProduct()->getId());
} while (empty($found) && false !== next($array));
This is a mechanical translation, and it relies merely on the definition of each (emphasis mine):
Return the current key and value pair from an array and advance the array cursor
It also suffers the same deficiency of the original each version: it doesn't handle empty arrays.
That said, please don't use each, or any of its siblings, for new code. This from a guy who voted "No" on the RFC! Why? Because the performance sucks:
1017$ cat trial.php
<?php
$array = range(0, 999);
$begin = -microtime(true);
for ($i = 0; $i < 10000; $i++) {
reset($array);
$target = rand(333, 666);
do {
$found = (current($array) === $target);
} while (empty($found) && false !== next($array));
}
printf("%.5f\n", $begin + microtime(true));
$begin = -microtime(true);
for ($i = 0; $i < 10000; $i++) {
$target = rand(333, 666);
foreach ($array as $current) {
if ($current === $target) break;
}
}
printf("%.5f\n", $begin + microtime(true));
1018$ php -d error_reporting=-1 trial.php
8.87178
0.33585
That's nearly nine seconds for the next/current version while not even half a second for the foreach version. Just don't.
It looks like each is basically a version of current() and next()
http://php.net/manual/en/function.current.php
http://php.net/manual/en/function.next.php
each() gives the current array item, and moves to the next index.
current() gives the current array item, but doen't increment the index.
So, you can replace each() with current(), and inside your foreach use next() to shift the index up
next() gives the next item, and increments the index.
while (list($key, $alreadyRP) = current($this->relatedProducts) && !$found) {
if ($alreadyRP->getProduct()->getId() === $rp->getProduct()->getId()) {
$found = true;
}
next($this->relatedProducts);
}
I have a very long list of strings called $stringfilter1 $stringfilter2 etc all the way up to $stringfilter50
I have another string $reporteremail and I want to make a conditional statement whereby if any of the $stringfilter strings is present in the $reporteremail, some code is executed. At the moment my code looks like this and it works:
if (stripos($reporteremail, $stringfilter1) !== false || stripos($reporteremail, $stringfilter2) !== false || stripos($reporteremail, $stringfilter3) !== false [...]) {
runcode();
}
This is very very long though. I have cut it short here.
I was wondering if there's a cleaner, more efficient way to do this?
EDIT:
I am writing a plugin for a bug tracker. The strings are entered on another page in text boxes. I access them on this page by running a function that looks like
$t_filter = plugin_config_get( 'filter1' );
$stringfilter1 = string_attribute( $t_filter1 );
I would agree looping through an array would be the best way to do this. How can I push each new string onto the end of an array without having to write that snippet above out 50 times?
How can I push each new string onto the end of an array without having to write that snippet above out 50 times?
Try this:
$needles = [];
for ($i = 0; $i < 50; $i++) {
$t_filter = plugin_config_get("filter$i");
$needles[] = string_attribute($t_filter);
}
I have a very long list of strings called $stringfilter1 $stringfilter2 etc all the way up to $stringfilter50
[...]
This is very very long though. I have cut it short here.
I was wondering if there's a cleaner, more efficient way to do this?
Try this, it should go after the code block above.
$flag = false;
foreach ($needles as $needle) {
if (stripos($reporteremail, $needle) !== false) {
$flag = true;
break;
}
}
if ($flag) {
runcode();
}
The code above works by iterating through the $needles array and sets a flag if stripos doesn't return false. After it's finished iterating, it checks if the flag is true, if so, this means that one of the needles was found in the array.
EDIT
Alternatively, you could do it all in one loop, which is both faster and more efficient.
$flag = false;
for ($i = 0; $i < 50; $i++) {
$t_filter = plugin_config_get("filter$i");
$needle = string_attribute($t_filter);
if (stripos($reporteremail, $needle) !== false) {
// One of the needles was found in $reporteremail.
runcode();
break;
}
}
You don't need a loop. First put all your filters in an array instead of having them in separate variables. I would try to do this by modifying the input source rather than doing it in your PHP script. (Based on your comments I'm not sure if that's possible or not, so maybe you do need a loop like the one in the other answer.) Then you can use str_ireplace to check for your filter strings in the $reporteremail. (This will not modify $reporteremail.)
str_ireplace($filters, '', $reporteremail, $count);
if ($count) {
// run code
}
The $count parameter will contain a count of how many replacements were performed. If it's nonzero, then at least one of the filters was found in $reporteremail.
So i'm having trouble getting a bit of code to work. Essentially what I want to do is:
in a foreach loop, if a given array value is set, compare that existing value to the current loop value, then set the existing value = current value (for the iteration) if the existing value is already greater than current val. Here is the code i'm working with:
if ($usedebayxml->ack == 'Success') {
foreach($usedebayxml->searchResult->item as $key => $value) {
if(isset($newarray[1]['TotalCost'])) {
if($newarray[1]['TotalCost'] > ((integer)$value->shippingInfo->shippingServiceCost + (integer)$value->sellingStatus->currentPrice)) {
$newarray[1]['Title'] = (string)$value->title ;
$newarray[1]['ShippingCost'] = (integer)$value->shippingInfo->shippingServiceCost;
$newarray[1]['Price'] = (integer)$value->sellingStatus->currentPrice;
$newarray[1]['Condition'] = 'New';
$newarray[1]['TotalCost'] = (integer)$value->shippingInfo->shippingServiceCost + (integer)$value->sellingStatus->currentPrice;
}
}
else
$newarray[1]['Title'] = (string)$value->title;
$newarray[1]['ShippingCost'] = (integer)$value->shippingInfo->shippingServiceCost;
$newarray[1]['Price'] = (integer)$value->sellingStatus->currentPrice;
$newarray[1]['Condition'] = 'Used';
$newarray[1]['TotalCost'] = (integer)$value->shippingInfo->shippingServiceCost + (integer)$value->sellingStatus->currentPrice;
}
}
With this code, what is returned is ultimately the values in the LAST key object in the xml file (im using simpleXML if that helps). In other words, i don't think the first if block (if isset) is being entered into, and the values are being set to whatever the values are for the current iteration. Can anyone see any flaw in my logic here? I've been stumped on this one for a while.
I am a supreme idiot. The logic here is fine, i was just missing a { for the opening else block. dur! After adding this, this bit of code works as intended :)
I'm surprised though that i wasn't throwing any errors without having this....I think that was probably throwing me off in determining why it wasn't working originally.
out of curiosity I'm wondering if there's a more elegant way to write the conditionals below? I can't see a shorter way of writing it but it feels pretty clunky, so any suggestions welcome!
// Check whether this page has any visuals
if (count($this->page->pagevisuals->find_all()) > 0)
{
// Ok to go ahead and assign
$visual = $this->page->pagevisuals->find_all();
}
// If this is a sub page, parent page may have visuals we can use
elseif (count($this->page->parent->pagevisuals->find_all()) > 0)
{
$visual = $this->page->parent->pagevisuals->find_all();
}
// If two levels deep, grandparent page might have visuals
elseif (count($this->page->parent->parent->pagevisuals->find_all()) > 0)
{
$visual = $this->page->parent->parent->pagevisuals->find_all();
}
You can write a loop instead:
$page = $this->page;
$visual = null;
while (!$visual && $page) {
$visual = $page->pagevisuals->find_all();
$page = $page->parent;
}
I believe this is equivalent, and will work no matter how many levels of parents/nesting you have.
You could assign $this->page to a variable and begin the statements with that, for a very slight improvement.
Alternatively, you could create nested ternary statements to assign $visual, but that's certainly not recommended practice.
A recursive approach:
function getVisuals($root) {
$visuals = $root->pagevisuals->find_all();
if(count($visuals) === 0 && isset($root->parent)) {
$visuals = getVisuals($root->parent);
}
return $visuals;
}
$visuals = getVisuals($this->page);
If you have control over whatever class $this->page is an instance of, then you can make it an instance method.
You could make a recursive method to get rid of those nasty conditionals. Also you're calling the find_all() method twice for every conditional branch which doubles the process time.
Here's an attempt at a recursive function (might not work though, recursive functions are always a bit tricky!). Beware of infinite loops.
<?php
$visual = $this->page->find_all_visuals();
class Page {
function find_all_visuals()
{
$found = $this->pagevisuals->find_all();
if (count($found) > 0) {
return $found;
} else if ($this->parent == null) {
return null;
} else {
return $this->parent->find_all_visuals();
}
}
}
?>
You might want make two changes in your code:
Ensure that getVisuals() returns an empty array instead of null in case there are no visuals
Consider making a null-object - a singleton page instance that has no visuals and has itself as a parent. It might have a method like isNull() so you can easily test if a given page is the null page.
If you make the two adjustments, most of the code concerning visuals will become easier to write and debug.
Getting all the visuals for two levels (I assume you don't want recursion):
$visuals = array_merge(
$this->page->pagevisuals->find_all(),
$this->page->parent->pagevisuals->find_all(),
$this->page->parent->parent->pagevisuals->find_all(),
);
Getting the visuals of the page OR of parent OR of grand parent:
($visuals = $this->page->pagevisuals->find_all()) ||
($visuals = $this->page->parent->pagevisuals->find_all()) ||
($visuals = $this->page->parent->parent->pagevisuals->find_all());
Recursive functions would be much simpler too (this is a method to add to the page object):
public function findRecursive(){
$my_visuals = $this->pagevisuals->find_all()
return $this->parent->isNull()?
$my_visuals
: array_merge($my_visuals, $this->parent->findRecursive());
}
$visual = $this->page->pagevisuals->find_all()
or $visual = $this->page->parent->pagevisuals->find_all()
or $visual = $this->page->parent->parent->pagevisuals->find_all();
What do you do if none of them match? In this code it will be set to the last one, which is not the same as what you did. (In your code $visual was not touched if none matched, in this code it will be set to zero. You could add or $visual = -1 or something similar.)
You can make a loop if you want to avoid all the ->parent, but you'll need some terminator.
$el = $this->page;
while(!$visual = $el->pagevisuals->find_all()) {
$el = $el->parent;
}
This could run forever if it never matches, but I don't know enough about your application to suggest a termination condition - you could add a counter, or something else.
I can't figure out how to log into my account here on Stackoverflow, I find it a little confusing. Anyway, I've asked a question here about a problem I'm having:
Other Question
I've since found an open source project that does exactly what I need done but the code is PHP and while I can understand most of it there are bits I don't get. I'll post it here with my comments through it and if someone can add extra details that would be appreciated.
public function productAttributeExists($attributesList, $currentProductAttribute = false)
{
$result = Db::getInstance()->ExecuteS('SELECT pac.`id_attribute`, pac.`id_product_attribute`
FROM `'._DB_PREFIX_.'product_attribute` pa
LEFT JOIN `'._DB_PREFIX_.'product_attribute_combination` pac ON (pac.`id_product_attribute` = pa.`id_product_attribute`)
WHERE pa.`id_product` = '.intval($this->id));
if (!$result OR empty($result))
return false;
$productAttributes = array();
foreach ($result AS $productAttribute)
$productAttributes[$productAttribute['id_product_attribute']][] = $productAttribute['id_attribute'];
foreach ($productAttributes AS $key => $productAttribute)
if (sizeof($productAttribute) == sizeof($attributesList))
{
$diff = false;
for ($i = 0; $diff == false AND isset($productAttribute[$i]); $i++)
if (!in_array($productAttribute[$i], $attributesList) OR $key == $currentProductAttribute)
$diff = true;
if (!$diff)
return true;
}
return false;
}
Ok turns out I can't comment this code in Stackoverflow without it all going to formatting hell. So my understanding is this:
1) Get the Data
2) If the dataset is empty return false
3) New array called productAttributes
4) Loop through the dataset and populate the array
5) Not sure what this last section is doing, the section beginning 'foreach' is unclear to me.
Any tips appreciated. Incidentally, C# is my preferred language and the one I understand best.
This function, as it is named, is only supposed to check if a particular attribute list of a product exists and is the same as the one provided in the first parameter.
The last part is confusing, but it's iterating through all the attributes to see if any of the retrieved attributes are the same as the ones provided.
in a loop iteration:
it first tries to see if the provided attribute list is the same length as the one coming from the database - if not, it just goes to the next attribute list in the for loop
if the length of the lists is the same, then it will go through each attribute in the attribute list and see if there's something different amongst any of the elements.
The function isn't very optimal, btw.