I have an array of part lengths, for examples sake:-
array(150, 180, 270);
I then have a measurement ($a = 440)
I need to calculate the two closest possible combinations of lengths which are greater than $a without manually having to write hundreds of possible combinations in order to work it out.
So:
150
180
270
150 + 150
150 + 180
150 + 270
180 + 180
180 + 270
270 + 270
150 + 150 + 150
150 + 150 + 180
..and so on.
This will need to run for a set number of times, rather than just finding the first two matches and stopping, as 150 + 150 + 150 would be a closer match to $a than 270 + 270 but may run after.
edit: I also need to store the combination of parts which made up the match, preferably in an array.
I hope I've explained this well enough for somebody to understand.
As this is quite a resource heavy script, I thought it would be a good idea to give the option to generate the choices beforehand, then use that data to create a variable/object/sql script to permanently store the data. For instance, doing something like
SELECT * FROM combination_total WHERE size > YOUR_SIZE ORDER BY size ASC LIMIT 2;
The new script I have is similar, but it just generates an array of all combinations without any duplicates. Seems pretty quick again. Notice the $maxLength variable, which is currently set to 2000, which can be modified with your own largest possible size.
<?php
$partLengths = array(150, 180, 270);
$currentCombinations = array(
array(
'total' => 150,
'combination' => array(150)
),
array(
'total' => 180,
'combination' => array(180)
),
array(
'total' => 270,
'combination' => array(270)
)
);
$maxLength = 2000;
$largestSize = 0;
function generateCombination() {
global $currentCombinations, $largestSize, $partLengths;
$tmpCombinations = $currentCombinations;
foreach ($tmpCombinations as $combination) {
foreach ($partLengths as $partLength) {
$newCombination = $combination['combination'];
$newCombination[] = $partLength;
sort($newCombination);
$newCombinationTotal = array_sum($newCombination);
if (!combinationExists($newCombination)) {
$currentCombinations[] = array(
'total' => $newCombinationTotal,
'combination' => $newCombination
);
}
$largestSize = ($newCombinationTotal > $largestSize) ? $newCombinationTotal : $largestSize;
}
}
}
function combinationExists($combination) {
global $currentCombinations;
foreach ($currentCombinations as $currentCombination) {
if ($combination == $currentCombination['combination']) {
return true;
}
}
return false;
}
while ($largestSize < $maxLength) {
generateCombination();
}
// here you can use $currentCombinations to generate sql/object/etc
var_dump($currentCombinations);
?>
This code works out the closest combination above $a, and the next closest one after that. It removes duplicates to speed things up a bit. It's not mega-optimized but initial tests show it's not too bad, depending on the initial value of $a not being massive.
<?php
/* value in cm */
$a = 1020;
$partLengths = array(150, 180, 270);
$closestValue = array();
$secondClosest = array();
$currentCombinations = array(
array(
'total' => 150,
'combination' => array(150)
),
array(
'total' => 180,
'combination' => array(180)
),
array(
'total' => 270,
'combination' => array(270)
)
);
function getCombinations(&$currentCombinations, $partLengths,$a, &$closestValue, &$secondClosest) {
$tmpCombinations = $currentCombinations;
static $secondMatch = true;
for ($x=0;$x<count($partLengths);$x++) {
for ($y=0;$y<count($tmpCombinations);$y++) {
$newCombination = $tmpCombinations[$y]['combination'];
$newCombination[] = $partLengths[$x];
$newCombinationTotal = array_sum($newCombination);
sort($newCombination);
if (!combinationExists($currentCombinations, $newCombination, $newCombinationTotal)) {
$currentCombinations[] = array('total' => $newCombinationTotal, 'combination' => $newCombination);
}
if ($closestValue['total'] < $a) {
$oldGap = $a - $closestValue['total'];
$newGap = $a - $newCombinationTotal;
$newGap = ($newGap < 0) ? 0 - $newGap : $newGap;
if ($newGap < $oldGap) {
$secondClosest = $closestValue;
$closestValue['total'] = $newCombinationTotal;
$closestValue['combination'] = $newCombination;
}
} else {
$oldGap = $a - $secondClosest['total'];
$newGap = $a - $newCombinationTotal;
$oldGap = ($oldGap < 0) ? 0 - $oldGap : $oldGap;
$newGap = ($newGap < 0) ? 0 - $newGap : $newGap;
if ($newCombinationTotal > $a && $newCombinationTotal > $closestValue['total']) {
if ($secondMatch || $newGap < $oldGap) {
$secondMatch = false;
$secondClosest['total'] = $newCombinationTotal;
$secondClosest['combination'] = $newCombination;
}
}
}
}
}
}
function combinationExists(&$currentCombinations, $newCombination, $newCombinationTotal) {
foreach ($currentCombinations as $currentCombination) {
if ($currentCombination['total'] != $newCombinationTotal && $currentCombination['combination'] != $newCombination) {
return false;
}
}
return false;
}
while ($secondClosest['total'] <= $a) {
getCombinations($currentCombinations, $partLengths, $a, $closestValue, $secondClosest);
}
var_dump($closestValue);
var_dump($secondClosest);
?>
A further suggestion, if speed does become an issue, is to pre generate all combinations and save them in some kind of hash/database/etc that you can easily access.
The following code is brute-force and tests only possible combinations of 2 values, so I know it's not complete. However, it is a start.
UPDATE: See my other answer, below, for a far better solution that works with any possible combination, not just 2, and that is optimized.
<?php
echo "<html><head><title>Test Array Sums</title></head><body>";
$testarray = array(2, 5, 9, 78, 332);
$target_value = 10;
$closest1 = 0;
$closest2 = 0;
$closest_sum = 0;
$closest_difference = 0;
$first_time_in_loop = TRUE;
foreach ($testarray AS $entry1)
{
foreach ($testarray AS $entry2)
{
if ($first_time_in_loop)
{
$first_time_in_loop = FALSE;
$closest1 = $entry1;
$closest2 = $entry2;
$closest_sum = $closest1 + $closest2;
$closest_difference = abs($target_value - $closest_sum);
}
$test_sum = $entry1 + $entry2;
if (abs($test_sum - $target_value) < $closest_difference)
{
if ($test_sum - $target_value >= 0)
{
// Definitely the best so far
$closest1 = $entry1;
$closest2 = $entry2;
$closest_sum = $closest1 + $closest2;
$closest_difference = abs($closest_sum - $target_value);
}
else if ($closest_sum - $target_value < 0)
{
// The sum isn't big enough, but neither was the previous best option
// and at least this is closer
$closest1 = $entry1;
$closest2 = $entry2;
$closest_sum = $closest1 + $closest2;
$closest_difference = abs($closest_sum - $target_value);
}
}
else
{
if ($closest_sum - $target_value < 0 && $test_sum - $target_value >= 0)
{
// $test_value is farther away from the target than the previous best option,
// but at least it's bigger than the target value (the previous best option wasn't)
$closest1 = $entry1;
$closest2 = $entry2;
$closest_sum = $closest1 + $closest2;
$closest_difference = abs($closest_sum - $target_value);
}
}
}
}
echo "Best pair: " . $closest1 . ", " . $closest2 . "<br />";
echo "</body></html>";
?>
Can you limit the total number of test values to 3 - or some larger number - or do you truly need to extend it to all possible combinations (i.e. if 4+4+5+4+4+5+3+5+4+5+3+4 is closer than 26+26, than you need to find it?)
If you can limit the number being tested to, say, 5, then you could just extend the loop above to handle up to 5 choices. Otherwise, a more sophisticated loop would need to be written.
Improving on my previous answer, here is a version that works to test any number of entries, up to a maximum number.
UPDATE: (Optimization added; see comments below)
For example, if the desired value is 15, and the list is (1, 17, 20), the best choice is 1+1+1+1+1+1+1+1+1+1+1+1+1+1+1, so you would have to allow $max_loops, below, to be at least 15 in order to find this match - even though there are only 3 values in the list! It's worse for (1, 133, 138) where the desired value is, say, 130. In that case, you need 130 recursions! You can see that this is could be an optimization nightmare. But, the below algorithm works and is fairly well optimized.
<?php
echo "<html><head><title>Test Array Sums</title></head><body>";
$testarray = array(1, 3, 6);
$target_value = 10;
$current_closest_sum = 0;
$current_closest_difference = 0;
$first_time_in_loop = TRUE;
$max_loops = 10;
$current_loop = 0;
$best_set = array();
$current_set = array();
$sums_already_evaluated = array();
function nestedLoop($current_test = 0)
{
global $testarray, $target_value, $current_closest_sum, $current_closest_difference, $first_time_in_loop, $max_loops, $current_loop, $best_set, $current_set, $sums_already_evaluated;
++$current_loop;
foreach ($testarray AS $entry)
{
$current_set_temp = $current_set;
$current_set[] = $entry;
if ($first_time_in_loop)
{
$first_time_in_loop = FALSE;
$current_closest_sum = $entry + $current_test;
$current_closest_difference = abs($target_value - $current_closest_sum);
$best_set[] = $entry;
}
$test_sum = $entry + $current_test;
if (in_array($test_sum, $sums_already_evaluated))
{
// no need to test a sum that has already been tested
$current_set = $current_set_temp;
continue;
}
$sums_already_evaluated[] = $test_sum;
if ($test_sum > $target_value && $current_closest_sum > $target_value && $test_sum >= $current_closest_sum)
{
// No need to evaluate a sum that is certainly worse even by itself
$current_set = $current_set_temp;
continue;
}
$set_best = FALSE;
if (abs($test_sum - $target_value) < $current_closest_difference)
{
if ($test_sum - $target_value >= 0)
{
// Definitely the best so far
$set_best = TRUE;
}
else if ($current_closest_sum - $target_value < 0)
{
// The sum isn't big enough, but neither was the previous best option
// and at least this is closer
$set_best = TRUE;
}
}
else
{
if ($current_closest_sum - $target_value < 0 && $test_sum - $target_value >= 0)
{
// $test_value is farther away from the target than the previous best option,
// but at least it's bigger than the target value (the previous best option wasn't)
$set_best = TRUE;
}
}
if ($set_best)
{
$current_closest_sum = $test_sum;
$current_closest_difference = abs($current_closest_sum - $target_value);
$best_set = $current_set;
}
if ($current_loop < $max_loops)
{
if ($test_sum - $target_value < 0)
{
nestedLoop($test_sum);
}
}
$current_set = $current_set_temp;
}
--$current_loop;
}
// make array unique
$testarray = array_unique($testarray);
rsort($testarray, SORT_NUMERIC);
// Enter the recursion
nestedLoop();
echo "Best set: ";
foreach ($best_set AS $best_set_entry)
{
echo $best_set_entry . " ";
}
echo "<br />";
echo "</body></html>";
?>
UPDATE: I have added two small optimizations that seem to help greatly, and avoid the memory overload or hash-table lookup. They are:
(1) Track all previously evaluated sums, and do not evaluate them again.
(2) If a sum is (by itself) already worse than a previous test, skip any further tests with that sum.
I think, with these two optimizations, the algorithm may work quite well for realistic use in your situation.
PREVIOUS COMMENTS BELOW, NOW SOMEWHAT IRRELEVANT
My previous comments, below, are somewhat moot because the above two optimizations do seem to work quite well. But I include the comments anyways.
Unfortunately, as noted, the above loop is HIGHLY non-optimized. It would have to be optimized in order to work in a realistic situation, by avoiding duplicate tests (and other optimizations). However, it demonstrates an algorithm that works.
Note that this is a complex area mathematically. Various optimizations might help in one scenario, but not another. Therefore, to make the above algorithm work efficiently, you would need to discuss realistic usage scenarios - Will there be a limit on the largest length in the list of parts? What is the range of lengths? And other, more subtle features of the parts list & desired goal, though subtle, are likely to make a big difference in how to go about optimizing the algorithm.
This is a case where the "theoretical" problem isn't sufficient to yield a desired solution, since optimization is so critically important. Therefore, it's not particularly useful to make optimization suggestions.
Leonard's optimization, for example, (avoiding duplicates by saving all combinations previously tested) works well for a small-ish set, but the memory usage would explode for larger sets (as he noted). It's not a simple problem.
(code edited ~2 hours later to handle possible missed combination due to limiting the recursion to a certain number of recursions - by sorting the array from high to low, initially)
Related
I have a function which gives me all combination of values, in an array with fixed length a fixed sum :
// $n_valeurs is the length of the array
// $x_entrees is the sum
function distributions_possibles($n_valeurs, $x_entrees, $combi_presences = array()) {
if ($n_valeurs == 1) {
$combi_presences[] = $x_entrees;
return array($combi_presences);
}
$combinaisons = array();
// on fait appel à une fonction récursive pour générer les distributions
for ($tiroir = 0; $tiroir <= $x_entrees; $tiroir++) {
$combinaisons = array_merge($combinaisons, distributions_possibles(
$n_valeurs - 1,
$x_entrees - $tiroir,
array_merge($combi_presences, array($tiroir))));
}
return $combinaisons;
}
distributions_possibles(4,2);
// output :
[0,0,0,2]
[0,0,1,1]
[0,0,2,0]
[0,1,0,1]
[0,1,1,0]
[0,2,0,0]
[1,0,0,1]
[1,0,1,0]
[1,1,0,0]
[2,0,0,0]
I need to generate all possible combinations adding another parameter : a reference array $ref whose values are considered as limits.
All combination $combi generated must respect the rule : $combi[x] <= $ref[x]
For example with [2,1,1,0] we can't have [0,0,2,0], [0,2,0,0].
I created the following function to add the new parameter :
// $distribution is the array reference
// $similitude is the sum of values
function SETpossibilites1distri($distribution, $similitude){
$possibilites = [];
$all_distri = distributions_possibles(count($distribution), $similitude);
foreach($all_distri as $distri){
$verif = true;
$distri_possi = [];
for($x = 0; $x < count($distri); $x++){
if($distri[$x] > $distribution[$x]){
$verif = false;
break;
}
if($distribution[$x] == 0){
$distri_possi[$x] = null;
}
elseif($distribution[$x] > $distri[$x] && $distri[$x] != 0){
// si c'est une valeur fixée qui informe sur la distri_cach
if($this->distri_cach[$x] == $distri[$x]){
$distri_possi[$x] = $distri[$x]+.1;
}
else{
$distri_possi[$x] = $distri[$x]+.2;
}
}
else{
$distri_possi[$x] = $distri[$x];
}
}
if($verif){
$possibilites[] = $distri_possi;
}
}
return $possibilites;
}
This function makes me generate and filter a big list of combinations with the new parameter.
I need to have a function which generates only the combinations I want.
Do you have ideas ?
Honestly, the simplest solution would be to generate the full set of possibilities and then filter the unsuitable results afterwards. Trying to apply a mask over a recursive function like this is going to be a giant pile of work, which will likely only complicate and bog down the process.
That said, there are a couple ways in which I think you could optimize your generation.
Caching
Write a simple cache layer so that you're not constantly re-computing smaller sub-lists, eg:
function cached_distributions_possibles($n_valeurs, $x_entrees, $combi_presences = array()) {
$key = "$n_valeurs:$x_entrees";
if( ! key_exists($key, $this->cache) ) {
$this->cache[$key] = distributions_possibles($n_valeurs, $x_entrees, $combi_presences);
}
return $this->cache[$key];
}
You might want to set a lower limit on the size of a list that will be cached so you can balance between memory usage and CPU time.
Generators: https://www.php.net/manual/en/language.generators.overview.php
As it stands the function is basically building out many redundant subtrees of combinations in-memory, and you're likely to run into memory usage concerns depending on how broad the sets of possibilities become.
Rather than something like:
function foo() {
$result = [];
for(...) {
result[] = foo(...);
}
return $result;
}
Something like:
function foo() {
for(...) {
yield foo(...);
}
}
Now you're essentially only ever holding in memory a single copy of the sublist segments you're currently interested in, and a handful of coroutines, rather than the whole subtree.
I have found a solution, here it is :
function sous($dist, $d){
$l = count($dist);
$L = [[]];
foreach(range(0,$l - 1) as $i){
$K = [];
$s = array_sum(array_slice($dist, $i+1));
foreach($L as $p){
$q = array_sum($p);
$m = max($d-$q-$s, 0);
$M = min($dist[$i], $d-$q);
foreach(range($m, $M) as $j){
$p_copy = $p;
$p_copy[] = $j;
$K[] = $p_copy;
}
}
$L = $K;
}
return $L;
}
Here is what I have tried but it is giving me wrong output. Can anyone point out what is the mistake?
function superPower($n) {
$response = false;
$n = abs($n);
if ($n < 2) {
$response = true;
}
for ($i=2;$i<$n;$i++) {
for ($j=2;$j<$n;$j++) {
if (pow($i,$j) == $n) {
$response = true;
}
}
}
return $response;
}
For example if I give it number 25, it gives 1 as output. //Correct
But if I give it 26 it still gives me 1 which is wrong.
By using superPower, you are essentially trying to put a certain defence to the power of an attack to see if it holds up. This can be done much more effectively than through the brute-force method you have now.
function superPower( $hp) { // Niet used Superpower!
if( $hp <= 1) return true;
for( $def = floor(sqrt($hp)); $def > 1; $def--) { // Niet's Defence fell
for( $atk = ceil(log($hp)/log($def)); $atk > 1; $atk--) { // Niet's Attack fell
if( pow($def,$atk) == $hp) return true;
break;
// you don't need the $atk loop, but I wanted to make a Pokémon joke. Sorry.
}
// in fact, all you really need here is:
// $atk = log($hp)/log($def);
// if( $atk-floor($atk) == 0) return true;
}
return false;
}
The maths on the accepted answer is absolutely brilliant, however there are a couple of issues with the solution:
the function erroneously returns true for all of the following inputs: monkey, -3 and 0. (Technically 0 is unsigned, so there is no way of getting it by taking a positive integer to the power of another positive integer. The same goes for any negative input.)
the function compares floating numbers with integers (floor() and ceil() return float), which should be avoided like the plague. To see why, try running php -r '$n = (-(4.42-5))/0.29; echo "n == {$n}\n".($n == 2 ? "OK" : "Surprise")."\n";'
The following solution improves on the idea by fixing all of the above issues:
function superPower($value)
{
// Fail if supplied value is not numeric
if (!is_numeric($value)) {
// throw new InvalidArgumentException("Value is not numeric: $value");
return false;
}
// Normalise numeric input
$number = abs($value);
// Fail if supplied number is not an integer
if (!is_int($number)) {
// throw new InvalidArgumentException("Number is not an integer: $number");
return false;
}
// Exit early if possible
if ($number == 1) {
// 1 to the power of any positive integer is one
return true;
} elseif ($number < 1) {
// X to the power of Y is never less then 1, if X & Y are greater then 0
return false;
}
// Determine the highest logarithm base and work backwards from it
for ($base = (int) sqrt($number); $base > 1; $base--) {
$coefficient = log($number)/log($base);
// Check that the result of division is a whole number
if (ctype_digit((string) $coefficient)) {
return true;
}
}
return false;
}
Assume the following variable values were set earlier in the code:
LSLATHOR = 1780, NRSLATVER = 34
Then I have these two lines of GWBASIC:
100 PITCHHOR=(LSLATHOR/(NRSLATVER+1)) : LSLATHOR=PITCHHOR*(NRSLATVER+1)
110 IF PITCHHOR>72 THEN NRSLATVER=NRSLATVER+1:GOTO 100
120 LPRINT "HORIZONTAL PITCH is equal to : ";PITCHHOR;
Now if I wanted to put this logic as a PHP function how would I go about it?:
function calc_h($slat_length_h, $slat_qty_v) {
$pitch_h = ($slat_length_h / ($v_slat_qty + 1));
if ($pitch_h > 72) {
while ($pitch_h > 72) {
$v_slat_qty += 1;
$slat_length_h = $pitch_h * ($v_slat_qty + 1);
$pitch_h = ($slat_length_h / ($v_slat_qty + 1));
}
}
return $pitch_h;
}
$slat_length_h = 1780;
$slat_qty_v = 34;
echo calc_h($slat_length_h, $slat_qty_v);
What you need to know is that a condition will sometimes exist where PITCHHOR > 72 then it needs to adjust/re-calculate the $pitch_h according to the GWBasic script.
I hope I provided enough info. Ty vm.
I'd write as follows. But since you have the original code, you can simply try to plug in a few sample values and compare the results.
function calc_pitchhor($lslathor, $nrslatver) {
do {
$pitchhor = ($lslathor/($nrslatver+1));
$lslathor = $pitchhor*($nrslatver+1);
++$nrslatver;
} while($pitchhor > 72)
return $pitchhor;
}
$lslathor = 1780;
$nrslatver = 34;
echo "HORIZONTAL PITCH is equal to: ", calc_pitchhor($slat_length_h, $slat_qty_v);
I'm trying to loop through a set of records, all of which have a "number" property. I am trying to check if there are 3 consecutive records, e.g 6, 7 and 8.
I think i'm almost there with the code below, have hit the wall though at the last stage - any help would be great!
$nums = array();
while (count($nums <= 3))
{
//run through entries (already in descending order by 'number'
foreach ($entries as $e)
{
//ignore if the number is already in the array, as duplicate numbers may exist
if (in_array($e->number, $num))
continue;
else
{
//store this number in the array
$num[] = $e->number;
}
//here i need to somehow check that the numbers stored are consecutive
}
}
function isConsecutive($array) {
return ((int)max($array)-(int)min($array) == (count($array)-1));
}
You can achieve the same result without looping, too.
If they just have to be consecutive, store a $last, and check to make sure $current == $last + 1.
If you're looking for n numbers that are consecutive, use the same, except also keep a counter of how many ones fulfilled that requirement.
$arr = Array(1,2,3,4,5,6,7,343,6543,234,23432,100,101,102,103,200,201,202,203,204);
for($i=0;$i<sizeof($arr);$i++)
{
if(isset($arr[$i+1]))
if($arr[$i]+1==$arr[$i+1])
{
if(isset($arr[$i+2]))
if($arr[$i]+2==$arr[$i+2])
{
if(isset($arr[$i+3]))
if($arr[$i]+3==$arr[$i+3])
{
echo 'I found it:',$arr[$i],'|',$arr[$i+1],'|',$arr[$i+2],'|',$arr[$i+3],'<br>';
}//if3
}//if 2
}//if 1
}
I haven't investigated it thoroughly, maybe can be improved to work faster!
This will confirm if all items of an array are consecutive either up or down.
You could update to return an array of [$up, $down] or another value instead if you need direction.
function areAllConsecutive($sequence)
{
$up = true;
$down = true;
foreach($sequence as $key => $item)
{
if($key > 0){
if(($item-1) != $prev) $up = false;
if(($item+1) != $prev) $down = false;
}
$prev = $item;
}
return $up || $down;
}
// areAllConsecutive([3,4,5,6]); // true
// areAllConsecutive([3,5,6,7]); // false
// areAllConsecutive([12,11,10,9]); // true
Here's an example that can check this requirement for a list of any size:
class MockNumber
{
public $number;
public function __construct($number)
{
$this->number = $number;
}
static public function IsListConsecutive(array $list)
{
$result = true;
foreach($list as $n)
{
if (isset($n_minus_one) && $n->number !== $n_minus_one->number + 1)
{
$result = false;
break;
}
$n_minus_one = $n;
}
return $result;
}
}
$list_consecutive = array(
new MockNumber(0)
,new MockNumber(1)
,new MockNumber(2)
,new MockNumber(3)
);
$list_not_consecutive = array(
new MockNumber(5)
,new MockNumber(1)
,new MockNumber(3)
,new MockNumber(2)
);
printf("list_consecutive %s consecutive\n", MockNumber::IsListConsecutive($list_consecutive) ? 'is' : 'is not');
// output: list_consecutive is consecutive
printf("list_not_consecutive %s consecutive\n", MockNumber::IsListConsecutive($list_not_consecutive) ? 'is' : 'is not');
// output: list_not_consecutive is not consecutive
If u don't wanna mess with any sorting, picking any of three numbers that are consecutive should give you:
- it either is adjacent to both the other numbers (diff1 = 1, diff2 = -1)
- the only number that is adjacent (diff = +-1) should comply the previous statement.
Test for the first condition. If it fails, test for the second one and under success, you've got your secuence; else the set doesn't comply.
Seems right to me. Hope it helps.
I think you need something like the following function (no need of arrays to store data)
<?php
function seqOfthree($entries) {
// entries has to be sorted descending on $e->number
$sequence = 0;
$lastNumber = 0;
foreach($entries as $e) {
if ($sequence==0 or ($e->number==$lastNumber-1)) {
$sequence--;
} else {
$sequence=1;
}
$lastNumber = $e->number;
if ($sequence ==3) {
// if you need the array of sequence you can obtain it easy
// return $records = range($lastNumber,$lastNumber+2);
return true;
}
}
// there isn't a sequence
return false;
}
function isConsecutive($array, $total_consecutive = 3, $consecutive_count = 1, $offset = 0) {
// if you run out of space, e.g. not enough array values left to full fill the required # of consecutive count
if ( $offset + ($total_consecutive - $consecutive_count ) > count($array) ) {
return false;
}
if ( $array[$offset] + 1 == $array[$offset + 1]) {
$consecutive_count+=1;
if ( $consecutive_count == $total_consecutive ) {
return true;
}
return isConsecutive($array, $total_consecutive, $consecutive_count, $offset+=1 );
} else {
return isConsecutive($array, $total_consecutive, 1, $offset+=1 );
}
}
The following function will return the index of the first of the consecutive elements, and false if none exist:
function findConsecutive(array $numbers)
{
for ($i = 0, $max = count($numbers) - 2; $i < $max; ++$i)
if ($numbers[$i] == $numbers[$i + 1] - 1 && $numbers[$i] == $numbers[$i + 2] - 2)
return $i;
return false;
}
Edit: This seemed to cause some confusion. Like strpos(), this function returns the position of the elements if any such exists. The position may be 0, which can evaluate to false. If you just need to see if they exist, then you can replace return $i; with return true;. You can also easily make it return the actual elements if you need to.
Edit 2: Fixed to actually find consecutive numbers.
I'm working with arrays of image filepaths. A typical array might have 5 image filepaths stored in it.
For each array, I want to pull out just the "best" photo to display as a thumbnail for the collection.
I find looping and arrays very confusing and after 4 hours of trying to figure out how to structure this, I'm at a loss.
Here are the rules I'm working with:
The very best photos have "-large" in their filepaths. Not all arrays will have images like this in them, but if they do, that's always the photo I want to pluck out.
The next best photos are 260px wide. I can look this up with getimagesize. If I find one of these, I want to stop looking and use it.
The next best photos are 265 wide. If I find one I want to use it and stop looking.
The next best photos are 600px wide. Same deal.
Then 220px wide.
Do I need 5 separate for loops? 5 nested for-loops
Here's what I'm trying:
if $image_array{
loop through $image_array looking for "-large"
if you find it, print it and break;
if you didn't find it, loop through $image_array looking for 260px wide.
if you find it, print it and break;
}
and so on....
But this doesn't appear to be working.
I want to "search" my array for the best single image based on these criteria. If it can't find the first type, then it looks for the second on so on. How's that done?
// predefined list of image qualities (higher number = best quality)
// you can add more levels as you see fit
$quality_levels = array(
260 => 4,
265 => 3,
600 => 2,
220 => 1
);
if ($image_arry) {
$best_image = null;
// first search for "-large" in filename
// because looping through array of strings is faster then getimagesize
foreach ($image_arry as $filename) {
if (strpos('-large', $filename) !== false) {
$best_image = $filename;
break;
}
}
// only do this loop if -large image doesn't exist
if ($best_image == null) {
$best_quality_so_far = 0;
foreach ($image_arry as $filename) {
$size = getimagesize($filename);
$width = $size[0];
// translate width into quality level
$quality = $quality_levels[$width];
if ($quality > $best_quality_so_far) {
$best_quality_so_far = $quality;
$best_image = $filename;
}
}
}
// we should have best image now
if ($best == null) {
echo "no image found";
} else {
echo "best image is $best";
}
}
Another approach (trivial, less generic, slower). Just check rules one by one:
function getBestFile($files) {
foreach ($files as $arrayKey => $file) {
if (strstr($file, '-large') !== FALSE) {
return $file;
}
}
foreach ($files as $arrayKey => $file) {
if (is260wide($file)) {
return $file;
}
}
// ...
}
You need 3 loops and a default selection.
loop through $image_array looking for "-large"
if you find it, return it;
if you didn't find it, loop through $image_array
get image width
if prefered width (260px), return it.
if $sizes[$width] not set, add filename
loop a list of prefered sizes in order and see if it is set in $sizes
if you find it, return it;
return the first image or default image;
<?php
// decide if 1 or 2 is better
function selectBestImage($image1, $image2) {
// fix for strange array_filter behaviour
if ($image1 === 0)
return $image2;
list($path1, $info1) = $image1;
list($path2, $info2) = $image2;
$width1 = $info1[0];
$width2 = $info2[0];
// ugly if-block :(
if ($width1 == 260) {
return $image1;
} elseif ($width2 == 260) {
return $image2;
} elseif ($width1 == 265) {
return $image1;
} elseif ($width2 == 265) {
return $image2;
} elseif ($width1 == 600) {
return $image1;
} elseif ($width2 == 600) {
return $image2;
} elseif ($width1 == 220) {
return $image1;
} elseif ($width2 == 220) {
return $image2;
} else {
// nothing applied, so both are suboptimal
// just return one of them
return $image1;
}
}
function getBestImage($images) {
// step 1: is the absolutley best solution present?
foreach ($images as $key => $image) {
if (strpos($image, '-large') !== false) {
// yes! take it and ignore the rest.
return $image;
}
}
// step 2: no best solution
// prepare image widths so we don't have to get them more than once
foreach ($images as $key => $image) {
$images[$key] = array($image, getImageInfo($image));
}
// step 3: filter based on width
$bestImage = array_reduce($images, 'selectBestImage');
// the [0] index is because we have an array of 2-index arrays - ($path, $info)
return $bestImage[0];
}
$images = array('image1.png', 'image-large.png', 'image-foo.png', ...);
$bestImage = getBestImage($images);
?>
this should work (i didn't test it), but it is suboptimal.
how does it work? first, we look for the absolutely best result, in this case, -large, because looking for a substrings is inexpensive (in comparsion).
if we don't find a -large image we have to analyze the image widths (more expensive! - so we pre-calculate them).
array_reduce calls a filtering function that takes 2 array values and replaces those two by the one return by the function (the better one). this is repeated until there is only one value left in the array.
this solution is still suboptimal, because comparisons (even if they're cheap) are done more than once. my big-O() notation skills are a bit (ha!) rusty, but i think it's O(n*logn). soulmerges solution is the better one - O(n) :)
you could still improve soulmerges solution, because the second loop is not necessary:
first, pack it into a function so you have return as a break-replacement. if the first strstr matches, return the value and ignore the rest. afterwards, you don't have to store the score for every array key. just compare to the highestKey variable and if the new value is higher, store it.
<?php
function getBestImage($images) {
$highestScore = 0;
$highestPath = '';
foreach ($images as $image) {
if (strpos($image, '-large') !== false) {
return $image;
} else {
list($width) = getImageInfo($image);
if ($width == 260 && $highestScore < 5) {
$highestScore = 5;
$highestPath = $image;
} elseif ($width == 265 && $highestScore < 4) {
$highestScore = 4;
$highestPath = $image;
} elseif ($width == 600 && $highestScore < 3) {
$highestScore = 3;
$highestPath = $image;
} elseif ($width == 220 && $highestScore < 2) {
$highestScore = 2;
$highestPath = $image;
} elseif ($highestScore < 1) {
// the loser case
$highestScore = 1;
$highestPath = $image;
}
}
}
return $highestPath;
}
$bestImage = getBestImage($images);
?>
didn't test, should work in O(n). can't imagine a faster, more efficient way atm.
I would assign points to the files depending on how many rules apply. If you want certain rules to supercede others, you can give more points for that rule.
define('RULE_POINTS_LARGE', 10);
define('RULE_POINTS_260_WIDE', 5);
// ...
$points = array();
foreach ($files as $arrayKey => $file) {
$points[$arrayKey] = 0;
if (strstr($filename, '-large') !== FALSE) {
$points[$arrayKey] += RULE_POINTS_LARGE;
}
// if ...
}
// find the highest value in the array:
$highestKey = 0;
$highestPoints = 0;
foreach ($points as $arrayKey => $points) {
if ($files[$arrayKey] > $highestPoints) {
$highestPoints = $files[$arrayKey];
$highestKey = $arrayKey;
}
}
// The best picture is $files[$highestKey]
One more side note: Givign your rules multiples of a value will ensure that a rule can be 'stronger' than all others. Example: 5 rules -> rule values (1, 2, 4, 8, 16).
1 < 2
1 + 2 < 4
1 + 2 + 4 < 8
etc.