I have data about some racers presented in the following form:
array(
array(name => "the first racer", places => [1,3,1,5,6,2,6,7,8]),
array(name => "the second racer", places => [2,4,2,5,7])
...
)
Advise the best way to sort them so that the first racers were who have better places. For example If the first racer has at least one first place and the other not, the first is higher in the list. If they both have the first places, compare the number of first place. If the number equal too, compare the second places and so on.
My solution (It does not look very elegant. Maybe it can be done somehow easier):
$racers = array(
array('name' => "the first racer", 'places' => [1,3,1,5,6,2,6,7,8,9]),
array('name' => "the second racer", 'places' => [1,3,1,5,6,2,6,7,8]),
array('name' => "the third racer", 'places' => [2,3,2,5,7,10]),
array('name' => "the fourth racer", 'places' => [2,3,10,6,6,10]),
array('name' => "the fifth", 'places' => [2,3,2,5,7,10,1]),
);
usort($racers, function($prev, $next) {
// order places for every racer
sort($prev['places']);
sort($next['places']);
//compare each place with each other
foreach ($prev['places'] AS $key => $prevRacerPlace) {
// if all values are equal, we compare the number of races
if (!isset($next['places'][$key])) {
return -1;
}
$nextRacerPlace = $next['places'][$key];
$diff = $prevRacerPlace - $nextRacerPlace;
if ($diff !== 0) {
return $diff;
}
}
// if all values are equal, we compare the number of races
if (count($next['places']) > count($prev['places'])) {
return 1;
}
});
var_dump($racers);
It would be a quite nice to make some preparations before custom sorting. So we avoid a nested sorts in lambda function:
foreach ($racers as $index => $racer) {
$racers[$index]['sorted_places'] = $racer['places'];
sort($racers[$index]['sorted_places']);
}
In sorting lambda function we compare a heads of prepared sorted places and return a first defined value. If racer B top place result better than A, return 1. If racer A top place result better than B, return -1. On equal results continue checks for next top places.
usort($racers, function ($a, $b) {
unset($value);
do {
$topA = array_shift($a['sorted_places']);
$topB = array_shift($b['sorted_places']);
if (is_null($topA) && is_null($topB)) {
$value = 0;
} elseif (is_null($topA)) {
$value = 1;
} elseif (is_null($topB)) {
$value = -1;
} elseif ($topA > $topB) {
$value = 1;
} elseif ($topA < $topB) {
$value = -1;
}
} while (!isset($value));
return $value;
});
Here is one more algorithm, but I think that Max Zuber's solution is more efficient. Anyway:
Define how many places were for each racers by array_count_values
foreach ($racers as &$racer) {
$racer['number_places'] = array_count_values($racer['places']);
}
and sorting
usort($racers, function($current, $next) {
$next_places = $next['number_places'];
$current_places = $current['number_places'];
for ($i=1; $i<=max($next_places, $current_places); $i++) {
if (!isset($current_places[$i]) && !isset($next_places[$i])) {
continue;
}
if (!isset($current_places[$i])) {
return 1;
}
if (!isset($current_places[$i])
|| $current_places[$i] > $next_places[$i])
{
return -1;
}
}
});
Related
I'm attempting to modify the OrderedImportsFixer class in php-cs-fixer so I can clean up my files the way I want. What I want is to order my imports in a fashion similar to what you'd see in a filesystem listing, with "directories" listed before "files".
So, given this array:
$indexes = [
26 => ["namespace" => "X\\Y\\Zed"],
9 => ["namespace" => "A\\B\\See"],
3 => ["namespace" => "A\\B\\Bee"],
38 => ["namespace" => "A\\B\\C\\Dee"],
51 => ["namespace" => "X\\Wye"],
16 => ["namespace" => "A\\Sea"],
12 => ["namespace" => "A\\Bees"],
31 => ["namespace" => "M"],
];
I'd like this output:
$sorted = [
38 => ["namespace" => "A\\B\\C\\Dee"],
3 => ["namespace" => "A\\B\\Bee"],
9 => ["namespace" => "A\\B\\See"],
12 => ["namespace" => "A\\Bees"],
16 => ["namespace" => "A\\Sea"],
26 => ["namespace" => "X\\Y\\Zed"],
51 => ["namespace" => "X\\Wye"],
31 => ["namespace" => "M"],
];
As in a typical filesystem listing:
I've been going at uasort for a while (key association must be maintained) and have come close. Admittedly, this is due more to desperate flailing than any sort of rigorous methodology. Not really having a sense of how uasort works is kind of limiting me here.
// get the maximum number of namespace components in the list
$ns_counts = array_map(function($val){
return count(explode("\\", $val["namespace"]));
}, $indexes);
$limit = max($ns_counts);
for ($depth = 0; $depth <= $limit; $depth++) {
uasort($indexes, function($first, $second) use ($depth, $limit) {
$fexp = explode("\\", $first["namespace"]);
$sexp = explode("\\", $second["namespace"]);
if ($depth === $limit) {
// why does this help?
array_pop($fexp);
array_pop($sexp);
}
$fexp = array_slice($fexp, 0, $depth + 1, true);
$sexp = array_slice($sexp, 0, $depth + 1, true);
$fimp = implode(" ", $fexp);
$simp = implode(" ", $sexp);
//echo "$depth: $fimp <-> $simp\n";
return strnatcmp($fimp, $simp);
});
}
echo json_encode($indexes, JSON_PRETTY_PRINT);
This gives me properly sorted output, but with deeper namespaces on the bottom instead of the top:
{
"31": {
"namespace": "M"
},
"12": {
"namespace": "A\\Bees"
},
"16": {
"namespace": "A\\Sea"
},
"3": {
"namespace": "A\\B\\Bee"
},
"9": {
"namespace": "A\\B\\See"
},
"38": {
"namespace": "A\\B\\C\\Dee"
},
"51": {
"namespace": "X\\Wye"
},
"26": {
"namespace": "X\\Y\\Zed"
}
}
I'm thinking I may have to build a separate array for each level of namespace and sort it separately, but have drawn a blank on how I might do that. Any suggestions for getting the last step of this working, or something completely different that doesn't involve so many loops?
We divide this into 4 steps.
Step 1: Create hierarchical structure from the dataset.
function createHierarchicalStructure($indexes){
$data = [];
foreach($indexes as $d){
$temp = &$data;
foreach(explode("\\",$d['namespace']) as $namespace){
if(!isset($temp[$namespace])){
$temp[$namespace] = [];
}
$temp = &$temp[$namespace];
}
}
return $data;
}
Split the namespaces by \\ and maintain a $data variable. Use & address reference to keep editing the same copy of the array.
Step 2: Sort the hierarchy in first folders then files fashion.
function fileSystemSorting(&$indexes){
foreach($indexes as $key => &$value){
fileSystemSorting($value);
}
uksort($indexes,function($key1,$key2) use ($indexes){
if(count($indexes[$key1]) == 0 && count($indexes[$key2]) > 0) return 1;
if(count($indexes[$key2]) == 0 && count($indexes[$key1]) > 0) return -1;
return strnatcmp($key1,$key2);
});
}
Sort the subordinate folders and use uksort for the current level of folders. Vice-versa would also work. If both 2 folders in comparison have subfolders, compare them as strings, else if one is a folder and another is a file, make folders come above.
Step 3: Flatten the hierarchical structure now that they are in order.
function flattenFileSystemResults($hierarchical_data){
$result = [];
foreach($hierarchical_data as $key => $value){
if(count($value) > 0){
$sub_result = flattenFileSystemResults($value);
foreach($sub_result as $r){
$result[] = $key . "\\" . $r;
}
}else{
$result[] = $key;
}
}
return $result;
}
Step 4: Restore the initial data keys back and return the result.
function associateKeys($data,$indexes){
$map = array_combine(array_column($indexes,'namespace'),array_keys($indexes));
$result = [];
foreach($data as $val){
$result[ $map[$val] ] = ['namespace' => $val];
}
return $result;
}
Driver code:
function foldersBeforeFiles($indexes){
$hierarchical_data = createHierarchicalStructure($indexes);
fileSystemSorting($hierarchical_data);
return associateKeys(flattenFileSystemResults($hierarchical_data),$indexes);
}
print_r(foldersBeforeFiles($indexes));
Demo: https://3v4l.org/cvoB2
I believe the following should work:
uasort($indexes, static function (array $entry1, array $entry2): int {
$ns1Parts = explode('\\', $entry1['namespace']);
$ns2Parts = explode('\\', $entry2['namespace']);
$ns1Length = count($ns1Parts);
$ns2Length = count($ns2Parts);
for ($i = 0; $i < $ns1Length && isset($ns2Parts[$i]); $i++) {
$isLastPartForNs1 = $i === $ns1Length - 1;
$isLastPartForNs2 = $i === $ns2Length - 1;
if ($isLastPartForNs1 !== $isLastPartForNs2) {
return $isLastPartForNs1 <=> $isLastPartForNs2;
}
$nsComparison = $ns1Parts[$i] <=> $ns2Parts[$i];
if ($nsComparison !== 0) {
return $nsComparison;
}
}
return 0;
});
What it does is:
split namespaces into parts,
compare each part starting from the first one, and:
if we're at the last part for one and not the other, prioritize the one with the most parts,
otherwise, if the respective parts are different, prioritize the one that is before the other one alphabetically.
Demo
Here's another version that breaks the steps down further that, although it might not be the most optimal, definitely helps my brain think about it. See the comments for more details on what is going on:
uasort(
$indexes,
static function (array $a, array $b) {
$aPath = $a['namespace'];
$bPath = $b['namespace'];
// Just in case there are duplicates
if ($aPath === $bPath) {
return 0;
}
// Break into parts
$aParts = explode('\\', $aPath);
$bParts = explode('\\', $bPath);
// If we only have a single thing then it is a root-level, just compare the item
if (1 === count($aParts) && 1 === count($bParts)) {
return $aPath <=> $bPath;
}
// Get the class and namespace (file and folder) parts
$aClass = array_pop($aParts);
$bClass = array_pop($bParts);
$aNamespace = implode('\\', $aParts);
$bNamespace = implode('\\', $bParts);
// If the namespaces are the same, sort by class name
if ($aNamespace === $bNamespace) {
return $aClass <=> $bClass;
}
// If the first namespace _starts_ with the second namespace, sort it first
if (0 === mb_strpos($aNamespace, $bNamespace)) {
return -1;
}
// Same as above but the other way
if (0 === mb_strpos($bNamespace, $aNamespace)) {
return 1;
}
// Just only by namespace
return $aNamespace <=> $bNamespace;
}
);
Online demo
I find no fault with Jeto's algorithmic design, but I decided to implement it more concisely. My snippet avoids iterated function calls and arithmetic in the for() loop, uses a single spaceship operator, and a single return. My snippet is greater than 50% shorter and I generally find it easier to read, but then everybody thinks their own baby is cute, right?
Code: (Demo)
uasort($indexes, function($a, $b) {
$aParts = explode('\\', $a['namespace']);
$bParts = explode('\\', $b['namespace']);
$aLast = count($aParts) - 1;
$bLast = count($bParts) - 1;
for ($cmp = 0, $i = 0; $i <= $aLast && !$cmp; ++$i) {
$cmp = [$i === $aLast, $aParts[$i]] <=> [$i === $bLast, $bParts[$i]];
}
return $cmp;
});
Like Jeto's answer, it iterates each array simultaneously and
checks if either element is the last element of the array, if so it is bumped down the list (because we want longer paths to win tiebreakers);
if neither element is the last in its array, then compare the elements' current string alphabetically.
The process repeats until a non-zero evaluation is generated.
Since duplicate entries are not expected to occur, the return value should always be -1 or 1 (never 0)
Note, I am halting the for() loop with two conditions.
If $i is greater than the number of elements in $aParts (only) -- because if $bParts has fewer elements than $aParts, then $cmp will generate a non-zero value before a Notice is triggered.
If $cmp is a non-zero value.
Finally, to explain the array syntax on either side of the spaceship operator...The spaceship operator will compare the arrays from left to right, so it will behave like:
leftside[0] <=> rightside[0] then leftside[1] <=> rightside[1]
Making multiple comparisons in this way does not impact performance because there are no function calls on either side of the <=>. If there were function calls involved, it would be more performant to make individual comparisons in a fallback manner like:
fun($a1) <=> fun($b1) ?: fun($a2) <=> fun($b2)
This way subsequent function calls are only actually made if a tiebreak is necessary.
i wondering how i can make it simpler...
the thing is
if($results['experience'] == 0) { loadViev('experience',$experience, $careerGoals,$mainSectionLines); }
if($results['Education'] == 0) { loadViev('Education',$graduate,$careerGoals,$mainSectionLines); }
if($results['Extra'] == 0) { loadViev('Extra',$extra,$careerGoals,$mainSectionLines); }
if($results['Licence'] == 0) { loadViev('Licence',$licence,$careerGoals,$mainSectionLines); }
if($results['Cert'] == 0) { loadViev('Certyfikaty',$cert,$careerGoals,$mainSectionLines); }
if($results['conferences'] == 0){ loadViev('Conferences',$conferences,$careerGoals,$mainSectionLines); }
if($results['Courses'] == 0) { loadViev('Courses',$Courses,$careerGoals,$mainSectionLines); }
if($results['Hobbys'] == 0) { loadViev('Hobby',$hobby,$careerGoals,$mainSectionLines); }
As you can see, if some "name" == 0 function will run, there is for now around 14 combination, i know there is possible to do it faster than copy and paste whole code 14 times...
result code:
$results =[];
foreach ($userChoice as $key => $value) {
$setVal = $value['key'];
$results[$setVal] = $value['order'];
}
Result only grab name of a section and order nr.
'$userChoice' is just a array with data
Do anybody have a idea how i can do it?
Also the thing is result collect all the section data (14 section) where as you can see i need only selected 8.
The only difference is the word being loaded and the name of the variable. The word remains the same, but the variable is lowercase.
As such, all you would need to do is loadViev the $setVal name (the word itself) as the first argument, and $$setval as the second. This executes the word as a variable, making use of variable variables.
Unfortunately you can't (easily) use something like strtolower() on a variable variable directly, so you'll have to convert these to lowercase independently first.
This can be seen in the following:
$results =[];
foreach ($userChoice as $key => $value) {
$setVal = $value['key'];
$results[$setVal] = $value['order'];
if ($value['order'] == 0) {
$lower = strtolower($setVal);
loadViev($setVal, $$lower, $careerGoals, $mainSectionLines);
}
}
How about creating a structure holding relevant values and iterating over it?
$mapping = [
['key' => 'experience', 'view' => 'experience','data' => $experience],
['key' => 'Education', 'view' => 'Education','data' => $graduate],
['key' => 'Extra', 'view' => 'Extra','data' => $extra],
...
...
];
foreach ($mapping as $m)
{
if ($results[$m['key']]==0)
{
loadViev($m['view'], $m['data'], $careerGoals,$mainSectionLines);
break;
}
}
if you can make your key/variable names consistent, you could simplify the code even further. E.g.
$validKeys = ['experience', 'education', ... ];
foreach($validKeys as $k)
{
if($results[$k] == 0)
{
loadviev($k, $$k, $careerGoals,$mainSectionLines)
}
}
So I am trying to find the best model within our database to match the user's inputted values for Height, Weight, etc. I have generated a variable called heightMatchMultiple based on weighted values and stored all the results in a global array $models. How can I apply math to only the ____MatchMultiple fields and then sort by the result? For example, I store the modelID of each model but I only want to add up the other values and then find the highest average.
Here is some relevant code:
while($row=mysqli_fetch_array($array))
{
$userHeight=$_POST['height'];
$userWeight=$_POST['weight'];
$userShoulder=$_POST['shoulder'];
$userWaist=$_POST['waist'];
$userInseam=$_POST['inseam'];
$heightMatchMultiple=0;
//creates a weighted variable for height
if(isset($row['modelHeight']))
{
if($userHeight==$row['modelHeight'])
{
$heightMatchMultiple=10;
}
elseif($userHeight==($row['modelHeight']+1) || $userHeight==($row['modelHeight']-1))
{
$heightMatchMultiple=9;
}
//same code down til multiple hits 1
else
{
$heightMatchMultiple=1;
}
//similar code for the weight, shoulders, etc.......
//....
array_push($models,array("modelID" => $row['modelID'],
"modelHeightMatch" => $heightMatchMultiple,
"modelWeightMatch" => $weightMatchMultiple,
"modelShoulderMatch" => $shoulderMatchMultiple,
"modelWaistMatch" => $waistMatchMultiple,
"modelInseamMatch" => $inseamMatchMultiple));
I would like to make a function that adds all of the _____MatchMultiple variables within the array (not including modelID) and then divides by the number of items within the array for each. This way I can find out which model is the closest match.
function getAverage($array) {
var $myTotal = 0
for ($i = 1; $i<= sizeof($array) - 1; $i++) {
$myTotal += $array[$i]
}
var $myAverage = $myTotal/(sizeof(array) - 1)
return($myAverage)
}
Does this sum_array() function solve it for you?
function safe_key_inc(&$arr, $key, $inc)
{
if (array_key_exists($key, $arr))
$arr[$key] += $inc;
else
$arr[$key] = $inc;
}
function sum_array($arr)
{
$sum_arr = [];
foreach ($arr as $el)
foreach ($el as $key => $val)
{
if (strstr($key, "Match"))
safe_key_inc($sum_arr,$key,$val);
}
return $sum_arr;
}
$records = array(
'123PP' => 3.63,
'123DDD' => 9.63,
'123D' => 6.63,
'123PPPP' => 9.63,
'123DD' => 9.63,
'123P' => 2.63,
'123PPP' => 1.53
);
After looping through the records, I have to get only one value
whose key should be 123D because the order of preference is:
123D, 123P, 123DD, 123PP, 123DDD, 123PPP, 123PPPP...
For e.g.:
If 123D is not found in the array, then 123P is the answer.
If 123P is not found in the array, then 123DD is the answer.
And I have found a solution :
foreach ($records as $key => $value) {
if (empty($this->minLength)) {
$this->invoiceTax = $value;
$this->minLength = strlen($key);
}
elseif (strpos($key, 'P') !== false && (strlen($key) < $this->minLength)) {
$this->invoiceTax = $value;
$this->minLength = strlen($key);
}
elseif (strpos($key, 'D') !== false && (strlen($key) <= $this->minLength)) {
$this->invoiceTax = $value;
$this->minLength = strlen($key);
}
But I want to know if this code can be optimised by not storing the string length of every key.
This function could easily be tidied but this is something that could be solved with recursion. This means that if 123D is in the array the code will be highly optimised and only run once, twice for 123P, three times for 123DD, etc.
function GetMostPref($records, $key = "123", $next = "D", $depth = 0)
{
if($depth == count($records))
{
// Hit end of array with nothing found
return false;
}
if(strpos($next, 'D') !== false)
{
// Currently looking at a 'D...' key.
// Next key is the same length as this key just with Ps.
$nextKey = str_repeat('P', strlen($next));
}
else if(strpos($next, 'P') !== false)
{
// Currently looking at a 'P...' key.
// Next key has one extra char and is all Ds.
$nextKey = str_repeat('D', strlen($next)+1);
}
else
{
// Key not valid
return false;
}
if(array_key_exists($key.$next, $records))
{
// Found the key in the array so return it.
return $records[$key.$next];
}
else
{
// Recursive call with the next key and increased depth.
return GetMostPref($records, $key, $nextKey, $depth + 1);
}
}
// Testing
$records = array(
'123PP' => 3.63,
'123DDD' => 9.63,
'123D' => 6.63,
'123PPPP' => 9.63,
'123DD' => 9.63,
'123P' => 2.63,
'123PPP' => 1.53
);
// Finds 123D and returns 6.63
echo GetMostPref($records);
function prepareFunctionCall(){
$records = array('123PP' => 3.63,'123DDD' => 9.63,'123PPPP' => 9.63
,'123DD' => 9.63,'123P' => 2.63,'123PPP' => 1.53);
// '123D' => 6.63,
foreach($records as $key=>$value){
$tmp = strlen($key).$key;
$arTmp[$tmp] = $value;
}
getFirstValue($arTmp);
}
function getFirstValue($pArray){
ksort($pArray);
reset($pArray);
print key($pArray).' = '.current($pArray);
}
This is an alternative for the good solution provided by MatthewMcGovern.
I provide the alternative, because this function makes use of the php functions ksort, reset and current. Those functions are created for this type of situation and if possible then would I advise you to rewrite the keys of the array before finding out which key is the first key to select. That is what I did with the addition of the strlen. But that is suboptimal compared to rewriting the keys at the moment of collecting the data. The core of the function are the calls to the functions ksort, reset and current.
I'm trying to create a function that will loop through an array of various lengths. During the loop, a function is run to see if the immediately prior item (the item at current key minus 1) matches what is in the array.
Here are two examples of arrays:
$terms1 = array(
0 => 'MEL',
1 => 'Appliances',
2 => 'Clothes Dryers',
3 => 'Clothes dryers - electric'
);
$terms2 = array(
0 => 'Clothes Dryers',
1 => 'Clothes dryers - electric'
);
And here is the function to be run within the loop... this function will return a value and then I will compare that to what is in the array in the immediately prior location (current key minus 1). This pulls from a db.
getParent($terms1[3]); //Would output the value I want to compare to $terms1[2]
I've tried something like this:
$fail = null;
foreach(array_reverse($terms1, true) as $key => $value){
if($key > 0){
$priorkey = $key - 1;
if(getParent($terms1[$key]) != $terms1[$priorkey]){
$fail = true;
}
}
}
return $fail;
I think I need a recursive function... any help or nudges in the right direction would be appreciated.
$prev = null;
foreach ($terms1 as $v) {
if ($prev == getParent($v)) {
$fail = true;
break;
}
$prev = $v;
}
I don't understand why your code doesn't work, but if you add a break; after $fail = true; it will run faster and return the same result. There's no need to check the rest after the first failure.