1

Here is the data structure I have (it's simplified for clearlier understanding):

• USA
  • Alabama
    • Montgomery
    • Birmingham
  • Arizona
    • Phoenix
    • Mesa
    • Gilbert
• Germany
  • West Germany
    • Bonn
    • Cologne

I need to return all path for given node – i.e.: if user enter Arizona, I need to return USA → Arizona. If enter Birmingham, I need to return USA → Alabama → Birmingham.

Is there in PHP simple way to search in such structures?

kuboslav
  • 1,430
  • 5
  • 16
  • 31
  • you will be best off creating your own tree data structure. There is no built in libraries, some user libraries do similar things but it would be better to build one urself rather than finding the perfect match. – arkoak Apr 20 '15 at 06:53
  • What happens if you have multiple entries that are the same, e.g. a search on `Birmingham`: there's a Birmingham in `United Kingdom → England → Birmingham` as well as `USA → Alabama → Birmingham` – Mark Baker Apr 20 '15 at 08:17
  • @MarkBaker I have another content in my case – this is just example for explaining relations between each nodes. I'm pretty sure, that this woudn't happen in my case. – kuboslav Apr 20 '15 at 09:53
  • Is this a `SQL` table? A `XML` scheme? ... Add more information please – Bram Apr 23 '15 at 06:15
  • @BramDriesen it's not SQL – I know Tree traversing. I want to accomplish the similar thing, but for performance reasons I can't query database. – kuboslav Apr 23 '15 at 08:35
  • @kuboslav is using [Redis](http://redis.io) an option for you? – divaka Apr 23 '15 at 15:02
  • 1
    @BramDriesen a XML looks good. – kuboslav Apr 28 '15 at 05:07
  • @divaka Redis is overkill. – kuboslav Apr 28 '15 at 05:07

5 Answers5

2

If you haven't huge data structure, you can use XML parsing. It's well known and easy to implement. It has desired ability to access parent element.

Here is an simple example:

$xml = <<<XML
<list>
  <state name="USA">
    <region name="Alabama">
      <city name="Montgomery" />
      <city name="Birmingham" />
    </region>
    <region name="Arizona">
      <city name="Phoenix" />
      <city name="Mesa" />
      <city name="Gilbert" />
    </region>
  </state>
  <state name="Germany">
    <region name="West Germany">
      <city name="Bonn" />
      <city name="Cologne" />
    </region>
  </state>
</list>
XML;


$doc = new \DOMDocument;
$doc->preserveWhiteSpace = false;
$doc->loadXML($xml);

$xpath = new \DOMXPath($doc);
// XPath query to match all elements with
// attribute name equals to your searched phrase
$locations = $xpath->query("//*[@name='Cologne']");

function parse($list) {

  $response  = [];

  foreach ($list as $node) {
      $response[] = $node->attributes->getNamedItem('name')->nodeValue;
      $parentNode = $node->parentNode;
      // traverse up to root element
      // root element has no attributes
      // feel free to use any other condition, such as checking to element's name
      while ($parentNode->hasAttributes()) {
          $response[] = $parentNode->attributes->getNamedItem('name')->nodeValue;
          $parentNode = $parentNode->parentNode;
      }
  }

  return $response;
}

$parsedLocations = array_reverse(parse($locations));

echo implode(' →  ', $parsedLocations), PHP_EOL;
jkucharovic
  • 4,214
  • 1
  • 31
  • 46
1

Here is a possible strategy that builds up the path piece by piece: you start from the first level of the array and you check whether the searc term equals the key. If not, you check the value and otherwise if the value is an array (is_array()) you repeat the search recursively by using a prefix.

The data set:

$str = array(
    "USA" => array(
        "Alabama" => array(
            "Montgomery",
            "Birmingham"
        ),
        "Arizona" => array(
            "Phoenix",
            "",
            "Gilbert"
        ),
        "West Germany" => array(
            "Bonn",
            "",
            "Cologne"
        )
    ),
    "Germany" => array(
        "West Germany" => array(
            "Bonn",
            "Mesa",
            "Cologne"
        )
    )
);

The function:

function getPath($haystack, $needle, $prefix=""){
    $path = "";
    foreach($haystack as $key=>$value){

        if($path!="")break;

        if($key===$needle){
            return $prefix.$key;
            break;
        }
        elseif($value===$needle) {
            return $prefix.$value;
            break;
        }
        elseif(is_array($value)) {
            $path.=getPath($value,$needle,$prefix.$key."=>");   
        }   
    }
    return $path;
}

a test:

echo getPath($str,"Mesa");

In case of duplicates you will get the first result. If the search term is not found, you get an empty string.

Danilo
  • 2,676
  • 7
  • 32
  • 36
1

Since "data structure" is very vague, and your only hint is that you're using PHP, I will assume that your "data structure" means the following:

[
    'USA' =>
    [
        'Alabama' =>
        [
            'Montgomery',
            'Birmingham'
        ],
        'Arizona' =>
        [
            'Phoenix',
            'Mesa',
            'Gilbert'
        ]
    ],
    'Germany' =>
    [
        'West Germany' =>
        [
            'Bonn',
            'Cologne'
        ]
    ]
]

And I assume that you want your result in the form

['USA', 'Alabama', 'Birmingham']

If this is not the case, please inform us about how your data is actually available and how you want your result.

Is there in PHP simple way to search in such structures?

That depends on your definition of "simple".
For me, a solution that fits into a single function is "simple".
However, there is no out-of-the-box solution for this that you can use in a one-liner.

If you only need to find the "leafs", you could use a RecursiveIteratorIterator over a RecursiveArrayIterator as in this StackOverflow question.
But since you need to find intermediary keys too, that it not really an option.
The same goes for array_walk_recursive.

You could probably use ArrayIterator or array_walk, but in this example they can't really do anything a foreach loop can't, besides complicate things.
So I'd just go with a foreach loop:

function findMyThing($needle, $haystack) // Keep argument order from PHP array functions
{
    // We need to set up a stack array + a while loop to avoid recursive functions for those are evil.
    // Recursive functions would also complicate things further in regard of returning.
    $stack =
    [
        [
            'prefix' => [],
            'value' => $haystack
        ]
    ];
    // As long as there's still something there, don't stop
    while(count($stack) > 0)
    {
        // Copy the current stack and create a new, empty one
        $currentStack = $stack;
        $stack = [];
        // Work the stack
        for($i = 0; $i < count($currentStack); $i++)
        {
            // Iterate over the actual array
            foreach($currentStack[$i]['value'] as $key => $value)
            {
                // If the value is an array, then
                //   1. the key is a string (so we need to match against it)
                //   2. we might have to go deeper
                if(is_array($value))
                {
                    // We need to build the current prefix list regardless of what we're gonna do below
                    $prefix = $currentStack[$i]['prefix'];
                    $prefix[] = $key;
                    // If the current key, is the one we're looking for, heureka!
                    if($key == $needle)
                    {
                        return $prefix;
                    }
                    // Otherwise, push prefix & value onto the stack for the next loop to pick up
                    else
                    {
                        $stack[] =
                        [
                            'prefix' => $prefix,
                            'value' => $value
                        ];
                    }
                }
                // If the value is NOT an array, then
                //   1. the key is an integer, so we DO NOT want to match against it
                //   2. we need to match against the value itself
                elseif($value == $needle)
                {
                    // This time append $value, not $key
                    $prefix = $currentStack[$i]['prefix'];
                    $prefix[] = $value;
                    return $prefix;
                }
            }
        }
    }
    // At this point we searched the entire array and didn't find anything, so we return an empty array
    return [];
}

Then just use it like

$path = findMyThing('Alabama', $array);
Community
  • 1
  • 1
Siguza
  • 21,155
  • 6
  • 52
  • 89
0

@Siguza

avoid recursive functions for those are evil

Recursion is not evil (or eval) and works well with stacks to

function df($v,array &$in,array &$stack,$search) {
    $stack[] = $v;
    if ( $v == $search ) {
        return [true,$stack];
    }
    if ( is_array($in) ) {
        foreach ($in as $vv => $k) {
            if ( is_array($k) ) {
                $r = df($vv, $k, $stack, $search);
                if ($r[0]) {
                    return $r;
                }
            }
            else if ($k == $search) {
                $stack[] = $k;
                return [true,$stack];
            }
        }
    }
    array_pop($stack);
    return [false,null];
}

Usage:

$s = [];
$r = df('',$in,$s,'Bonn');
print_r($r);
$s = [];
$r = df('',$in,$s,'West Germany');
print_r($r);
$s = [];
$r = df('',$in,$s,'NtFound');
print_r($r);

Output:

Array
(
    [0] => 1
    [1] => Array
(
    [0] =>
        [1] => Germany
            [2] => West Germany
            [3] => Bonn
        )

)
Array
(
    [0] => 1
    [1] => Array
(
    [0] =>
        [1] => Germany
            [2] => West Germany
        )

)
Array
(
    [0] =>
        [1] =>
)
cske
  • 2,233
  • 4
  • 26
  • 24
0

According to you data structure.

$data['USA'] = ['Alabama' => ['Montgomery','Birmingham'],'Arizona' => ['Phoenix','Mesa','Gilbert']];
$data['Germany'] = ['West Germany' => ['Bonn','Cologne']];

function getHierarchy($location, $data){
    $totalCountries = count($data);

    //Get Array Keys of rows eg countries.
    $keys = array_keys($data);
    $hierarchy= [];

    //Loop Through Countries
    for($i = 0; $i < $totalCountries; $i++){
        //If we have found the country then return it.
        if($location == $keys[$i]) return [$keys[$i]];
        $hierarchy[] = $keys[$i];
        foreach($data[$keys[$i]] as $city => $places){

            // if we have found the city then return it with country.
            if($city == $location){
                $hierarchy[] = $city;
                return $hierarchy;
            }

            // if we have found the place in our places array then return it with country -> city -> place.
            if(in_array($location, $places)){
                $hierarchy[] = $city;
                $hierarchy[] = $location;
                return $hierarchy;
            }
        }
        // Reset Hirarcy if we do not found our location in previous country.
        $hierarchy = [];
    }
}

$found = getHierarchy('Birmingham', $data);
if($found){
    echo implode(' -> ', $found);
    // Output will be USA -> Alabama -> Birmingham
}

It can only find only one country city and places and if any location is found then it will break the whole function and return the first location with city and place.

Here is more improved version which can find multiple locations as well. https://gist.github.com/touqeershafi/bf89351f3b226aae1a29

Hope it helps you.

Touqeer Shafi
  • 5,084
  • 3
  • 28
  • 45