I'm trying to convert from decimal to fraction sporting odds. I have found via a search a PHP function which works well, but certain decimals cause problems, such as 2.1 which maxs out the server:
function dec2frac($dec) {
$decBase = --$dec;
$div = 1;
do {
$div++;
$dec = $decBase * $div;
} while (intval($dec) != $dec);
if ($dec % $div == 0) {
$dec = $dec / $div;
$div = $div / $div;
}
return $dec.'/'.$div;
}
$decimal = 2.3;
echo $decimal.' --> '.dec2frac($decimal);
A decimal odds of 6 should give 5/1. This is calulated as 6-1=5 = 5/1
I have found that decimal input of 2.2 and 2.3 trips the function up but other values seem to be ok. What is causing this anomaly, is there way around it?
Thanks.
This problem consists of two seperate steps
Create a fraction from a decimal number
Convert between betting odds and fractions
Let's start with the latter: A betting odd of 5 means, for every $1 invested, you get $5 if you win. Since you invested $1, your actual win is just $4. So the odds are 4-1 or 4/1
Analogous, betting odds of 2.5 mean, for every $1 you invest, you win $1.5, giving you 1.5-1 or 3-2 or 3/2
This leads us to the conclusion, that what we need is the fraction of ($odds-1)
Next part: Fractionizing. I didn't analyze the given algorithm, but wrote a very bad (but easily readable) one:
function dec2frac($val) {
//first pump denominator up
$tmp=strstr("$val",'.');
if ($tmp) $tmp=strlen($tmp)-1;
else $tmp=0;
$n=$val;
$d=1;
for (;$tmp>0;$tmp--) {
$n*=10;
$d*=10;
}
$n=intval(round($n));
$d=intval(round($d));
//Now shorten the fraction
//Find limit for pseudoprime search
$min=$n;
if ($d<$n) $min=$d;
$min=ceil($min/2);
if (ceil($d/2)>$min) $min=ceil($d/2);
if (ceil($n/2)>$min) $min=ceil($n/2);
$pseudoprime=2;
while ($pseudoprime<=$min) {
//Shorten by current pseudoprime as long as possible
while (true) {
$nn=$n/$pseudoprime;
if ($nn!=round($nn)) break;
$dd=$d/$pseudoprime;
if ($dd!=round($dd)) break;
$n=intval($nn);
$d=intval($dd);
}
//Move on to next pseudoprime
$pseudoprime+=($pseudoprime==2)?1:2;
if ($pseudoprime>3)
if (($pseudoprime/3)==floor($pseudoprime/3)) $pseudoprime+=2;
}
return "$n/$d";
}
This was tested to work with the values 0.25, 2.5, 3.1, 3.14, 3.141, 3.1415, 3.14159 and 3.141592.
The very unoptimized nature of the algorithm is a less important limit, as betting odds tend to have not very many decimal digits.
Together with
function odds2fract($odds) {
return dec2frac($odds-1);
}
derived from the other step, we get successfull conversion of
5 --> 4/1
2.5 --> 3/2
2.1 --> 11/10
2.2 --> 6/5
Edit
The original version had a bug in the search limit calculation, which led to some fractions (e.g. completeley shortenable) failed to be shortened. The updated version fixes this.
Edit 2
Again a fixed bug: Failing to round() the values obtained in the first step before intval()ing them gave wrong results on fractions, that have a very bad fidelity in floatingpoint. Fixed by applying the missing round()
The first thing that just immediately triggers is that you used '.1' in there. This suggests, to me, that we're dealing with floating point problems... check your code... yeah, looks like floating point problems to me. The problem here is that your numbers are stored in binary, and there's no convenient way for binary to use a lot of decimal values. When you start using simple math, you're not getting exact results, you're getting shortcut estimations.
Floating Point numbers in PHP
That link should give you a detailed run-down on what's going wrong, links to more pages that will give you still more information, and links to libraries that exist to fix it.
Related
I'm working on a system where I need to round down to the nearest penny financial payments. Naively I thought I would multiply up by 100, take the floor and then divide back down. However the following example is misbehaving:
echo 1298.34*100;
correctly shows:
129834
but
echo floor(1298.34*100);
unexpectedly shows:
129833
I get the same problem using intval for example.
I suspect the multiplication is falling foul of floating point rounding. But if I can't rely on multiplication, how can I do this? I always want to round down reliably, and I don't need to take negative amounts into consideration.
To be clear, I want any fractional penny amounts to be stripped off:
1298.345 should give 1298.34
1298.349 should give 1298.34
1298.342 should give 1298.34
Since you mention you only use this for displaying purposes, you could take the amount, turn it into a string and truncate anything past the second decimal. A regular expression could do the job:
preg_match('/\d+\.{0,1}\d{0,2}/', (string) $amount, $matches);
This expression works with any number of decimals (including zero). How it works in detail:
\d+ matches any number of digits
\.{0,1} matches 0 or 1 literal dot
\d{0,2} matches zero or two digits after the dot
You can run the following code to test it:
$amounts = [
1298,
1298.3,
1298.34,
1298.341,
1298.349279745,
];
foreach ($amounts as $amount) {
preg_match('/\d+\.{0,1}\d{0,2}/', (string) $amount, $matches);
var_dump($matches[0]);
}
Also available as a live test in this fiddle.
You can use round() to round to the required precision, and with the expected behavior when rounding the final 5 (which is another financial hurdle you might encounter).
$display = round(3895.0 / 3.0, 2);
Also, as a reminder, I have the habit of always writing floating point integers with a final dot or a ".0". This prevents some languages from inferring the wrong type and doing, say, integer division, so that 5 / 3 will yield 1.
If you need a "custom rounding" and want to be sure, well, the reason it didn't work is because not all floating point numbers exist in machine representation. 1298.34 does not exist; what does exist (I'm making the precise numbers up!) in its place might be 1298.33999999999999124.
So when you multiply it by 100 and get 129833.999999999999124, of course truncating it will yield 129833.
What you need to do then is to add a small quantity that must be enough to cover the machine error but not enough to matter in the financial calculation. There is an algorithm to determine this quantity, but you can probably get away with "one thousandth after upscaling".
So:
$display = floor((3895.0 / 3.0)*100.0 + 0.001);
Please be aware that this number, which you will "see" as 1234.56, might again not exist precisely. It might really be 1234.5600000000000123 or 1234.559999999999876. This might have consequences in complex, composite calculations.
Since You're working with financial, You should use some kind of Money library (https://github.com/moneyphp/money). Almost all other solutions are asking for trouble.
Other ways, which I don't recommend, are: a) use integers only, b) calculate with bcmath or c) use Number class from the Money library e.g.:
function getMoneyValue($value): string
{
if (!is_numeric($value)) {
throw new \RuntimeException(sprintf('Money value has to be a numeric value, "%s" given', is_object($value) ? get_class($value) : gettype($value)));
}
$number = \Money\Number::fromNumber($value)->base10(-2);
return $number->getIntegerPart();
}
he other function available is round(), which takes two parameters -
the number to round, and the number of decimal places to round to. If
a number is exactly half way between two integers, round() will always
round up.
use round :
echo round (1298.34*100);
result :
129834
The usual advice to handle money and other decimal numbers where accuracy is crucial is to either use integers or strings (plus arbitrary precision libraries) and it makes sense if you understand how floating point maths work. However, I don't have at hand any specific example to illustrate this, as every wrong calculation I've spot in the wild was due to some other mistake: naive comparisons using ==, lack of proper rounding when displaying results, blatantly wrong logic (e.g. calculating taxes with an inconsistent algorithm that also doesn't work on paper)... I've done some research and results either only apply to C/C++ (float/double having different precision) or were mere elaborations on why you can't trust two floats to be equal.
Can you share a self-contained PHP code snippet with carefully selected floating point figures and a correct algorithm that renders an incorrect result explicitly caused by floating point limitations?
Disclaimer: I don't intend to argue, refute or debunk anything, I honestly need an example for my toolbelt.
Things can break fairly easy with much less than a billion iterations. The thing is, by using floats and arithmetic you can very easily find yourself with unexpected results, and even if numbers superficially look fine, the subtle imprecisions can lead to an application bugging out.
Let's try a variation of the example in your answer:
$total = 0.0;
for ($i = 0; $i < 10; $i++) {
$total += 0.1;
}
echo "added ten cents, ten times\n";
// since we added 0.1 € x 10 times, we now have 1€ in total, right?
if ($total == 1) {
echo "I have 1€. All is good in the realm.";
}
else {
echo "WTF? Where is my money? I only have {$total}€!!!!\n";
echo "\$total holds: ";
var_dump($total);
}
The output for the above is:
added ten cents
WTF? Where is my money? I only have 1€!!!!
$total holds: float(1)
Even if $total appears to be float(1), the code follow the 'wrong' branch of execution, breaking our application.
If we execute the same code in PHP8 (beta so far), you'll an easier to understand result:
added ten cents
WTF? Where is my money? I only have 1€!!!!
$total holds: float(0.9999999999999999)
Another simple example:
$balance = 50.03;
$debit = 45.42;
$expected_balance = 4.61;
$real_balance = $balance - $debit;
if ($real_balance !== $expected_balance) {
echo "problems: ";
var_dump($real_balance);
}
The output for the above is:
balance mismatch: float(4.61)
or, in PHP8:
balance mismatch: float(4.609999999999999)
Either of the above examples show that practically, using floating numbers to do (specifically) money arithmetic can be problematic. Since the results no longer match your expectations, not only it can lead to plainly wrong results, but the subtle different results can make the whole application behaving in unexpected ways.
Examples and results, here.
echo floor((0.1 + 0.7) * 10);
Expected result: 0.1 + 0.7 = 0.8; 0.8 * 10 = 8;
Result: 7
Tested on PHP 7.2.12
The question makes little sense because that isn't how floating point errors work.
Inaccuracies are tiny. They happen in remote decimals and they're only noticeable when you require very high precision levels. After all, IEEE 754 powers a vast majority of computer systems and it offers an excellent precision. To put it in context, 0.1 kilometres expressed as float is 0.100000001490116119384765625, what makes accurate up to 1/10 of a µm
if I didn't get maths wrong.
There probably isn't a set of carefully chosen figures and a real-life calculation you'd be expected to use PHP for (an invoice, a stock exchange index...) that renders incorrect results no matter how careful you are with precision levels. Because that's not the problem.
The problem with floating point maths is that it forces you to be extremely careful on every step and it makes it very easy for bugs to slip in.
For applications where accuracy matters, you can write correct software using floats, but it won't be as easy, maintainable or robust.
Original answer:
This is the best I've got so far (thanks to chtz for the hint):
// Set-up and display settings (shouldn't affect internal calculations or final result)
set_time_limit(0);
ini_set('precision', -1);
// Expected accuracy: 2 decimal positions
$total = 0;
for ($i = 0; $i < 1e9; $i++) {
$total += 0.01;
// It's important to NOT round inside the loop, e.g.: $total = round($total + 0.01, 2);
}
var_dump($total, number_format($total, 2));
float(9999999.825158669)
string(12) "9,999,999.83" // Correct value would be "10,000,000.00"
Unfortunately, it relies on the accumulation of a very large number of precision errors (it needs around 1,000,000,000 of them to happen and it needs more than 4 minutes to run in my PC), so it isn't as real-life as I would have liked, but it certainly illustrates the underlying issue.
What I'm trying to do isn't exactly a Gaussian distribution, since it has a finite minimum and maximum. The idea is closer to rolling X dice and counting the total.
I currently have the following function:
function bellcurve($min=0,$max=100,$entropy=-1) {
$sum = 0;
if( $entropy < 0) $entropy = ($max-$min)/15;
for($i=0; $i<$entropy; $i++) $sum += rand(0,15);
return floor($sum/(15*$entropy)*($max-$min)+$min);
}
The idea behind the $entropy variable is to try and roll enough dice to get a more even distribution of fractional results (so that flooring it won't cause problems).
It doesn't need to be a perfect RNG, it's just for a game feature and nothing like gambling or cryptography.
However, I ran a test over 65,536 iterations of bellcurve() with no arguments, and the following graph emerged:
(source: adamhaskell.net)
As you can see, there are a couple of values that are "offset", and drastically so. While overall it doesn't really affect that much (at worst it's offset by 2, and ignoring that the probability is still more or less where I want it), I'm just wondering where I went wrong.
Any additional advice on this function would be appreciated too.
UPDATE: I fixed the problem above just by using round instead of floor, but I'm still having trouble getting a good function for this. I've tried pretty much every function I can think of, including gaussian, exponential, logistic, and so on, but to no avail. The only method that has worked so far is this approximation of rolling dice, which is almost certainly not what I need...
If you are looking for a bell curve distribution, generate multiple random numbers and add them together. If you are looking for more modifiers, simply multiply them to the end result.
Generate a random bell curve number, with a bonus of 50% - 150%.
Sum(rand(0,15), rand(0,15) , rand(0,15))*(rand(2,6)/2)
Though if you're concerned about rand not providing random enough numbers you can use mt_rand which will have a much better distribution (uses mersenne twister)
The main issue turned out to be that I was trying to generate a continuous bell curve based on a discrete variable. That's what caused holes and offsets when scaling the result.
The fix I used for this was: +rand(0,1000000)/1000000 - it essentially takes the whole number discrete variable and adds a random fraction to it, more or less making it continuous.
The function is now:
function bellcurve() {
$sum = 0;
$entropy = 6;
for($i=0; $i<$entropy; $i++) $sum += rand(0,15);
return ($sum+rand(0,1000000)/1000000)/(15*$entropy);
}
It returns a float between 0 and 1 inclusive (although those exact values are extremely unlikely), which can then be scaled and rounded as needed.
Example usage:
$damage *= bellcurve()-0.5; // adjusts $damage by a random amount
// between 50% and 150%, weighted in favour of 100%
I've been wrestling with PHP's ceil() function giving me slightly wrong results - consider the following:
$num = 2.7*3; //float(8.1)
$num*=10; //float(81)
$num = ceil($num); //82, but shouldn't this be 81??
$num/=10; //float(8.2)
I have a number which may have any number of decimal places, and I need it rounded up to one decimal place.
i.e 8.1 should be 8.1, 8.154 should be 8.2, and 8 should be left as 8.
How I've been getting there is to take the number, multiply by 10, ceil() it, then divide by ten but as you can see I'm getting an extra .1 added in some circumstances.
Can anyone tell my why this is happening, and how to fix it?
Any help greatly appreciated
EDIT: had +=10 instead of *=10 :S
EDIT 2:
I didn't explicitly mention this but I need the decimal to ALWAYS round UP, never down - this answer is closest so far:
rtrim(rtrim(sprintf('%.1f', $num), '0'), '.');
However rounds 3.84 down to 3.8 when I need 3.9.
Sorry this wasn't clearer :(
Final Edit:
What I ended up doing was this:
$num = 2.7*3; //float(8.1)
$num*=10; //float(81)
$num = ceil(round($num, 2)); //81 :)
$num/=10; //float(8.1)
Which works :)
This is more than likely due to floating point error.
http://support.microsoft.com/kb/42980
http://download.oracle.com/docs/cd/E19957-01/806-3568/ncg_goldberg.html
http://joshblog.net/2007/01/30/flash-floating-point-number-errors/
http://en.wikipedia.org/wiki/Floating_point
You may have luck trying this procedure instead.
<?php
$num = 2.7*3;
echo rtrim(rtrim(sprintf('%.1f', $num), '0'), '.');
Floats can be a fickle thing. Not all real numbers can be properly represented in a finite number of binary bits.
As it turns out, a decimal section of 0.7 is one of those numbers (comes out 0.10 with an infinity repeating "1100" after it). You end up with a number that's ever so slightly above 0.7, so when you multiply by 10, you have a one's digit slightly above 7.
What you can do is make a sanity check. Take you float digit and subtract it's integer form. If the resulting value is less than, say, 0.0001, consider it to be an internal rounding error and leave it as-is. If the result is greater than 0.0001, apply ceil() normally.
Edit: A fun example you can do if you're on windows to show this is to open up the built in calculator application. Put in "4" then apply a square root function (with x^y where y=0.5). You'll see it properly displays "2". Now, subtract 2 from it and you'll see that you don't have 0 as a result. This is caused by internal rounding errors when it attempted to compute the square root of 4. When displaying the number 2 earlier, it knew that those very distant trailing digits were probably a rounding error, but when those are all that's left, it gets a bit confused.
(Before anybody gets onto me about this, I understand that this is oversimplified, but nonetheless I consider it a decent example.)
Convert your number to a string and ceil the string.
function roundUp($number, $decimalPlaces){
$multi = pow(10, $decimalPlaces);
$nrAsStr = ($number * $multi) . "";
return ceil($nrAsStr) / $multi;
}
The problem is that floating point numbers are RARELY what you expect them to be. Your 2.7*3 is probably coming out to be something like 81.0000000000000000001, which ceil()'s up to 82. For this sort of thing, you'll have to wrap your ceil/round/floor calls with some precision checks, to handle those extra microscopic differences.
Use %f instead of %.1f.
echo rtrim(rtrim(sprintf('%f', $num), '0'), '.');
Why not try this:
$num = 2.7*3;
$num *= 100;
$num = floor($num);
$num /= 10;
$num = ceil($num);
$num /= 10;
In PHP I have a 64 bit number which represents tasks that must be completed. A second 64 bit number represents the tasks which have been completed:
$pack_code = 1001111100100000000000000011111101001111100100000000000000011111
$veri_code = 0000000000000000000000000001110000000000000000000000000000111110
I need to compare the two and provide a percentage of tasks completed figure. I could loop through both and find how many bits are set, but I don't know if this is the fastest way?
Assuming that these are actually strings, perhaps something like:
$pack_code = '1001111100100000000000000011111101001111100100000000000000011111';
$veri_code = '0000000000000000000000000001110000000000000000000000000000111110';
$matches = array_intersect_assoc(str_split($pack_code),str_split($veri_code));
$finished_matches = array_intersect($matches,array(1));
$percentage = (count($finished_matches) / 64) * 100
Because you're getting the numbers as hex strings instead of ones and zeros, you'll need to do a bit of extra work.
PHP does not reliably support numbers over 32 bits as integers. 64-bit support requires being compiled and running on a 64-bit machine. This means that attempts to represent a 64-bit integer may fail depending on your environment. For this reason, it will be important to ensure that PHP only ever deals with these numbers as strings. This won't be hard, as hex strings coming out of the database will be, well, strings, not ints.
There are a few options here. The first would be using the GMP extension's gmp_xor function, which performs a bitwise-XOR operation on two numbers. The resulting number will have bits turned on when the two numbers have opposing bits in that location, and off when the two numbers have identical bits in that location. Then it's just a matter of counting the bits to get the remaining task count.
Another option would be transforming the number-as-a-string into a string of ones and zeros, as you've represented in your question. If you have GMP, you can use gmp_init to read it as a base-16 number, and use gmp_strval to return it as a base-2 number.
If you don't have GMP, this function provided in another answer (scroll to "Step 2") can accurately transform a string-as-number into anything between base-2 and 36. It will be slower than using GMP.
In both of these cases, you'd end up with a string of ones and zeros and can use code like that posted by #Mark Baker to get the difference.
Optimization in this case is not worth of considering. I'm 100% sure that you don't really care whether your scrip will be generated 0.00000014 sec. faster, am I right?
Just loop through each bit of that number, compare it with another and you're done.
Remember words of Donald Knuth:
We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.
This code utilizes the GNU Multi Precision library, which is supported by PHP, and since it is implemented in C, should be fast enough, and supports arbitrary precision.
$pack_code = gmp_init("1001111100100000000000000011111101001111100100000000000000011111", 2);
$veri_code = gmp_init("0000000000000000000000000001110000000000000000000000000000111110", 2);
$number_of_different_bits = gmp_popcount(gmp_xor($pack_code, $veri_code));
$a = 11111;
echo sprintf('%032b',$a)."\n";
$b = 12345;
echo sprintf('%032b',$b)."\n";
$c = $a & $b;
echo sprintf('%032b',$c)."\n";
$n=0;
while($c)
{
$n += $c & 1;
$c = $c >> 1;
}
echo $n."\n";
Output:
00000000000000000010101101100111
00000000000000000011000000111001
00000000000000000010000000100001
3
Given your PHP-setuo can handle 64bit, this can be easily extended.
If not you can sidestep this restriction using GNU Multiple Precision
You could also split up the HEx-Representation and then operate on those coresponding parts parts instead. As you need just the local fact of 1 or 0 and not which number actually is represented! I think that would solve your problem best.
For example:
0xF1A35C and 0xD546C1
you just compare the binary version of F and D, 1 and 5, A and 4, ...