<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Boolean Loose Casting Function Tests</title>
<style>
table {
width: 100%;
}
td {
width: 50%;
border-bottom: 1px solid #ccc;
}
td.type {
width: auto;
font-weight: bold;
color: #ccc;
}
section {
margin-bottom: 10em;
}
section h1 {
background-color: #eee;
}
.truthy {
color: limegreen;
}
.falsy {
color: red;
}
.null {
color: orange;
}
</style>
</head>
<body>
<h1>Boolean Loose Casting Function Tests</h1>
<h2>PHP Version <?php echo phpversion(); ?></h2>
<?php
error_reporting(-1);
/**
* some dummy classes used to create test objects
*/
class StringyThing {
private $value;
public function __construct($value) {
$this->value = $value;
}
public function __toString() {
return (string) $this->value;
}
}
class CountableThing implements Countable {
private $values = array();
public function __construct($values) {
$this->values = $values;
}
public function count() {
return count($this->values);
}
}
class StringyAndCountableThing implements Countable {
private $values = array();
private $value;
public function __construct($stringy_value, $countable_values) {
$this->value = $stringy_value;
$this->values = $countable_values;
}
public function count() {
return count($this->values);
}
public function __toString() {
return (string) $this->value;
}
}
/**
* runs a test function on all test data, outputting tables of results
*/
function run_test($test) {
global $test_data;
echo '<section>';
echo '<h1>'.$test.'</h1>';
foreach($test_data as $set_name => $set) {
echo '<h2>'.$set_name.'</h2>';
echo '<table>';
foreach($set as $index => $value) {
echo '<tr>';
echo '<td class="type">';
echo gettype($value);
echo '</td>';
echo '<td class="test-value">';
// be clever with test values
if(is_string($index)) {
// use the index as a placeholder representation
echo $index;
} else if(is_resource($value)) {
echo get_resource_type($value);
} else {
var_export($value);
}
echo '</td>';
$result = $test($value);
$class = ($result ? 'truthy' : 'falsy');
if(is_null($result)) $class .= ' null';
echo '<td class="test-result '.$class.'">';
var_export($result);
echo '</td>';
echo '</tr>';
}
echo '</table>';
}
echo '</section>';
};
/**
* data to run through for all tests
* string keys in subarrays can be used to represent values in test output
*/
$test_data = array(
'ought to be true' => array(
true,
1,
'true',
'yes',
'YES',
'on',
'1',
'y',
'this should probably be TRUE',
'รก',
100,
0.1,
-1,
INF,
-INF,
// for performance testing more than anything else
// depending on the length used, may hit PHP's memory limit
// http://www.php.net/manual/en/ini.core.php#ini.memory-limit
// '(A VERY LONG STRING)' => str_repeat('.', 100000000),
array(1),
array('a key' => true),
array(null => true),
simplexml_load_string('<test>i have content</test>'),
new StringyThing(true),
new StringyThing('true'),
new StringyThing('a string'),
new CountableThing(array(1, 2, 3)),
new CountableThing('count() returns 1 for this'),
new StringyAndCountableThing(true, array(true)),
),
'ought to be false' => array(
false,
0,
0.0,
(float) '-0',
'0',
'',
'no',
'off',
'OFF',
'false',
array(),
// this is explicitly mentioned to be false in the manual:
// http://www.php.net/manual/en/language.types.boolean.php#language.types.boolean.casting
simplexml_load_string('<test></test>'),
new StringyThing(false),
new StringyThing('no'),
new StringyThing(''),
new CountableThing(array()),
new StringyAndCountableThing(false, array()),
),
'iffy' => array(
null,
NAN,
// the list of things like this could go on forever (not even taking translations into account)
'null',
'n',
'unset',
'nothing',
'nil',
'nope',
// equivalent to an empty string in ini directives
'none',
'NONE',
// this is not falsy in PHP (even though '0' is)
'0.0',
'0x0',
' ',
'PHP_EOL' => PHP_EOL,
'"\t"' => "\t",
// mixed signals, should countability or string value take precedence (or should these be null)?
new StringyAndCountableThing(false, array(true)),
new StringyAndCountableThing('yes', array()),
// in PHP4 this would have been consider falsy
new stdClass(),
// could check for a ->scalar property to handle this case
(object) false,
array(false),
array(null),
array('a key' => false),
array(null => false),
// give these readable keys since var_export is not helpful for anonymous functions
'function() {}' => function() {},
'function() { return false; }' => function() { return false; },
'function() { return true; }' => function() { return true; },
"function() { return 'no'; }" => function() { return 'no'; },
"function() { return 'on'; }" => function() { return 'on'; },
"function() { return 'something completely different'; }" => function() { return 'something completely different'; },
// resources
xml_parser_create(),
fopen(__FILE__, 'r'),
),
);
/**
* functions to run tests with
*/
// PHP's built in behavior for things like if($value)
function typecast($value) {
return (boolean) $value;
}
function filter_var_bool($value) {
return filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
}
// see http://us2.php.net/manual/en/filter.filters.validate.php#108218
// and https://bugs.php.net/bug.php?id=49510
function filter_var_bool_with_null_fallback($value) {
$filtered = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if(is_null($filtered)) return (boolean) $value;
else return $filtered;
}
// from http://www.php.net/manual/en/function.is-bool.php#93165
function kim_s_solution($var){
if(is_bool($var)){
return $var;
} else if($var === NULL || $var === 'NULL' || $var === 'null'){
return false;
} else if(is_string($var)){
$var = trim($var);
if($var=='false'){ return false;
} else if($var=='true'){ return true;
} else if($var=='no'){ return false;
} else if($var=='yes'){ return true;
} else if($var=='off'){ return false;
} else if($var=='on'){ return true;
} else if($var==''){ return false;
} else if(ctype_digit($var)){
if((int) $var)
return true;
else
return false;
} else { return true; }
/* NOTE: i changed these lines from the original code. string casting results in an error for some objects, and handling floats correctly is a good idea anyway.
} else if(ctype_digit((string) $var)){
if((int) $var)
*/
} else if(is_numeric($var)){
if((float) $var)
/* end of changed lines */
return true;
else
return false;
} else if(is_array($var)){
if(count($var))
return true;
else
return false;
} else if(is_object($var)){
return true;// No reason to (bool) an object, we assume OK for crazy logic
} else {
return true;// Whatever came though must be something, OK for crazy logic
}
}
function custom_solution($value) {
// FIXME? if an object is both countable and string-castable, this will favor the count over the string value.
// which should take precedence (if any)? maybe it should return null if there are mixed signals? or true?
if(is_array($value) || $value instanceof Countable) {
return (boolean) count($value);
} else if(is_string($value) || is_object($value) && method_exists($value, '__toString')) {
$value = (string) $value;
// see http://www.php.net/manual/en/filter.filters.validate.php#108218
// see https://bugs.php.net/bug.php?id=49510
$filtered = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if(!is_null($filtered)) {
return $filtered;
} else {
// "none" gets special treatment to be consistent with ini file behavior.
// see documentation in php.ini for more information, in part it says:
// "An empty string can be denoted by simply not writing anything after
// the equal sign, or by using the None keyword".
if(strtolower($value) === 'none') {
$value = '';
}
return (boolean) $value;
}
} else {
return (boolean) $value;
}
}
/**
* test the various solutions
*/
run_test('typecast');
run_test('filter_var_bool');
run_test('filter_var_bool_with_null_fallback');
run_test('kim_s_solution');
run_test('custom_solution');
?>
</body>
</html>