Related
I am trying create a function which returns the number of all unique case-insensitive
characters that occur >= $n times in a given string.
For example:
function getNumOfUniqueCharacters($str, $n) {
// ...
}
getNumOfUniqueCharacters('A1B2C3', 2); // 0
getNumOfUniqueCharacters('A1a1C1', 2);
// 2, because A and 1 both occur 2 or more times.
getNumOfUniqueCharacters('Alabama', 3); // 1
I did this:
function getNumOfUniqueCharacters($text)
{
$ret = 0;
$a = [];
$t = str_split(strtolower($text));
$l = count($t);
for ($i = 0; $i < $l; $i++) {
$c = $t[$i];
if (array_key_exists($c, $t)) {
if ($t[$c] === 1)
$ret += 1;
$t[$c] += 1;
} else {
$t[$c] = 1;
}
}
return $ret;
}
But it does not work so good, I need to add second argument $n.
How to add it correctly?
I hope I got your question right.
Here's my idea for this code:
<?php
$string = "A1B2C1A2b2b4b5";
function getNumOfUniqueCharacters($string, $n)
{
$occurrenceArray = array();
$text = str_split(strtolower($string));
//put each character in a keyValue array and count them
foreach($text as $character){
if(!array_key_exists($character, $occurrenceArray)) $occurrenceArray[$character] = 1;
else $occurrenceArray[$character]++;
}
//loop through keyValue array and remove everything that has value < $n
foreach($occurrenceArray as $key => $value)
{
if($value < $n) unset($occurrenceArray[$key]);
}
//return array
return $occurrenceArray;
}
print_r(getNumOfUniqueCharacters($string, 2));
This code right here will print the following:
Array (
[a] => 2
[1] => 2
[b] => 4
[2] => 3 )
Edit: If you need the count of how many characters repeat more than $n, you can simply replace the return with return count($occurrenceArray);
This task is pretty easy, if you use array functions of PHP:
function getNumOfUniqueCharacters(string $string = '', int $n = 1): int {
// Split the string by character and count the occurences of all values
$counted = array_count_values(mb_str_split(mb_strtolower($str)));
// Discard everything, that is does not match the $n parameter
$counted = array_filter($counted, function($a) use($n) {
return $a >= $n;
});
// Return the length of the remaining array
return count($counted);
}
Also note, that you may use mb_* functions, so your code will work with multibyte characters.
I have written you a function with a lot of comments to explain the thought process,
function getNumOfUniqueCharacters($string, $n = null) {
// Map all case-insensitive characters to an array
$map = str_split(strtolower($string), 1);
// Character registry
$reg = array_count_values($map);
// Filter out single occurances
$reg = array_filter($reg, function($v){return $v > 1;});
// Filter out less than $n occurances (if $n is not null)
if (null !== $n) {
$reg = array_filter($reg, function($v)use($n){return $v >= $n;});
}
// Return the number duplicate occurances (or more than n occurances)
return count($reg);
}
Usage:
echo getNumOfUniqueCharacters('A1B2C3', 2) . PHP_EOL;
echo getNumOfUniqueCharacters('A1a1C1', 2) . PHP_EOL;
echo getNumOfUniqueCharacters('Alabama', 3) . PHP_EOL;
echo getNumOfUniqueCharacters('Mississippi') . PHP_EOL;
Output:
0
2
1
3
I was hoping to convert a single integer variable (coming in from a POST parameter) into a list, within a range of the original number, for example +/-3, using PHP.
So if var = 5
using PHP, output is = 2,3,4,5,6,7,8
or if var = 17
output is = 14,15,16,17,18,19,20
This is early guesswork, but I was thinking something like this:
<?php
$single = $_POST['number'];
$mc = $single - 3;
$mb = $single - 2;
$ma = $single - 1;
$pa = $single + 1;
$pb = $single + 2;
$pc = $single + 3;
$list = [$mc, $mb, $ma, $single, $pa, $pb, $pc,]
echo $list
?>
But it is just printing 'Array',
New to PHP, feel like I am overlooking a lot of things. Is it possible to assemble an array like this? Is there a quicker way to do what I'm trying?
Was hoping to do more like +/-30 .. was hoping there might be a shortcut / function that could help?
You can use range for this.
<?php
$num = filter_input(INPUT_GET, 'num', FILTER_VALIDATE_INT);
$range = 3;
if($num !== false) {
$result = range($num-$range, $num+$range);
echo implode(',', $result);
}
Output when (num is 2 and range is 3):
-1,0,1,2,3,4,5
Just use implode function after that.
$list = [$mc, $mb, $ma, $single, $pa, $pb, $pc];
echo implode(',', $list);
If you want to extend it for variable range too.
$single = 17;
$range = 3;
$data = range($single - $range, $single + $range);
echo implode(',', $data);
Was hoping to do more like +/-30 .. was hoping there might be a shortcut / function that could help?
Yes, use a loop. Because you know the range you want, you can use a for loop:
NOTE: This code works with negative numbers, too.
$range = 3; // your given range
$value = (int)$_POST['number'];
$result = [];
$result[] = $value;
for ($i = 1; $i <= $range; $i++){
$result[] = $value - $i ;
$result[] = $value + $i;
}
// Sort array and then output:
sort($result, SORT_NUMERIC);
print implode(', ', $result);// outputs a sorted list of values
// Value = -2
// -5, -4, -3, -2, -1, 0, 1
//
// Value = 5
// 2, 3, 4, 5, 6, 7, 8
I have two arrays, one of values, and the other for ranges:
$ranges = array(10,15,30);
$values = array(1,4,12,15,27,32);
I want to count the number of values in between each ranges, as such:
$output = array(
"<10" => 2, // number of values < 10
"10-15" => 1, // number of values >= 10 && < 15
"15-30" => 2, // number of values >= 15 && < 30
">=30" => 1, // number of values > 30
);
Obviously, ranges and values are dynamic and can't be hard-coded if-conditions.
What I made so far is working:
$output = array();
foreach ( $values as $val ) {
foreach ( $ranges as $k => $range ) {
if ( $k == 0 ) { // first range
$max = $range;
$label = '<' . $max;
if ( $val < $max ) {
$output[$label] += 1;
}
} else if ( $k == count($ranges) - 1 ) { // last range
$min = $ranges[$k-1];
$max = $range;
$label = $min . '-' . $max;
if ( $val >= $min && $val < $max ) {
$output[$label] += 1;
}
$min = $range;
$label = '>=' . $min;
if ( $val >= $min ) {
$output[$label] += 1;
}
} else {
$min = $ranges[$k-1];
$max = $range;
$label = $min . '-' . $max;
if ( $val >= $min && $val < $max ) {
$output[$label] += 1;
}
}
}
}
print_r($output);
This seems costly and I'm really not sure about it. Is there a simpler way to achieve what I'm looking for?
You can simplify the logic a bit by added limiting values to the beginning and end of the $ranges array and then just processing the whole array by pairs.
<?php
$ranges = [10,15,30];
$values = [1,4,12,15,27,32];
\array_push($ranges, null); // append null to array
\array_unshift($ranges, null); // prepend null to array
$output = [];
$count = \count($ranges);
for ($i = 0; $i < $count - 1; $i++) {
$output[] = ['start' => $ranges[$i], 'end' => $ranges[$i+1], 'count' => 0];
}
foreach ($values as $value) {
foreach ($output as $key => $range) {
if (
($range['start'] === null || $range['start'] <= $value) &&
($range['end'] === null || $range['end'] > $value)
) {
$output[$key]['count']++;
break;
}
}
}
var_dump($output);
This starts off by creating a zeroed array with the keys from the $ranges array (using array_fill_keys()), plus one for the values 'over' the last entry.
The loops over each value and checks it against the range, if it finds it, it just adds 1 to the corresponding count and stops looking. If after finishing the loop, the value is greater than the last range, it adds 1 to the 'over' entry.
$ranges = array(10,15,30);
$values = array(1,4,12,15,27,32);
$rangeCount = array_fill_keys($ranges, 0);
$rangeCount[ "over" ] = 0;
foreach ( $values as $value ) {
foreach ( $ranges as $range ) {
if ( $value < $range ) {
$rangeCount [ $range ]++;
break;
}
}
if ( $value >= $range ) {
$rangeCount[ "over" ]++;
}
}
print_r($rangeCount);
which gives...
Array
(
[10] => 2
[15] => 1
[30] => 2
[over] => 1
)
Just to add an optimised version which only does one loop. But assumes the values are in ascending order. Each time it passes the 'current' range it moves onto the next output counter and the last part doesn't even loop for the over the top value, it subtracts the current count from the total count and does a break...
$currentRange = 0;
$numberValues = count($values);
$numberRanges = count($ranges);
$rangeCount = array_fill(0, $numberRanges, 0);
$rangeCount[ "over" ] = 0;
foreach ( $values as $count => $value ) {
if ( $value >= $ranges[$currentRange] ) {
$currentRange++;
if ( $currentRange >= $numberRanges ) {
$rangeCount[ "over" ] = $numberValues - $count;
break;
}
}
$rangeCount[$currentRange]++;
}
print_r($rangeCount);
The following solution first sorts the ranges in ascending/non-decreasing order.
Then, we create a range_map which is a collection of all possible ranges from $ranges.
Then, we loop over all values in $values and do a binary search over $ranges to get the exact range index a particular value belongs to. In the below code, exact index is stored in $low.
Then, we just collect the count by taking the range key from $range_map and incrementing it's counter by 1.
This is faster from nested looping since time complexity of nested looping is O(m*n) where m is size of $ranges and n is size of $values, whereas time complexity of current solution is O(m logm) + O(n logm) where m is size of $ranges and n is size of $values.
Snippet:
<?php
$ranges = array(10,15,30);
$values = array(1,4,12,15,27,32);
sort($ranges);
$range_map = [];
$ptr = 0;
foreach($ranges as $index => $value){
if($index === 0) $range_map[$ptr++] = "<" . $value;
if($index > 0) $range_map[$ptr++] = $ranges[$index - 1] . "-" . $value;
if($index === count($ranges) - 1) $range_map[$ptr++] = ">=" . $value;
}
$result = [];
foreach($values as $value){
$low = 0; $high = count($ranges) - 1;
while($low <= $high){
$mid = $low + intval(($high - $low) / 2);
if($value === $ranges[ $mid ]){
$low = $mid + 1;
break;
}else if($value < $ranges[ $mid ]){
$high = $mid - 1;
}else{
$low = $mid + 1;
}
}
if(!isset($result[$range_map[$low]])) $result[$range_map[$low]] = 0; // get the range key from range_map
$result[$range_map[$low]]++; // increment the value for that range
}
print_r($result);
Demo: https://3v4l.org/JcYBv
Assuming you have pre-sorted ranges and values.
<?php
$ranges = array(10,15,30);
$values = array(1,4,12,15,27,32);
$lower = null;
$i = 0;
$upper = $ranges[$i];
foreach($values as $item) {
if(!is_null($upper) && $item >= $upper) {
$lower = $upper;
$upper = $ranges[++$i] ?? null;
}
$result["$lower<$upper"][] = $item;
}
var_export(array_map('count', $result));
Output:
array (
'<10' => 2,
'10<15' => 1,
'15<30' => 2,
'30<' => 1,
)
Given an array such as the following
$array = ('1', '2', '3', '4', '5', '6', '7');
I'm looking for a method to generate all possible combinations, with a minimum number of elements required in each combination r. (eg if r = 5 then it will return all possible combinations containing at least 5 elements)
Combinations of k out of n items can be defined recursively using the following function:
function combinationsOf($k, $xs){
if ($k === 0)
return array(array());
if (count($xs) === 0)
return array();
$x = $xs[0];
$xs1 = array_slice($xs,1,count($xs)-1);
$res1 = combinationsOf($k-1,$xs1);
for ($i = 0; $i < count($res1); $i++) {
array_splice($res1[$i], 0, 0, $x);
}
$res2 = combinationsOf($k,$xs1);
return array_merge($res1, $res2);
}
The above is based on the recursive definition that to choose k out n elements, one can fix an element x in the list, and there are C(k-1, xs\{x}) combinations that contain x (i.e. res1), and C(k,xs\{xs}) combinations that do not contain x (i.e. res2 in code).
Full example:
$array = array('1', '2', '3', '4', '5', '6', '7');
function combinationsOf($k, $xs){
if ($k === 0)
return array(array());
if (count($xs) === 0)
return array();
$x = $xs[0];
$xs1 = array_slice($xs,1,count($xs)-1);
$res1 = combinationsOf($k-1,$xs1);
for ($i = 0; $i < count($res1); $i++) {
array_splice($res1[$i], 0, 0, $x);
}
$res2 = combinationsOf($k,$xs1);
return array_merge($res1, $res2);
}
print_r ($array);
print_r(combinationsOf(5,$array));
//print_r(combinationsOf(5,$array)+combinationsOf(6,$array)+combinationsOf(7,$array));
A combination can be expressed as
nCr = n! / (r! - (n - r)!)
First, we determine $n as the number of elements in the array. And $r is the minimum number of elements in each combination.
$a = ['1', '2', '3', '4', '5', '6', '7']; // the array of elements we are interested in
// Determine the `n` and `r` in nCr = n! / (r! * (n-r)!)
$r = 5;
$n = count($a);
Next, we determine $max as the maximum number that can be represented by $n binary digits. That is, if $n = 3, then $max = (111)2 = 7. To do this, we first create a empty string $maxBinary and add $n number of 1s to it. We then convert it to decimal, and store it in $max.
$maxBinary = "";
for ($i = 0; $i < $n; $i++)
{
$maxBinary .= "1";
}
$max = bindec($maxBinary); // convert it into a decimal value, so that we can use it in the following for loop
Then, we list out every binary number from 0 to $max and store those that have more than $r number of 1s in them.
$allBinary = array(); // the array of binary numbers
for ($i = 0; $i <= $max; $i++)
{
if (substr_count(decbin($i), "1") >= $r) // we count the number of ones to determine if they are >= $r
{
// we make the length of the binary numbers equal to the number of elements in the array,
// so that it is easy to select elements from the array, based on which of the digits are 1.
// we do this by padding zeros to the left.
$temp = str_pad(decbin($i), $n, "0", STR_PAD_LEFT);
$allBinary[] = $temp;
}
}
Then, we use the same trick as above to select elements for our combination. I believe the comments explain enough.
$combs = array(); // the array for all the combinations.
$row = array(); // the array of binary digits in one element of the $allBinary array.
foreach ($allBinary as $key => $one)
{
$combs[$key] = "";
$row = str_split($one); // we store the digits of the binary number individually
foreach ($row as $indx => $digit)
{
if ($digit == '1') // if the digit is 1, then the corresponding element in the array is part of this combination.
{
$combs[$key] .= $a[$indx]; // add the array element at the corresponding index to the combination
}
}
}
And that is it. You are done!
Now if you have something like
echo count($combs);
then it would give you 29.
Additional notes:
I read up on this only after seeing your question, and as a newcomer, I found these useful:
Wikipedia - http://en.wikipedia.org/wiki/Combination
Php recursion to get all possibilities of strings
Algorithm to return all combinations of k elements from n
Also, here are some quick links to the docs, that should help people who see this in the future:
http://php.net/manual/en/function.decbin.php
http://php.net/manual/en/function.bindec.php
http://php.net/manual/en/function.str-pad.php
function arrToBit(Array $element) {
$bit = '';
foreach ($element as $e) {
$bit .= '1';
}
$length = count($element);
$num = bindec($bit);
$back = [];
while ($num) {
$back[] = str_pad(decbin($num), $length, '0', STR_PAD_LEFT);
$num--;
}
//$back[] = str_pad(decbin(0), $length, '0', STR_PAD_LEFT);
return $back;
}
function bitToArr(Array $element, $bit) {
$num = count($element);
$back = [];
for ($i = 0; $i < $num; $i++) {
if (substr($bit, $i, 1) == '1') {
$back[] = $element[$i];
}
}
return $back;
}
$tags = ['a', 'b', 'c'];
$bits = arrToBit($tags);
$combination = [];
foreach ($bits as $b) {
$combination[] = bitToArr($tags, $b);
}
var_dump($combination);
$arr = array(1,2,3,4,5,6);
$check_value =[];
$all_values = [];
CONT:
$result = $check_value;
shuffle($arr);
$check_value = array_slice($arr,0,3);
if(count($check_value) == 3 && serialize($check_value) !== serialize($result)){
$result = $check_value;
array_push($all_values,$result);
goto CONT;
}
print_r($all_values);
How can I search and find, for a given target value, the closest value in an array?
Let's say I have this exemplary array:
array(0, 5, 10, 11, 12, 20)
For example, when I search with the target value 0, the function shall return 0; when I search with 3, it shall return 5; when I search with 14, it shall return 12.
Pass in the number you're searching for as the first parameter and the array of numbers to the second:
function getClosest($search, $arr) {
$closest = null;
foreach ($arr as $item) {
if ($closest === null || abs($search - $closest) > abs($item - $search)) {
$closest = $item;
}
}
return $closest;
}
A particular lazy approach is having PHP sort the array by the distance to the searched number:
$num = 3;
$array = array(0, 5, 10, 11, 12, 20);
$smallest = [];
foreach ($array as $i) {
$smallest[$i] = abs($i - $num);
}
asort($smallest);
print key($smallest);
This is high-performance function I wrote for sorted big arrays
Tested, main loop needs only ~20 iterations for an array with 20000 elements.
Please mind array has to be sorted (ascending)!
define('ARRAY_NEAREST_DEFAULT', 0);
define('ARRAY_NEAREST_LOWER', 1);
define('ARRAY_NEAREST_HIGHER', 2);
/**
* Finds nearest value in numeric array. Can be used in loops.
* Array needs to be non-assocative and sorted.
*
* #param array $array
* #param int $value
* #param int $method ARRAY_NEAREST_DEFAULT|ARRAY_NEAREST_LOWER|ARRAY_NEAREST_HIGHER
* #return int
*/
function array_numeric_sorted_nearest($array, $value, $method = ARRAY_NEAREST_DEFAULT) {
$count = count($array);
if($count == 0) {
return null;
}
$div_step = 2;
$index = ceil($count / $div_step);
$best_index = null;
$best_score = null;
$direction = null;
$indexes_checked = Array();
while(true) {
if(isset($indexes_checked[$index])) {
break ;
}
$curr_key = $array[$index];
if($curr_key === null) {
break ;
}
$indexes_checked[$index] = true;
// perfect match, nothing else to do
if($curr_key == $value) {
return $curr_key;
}
$prev_key = $array[$index - 1];
$next_key = $array[$index + 1];
switch($method) {
default:
case ARRAY_NEAREST_DEFAULT:
$curr_score = abs($curr_key - $value);
$prev_score = $prev_key !== null ? abs($prev_key - $value) : null;
$next_score = $next_key !== null ? abs($next_key - $value) : null;
if($prev_score === null) {
$direction = 1;
}else if ($next_score === null) {
break 2;
}else{
$direction = $next_score < $prev_score ? 1 : -1;
}
break;
case ARRAY_NEAREST_LOWER:
$curr_score = $curr_key - $value;
if($curr_score > 0) {
$curr_score = null;
}else{
$curr_score = abs($curr_score);
}
if($curr_score === null) {
$direction = -1;
}else{
$direction = 1;
}
break;
case ARRAY_NEAREST_HIGHER:
$curr_score = $curr_key - $value;
if($curr_score < 0) {
$curr_score = null;
}
if($curr_score === null) {
$direction = 1;
}else{
$direction = -1;
}
break;
}
if(($curr_score !== null) && ($curr_score < $best_score) || ($best_score === null)) {
$best_index = $index;
$best_score = $curr_score;
}
$div_step *= 2;
$index += $direction * ceil($count / $div_step);
}
return $array[$best_index];
}
ARRAY_NEAREST_DEFAULT finds nearest element
ARRAY_NEAREST_LOWER finds nearest element which is LOWER
ARRAY_NEAREST_HIGHER finds nearest element which is HIGHER
Usage:
$test = Array(5,2,8,3,9,12,20,...,52100,52460,62000);
// sort an array and use array_numeric_sorted_nearest
// for multiple searches.
// for every iteration it start from half of chunk where
// first chunk is whole array
// function doesn't work with unosrted arrays, and it's much
// faster than other solutions here for sorted arrays
sort($test);
$nearest = array_numeric_sorted_nearest($test, 8256);
$nearest = array_numeric_sorted_nearest($test, 3433);
$nearest = array_numeric_sorted_nearest($test, 1100);
$nearest = array_numeric_sorted_nearest($test, 700);
<?php
$arr = array(0, 5, 10, 11, 12, 20);
function getNearest($arr,$var){
usort($arr, function($a,$b) use ($var){
return abs($a - $var) - abs($b - $var);
});
return array_shift($arr);
}
?>
Tim's implementation will cut it most of the time. Nevertheless, for the performance cautious, you can sort the list prior to the iteration and break the search when the next difference is greater than the last.
<?php
function getIndexOfClosestValue ($needle, $haystack) {
if (count($haystack) === 1) {
return $haystack[0];
}
sort($haystack);
$closest_value_index = 0;
$last_closest_value_index = null;
foreach ($haystack as $i => $item) {
if (abs($needle - $haystack[$closest_value_index]) > abs($item - $needle)) {
$closest_value_index = $i;
}
if ($closest_value_index === $last_closest_value_index) {
break;
}
}
return $closest_value_index;
}
function getClosestValue ($needle, $haystack) {
return $haystack[getIndexOfClosestValue($needle, $haystack)];
}
// Test
$needles = [0, 2, 3, 4, 5, 11, 19, 20];
$haystack = [0, 5, 10, 11, 12, 20];
$expectation = [0, 0, 1, 1, 1, 3, 5, 5];
foreach ($needles as $i => $needle) {
var_dump( getIndexOfClosestValue($needle, $haystack) === $expectation[$i] );
}
To search the nearest value into an array of objects you can use this adapted code from Tim Cooper's answer.
<?php
// create array of ten objects with random values
$images = array();
for ($i = 0; $i < 10; $i++)
$images[ $i ] = (object)array(
'width' => rand(100, 1000)
);
// print array
print_r($images);
// adapted function from Tim Copper's solution
// https://stackoverflow.com/a/5464961/496176
function closest($array, $member, $number) {
$arr = array();
foreach ($array as $key => $value)
$arr[$key] = $value->$member;
$closest = null;
foreach ($arr as $item)
if ($closest === null || abs($number - $closest) > abs($item - $number))
$closest = $item;
$key = array_search($closest, $arr);
return $array[$key];
}
// object needed
$needed_object = closest($images, 'width', 320);
// print result
print_r($needed_object);
?>
Best method I've found based on Piyush Dholariya's answer:
$array = [4, 9, 15, 6, 2];
$goal = 7;
$closest = array_reduce($array, function($carry, $item) use($goal) {
return (abs($item - $goal) < abs($carry - $goal) ? $item : $carry);
}, reset($array)); // Returns 6
This is the same approach as Mario's answer, but I use array_search() and min() instead of sorting. The performance is the same, so it just comes down to the matter of preference.
function findClosest(array $values, $match)
{
$map = [];
foreach ($values as $v) {
$map[$v] = abs($match - $v);
}
return array_search(min($map), $map);
}
You can simply use array_search for that, it returns one single key, if there are many instances of your search found within the array, it would return the first one it finds.
Quote from PHP:
If needle is found in haystack more than once, the first matching key is returned. To return the keys for all matching values, use array_keys() with the optional search_value parameter instead.
Example Usage:
if(false !== ($index = array_search(12,array(0, 5, 10, 11, 12, 20))))
{
echo $index; //5
}
Update:
function findNearest($number,$Array)
{
//First check if we have an exact number
if(false !== ($exact = array_search($number,$Array)))
{
return $Array[$exact];
}
//Sort the array
sort($Array);
//make sure our search is greater then the smallest value
if ($number < $Array[0] )
{
return $Array[0];
}
$closest = $Array[0]; //Set the closest to the lowest number to start
foreach($Array as $value)
{
if(abs($number - $closest) > abs($value - $number))
{
$closest = $value;
}
}
return $closest;
}
Considering that the input array is sorted in ascending order asort() for example, you'll be far faster to search using a dichotomic search.
Here's a quick and dirty adaptation of some code I'm using to insert a new event in an Iterable event list sorted by DateTime objects…
Thus this code will return the nearest point at the left (before / smaller).
If you'd like to find the mathematically nearest point: consider comparing the distance of the search value with the return value and the point immediately at the right (next) of the return value (if it exists).
function dichotomicSearch($search, $haystack, $position=false)
{
// Set a cursor between two values
if($position === false)
{ $position=(object) array(
'min' => 0,
'cur' => round(count($haystack)/2, 0, PHP_ROUND_HALF_ODD),
'max' => count($haystack)
);
}
// Return insertion point (to push using array_splice something at the right spot in a sorted array)
if(is_numeric($position)){return $position;}
// Return the index of the value when found
if($search == $haystack[$position->cur]){return $position->cur;}
// Searched value is smaller (go left)
if($search <= $haystack[$position->cur])
{
// Not found (closest value would be $position->min || $position->min+1)
if($position->cur == $position->min){return $position->min;}
// Resetting the interval from [min,max[ to [min,cur[
$position->max=$position->cur;
// Resetting cursor to the new middle of the interval
$position->cur=round($position->cur/2, 0, PHP_ROUND_HALF_DOWN);
return dichotomicSearch($search, $haystack, $position);
}
// Search value is greater (go right)
// Not found (closest value would be $position->max-1 || $position->max)
if($position->cur < $position->min or $position->cur >= $position->max){return $position->max;}
// Resetting the interval from [min,max[ to [cur,max[
$position->min = $position->cur;
// Resetting cursor to the new middle of the interval
$position->cur = $position->min + round(($position->max-$position->min)/2, 0, PHP_ROUND_HALF_UP);
if($position->cur >= $position->max){return $position->max;}
return dichotomicSearch($search, $haystack, $position);
}
Binary search to find closest value (array must be sorted):
function findClosest($sortedArr, $val)
{
$low = 0;
$high = count($sortedArr) - 1;
while ($low <= $high) {
if ($high - $low <= 1) {
if (abs($sortedArr[$low] - $val) < abs($sortedArr[$high] - $val)) {
return $sortedArr[$low];
} else {
return $sortedArr[$high];
}
}
$mid = (int)(($high + $low) / 2);
if ($val < $sortedArr[$mid]) {
$high = $mid;
} else {
$low = $mid;
}
}
// Empty array
return false;
}
function closestnumber($number, $candidates) {
$last = null;
foreach ($candidates as $cand) {
if ($cand < $number) {
$last = $cand;
} elseif ($cand == $number) {
return $number;
} elseif ($cand > $number) {
return $last;
}
}
return $last;
}
I'll provide a late answer that endeavors to avoid needless iterations and excessive function calls by maintaining two temporary variables and implementing an early return.
An elegant solution should not require a time complexity greater than n -- in other words, the big O should be O(n) and the little o should be o(1). The big O only gets worse by pre-sorting the haystack, then iterating the haystack again. To get achieve o(1), you will need an early return when an identical match is encountered -- there is no need to search further.
My snippet will arbitrarily return the first occurring value with the lowest distance (in case multiple values have the same distance). Any other behavior is not specified by the OP.
A trivial performance improvement over some other answers is that abs() is the lone function call within the loop and it is called a maximum of 1 time per iteration. Some previous answers recalculate the distance of the current value as well as the current closest match on each iteration -- this is more work than is necessary.
Code: (Demo)
$haystack = [-6, 0, 5, 10, 11, 12, 20];
$needles = [0, 3, 14, -3];
function getNearest($needle, $haystack) {
if (!$haystack) {
throw new Exception('empty haystack');
}
$bestDistance = PHP_INT_MAX;
foreach ($haystack as $value) {
if ($value === $needle) {
return $needle;
}
$distance = abs($value - $needle);
if ($distance < $bestDistance) {
$bestDistance = $distance;
$keep = $value;
}
}
return $keep ?? $value; // coalesce to silence potential IDE complaint
}
foreach ($needles as $needle) { // each test case
echo "$needle -> " . getNearest($needle, $haystack) . "\n";
}
Output:
0 -> 0
3 -> 5
14 -> 12
-3 -> -6