Finding the best-fit object from an array of objects - php

Given an array in the following structure (although obviously with many more items in it):
Array
(
[0] => stdClass Object
(
[currency] => 1
[role] => 3
[client_company] =>
[client_group] =>
[hourly_rate] => 115.00
)
[1] => stdClass Object
(
[currency] => 1
[role] => 1
[client_company] =>
[client_group] =>
[hourly_rate] => 115.00
)
[2] => stdClass Object
(
[currency] => 1
[role] => 3
[client_company] => 58
[client_group] =>
[hourly_rate] => 110.00
)
)
I'm trying to create a function that will take four parameters:
$role
$currency
$company [optional]
$group [optional]
("groups" are children of "companies": if a group is specified, a parent company will always also be specified)
...and that will return the "hourly rate" value from the item that best fits those parameters, on the basis that:
if $row, $currency, $company and $group are specified:
find a rate that matches the role, currency, company and group.
if there isn't one, find one that matches the role, currency and company
if there isn't one, find one that matches the role and currency
if there isn't one, return FALSE
if just $row, $currency and $company are specified:
find a rate that matches the role, currency and company
if there isn't one, find one that matches the role and currency
if there isn't one, return FALSE
if just $row and $currency are specified:
find a rate that matches the role and currency
if there isn't one, return FALSE
What I've got is below, and it works. However, it's ugly as sin. There must be a more elegant way than just bashing a load of if/else and loops together. However, it's Friday and I've had too much pizza for lunch and my brain has become ensludged with cheese.
Can you help?
$hourly_rate = FALSE;
if ( !empty($group) && !empty($company) ) {
foreach ( $rates_cache as $rate ) {
if ( $rate->currency == $currency && $rate->role == $role && (int) $rate->client_company === (int) $company && (int) $rate->client_group === (int) $group ) {
$hourly_rate = $rate->hourly_rate;
}
}
if ( empty($hourly_rate) ) {
foreach ( $rates_cache as $rate ) {
if ( $rate->currency == $currency && $rate->role == $role && (int) $rate->client_company === (int) $company ) {
$hourly_rate = $rate->hourly_rate;
}
}
}
if ( empty($hourly_rate) ) {
foreach ( $rates_cache as $rate ) {
if ( $rate->currency == $currency && $rate->role == $role ) {
$hourly_rate = $rate->hourly_rate;
}
}
}
}else if ( !empty($company) ) {
foreach ( $rates_cache as $rate ) {
if ( $rate->currency == $currency && $rate->role == $role && (int) $rate->client_company === (int) $company ) {
$hourly_rate = $rate->hourly_rate;
}
}
if ( empty($hourly_rate) ) {
foreach ( $rates_cache as $rate ) {
if ( $rate->currency == $currency && $rate->role == $role ) {
$hourly_rate = $rate->hourly_rate;
}
}
}
}else{
foreach ( $rates_cache as $rate ) {
if ( $rate->currency == $currency && $rate->role == $role ) {
$hourly_rate = $rate->hourly_rate;
}
}
}
return $hourly_rate;

