This is an interesting question. Here is my naive attempt at a solution that is pretty mechanical, but trying to be fairly homogeneous. I am sure that it could be significantly improved / optimized.
Edit
I was not happy with my previous answer (I threw myself so that there were some errors - thanks for pointing them out!). I decided to remove this and try a different approach, which is a little crazy with regular expressions and filter_input , to determine if user input is valid - and if so - apply it to the appropriate type.
This makes checking the rating against the selected scale (by type and comparison of values) much more uniform. I have sensibly tested this a bit and I think I'm happier with this approach;)
Once again ... Assuming an HTML form, for example:
<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 for user login:
<?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']); } ?>
Maybe it's worth explaining what the hell is going on with regular expressions and callbacks;)
For example, take the following regular expression:
/^(?<char>A\+?)$|^(?<digit>100|10|6|5|4)$/'
This regular expression defines two named subpatterns. One, called <char> , captures A and A+ ; another called <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 this case, since this means that the user input (or scale) of POST ed was not valid.
Otherwise, the $match array will contain any committed values ββusing char and (optionally) digit as keys. If the digit key exists, we know that the match is a digit, and we can pass it to a float and return it. Otherwise, we had to map to char , so we can strtoupper() this value and return it:
return isset($matches['digit']) ? (float) $matches['digit'] : strtoupper($matches['char']);
Both callbacks are identical (except for the regular expressions themselves), so you can create the called code and possibly save some duplication.
Hopefully at this point it won't start to seem a bit confusing! Hope this helps :)