<?php
/**
* Seek the input value that generates the required output value for a given function and context
*
* Given a supplied (callback) function, finds out what input value to the function is required to generate the specified
* result value for a given set of conditions. For example, what value of the quality parameter is needed to
* achieve a specified file size for a given image. The binary chop search method is used to minimise the
* number of attempts. This method requires a comparison between the required value and the most recent achieved value.
* This comparison is also carried out in a callback function so that the comparison is under user control.
*
* @property callback string The name of a function which maps the independent variable to the dependent variable
* @property comparator string The name of a function which compares the result of callback to the sought value
* @property maxattempts integer Max number of times to iterate. Default 30
* @property boundLower integer the minimum value of the independent variable. Default 0
* @property boundUpper integer the maximum value of the independent variable. Default 99
* @property searchvalue mixed this value will be passed to comparator for comparison with the result of callback function
* @property items mixed cargo for the callback function. Invariate value(s)
* @property-read status integer how the search was completed. Values 1-4
* @ property-read statusmsg string how the search was completed.
* @ property-read outputvalue mixed contains the final result from callback
* @method mixed search returns outputvalue, documented above
*
*/
class ValueSeeker
{ private $callback; //The function which transforms the input value into the output value - default: pick element from array
private $comparator; //used for tests of $searchvalue against outputvalue of callback function - default: simple comparison a gt/lt/= b
private $maxattempts; //maximum number of times the search is iterated before giving up - default 30
private $boundLower; //lowest input value to the callback function - default 0
private $boundUpper; //highest input value to the callback function - default 99
private $searchvalue; //The output value to be aimed for
private $items; //parametric data for the callback function
private $status;
private $statusmsg;
private $inputvalue; //used to probe output values of callback. value range between $boundUpper and $boundLower
private $outputvalue; //hold the result of the callback function
/**
* Function to compare two result values
*
* This is a simple comparison between two integers or strings. It is the default
* callback function for comparator. It will normally be overridden by a more complex
* comparison.
*
* @param mixed a the value of the searchvalue property
* @param mixed b the return value after calling the callback function defined in the callback property
* @return integer -1: a is less, +1: b is less; 0: they are the same
*
*/
private function compare($a, $b)
{ //default function for comparator
if ($a == $b) return 0;
if ($a > $b) return 1;
return -1;
}
/**
* Function to map an independent variable to a dependent variable
*
* This is a sample function. It assumes that the property 'items' contains a number
* indexed array. This sample callback function transforms the index of the array to
* the value of that indexed item.
*
*/
private function arrayselect($array, $index)
{ //default function for callback
return $array[$index];
}
function __construct()
{
$this->callback = 'arrayselect';
$this->comparator = 'compare';
$this->maxattempts = 30;
$this->boundLower = 0;
$this->boundUpper = 99;
}
public function __set ($var, $value)
{
switch ($var)
{
case 'searchvalue':
$this->searchvalue = $value;
break;
case 'items':
$this->items = $value;
break;
case 'callback':
if(!function_exists($value)) throw new Exception("$var function '$value()' does not exist",4);
$this->callback = $value;
break;
case 'comparator':
if(!function_exists($value)) throw new Exception("$var function '$value()' does not exist",5);
$this->comparator = $value;
break;
case 'maxAttempts':
if(!is_integer($value)) throw new Exception("maxattempts is not an integer",2);
if( $maxattempts < 1 ) throw new Exception("maxattempts must be at least 1", 3);
$this->maxattempts = $value;
break;
case 'boundLower':
if(!is_integer($value)) throw new Exception("lower bound of range is not an integer",2);
$this->boundLower = $value;
break;
case 'boundUpper':
if(!is_integer($value)) throw new Exception("upper bound of range is not an integer",2);
$this->boundUpper = $value;
break;
default:
throw new Exception("Attempt to set non-existent property:$var\n\r",10);
}
}
public function __get($var)
{
switch ($var)
{
case 'searchvalue': return $this->searchvalue; break;
case 'items': return $this->items; break;
case 'maxattempts': return $this->maxattempts; break;
case 'boundLower': return $this->boundLower; break;
case 'boundUpper': return $this->boundUpper; break;
case 'comparator': return $this->comparator; break;
case 'callback': return $this->callback; break;
case 'outputvalue': return $this->outputvalue; break;
case 'status': return $this->status; break;
case 'statusmsg': return $this->statusmsg; break;
default: throw new Exception("Attempt to get non-existent property:$var\n\r",11);
}
}
function search()
{
if($this->boundUpper < $this->boundLower) throw new Exception("range invalid:lower bound > upper bound",1);
$boundUpper = $this->boundUpper;
$boundLower = $this->boundLower;
$inputValue = round(($boundUpper + $boundLower) / 2, 0);
$attempts = 0;
$comparison = 0;
while( $attempts < $this->maxattempts )
{
$this->outputvalue = call_user_func($this->callback, $this->items, $inputValue);
$comparison = call_user_func($this->comparator, $this->searchvalue, $this->outputvalue );
if ( $comparison > 0 )
{
$boundLower = $inputValue;
$inputValue = round(($boundUpper + $inputValue) / 2, 0);
if($inputValue == $boundLower)
{
if($boundLower == $this->boundLower)
{
$this->statusmsg = "Hit lower bound";
$this->status = 3;
}
else
{
$this->statusmsg = "Found nearest input value(-)";
$this->status = 2;
}
return $inputValue;//gone as low as we can
}
}
else
{
if ( $comparison < 0 )
{
$boundUpper = $inputValue;
$inputValue = round(($boundLower + $inputValue) / 2, 0);
if( $inputValue == $boundUpper )
{
if($boundUpper == $this->boundUpper)
{
$this->statusmsg = "Hit upper bound";
$this->status = 4;
}
else
{
$this->statusmsg = "Found nearest input value(-)";
$this->status = 2;
}
return $inputValue - 1;//gone as high as we can
}
}
else
{
$this->statusmsg = "Reached target value";
$this->status = 1;
return $inputValue;
}
}
$attempts++;
}
$this->statusmsg = "Performed maximum no of attempts";
$this->status = 4;
return $inputValue; //bombed out on attempts limit
}
}
?>
|