Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Converting user submitted value into local value

Tags:

algorithm

php

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:

rating

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...

like image 432
Henrik Petterson Avatar asked Aug 22 '15 17:08

Henrik Petterson


1 Answers

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) POSTed 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 :)

like image 134
Darragh Enright Avatar answered Sep 30 '22 11:09

Darragh Enright