4

Trying to do server side validation (PHP), where there is the HTM5 number input:

<input ... min="1" max="20" step="0.1" />

The browser will allow you to enter a value such as "10.5", but how should this be double checked in PHP? for those browsers which won't do the validation (and the fact you shouldn't trust data from the browser).

if (fmod(floatval($value), 0.1) == 0) {
    // valid
}

This does not work, as fmod() in this case returns "0.099999...", as per:

Why different results of 0.5 mod 0.1 in different programming languages?

You could multiply the $value by 10, and use the modulus check of 1 (rather than 0.1), so your doing integer maths... but what happens if the step was 0.01, or 0.003?

Community
  • 1
  • 1
Craig Francis
  • 1,855
  • 3
  • 22
  • 35
  • You could round that final number to `-log(step)` decimal places. Or check `$value == round($value, -log($step)` – Blender Oct 15 '12 at 15:58
  • Take a look at `number_format` http://php.net/manual/en/function.number-format.php might be what you need. – Erik Oct 15 '12 at 16:00
  • what are you trying to validate? – raidenace Oct 15 '12 at 16:00
  • @Erik, not sure number_format is what I'm after, as thats for formatting the output (see next comment). – Craig Francis Oct 15 '12 at 16:03
  • @Raidenace I'm trying to validate the value that has been typed in by the end user, and while in this case I'm allowing the value to have 1 decimal place, I want the code to be fairly generic, so the step value could be anything (e.g. 0.001). – Craig Francis Oct 15 '12 at 16:04
  • @Blender I'm not really sure what you mean... log(0.1) is 2.30258 (where its defaulting to using the base M_E)... and I must admit, I'm not sure I know the function which returns the number of decimal places being used by a number (I'm pretty sure you shouldn't use strpos/strlen). – Craig Francis Oct 15 '12 at 16:10
  • @CraigFrancis: Use log base 10. – Blender Oct 15 '12 at 16:11
  • @CraigFrancis The user input will be supplied as a string. Validating as a string will not be subject to floating point weirdness. I may get shot for suggesting this, but... regex? – DaveRandom Oct 15 '12 at 16:20
  • @DaveRandom: I think OP wants to validate that the entered value is divisible by the value of `step`. Not sure if regex can do that..for that matter, not sure if that is what the OP wants to validate either..:) – raidenace Oct 15 '12 at 16:22
  • @Raidenace Basically I'm trying to replicate the validation in the browser, but on the server :-) – Craig Francis Oct 15 '12 at 16:23
  • @CraigFrancis: Am I mistaken, browsers only do required field validation, right? On a side note, do you think `is_float($value)` will work for you? – raidenace Oct 15 '12 at 16:33
  • @Raidenace, this is more for the HTML5 stuff, so recent versions of Google Chrome / Firefox etc will do this validation for you, but obviously there are older browsers that won't... and unfortunately is_float() won't work either... below Jakub has given me an idea, where I have commented (and would like a second pair of eyes to check). – Craig Francis Oct 15 '12 at 16:47
  • @CraigFrancis Well if BCMath is available you can do [this](http://codepad.viper-7.com/UNZqlH) – DaveRandom Oct 15 '12 at 16:50
  • @Blender, thanks for your input, I have started some work on your idea, where I was got to `$dividend = ($value * pow(10, $decimal_places));` and `$divisor = ($step * pow(10, $decimal_places));` ... but $decimal_places isn't a simple case of log base 10, as the step could be 0.2... below Jacub has given me an idea. – Craig Francis Oct 15 '12 at 16:51

1 Answers1

2

If the number will be stored as a float underneath the hood, it will always get rounded to the nearest representable number.

You can either accept the value as float, and simply do the check like this:

// get the nearest multiple of $step that is representable in float
$normalized = round($number, $allowed_digits); // for steps like '0.1', '0.01' only
$normalized = round($number / $step) * $step;  // works for any arbitrary step, like 0.2 or 0.3

// verify if they are not too different, where EPSILON is a very small constant
// we cannot directly == as the calculations can introduce minuscule errors
if (abs($normalized - $number) > EPSILON)
   die("Did not validate.");

Or, you can simply treat the value from the client-side as a string, and verify the number of digits used in the string (and convert to float later). You should do this if you want to be 100% sure that the user entered something like 0.01, not 0.099999999999 (which would get rounded to the same thing as 0.01).

Jakub Wasilewski
  • 2,916
  • 22
  • 27
  • Thanks @Jakub... just to check I've got this right, for validation purposes, the condition should be something like `if ($value != (round($value / $step) * $step)) { // Invalid }` – Craig Francis Oct 15 '12 at 16:45
  • Nope. Any calculation on floating point numbers can introduce small errors, which will make the values not strictly equal. That's why you check they *do not differ by more than* a very small value (the if in my code sample does that, where EPSILON can be replaced by something like 0.000001. – Jakub Wasilewski Oct 15 '12 at 16:48
  • Also, see the edit at the end of the answer - if you want to validate what the input was, you have to validate the input, not its conversion to float. – Jakub Wasilewski Oct 15 '12 at 16:50
  • 1
    aka `if (abs((round($value / $step) * $step) - $value) > 0.00001)` once I had woken up a bit more... thanks for checking Jakub. – Craig Francis Oct 15 '12 at 17:10