I'm creating a metacritic type of site where users link review posts (from external sources).
In the form, I allow users to choose the type of rating the source site uses:
For example, the source post can have rating 50
out of 100
.
To display the rating on my site, I want to convert the source rating to a simple 5 star rating. So, rating for above example becomes 2.5
My question is, what would be the best way in PHP to make these types of calculations when considering performance and efficiency?
I'm struggling to find a good ruote to go, particularly with A+
etc...
This is an interesting question. Here's my naive attempt at a solution that's pretty mechanical, but attempts to be fairly uniform. I'm sure it could be improved/optimised significantly.
Edit
I wasn't satisfied with my previous answer (I rushed it so there were some errors - thanks for pointing those out!). I decided to remove that and attempt a different approach, which goes a bit crazy with regexes and filter_input
to determine if user input is valid - and if so - cast it to the appropriate type.
This makes validating the rating against the chosen scale, (by type and value comparison) a lot more uniform. I sanity-checked this a bit and I think I am happier with this approach ;)
Once again... Assuming a HTML form like:
<form method="post">
<label>rating</label>
<input name="rating" type="text" autofocus>
<label>out of</label>
<select name="scale">
<option value="100">100</option>
<option value="10">10</option>
<option value="6">6</option>
<option value="5">5</option>
<option value="4">4</option>
<option value="A">A</option>
<option value="A+">A+</option>
</select>
<input type="submit">
</form>
And the following PHP to user-submitted input:
<?php
const MAX_STARS = 5;
const REGEX_RATING = '/^(?<char>[a-fA-F]{1}[-+]?)$|^(?<digit>[1-9]?[0-9](\.\d+)?|100)$/';
const REGEX_SCALE = '/^(?<char>A\+?)$|^(?<digit>100|10|6|5|4)$/';
$letters = [
'F-', 'F', 'F+',
'G-', 'G', 'G+',
'D-', 'D', 'D+',
'C-', 'C', 'C+',
'B-', 'B', 'B+',
'A-', 'A', 'A+',
];
if ('POST' === $_SERVER['REQUEST_METHOD']) {
// validate user-submitted `rating`
$rating = filter_input(INPUT_POST, 'rating', FILTER_CALLBACK, [
'options' => function($input) {
if (preg_match(REGEX_RATING, $input, $matches)) {
return isset($matches['digit'])
? (float) $matches['digit']
: strtoupper($matches['char']);
}
return false; // no match on regex
},
]);
// validate user-submitted `scale`
$scale = filter_input(INPUT_POST, 'scale', FILTER_CALLBACK, [
'options' => function($input) {
if (preg_match(REGEX_SCALE, $input, $matches)) {
return isset($matches['digit'])
? (float) $matches['digit']
: strtoupper($matches['char']);
}
return false; // no match on regex
}
]);
// if a valid letter rating, convert to calculable values
if (in_array($scale, ['A+', 'A']) && in_array($rating, $letters)) {
$scale = array_search($scale, $letters);
$rating = array_search($rating, $letters);
}
// error! types don't match
if (gettype($rating) !== gettype($scale)) {
$error = 'rating %s and scale %s do not match';
exit(sprintf($error, $_POST['rating'], $_POST['scale']));
}
// error! rating is higher than scale
if ($rating > $scale) {
$error = 'rating %s is greater than scale %s';
exit(sprintf($error, $_POST['rating'], $_POST['scale']));
}
// done! print our rating...
$stars = round(($rating / $scale) * MAX_STARS, 2);
printf('%s stars out of %s (rating: %s scale: %s)', $stars, MAX_STARS, $_POST['rating'], $_POST['scale']);
}
?>
It's probably worth explaining what the hell is going on with the regexes and callbacks ;)
For example, take the following regex:
/^(?<char>A\+?)$|^(?<digit>100|10|6|5|4)$/'
This regex defines two named subpatterns. One, named <char>
, captures A
and A+
; the other, named <digit>
captures 100
, 10
, 6
etc.
preg_match()
returns 0
if there is no match (or false
on error) so we can return false
in that case, because this means the user input (or the scale) POST
ed was not valid.
Otherwise, the $match
array will contain any captured values, with char
and (optionally) digit
as keys. If the digit
key exists, we know the match is a digit and we can cast it to a float
and return it. Otherwise, we must have matched on a char
, so we can strtoupper()
that value and return it:
return isset($matches['digit'])
? (float) $matches['digit']
: strtoupper($matches['char']);
Both callbacks are identical (apart from the regexes themselves) so you could create a callable there and maybe save some duplication.
I hope it's not starting to feel a bit convoluted at this stage! Hope this helps :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With