Assumption
I believe your cache is always in the format below
Cache Format:
$cache = array(
0 => (object) (array(
'currency' => 1,
'role' => 3,
'client_company' => '',
'client_group' => '',
'hourly_rate' => '115.00'
)),
1 => (object) (array(
'currency' => 1,
'role' => 1,
'client_company' => '',
'client_group' => '',
'hourly_rate' => '115.00'
)),
2 => (object) (array(
'currency' => 1,
'role' => 3,
'client_company' => 58,
'client_group' => '',
'hourly_rate' => '110.00'
))
);
Your Revised Function
$param = array(
"role" => 1,
"currency" => 1
);
echo find($cache, $param)->hourly_rate;
Function Used
function find($cache, $param) {
$mx = array();
if (! isset($param['role']) || ! isset($param['currency']))
throw new Exception("Missing Role Or Currency");
foreach ( $cache as $k => $r ) {
foreach ( array_keys(array_intersect($param, (array) $r)) as $key ) {
if ($r->{$key} == $param[$key]) {
isset($mx[$k]) ? $mx[$k] ++ : $mx[$k] = 1;
}
}
}
arsort($mx);
return $cache[key($mx)];
}
More Complex: Another Approach
Usage
$param = array(
"role" => 1,
"currency" => 1
);
$process = new Process($cache);
echo $process->find($param)->best()->hourly_rate; // Outputs 115.00
Multiple Results
When find best fit .. there is possibility you would get more than one result
$param = array(
"role" => 3,
"currency" => 1
);
$process = new Process($cache);
var_dump($process->find($param)->results());
Output
array (size=2)
0 =>
object(stdClass)[1]
public 'currency' => int 1
public 'role' => int 3
public 'client_company' => string '' (length=0)
public 'client_group' => string '' (length=0)
public 'hourly_rate' => string '115.00' (length=6)
2 =>
object(stdClass)[3]
public 'currency' => int 1
public 'role' => int 3
public 'client_company' => int 58
public 'client_group' => string '' (length=0)
public 'hourly_rate' => string '110.00' (length=6)
Not getting best result
You can see based on your parameters you are getting 2 if you are looking for cheapest prize and you call
$param = array(
"role" => 3,
"currency" => 1
);
echo Process::quick($cache, $param)->best()->hourly_rate; // returns 115.00 but that is not the cheapest
Resolution
The solution is you can add filter and sort
$param = array(
"role" => 3,
"currency" => 1
);
$sort = function ($a, $b) {
return $a->hourly_rate < $b->hourly_rate ? - 1 : 1;
};
echo Process::quick($cache, $param)->sort($sort)->best()->hourly_rate; // 110
Getting all Related
You can also just loop through all the result and select the columns you want insted of just getting best result
foreach ( Process::quick($cache, $param)->sort($sort)->getColoum("client_company", "hourly_rate") as $result ) {
print_r($result);
}
Output
stdClass Object
(
[client_company] => 58
[hourly_rate] => 110.00
)
stdClass Object
(
[client_company] =>
[hourly_rate] => 115.00
)
Updated Class
To add all this additional functions you need to upgrade your class to
class Process implements JsonSerializable, IteratorAggregate {
private $cache;
private $matrix = array();
private $final = array();
function __construct($cache) {
$this->cache = $cache;
}
function find($param) {
if (! isset($param['role']) || ! isset($param['currency']))
throw new Exception("Missing Role Or Currency");
foreach ( $this->cache as $k => $rate ) {
$keys = array_intersect($param, (array) $rate);
foreach ( array_keys($keys) as $key ) {
if ($rate->{$key} == $param[$key]) {
isset($this->matrix[$k]) ? $this->matrix[$k] ++ : $this->matrix[$k] = 1;
}
}
}
arsort($this->matrix);
$this->matrix = array_keys(array_filter($this->matrix, function ($v) {
return $v >= 2;
}));
$this->final = $this->sortArray($this->cache, $this->matrix);
return $this;
}
public static function quick($cache, $param) {
$process = new Process($cache);
return $process->find($param);
}
public function best() {
reset($this->final);
return empty($this->final) ? : current($this->final);
}
public function results() {
return $this->final;
}
public function limit($length = 0) {
$this->final = array_slice($this->final, 0, $length);
return $this;
}
public function sort(Callable $function) {
usort($this->final, $function);
return $this;
}
public function getColoum() {
$arg = array_flip(func_get_args());
foreach ( $this->final as &$s ) {
foreach ( $s as $k => $v ) {
if (! isset($arg[$k]))
unset($s->{$k});
}
}
return $this;
}
public function getIterator() {
return new ArrayIterator($this->final);
}
public function jsonSerialize() {
return json_encode($this->final);
}
public function __toString() {
return $this->jsonSerialize();
}
private function sortArray(array $array, array $orderArray) {
$ordered = array();
foreach ( $orderArray as $key => $value ) {
array_key_exists($value, $array) and $ordered[$value] = $array[$value];
}
return $ordered;
}
}

