Cluster PHP array - php

Let's say I have an array of items with each item a value. I'd like to
create a new array where the items are clustered by their relative distance to each other.
When an item has a distance of one to another item, they belong to each other.
$input = [
'item-a' => 1,
'item-b' => 2,
'item-c' => 3,
'item-d' => 5,
];
$output = [
['item-a', 'item-b'],
['item-b', 'item-c'],
['item-d'],
];
This will create an output of overlapping arrays. What I want is that, because item-a and item-b are related, and item-b is also
related to item-c, I'd like to group item-a, item-b, and item-c to each other. The distance to item-c and item-d is greater than
1 so it will for a cluster of itself.
$output = [
['item-a', 'item-b', 'item-c'],
['item-d'],
];
How do I even start coding this?
Thanks in advance and have a nice day!

This can only be tested in your environment but here is what it does
it attempts to find relative distances based on array index 0's hash
it resorts the input array by distances (assuming that in this stage some will be positive and some negative) - that gives us the info to put the hash array in an order
Take this new array and put the hash back in
build a final output array measuring distances and sorting the level of output array by a threshhold.
I put in a couple dummy functions to return distances, obviously replace with your own. This might need tweaking but at this point, it's in your hands.
<?php
// example code
$input = [
'item-a' => 'a234234d',
'item-f' => 'h234234e',
'item-h' => 'e234234f',
'item-b' => 'f234234g',
'item-m' => 'd234234j',
'item-d' => 'm234234s',
'item-e' => 'n234234d',
'item-r' => 's234234g',
'item-g' => 'f234234f',
];
function getDistanceFrom($from, $to) {
return rand(-3,3);
}
function getDistanceFrom2($from, $to) {
return rand(0,7);
}
// first sort by relative distance from the first one
$tmp = [];
$ctr = 0;
foreach ($input as $item => $hash) {
if ($ctr === 0) { $ctr ++; continue; }
$tmp[$item]=getDistanceFrom(reset($input), $hash);
}
uasort($tmp, function ($a, $b)
{
return ($a < $b) ? -1 : 1;
});
//now they're in order, ditch the relative distance and put the hash back in
$sortedinput = [];
foreach ($tmp as $item => $d) {
$sortedinput[$item] = $input[$item];
}
$output=[];
$last=0;
$level=0;
$thresh = 3; // if item is within 3 of the previous, group
foreach($sortedinput as $v=>$i) {
$distance = getDistanceFrom2($last, $i);
if (abs($distance) > $thresh) $level++;
$output[$level][]=array("item" => $v, "distance" => $distance, "hash" => $i);
$last = $i;
}
print_r($output);

Related

Rank keys of an associative array according to their values (error in function)

