Last active
October 4, 2022 23:57
-
-
Save stracker-phil/6a80e6faedea8dab090b4bf6668ee461 to your computer and use it in GitHub Desktop.
Performance comparison for various PHP functions that test, if a string is valid JSON.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// https://stackoverflow.com/a/6041773/313501#answer-6041773 | |
function test1( $value ) { | |
if ( ! is_scalar( $value ) ) { | |
return null; | |
} | |
json_decode( $value ); | |
return ( json_last_error() == JSON_ERROR_NONE ); | |
} | |
// https://stackoverflow.com/a/12311839/313501#answer-12311839 | |
function test2( $value ) { | |
if ( ! is_scalar( $value ) ) { | |
return null; | |
} | |
return is_object( json_decode( $value ) ); | |
} | |
// https://stackoverflow.com/a/6041857/313501#answer-43244302 | |
function test3( $value ) { | |
if ( ! is_scalar( $value ) ) { | |
return null; | |
} | |
$json = json_decode( $value ); | |
return $json && $value != $json; | |
} | |
// https://stackoverflow.com/a/6041857/313501#answer-6041857 | |
function test4( $value ) { | |
if ( ! is_scalar( $value ) ) { | |
return null; | |
} | |
return ! preg_match( '/[^,:{}\\[\\]0-9.\\-+Eaeflnr-u \\n\\r\\t]/', | |
preg_replace( '/"(\\.|[^"\\\\])*"/', '', strval( $value ) ) ); | |
} | |
// https://stackoverflow.com/a/6041857/313501#answer-45241792 | |
function test5( $value ) { | |
// Numeric strings are always valid JSON. | |
if ( is_numeric( $value ) ) { return true; } | |
// A non-string value can never be a JSON string. | |
if ( ! is_string( $value ) ) { return false; } | |
// Any non-numeric JSON string must be longer than 2 characters. | |
if ( strlen( $value ) < 2 ) { return false; } | |
// "null" is valid JSON string. | |
if ( 'null' === $value ) { return true; } | |
// "true" and "false" are valid JSON strings. | |
if ( 'true' === $value ) { return true; } | |
if ( 'false' === $value ) { return false; } | |
$value = trim($value); | |
// Any other JSON string has to be wrapped in {}, [] or "". | |
if ( '{' !== $value[0] & '[' !== $value[0] && '"' !== $value[0] ) { return false; } | |
// Verify that the trailing character matches the first character. | |
$last_char = $value[strlen($value) -1]; | |
if ( '{' == $value[0] && '}' != $last_char ) { return false; } | |
if ( '[' == $value[0] && ']' != $last_char ) { return false; } | |
if ( '"' == $value[0] && '"' != $last_char ) { return false; } | |
// See if the string contents are valid JSON. | |
return null !== json_decode( $value ); | |
} | |
// ---------------------------------------------------------------------------- | |
// Verify the correct result of the test functions. | |
function verify_results( $label, $testcase, $args, $echo = false ) { | |
$result = []; | |
$errors = false; | |
if ( $echo ) { | |
echo "$testcase: $label\n"; | |
} | |
foreach ( $args as $test_num => $arg ) { | |
$fn_res = $testcase($arg['value']); | |
$result[$test_num] = $arg['is_valid'] === $fn_res; | |
if ( $echo ) { | |
if ( ! $result[$test_num] ) { | |
$errors = true; | |
if (is_scalar($arg['value'])) { | |
$value_string = $arg['value']; | |
} else { | |
$value_string = '<' . gettype( $arg['value'] ) . '>'; | |
} | |
if ( strlen( $value_string ) > 40 ) { | |
$value_string = substr( $value_string, 0, 37 ) . '...'; | |
} | |
printf( | |
"\tTest #%d returns %s but should be %s: %s\n", | |
$test_num, | |
$result ? 'VALID' : 'INVALID', | |
$arg['is_valid'] ? 'VALID' : 'INVALID', | |
$value_string | |
); | |
} | |
} | |
if ( is_null( $fn_res ) ) { | |
$result[$test_num] = null; | |
} | |
} | |
if ( $echo ) { | |
if ( ! $errors ) { | |
echo "\tAll results correct\n"; | |
} | |
echo "\n"; | |
} | |
return $result; | |
} | |
// Test-Runner runs the testcase with each test-string 10.000 times. | |
function run_test( $label, $testcase, $args, $num_tests = 10000 ) { | |
$durations = []; | |
echo "$testcase: $label...\n"; | |
foreach ( $args as $test_num => $arg ) { | |
$start = microtime( true ); | |
for ( $i = 0; $i < $num_tests; $i ++ ) { | |
$testcase( $arg['value'] ); | |
} | |
$durations[$test_num] = microtime( true ) - $start; | |
} | |
return $durations; | |
} | |
// ---------------------------------------------------------------------------- | |
// Generate the test-data and run all tests. | |
$large_array = []; | |
while ( count( $large_array ) < 1000 ) { | |
$large_array[] = rand( 1000, 9999 ); | |
} | |
$testdata = [ | |
[ | |
'value' => '{"validJson":true}', | |
'is_valid' => true, | |
], | |
[ | |
'value' => '{"invalidJson":true', | |
'is_valid' => false, | |
], | |
[ | |
'value' => '{invalidJson:true}', | |
'is_valid' => false, | |
], | |
[ | |
'value' => '{"invalidJson":true,}', | |
'is_valid' => false, | |
], | |
[ | |
'value' => '"valid"', | |
'is_valid' => true, | |
], | |
[ | |
'value' => '[1,2,3]', | |
'is_valid' => true, | |
], | |
[ | |
'value' => '[[[1,2,3]],[4,5]]', | |
'is_valid' => true, | |
], | |
[ | |
'value' => 'null', | |
'is_valid' => true, | |
], | |
[ | |
'value' => 1000, | |
'is_valid' => true, | |
], | |
[ | |
'value' => json_encode( $large_array ), | |
'is_valid' => true, | |
], | |
[ | |
'value' => '{"id":"area","data":{"version":"2.3.1","layout_type":"inline","inline_location":"css","location_position":"after","location_selector":".weekly-schedule","close_trigger":null,"position":null,"overflow":"clip","size":"auto","close_delay":0,"close_for":"1day","min_width":"none","width":"auto","max_width":"none","min_height":"none","height":"auto","max_height":"none","z_index":"auto","role_limitation":"off","page_limitation":"on","show_close":"on","not_modal":"off","not_blocking":"off","singleton":"off","keep_closed":"off","box_shadow":"off","with_loader":"off","dark_close":"off","alt_close":"off","is_static":"off","push_content":"off","limit_role":[],"postlist":{"singular:post_type:post:id:75":"use_on"},"triggers":[{"trigger_type":"time","trigger_scroll_type":"","trigger_selector":"","trigger_delay":0,"trigger_distance":"50%","trigger_label":"After Delay Trigger","cond_custom_area":"on","cond_device_area":null,"cond_use_url_param_area":null,"cond_use_referrer_area":null,"cond_url_param_area":"","cond_referrer_area":null,"cond_custom_referrer_area":"","cond_schedule_area":null,"cond_schedule_dates_area":[],"cond_schedule_weekly_area":[]}],"triggers_hover":[],"cond_device":["desktop","tablet","phone"],"cond_use_url_param":"off","cond_use_referrer":"off","cond_url_param":"","cond_referrer":null,"cond_custom_referrer":"","cond_schedule":"weekly","cond_schedule_dates":[{"schedule_range":"2021-02-06 - 2021-02-06","schedule_start":"00:00","schedule_end":"00:00","schedule_label":""},{"schedule_range":"2021-02-06 - 2021-02-06","schedule_start":"00:00","schedule_end":"00:00","schedule_label":""},{"weekly_days":["We","Fr"],"weekly_start":"00:00","weekly_end":"23:59","weekly_label":"We, Fr"}],"cond_schedule_weekly":[]}}', | |
'is_valid' => true, | |
], | |
[ | |
// The first "[" should be a "{": | |
'value' => '["id":"area","data":{"version":"2.3.1","layout_type":"inline","inline_location":"css","location_position":"after","location_selector":".weekly-schedule","close_trigger":null,"position":null,"overflow":"clip","size":"auto","close_delay":0,"close_for":"1day","min_width":"none","width":"auto","max_width":"none","min_height":"none","height":"auto","max_height":"none","z_index":"auto","role_limitation":"off","page_limitation":"on","show_close":"on","not_modal":"off","not_blocking":"off","singleton":"off","keep_closed":"off","box_shadow":"off","with_loader":"off","dark_close":"off","alt_close":"off","is_static":"off","push_content":"off","limit_role":[],"postlist":{"singular:post_type:post:id:75":"use_on"},"triggers":[{"trigger_type":"time","trigger_scroll_type":"","trigger_selector":"","trigger_delay":0,"trigger_distance":"50%","trigger_label":"After Delay Trigger","cond_custom_area":"on","cond_device_area":null,"cond_use_url_param_area":null,"cond_use_referrer_area":null,"cond_url_param_area":"","cond_referrer_area":null,"cond_custom_referrer_area":"","cond_schedule_area":null,"cond_schedule_dates_area":[],"cond_schedule_weekly_area":[]}],"triggers_hover":[],"cond_device":["desktop","tablet","phone"],"cond_use_url_param":"off","cond_use_referrer":"off","cond_url_param":"","cond_referrer":null,"cond_custom_referrer":"","cond_schedule":"weekly","cond_schedule_dates":[{"schedule_range":"2021-02-06 - 2021-02-06","schedule_start":"00:00","schedule_end":"00:00","schedule_label":""},{"schedule_range":"2021-02-06 - 2021-02-06","schedule_start":"00:00","schedule_end":"00:00","schedule_label":""},{"weekly_days":["We","Fr"],"weekly_start":"00:00","weekly_end":"23:59","weekly_label":"We, Fr"}],"cond_schedule_weekly":[]}}', | |
'is_valid' => false, | |
], | |
[ | |
'value' => [1, 2, 3], | |
'is_valid' => false, | |
], | |
[ | |
'value' => (object) ['json' => true], | |
'is_valid' => false, | |
] | |
]; | |
$functions = [ | |
'test1' => 'json_last_error() == JSON_ERROR_NONE', | |
'test2' => 'is_object( json_decode() )', | |
'test3' => 'json_decode() && $res != $string', | |
'test4' => 'preg_match()', | |
'test5' => '"maybe decode" approach', | |
]; | |
echo "\nPHP version: " . PHP_VERSION . "\n\n"; | |
// First, verify that all functions return correct values. | |
$valid = []; | |
foreach ( $functions as $fn => $label ) { | |
$valid[$fn] = verify_results( $label, $fn, $testdata ); | |
} | |
// Measure performance. | |
$speed = []; | |
foreach ( $functions as $fn => $label ) { | |
$speed[$fn] = run_test( $label, $fn, $testdata ); | |
} | |
// Sort results. | |
$performance = []; | |
foreach ( $testdata as $test_num => $arg ) { | |
$performance[$test_num] = []; | |
foreach ( $functions as $fn => $label ) { | |
if ( $valid[$fn][$test_num] ) { | |
$performance[$test_num][$fn] = $speed[$fn][$test_num]; | |
} else { | |
unset( $speed[$fn][$test_num] ); | |
} | |
} | |
} | |
$average = []; | |
foreach ( $functions as $fn => $label ) { | |
$average[$fn] = array_sum( $speed[$fn] ) / count( array_filter( $speed[$fn] ) );; | |
} | |
// Display results. | |
echo "\n "; | |
foreach ( $functions as $fn => $label ) { | |
echo "| $fn "; | |
} | |
echo "\n"; | |
foreach ( $testdata as $test_num => $arg ) { | |
printf( | |
'%s #%s ', | |
str_repeat(' ', 3 - strlen($test_num)), | |
$test_num | |
); | |
foreach ( $functions as $fn => $label ) { | |
if ( $valid[$fn][$test_num] ) { | |
printf( | |
"| %.4f %s ", | |
$speed[$fn][$test_num], | |
$speed[$fn][$test_num] === min( $performance[$test_num] ) ? '✓︎' : ' ' | |
); | |
} elseif ( is_null( $valid[$fn][$test_num] ) ) { | |
echo "| - ERR - "; | |
} else { | |
echo "| - INV - "; | |
} | |
} | |
echo "\n"; | |
} | |
echo " Avg "; | |
foreach ( $functions as $fn => $label ) { | |
printf( | |
"| %.4f %s ", | |
$average[$fn], | |
$average[$fn] === min( $average ) ? '✓︎' : ' ' | |
); | |
} | |
echo "\n\n"; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Updates the test cases and improved the reporting.
The results display the average time in milliseconds for each test (lower numbers are better/faster). Also, the results now display "INV" when a comparison returns an incorrect value or throws an error.
New results:
Results when only comparing test1 and test5 (the two most reliable tests):