Related

PHP - How to filter multilevel associative array? [duplicate]

I want to remove all null or blank value but not false and 0 value from recursive array.
function isNotNull($val) {
if(is_array($val)) {
$ret = array_filter($val, 'isNotNull');
return $ret;
} else {
return (!is_null($val) && $val !== '');
}
}
$arr = array_filter($arr, 'isNotNull');
Input:
$arr = array(
"stringKey" => "Abc",
"boolKey" => false,
"zeroKey" => 0,
"blankKey" => '',
"newArr" => array(
"stringKey2"=>"Abc2",
"boolKey2"=>false,
"zeroKey2" => 0,
"blankKey2"=>"",
"blankArr" => array()
)
);
This give output:
Array
(
[stringKey] => Abc
[boolKey] =>
[zeroKey] => 0
[newArr] => Array
(
[stringKey2] => Abc2
[boolKey2] =>
[zeroKey2] => 0
[blankKey2] =>
[blankArr] => Array
(
)
)
)
But i want to bellow output:
Array
(
[stringKey] => Abc
[boolKey] =>
[zeroKey] => 0
[newArr] => Array
(
[stringKey2] => Abc2
[boolKey2] =>
[zeroKey2] => 0
)
)
I used array_filter with callback function but it only filter simple array not multidimensional array. I don't want to use loop.
You could combine array_map and array_filter in an recursive called function. Something like this could work for you.
function filterNotNull($array) {
$array = array_map(function($item) {
return is_array($item) ? filterNotNull($item) : $item;
}, $array);
return array_filter($array, function($item) {
return $item !== "" && $item !== null && (!is_array($item) || count($item) > 0);
});
}
Don't need to reinvent recursion yourself. You can use RecursiveCallbackFilterIterator:
$iterator = new RecursiveIteratorIterator(
new RecursiveCallbackFilterIterator(
new RecursiveArrayIterator($arr),
function ($value) {
return $value !== null && $value !== '';
}
)
);
$result = iterator_to_array($iterator);
Here is working demo.
You should try to use as much stuff from Standard PHP Library (SPL) as you can.
UPDATE:
As it is stated in the comments, the solution with iterator not actually suits for this purpose.
In the comments to the array_walk_recursive function you can find the implementation of walk_recursive_remove function:
function walk_recursive_remove (array $array, callable $callback) {
foreach ($array as $k => $v) {
if (is_array($v)) {
$array[$k] = walk_recursive_remove($v, $callback);
} else {
if ($callback($v, $k)) {
unset($array[$k]);
}
}
}
return $array;
}
This generalized version of recursion function takes criteria in the form of callback. Having this function you can remove empty elements like this:
$result = walk_recursive_remove($arr, function ($value) {
return $value === null || $value === '';
});
Here is working demo.
Here you require filtering of an array for empty string. In this code below you can add no of checks to filter it recursively. Hope it will work fine.
Try this code snippet here
<?php
ini_set('display_errors', 1);
//Using more complexed sample array for filter it.
$arr = array(
"stringKey" => "Abc",
"boolKey" => false,
"zeroKey" => 0,
"blankKey" => '',
"newArr" => array(
"stringKey2" => "Abc2",
"boolKey2" => false,
"zeroKey2" => 0,
"blankKey2" => "",
"blankArr" => array(
"blankString"=>"",
"zeroKey"=>0,
"blankArr3"=>array()
)
)
);
$result=recursiveFilter($arr);
print_r($result);
function recursiveFilter($value)
{
foreach ($value as $key => $value1)
{
if ($value1 === "") unset($value[$key]);//you can add no. of conditions here.
else if (is_array($value1)) $value[$key] = recursiveFilter($value1);
}
return $value;
}
Output:
Array
(
[stringKey] => Abc
[boolKey] =>
[zeroKey] => 0
[newArr] => Array
(
[stringKey2] => Abc2
[boolKey2] =>
[zeroKey2] => 0
[blankArr] => Array
(
[zeroKey] => 0
[blankArr3] => Array
(
)
)
)
)