I want to rank the keys of an associative array in php based upon their values. (top to down as 1, 2, 3....). Keys having same value will have same rank.
Here function getRanks() is meant to return an array containing keys and the ranks (number).
I expect it to return like this (this is sorted value wise in descending)
Array
(
[b] => 1
[a] => 2
[d] => 3
[c] => 3
[e] => 4
)
There is issue in assigning the ranks (values) in the $ranks array which is to be returned.
What am I doing wrong? Do these loops even do something?
Code:
$test = array('a'=> 50, 'b'=>60, 'c'=>20, 'd'=>20, 'e'=>10);
$json = json_encode($test);
print_r(getRanks($json));
function getRanks($json) {
$tmp_arr = json_decode($json, TRUE);
$ranks = array();
uasort($tmp_arr, function($a, $b){
return $a == $b ? 0 : $a > $b ? -1 : 1; //descending
});
$keys = array_keys($tmp_arr); //after sorting
$ranks = array_fill_keys($keys, 0); //copy keys
$ranks[$keys[0]] = 1; //first val => rank 1
//------- WORKS FINE UNTIL HERE ------------------
// need to fix the ranks assignment
for($i=1; $i<count($keys)-1; $i++) {
for($j=$i; $j < count($keys)-1; $j++) {
if($tmp_arr[$keys[$j]] == $tmp_arr[$keys[$j+1]]) {
$rank[$keys[$j]] = $i;
}
}
}
return $ranks;
}
Your approach seems unnecessarily complicated. In my version I kept the json-related copying part of it but finished it off in a simpler way:
function getRanks($json) {
$tmp_arr = json_decode($json, TRUE);
asort($tmp_arr);. // sort ascending
$i=0; $lv=null;$ranks = array();
foreach ($tmp_arr as $k=>$v) {
if ($v>$lv){ $i++; $lv=$v;}
$ranks[$k]=$i;
}
return $ranks;
}
See the demo here: https://rextester.com/LTOA23372
In a slightly modified version you you can also do the ranking in a descending order, see here: https://rextester.com/HESQP10053
I've also tried with another approach.
I think it may not be the good solution because of high memory & CPU time consumption.
For small arrays (in my case) it works fine.
(I've posted because it may be an answer)
It creates array of unique values and fetches ranks accordingly.
$test = array('a'=> 50, 'b'=>60, 'c'=>20, 'd'=>20, 'e'=>10);
$json = json_encode($test);
print_r(getRanks($json));
function getRanks($json) {
$tmp_arr = json_decode($json, TRUE);
arsort($tmp_arr);
$uniq_vals = array_values(array_unique($tmp_arr)); // unique values indexed numerically from 0
foreach ($tmp_arr as $k => $v) {
$tmp_arr[$k] = array_search($v, $uniq_vals) + 1; //as rank will start with 1
}
return $tmp_arr;
}
This is the simple thing that you can do it by using php array function check example given below.
<?php
$fruits = array("d" => "lemon", "a" => "orange", "b" => "banana", "c" => "apple");
asort($fruits);
foreach ($fruits as $key => $val) {
echo "$key = $val\n";
}
?>

PHP class or function for eliminate redundancy in an ip block list

I have a list of ip blocks like this that I obtained by stitching together different sources.
160.11.0.0/16
160.11.14.0/20
160.12.0.0/14
160.16.5.0/15
160.16.1.0/14
160.18.0.0/16
160.20.0.0/14
160.24.0.0/16
160.26.0.0/15
160.28.0.0/15
160.74.0.0/16
...
I would like to have a php function that could take in input this list and optimize it's size by removing unnecessary redundancies because the very final output of all this will be given to a software able to compare IPs and it's performances will depend of how much lines of input it will get (the shorter the list, better will be software performances).
My code would be something like:
$input_array = file("list.txt");
$output_array = optimize_ipblocks($input_array);
file_put_contents(implode("\n", $output));
This optimize_ipblocks function should be able to:
Isolate all blocks and see if there are smaller blocks already contained inside bigger blocks, remove the smaller ones.
If there are duplicates of the same kind, remove them.
If there are blocks that can be joined together because they share partially the content or if they touch themselves merge them into a bigger block.
If some aggregation of blocks can be achieved, aggregate them into and so on.
My knowledge of ip blocks unpacking and comparing is kinda limited so for now the only part i can get rid of is a duplicate check modelling the function like:
function optimize_ipblocks($input_array) {
$blocks = array();
foreach($input_array as $key => $val) {
if(!in_array($val, $blocks)) $blocks[] = $val;
}
return $blocks;
}
I have no idea how to carry out comparisons of blocks and aggregation.
Helpful links for resolution:
This wikipedia article can help maybe https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing down to the section it talks about IPv4 CIDR blocks.
This perl script to aggregate cidr addresses http://www.zwitterion.org/software/aggregate-cidr-addresses/
http://www.perlmonks.org/?node_id=118346
I wrote a class for merging Ip ranges into an optimized superset of IP ranges. For your usage, you should first convert your CIDR addresses into from-to IP ranges, as in the $ranges array in this usage example:
$ranges=[
["from" => '192.168.0.1',"to" => '192.168.0.9'],
["from" => '192.168.0.3',"to" => '192.168.0.6'],
["from" => '192.168.0.1',"to" => '192.168.0.5'],
["from" => '192.168.0.13',"to" => '192.168.0.17'],
["from" => '192.168.0.2',"to" => '192.168.0.4'],
["from" => '192.168.0.2',"to" => '192.168.0.7'],
["from" => '192.168.0.12',"to" => '192.168.0.14'],
];
$rm=new C_RangeMerger($ranges,'ip');
$mergedRanges=$rm->getMergedRanges();
echo("<pre>".print_r($mergedRanges,1)."</pre>");
class C_RangeMerger
Output:
Array
(
[0] => Array
(
[from] => 192.168.0.1
[to] => 192.168.0.9
)
[1] => Array
(
[from] => 192.168.0.12
[to] => 192.168.0.17
)
)
Hope that helps!
This function should do what you want..
<?php
function merge_cidr(array $cidr_or_ipv4_list)
{ // Main function
$valid_ip='0*?((?:0)|(?:2(?:(?:[0-4][0-9])|(?:5[0-5])))|(?:1?[0-9]{1,2}))'; // Build the valid ipv4 regex
$valid_ip.=str_repeat(".$valid_ip",3); // Finalize the ipv4 regex accepting leading zeros for each part
$valid_routing_prefix='(?:0*?((?:(?:0)|(?:3[0-2])|(?:[1-2]?[0-9]))))'; // Build a regex for the routing prefix (accepting leading zeros)
foreach($cidr_or_ipv4_list as $a) // For each entry you pass to the function
if (is_string($a) && preg_match("#^[^0-9]*$valid_ip(?:/$valid_routing_prefix)?[^0-9]*$#", $a, $m))
{ // Extracting the valid ipv4 and optionnaly the routing prefix
$m[5] = ctype_digit($m[5]) ? ((int)$m[5]) : 32; // Initialize the valid routing prefix to the extracted value or 32 if mismatch
$c[$m[5]][] = ip2long("$m[1].$m[2].$m[3].$m[4]") & (-1 << (32 - $m[5])); // Initialize the working array with key (prefix) and value as subnet by bitwise the decimal ip
}
if ($c) // If any valid ipv4 with optional routing prefix matched
{
foreach($c as &$unique) $unique=array_unique($unique); //Make unique as possible before processing
$c = merge_cidr_summarize($c); // Pass the valid array to the slave function
foreach($c as $d => & $e) // For each result as routing prefix => Decimal value
$e = array_map(
function ($a) use($d)
{
return [$a, $a + (1 << (32 - $d)) - 1];
}
, $e); // Change it to an array containing the range of ip
foreach($c as $f => $g) // For each result as routing prefix => array of decimal value
foreach($c as $h => $i) // For each result as routing prefix => array of decimal value
if ($f > $h) // If we are not in the same line and the second line have a lower routing prefix
foreach($g as $j => $k) // For each line as id => array of decimal values
foreach($i as $l) // For each line as decimal value in the second foreach
if ($k[0] >= $l[0] && $k[1] <= $l[1]) // If the block with lower routing prefix is totally including the first
unset($c[$f][$j]); // Unset the smaller block
foreach($c as $f => $g) // For each result as routing prefix => array of decimal value
{
usort($g,
function (array $a, array $b)
{
return $b[0]>$a[0]?1:($b[0]<$a[0]?-1:0);
}); // Order the result "naturally" inversed
foreach($g as $h) // For each ordered result
$z[] = long2ip($h[0]) . '/' . $f; // Convert it to human readable
}
return array_reverse($z); // And return the reversed result (order by routing prefix DESC and ip DESC)
}
}
function merge_cidr_summarize(array $a)
{ // Slave function
$b = false; // Did we make a change ?
$c = []; // Initialize the result to an empty array
krsort($a); // Order the input by routing prefix DESC
foreach($a as $d => $e) { // For each entry as cidr => Array of decimal values
sort($a[$d]); // Order the values "naturally"
$k = count($a[$d]); // Count the values for the loop
for ($i = 0; $i < $k; $i++) // Loop to check all values with this routing prefix
if ($a[$d][$i] == $a[$d][$i + 1]) continue; // If the subnet is the same as the next, then directly goto the next
elseif (($a[$d][$i] & (-1 << 33 - $d)) == ($a[$d][$i + 1] & (-1 << 33 - $d))) { // Check if subnet of this line and the next line are equals
$c[$d - 1][] = $a[$d][$i++] & (-1 << 33 - $d); // If yes add the new subnet in result array and skip the next line
$b = true; // And tell the script to run again
}
else $c[$d][] = $a[$d][$i]; // Else don't make anything
}
return $b ? merge_cidr_summarize($c) : $a; // If any change run again else return the result
}
To try it
<?php
$your_array=['160.11.0.0/16','160.11.14.0/20','160.12.0.0/14','160.16.5.0/15','160.16.1.0/14','160.18.0.0/16','160.20.0.0/14','160.24.0.0/16','160.26.0.0/15','160.28.0.0/15','160.74.0.0/16'];
print_r(merge_cidr($your_array));
Output.. Check it in https://eval.in/745895
/*
Array
(
[0] => 160.16.0.0/13
[1] => 160.12.0.0/14
[2] => 160.28.0.0/15
[3] => 160.26.0.0/15
[4] => 160.74.0.0/16
[5] => 160.24.0.0/16
[6] => 160.11.0.0/16
)
*/

Average from inner array

my task is to calculate average value from an array.
$arrayToTest = [[[1], 1], [[1,3,5,7], 4], [[2,5,4,1,2,3], 2.8],
[[-1,-1,-1,-1,-1], -1], [[4,23,84,12,76,34,-7,-23], 25.375]];
From inner array, so for example [1,3,5,7] and expected value is 4.
I have to use a function, I tried this:
function arrayAverage ($arrayToTest)
{
foreach($arrayToTest as $case)
foreach ($case as $item)
{
$arraySum = array_sum($item);
$arrayCount = array_count_values($item);
$average = $arraySum / $arrayCount;
return $average;
}
}
but it does not work. I feel I'm doing something wrong with calling the inner array.
Comment:
I assume that you wish to calculate the average values of the innermost arrays.
The solution below returns the average of each array - not the average of all arrays. But - of course you easily could calculate the average of all arrays.
Therefore the function arrayAverage(…) returns an array of average values instead of the average value of (only) the last array.
I declared the input array (arrayToTest) explicitely, for the reason that one can better see the array structure (array of arrays and scalars) like this.
Code:
<?php
$arrayToTest = array (
array(
array(1),
1
),
array(
array(1,3,5,7),
4
),
array(
array(2,5,4,1,2,3),
2.8
),
array(
array(-1,-1,-1,-1,-1),
-1
),
array(
array(4,23,84,12,76,34,-7,-23),
25.375
)
);
echo '<pre>'; print_r($arrayToTest); echo '</pre>';
$average = arrayAvarage ($arrayToTest);
echo '<pre>'; print_r($average); echo '</pre>';
function arrayAvarage ($arrayToTest) {
$result = array();
foreach($arrayToTest as $case) {
foreach ($case as $items) {
if (!is_array($items)) continue;
$result[] = array_sum($items) / count($items);
}
}
return $result;
}
?>
Result:
Array
(
[0] => 1
[1] => 4
[2] => 2.8333333333333
[3] => -1
[4] => 25.375
)
if your array contains internal arrays in the index 0, you can do this by:
function arrayAvarage ($arrayToTest)
{
$out_put_arr = array();
foreach($arrayToTest as $case)
{
$arraySum = array_sum($case[0]);
$arrayCount = array_count_values($case[0]);
$avarage = $arraySum / $arrayCount;
$out_put_arr[]= $avarage;
}
return $out_put_arr;
}
so the loop for the main array, each item in the main array will give you array, and int $case[0] = [1,3,5,7] and $case[1] = 4, also you shouldn't return in for loop because this will return the first average only. so you can declare new array to fill with all averages.
function average($array){
return array_sum($array) / count($array);
}
foreach($arrayToTest as $array){
echo "Average: " . average($array[0]);
}
You should look at the first element of the $case array, which is the actual place where the array with values is situated. Note that you can also use the array_sum function.
Also, you should not return just like that, because that will interrupt the function from doing anything more. So, only return when you really want to do that.
As you already have expected values, I see no reason why your function should return those averages again. Instead it could verify the correctness of these expected values, and return the index of the array when that comparison fails.
function arrayAverage ($arrayToTest)
{
foreach($arrayToTest as $index => $case) {
$average = array_sum($case[0]) / count($case[0]);
if ($average !== $case[1]) {
return $index; // not expected value
}
}
return false; // all averages are equal to expected value
}
So, the above function will return FALSE when all averages are as expected. Otherwise it will return the index of the first mismatch.

Sorting Elements of an Array by number in string?

I have a .txt file that looks like this:
john 1000
mike 8393
tom 1000
bob 233
roger 2
daniel 233
... ...
I need to put every line into array and sort by number size without losing what name goes with what number.
Also some numbers are repeating through the file.
Finally I want to echo elements of an array sorted by number size.
You could break each line into an array of integers and strings (split your current strings on the space) and then sort the array using ksort (assuming the key is the integer) and there you go!
You can of course alternatively use a more robust sort, but this will get you there.
You can then print it by using print_r to print the human readable version of the array
http://php.net/manual/en/array.sorting.php
http://php.net/manual/en/function.print-r.php
If you're running php 5.3+
$fileLines = explode("\n", $fileContents);
usort($fileLines, function($a, $b) {
$aNumber = (int)substr($a, strpos($a, ' ')+1);
$bNumber = (int)substr($b, strpos($b, ' ')+1);
if($aNumber === $bNumber) {
return 0;
}
return $aNumber > $bNumber ? 1 : -1;
});
If you're running a lower version, convert the function into a global function and provide the name as a callback string.
usort($fileLines, 'sortlines');
function sortlines($a, $b) {
$aNumber = (int)substr($a, strpos($a, ' ')+1);
$bNumber = (int)substr($b, strpos($b, ' ')+1);
if($aNumber === $bNumber) {
return 0;
}
return $aNumber > $bNumber ? 1 : -1;
}
then
var_dump($fileLines);
You can go three ways:
1.Create two-dimensional array with numeric indexes and sort it with usort(); and lambda function
$list = array(array('name' => 'john', 'number' => 1000),
array('name' => 'mike', 'number' => 8393),
array('name' => 'tom', 'number' => 1000)
);
$by = 'number';
usort($list, function($first, $second) use ($by)
{
if ($first[$by] > $second[$by] { return 1; }
elseif (first[$by] < $second[$by]) { return -1; }
return 0;
}
);
2.Create array with indexes as names and sort it with sort();
$list = array('john' => 1000,
'mike' => 8393,
'tom' => 1000
);
sort($list);
3.Create array with indexes as numbers and sort it with ksort();
$list = array(1000 => 'john',
8393 => 'mike',
1000 => 'tom'
);
ksort($list);
If you choose first way you can address the element as
$list[0][name] = 'bob'
$list[1][number] = 1000;
Second
$list['john'] = 1001;
Third
$list[1000] = 'bob';
In last two ways you should use foreach to go through array
Use
print_r($list);
or
var_dump($list);
to print the array, or create your own code
P.S. Don't forget thar calling usort with lambda function is PHP 5.3 way, if you use earlier version of PHP, you should use discrete function

Replace non-specified array values with 0

I want to replace all array values with 0 except work and home.
Input:
$array = ['work', 'homework', 'home', 'sky', 'door']
My coding attempt:
$a = str_replace("work", "0", $array);
Expected output:
['work', 0, 'home', 0, 0]
Also my input data is coming from a user submission and the amount of array elements may be very large.
A bit more elegant and shorter solution.
$aArray = array('work','home','sky','door');
foreach($aArray as &$sValue)
{
if ( $sValue!='work' && $sValue!='home' ) $sValue=0;
}
The & operator is a pointer to the particular original string in the array. (instead of a copy of that string)
You can that way assign a new value to the string in the array. The only thing you may not do is anything that may disturb the order in the array, like unset() or key manipulation.
The resulting array of the example above will be
$aArray = array('work','home', 0, 0)
A loop will perform a series of actions many times. So, for each element in your array, you would check if it is equal to the one you want to change and if it is, change it. Also be sure to put quote marks around your strings
//Setup the array of string
$asting = array('work','home','sky','door')
/**
Loop over the array of strings with a counter $i,
Continue doing this until it hits the last element in the array
which will be at count($asting)
*/
for($i = 0; $i < count($asting);$i++){
//Check if the value at the 'ith' element in the array is the one you want to change
//if it is, set the ith element to 0
if ($asting[$i] == 'work' || $asting[$i] == 'home')
$asting[$i] = 0;
}
Here is some suggested reading:
http://www.php.net/manual/en/language.types.array.php
http://www.php.net/manual/en/language.control-structures.php
But if you are struggling on stuff such as looping, you may want to read some introductory programming material. Which should help you really understand what's going on.
A bit other and much quicker way, but true, need a loop:
//Setup the array of string
$asting = array('bar', 'market', 'work', 'home', 'sky', 'door');
//Setup the array of replacings
$replace = array('home', 'work');
//Loop them through str_replace() replacing with 0 or any other value...
foreach ($replace as $val) $asting = str_replace($val, 0, $asting);
//See what results brings:
print_r ($asting);
Will output:
Array
(
[0] => bar
[1] => market
[2] => 0
[3] => 0
[4] => sky
[5] => door
)
An alternative using array_map:
$original = array('work','home','sky','door');
$mapped = array_map(function($i){
$exclude = array('work','home');
return in_array($i, $exclude) ? 0 : $i;
}, $original);
you may try array_walk function:
function zeros(&$value)
{
if ($value != 'home' && $value != 'work'){$value = 0;}
}
$asting = array('work','home','sky','door','march');
array_walk($asting, 'zeros');
print_r($asting);
You can also give array as a parameter 1 and 2 on str_replace...
Just a small point to the for loop. Many dont realize the second comparing task is done every new iteration. So if it was a case of big array or calculation you could optimize loop a bit by doing:
for ($i = 0, $c = count($asting); $i < $c; $i++) {...}
You may also want to see http://php.net/manual/en/function.array-replace.php for original problem unless the code really is final :)
Try This
$your_array = array('work','home','sky','door');
$rep = array('home', 'work');
foreach($rep as $key=>$val){
$key = array_search($val, $your_array);
$your_array[$key] = 0;
}
print_r($your_array);
There are a few techniques on this page that make zero iterated function calls -- which is good performance-wise. For best maintainability, I recommend separating your list of targeted string as a lookup array. By modifying the original array values by reference, you can swiftly replace whole strings and null coalesce non-targeted values to 0.
Code: (Demo)
$array = ['work', 'homework', 'home', 'sky', 'door'];
$keep = ['work', 'home'];
$lookup = array_combine($keep, $keep);
foreach ($array as &$v) {
$v = $lookup[$v] ?? 0;
}
var_export($array);
Output:
array (
0 => 'work',
1 => 0,
2 => 'home',
3 => 0,
4 => 0,
)
You can very easily, cleanly extend your list of targeted strings by merely extending $keep.
If you don't want a classic loop, you can use the same technique without modifying the original array. (Demo)
var_export(
array_map(fn($v) => $lookup[$v] ?? 0, $array)
);
this my final code
//Setup the array of string
$asting = array('work','home','sky','door','march');
/**
Loop over the array of strings with a counter $i,
Continue doing this until it hits the last element in the array
which will be at count($asting)
*/
for($i = 0; $i < count($asting); $i++) {
//Check if the value at the 'ith' element in the array is the one you want to change
//if it is, set the ith element to 0
if ($asting[$i] == 'work') {
$asting[$i] = 20;
} elseif($asting[$i] == 'home'){
$asting[$i] = 30;
}else{
$asting[$i] = 0;
}
echo $asting[$i]."<br><br>";
$total += $asting[$i];
}
echo $total;

Categories