Find the position of a key in a multidimensional array in PHP

I have this array:
$actualPlan = 'medium';
$plans = array(
array(
'plans' => array(
'tiny' => 29,
'small' => 69,
'medium' => 179,
'big' => 359
)
)
);
During a foreach, I display the contents of plans of this array like this:
foreach($plans as $key => $data) {
foreach($data['plans'] as $plan => $rate) {
...
}
}
But how can I know the position of the $actualPlan ?
For example, for :
if $actualPlan == medium it should return me 3.
if $actualPlan == tiny it should return me 1.
Thanks.
inside the loop:
echo array_search($actualPlan, array_keys($rate)); // returns the index position as int
here will output 1,2,3,4 for the inputs 'tiny','small','medium','big'
Within the foreach($plans as $key => $data) {, you could make the conditional like this :
$current_plan = explode('___', $actualPlan);
$current_key = array_search($current_plan[0],array_keys($data['plans']));
foreach($data['plans'] as $plan => $rate) {
$current_iterated_key = array_search($plan,array_keys($data['plans']));
if ($current_iterated_key < $current_key) {
echo "$plan => Downgrade\r\n";
} elseif ($current_iterated_key > $current_key) {
echo "$plan => Upgrade\r\n";
} elseif($current_iterated_key == $current_key) {
echo "$plan => Current\r\n";
}
}

Filter recursive array and only remove NULL value

I want to remove all null or blank value but not false and 0 value from recursive array.
function isNotNull($val) {
if(is_array($val)) {
$ret = array_filter($val, 'isNotNull');
return $ret;
} else {
return (!is_null($val) && $val !== '');
}
}
$arr = array_filter($arr, 'isNotNull');
Input:
$arr = array(
"stringKey" => "Abc",
"boolKey" => false,
"zeroKey" => 0,
"blankKey" => '',
"newArr" => array(
"stringKey2"=>"Abc2",
"boolKey2"=>false,
"zeroKey2" => 0,
"blankKey2"=>"",
"blankArr" => array()
)
);
This give output:
Array
(
[stringKey] => Abc
[boolKey] =>
[zeroKey] => 0
[newArr] => Array
(
[stringKey2] => Abc2
[boolKey2] =>
[zeroKey2] => 0
[blankKey2] =>
[blankArr] => Array
(
)
)
)
But i want to bellow output:
Array
(
[stringKey] => Abc
[boolKey] =>
[zeroKey] => 0
[newArr] => Array
(
[stringKey2] => Abc2
[boolKey2] =>
[zeroKey2] => 0
)
)
I used array_filter with callback function but it only filter simple array not multidimensional array. I don't want to use loop.
You could combine array_map and array_filter in an recursive called function. Something like this could work for you.
function filterNotNull($array) {
$array = array_map(function($item) {
return is_array($item) ? filterNotNull($item) : $item;
}, $array);
return array_filter($array, function($item) {
return $item !== "" && $item !== null && (!is_array($item) || count($item) > 0);
});
}
Don't need to reinvent recursion yourself. You can use RecursiveCallbackFilterIterator:
$iterator = new RecursiveIteratorIterator(
new RecursiveCallbackFilterIterator(
new RecursiveArrayIterator($arr),
function ($value) {
return $value !== null && $value !== '';
}
)
);
$result = iterator_to_array($iterator);
Here is working demo.
You should try to use as much stuff from Standard PHP Library (SPL) as you can.
UPDATE:
As it is stated in the comments, the solution with iterator not actually suits for this purpose.
In the comments to the array_walk_recursive function you can find the implementation of walk_recursive_remove function:
function walk_recursive_remove (array $array, callable $callback) {
foreach ($array as $k => $v) {
if (is_array($v)) {
$array[$k] = walk_recursive_remove($v, $callback);
} else {
if ($callback($v, $k)) {
unset($array[$k]);
}
}
}
return $array;
}
This generalized version of recursion function takes criteria in the form of callback. Having this function you can remove empty elements like this:
$result = walk_recursive_remove($arr, function ($value) {
return $value === null || $value === '';
});
Here is working demo.
Here you require filtering of an array for empty string. In this code below you can add no of checks to filter it recursively. Hope it will work fine.
Try this code snippet here
<?php
ini_set('display_errors', 1);
//Using more complexed sample array for filter it.
$arr = array(
"stringKey" => "Abc",
"boolKey" => false,
"zeroKey" => 0,
"blankKey" => '',
"newArr" => array(
"stringKey2" => "Abc2",
"boolKey2" => false,
"zeroKey2" => 0,
"blankKey2" => "",
"blankArr" => array(
"blankString"=>"",
"zeroKey"=>0,
"blankArr3"=>array()
)
)
);
$result=recursiveFilter($arr);
print_r($result);
function recursiveFilter($value)
{
foreach ($value as $key => $value1)
{
if ($value1 === "") unset($value[$key]);//you can add no. of conditions here.
else if (is_array($value1)) $value[$key] = recursiveFilter($value1);
}
return $value;
}
Output:
Array
(
[stringKey] => Abc
[boolKey] =>
[zeroKey] => 0
[newArr] => Array
(
[stringKey2] => Abc2
[boolKey2] =>
[zeroKey2] => 0
[blankArr] => Array
(
[zeroKey] => 0
[blankArr3] => Array
(
)
)
)
)

Return true/false on searching multidimensional array

I have the following multidimensional $array:
Array
(
[0] => Array
(
[domain] => example.tld
[type] => 2
)
[1] => Array
(
[domain] => other.tld
[type] => 2
)
[2] => Array
(
[domain] => blaah.tld
[type] => 2
)
)
I simply want to recursively search all the arrays on both key and value, and return true if the key/value was found or false if nothing was found.
Expected output:
search_multi_array($array, 'domain', 'other.tld'); // Will return true
search_multi_array($array, 'type', 'other.tld'); // Will return false
search_multi_array($array, 'domain', 'google.com'); // Will return false
I've figured out a ugly-ugly method to search against the domain against all keys with this function:
function search_multi_array($search_value, $the_array) {
if (is_array($the_array)) {
foreach ($the_array as $key => $value) {
$result = search_multi_array($search_value, $value);
if (is_array($result)) {
return true;
} elseif ($result == true) {
$return[] = $key;
return $return;
}
}
return false;
} else {
if ($search_value == $the_array) {
return true;
}
else
return false;
}
}
Can anyone do better and match both against the key and value in a more elegant way?
If it doesn't go beyond those 2 levels, flipping keys/merging makes life a lot more pleasant:
<?php
$data = array
(
'0' => array
(
'domain' => 'example.tld',
'type' => 2
),
'1' => array
(
'domain' => 'other.tld',
'type' => 2,
),
'2' => array
(
'domain' => 'blaah.tld',
'type' => 2
)
);
$altered = call_user_func_array('array_merge_recursive',$data);
var_dump($altered);
var_dump(in_array('other.tld',$altered['domain']));
var_dump(in_array('other.tld',$altered['type']));
var_dump(in_array('google.com',$altered['domain']));
To go beyond 2nd level, we have to loop once through all the nodes:
$option2 = array();
foreach(new RecursiveIteratorIterator(new RecursiveArrayIterator($data)) as $key => $value){
$option2[$key][] = $value;
}
var_dump($option2);
One way is to create a reverse mapping from [domain] => [indices] and from [type] => [indices]. It's probably not going to save you much unless you do lots of searches.
(hint: you probably want to wrap it into a class to prevent inconsistencies in the mappings)
also, anytime you see something like this:
if ($search_value == $the_array) {
return true;
} else {
return false;
}
you can always turn it into:
return $search_value == $the_array;
function search_mutli_array($SearchKey, $SearchValue, $Haystack)
{
$Result = false;
if (is_array($Haystack))
{
foreach ($Haystack as $Key => $Value)
{
if (is_array($Value))
{
if (search_mutli_array($SearchKey, $SearchValue, $Value))
{
$Result = true;
break;
}
}
else if ($SearchKey == $Key && $SearchValue == $Value)
{
$Result = true;
break;
}
}
}
return $Result;
}

php, long and deep matrix

I have a deep and long array (matrix). I only know the product ID.
How found way to product?
Sample an array of (but as I said, it can be very long and deep):
Array(
[apple] => Array(
[new] => Array(
[0] => Array([id] => 1)
[1] => Array([id] => 2))
[old] => Array(
[0] => Array([id] => 3)
[1] => Array([id] => 4))
)
)
I have id: 3, and i wish get this:
apple, old, 0
Thanks
You can use this baby:
function getById($id,$array,&$keys){
foreach($array as $key => $value){
if(is_array( $value )){
$result = getById($id,$value,$keys);
if($result == true){
$keys[] = $key;
return true;
}
}
else if($key == 'id' && $value == $id){
$keys[] = $key; // Optional, adds id to the result array
return true;
}
}
return false;
}
// USAGE:
$result_array = array();
getById( 3, $products, $result_array);
// RESULT (= $result_array)
Array
(
[0] => id
[1] => 0
[2] => old
[3] => apple
)
The function itself will return true on success and false on error, the data you want to have will be stored in the 3rd parameter.
You can use array_reverse(), link, to reverse the order and array_pop(), link, to remove the last item ('id')
Recursion is the answer for this type of problem. Though, if we can make certain assumptions about the structure of the array (i.e., 'id' always be a leaf node with no children) there's further optimizations possible:
<?php
$a = array(
'apple'=> array(
'new'=> array(array('id' => 1), array('id' => 2), array('id' => 5)),
'old'=> array(array('id' => 3), array('id' => 4, 'keyname' => 'keyvalue'))
),
);
// When true the complete path has been found.
$complete = false;
function get_path($a, $key, $value, &$path = null) {
global $complete;
// Initialize path array for first call
if (is_null($path)) $path = array();
foreach ($a as $k => $v) {
// Build current path being tested
array_push($path, $k);
// Check for key / value match
if ($k == $key && $v == $value) {
// Complete path found!
$complete= true;
// Remove last path
array_pop($path);
break;
} else if (is_array($v)) {
// **RECURSION** Step down into the next array
get_path($v, $key, $value, $path);
}
// When the complete path is found no need to continue loop iteration
if ($complete) break;
// Teardown current test path
array_pop($path);
}
return $path;
}
var_dump( get_path($a, 'id', 3) );
$complete = false;
var_dump( get_path($a, 'id', 2) );
$complete = false;
var_dump( get_path($a, 'id', 5) );
$complete = false;
var_dump( get_path($a, 'keyname', 'keyvalue') );
I tried this for my programming exercise.
<?php
$data = array(
'apple'=> array(
'new'=> array(array('id' => 1), array('id' => 2), array('id' => 5)),
'old'=> array(array('id' => 3), array('id' => 4))
),
);
####print_r($data);
function deepfind($data,$findfor,$depth = array() ){
foreach( $data as $key => $moredata ){
if( is_scalar($moredata) && $moredata == $findfor ){
return $depth;
} elseif( is_array($moredata) ){
$moredepth = $depth;
$moredepth[] = $key;
$isok = deepfind( $moredata, $findfor, $moredepth );
if( $isok !== false ){
return $isok;
}
}
}
return false;
}
$aaa = deepfind($data,3);
print_r($aaa);
If you create the array once and use it multiple times i would do it another way...
When building the initial array create another one
$id_to_info=array();
$id_to_info[1]=&array['apple']['new'][0];
$id_to_info[2]=&array['apple']['new'][2];

Categories