Converting float decimal to fraction

15,751

Solution 1

Continued fractions can be used to find rational approximations to real numbers that are "best" in a strict sense. Here's a PHP function that finds a rational approximation to a given (positive) floating point number with a relative error less than $tolerance:

<?php
function float2rat($n, $tolerance = 1.e-6) {
    $h1=1; $h2=0;
    $k1=0; $k2=1;
    $b = 1/$n;
    do {
        $b = 1/$b;
        $a = floor($b);
        $aux = $h1; $h1 = $a*$h1+$h2; $h2 = $aux;
        $aux = $k1; $k1 = $a*$k1+$k2; $k2 = $aux;
        $b = $b-$a;
    } while (abs($n-$h1/$k1) > $n*$tolerance);

    return "$h1/$k1";
}

printf("%s\n", float2rat(66.66667)); # 200/3
printf("%s\n", float2rat(sqrt(2)));  # 1393/985
printf("%s\n", float2rat(0.43212));  # 748/1731

I have written more about this algorithm and why it works, and even a JavaScript demo here: https://web.archive.org/web/20180731235708/http://jonisalonen.com/2012/converting-decimal-numbers-to-ratios/

Solution 2

Converted Python code in answer from @APerson241 to PHP

<?php
function farey($v, $lim) {
    // No error checking on args.  lim = maximum denominator.
    // Results are array(numerator, denominator); array(1, 0) is 'infinity'.
    if($v < 0) {
        list($n, $d) = farey(-$v, $lim);
        return array(-$n, $d);
    }
    $z = $lim - $lim;   // Get a "zero of the right type" for the denominator
    list($lower, $upper) = array(array($z, $z+1), array($z+1, $z));
    while(true) {
        $mediant = array(($lower[0] + $upper[0]), ($lower[1] + $upper[1]));
        if($v * $mediant[1] > $mediant[0]) {
            if($lim < $mediant[1]) 
                return $upper;
            $lower = $mediant;
        }
        else if($v * $mediant[1] == $mediant[0]) {
            if($lim >= $mediant[1])
                return $mediant;
            if($lower[1] < $upper[1])
                return $lower;
            return $upper;
        }
        else {
            if($lim < $mediant[1])
                return $lower;
            $upper = $mediant;
        }
    }
}

// Example use:
$f = farey(66.66667, 10);
echo $f[0], '/', $f[1], "\n"; # 200/3
$f = farey(sqrt(2), 1000);
echo $f[0], '/', $f[1], "\n";  # 1393/985
$f = farey(0.43212, 2000);
echo $f[0], '/', $f[1], "\n";  # 748/1731

Solution 3

Farey fractions can be quite useful in this case.

They can be used to convert any decimal into a fraction with the lowest possible denominator.

Sorry - I don't have a prototype in PHP, so here's one in Python:

def farey(v, lim):
    """No error checking on args.  lim = maximum denominator.
        Results are (numerator, denominator); (1, 0) is 'infinity'."""
    if v < 0:
        n, d = farey(-v, lim)
        return (-n, d)
    z = lim - lim   # Get a "zero of the right type" for the denominator
    lower, upper = (z, z+1), (z+1, z)
    while True:
        mediant = (lower[0] + upper[0]), (lower[1] + upper[1])
        if v * mediant[1] > mediant[0]:
            if lim < mediant[1]:
                return upper
            lower = mediant
        elif v * mediant[1] == mediant[0]:
            if lim >= mediant[1]:
                return mediant
            if lower[1] < upper[1]:
                return lower
            return upper
        else:
            if lim < mediant[1]:
                return lower
            upper = mediant

Solution 4

Based upon @Joni's answer, here is what I used to pull out the whole number.

function convert_decimal_to_fraction($decimal){

    $big_fraction = float2rat($decimal);
    $num_array = explode('/', $big_fraction);
    $numerator = $num_array[0];
    $denominator = $num_array[1];
    $whole_number = floor( $numerator / $denominator );
    $numerator = $numerator % $denominator;

    if($numerator == 0){
        return $whole_number;
    }else if ($whole_number == 0){
        return $numerator . '/' . $denominator;
    }else{
        return $whole_number . ' ' . $numerator . '/' . $denominator;
    }
}

function float2rat($n, $tolerance = 1.e-6) {
    $h1=1; $h2=0;
    $k1=0; $k2=1;
    $b = 1/$n;
    do {
        $b = 1/$b;
        $a = floor($b);
        $aux = $h1; $h1 = $a*$h1+$h2; $h2 = $aux;
        $aux = $k1; $k1 = $a*$k1+$k2; $k2 = $aux;
        $b = $b-$a;
    } while (abs($n-$h1/$k1) > $n*$tolerance);

    return "$h1/$k1";
}

Solution 5

Based on @APerson's and @Jeff Monteiro's answers I've created PHP version of Farey fractions that will be simplified to whole values with fractions with lowest possible denominator:

<?php

class QuantityTransform
{
    /**
     * @see https://stackoverflow.com/questions/14330713/converting-float-decimal-to-fraction
     */
    public static function decimalToFraction(float $decimal, $glue = ' ', int $limes = 10): string
    {
        if (null === $decimal || $decimal < 0.001) {
            return '';
        }

        $wholeNumber = (int) floor($decimal);
        $remainingDecimal = $decimal - $wholeNumber;

        [$numerator, $denominator] = self::fareyFraction($remainingDecimal, $limes);

        // Values rounded to 1 should be added to base value and returned without fraction part
        if (is_int($simplifiedFraction = $numerator / $denominator)) {
            $wholeNumber += $simplifiedFraction;
            $numerator = 0;
        }

        return (0 === $wholeNumber && 0 === $numerator)
            // Too small values will be returned in original format
            ? (string) $decimal
            // Otherwise let's format value - only non-0 whole value / fractions will be returned
            : trim(sprintf(
                '%s%s%s',
                (string) $wholeNumber ?: '',
                $wholeNumber > 0 ? $glue : '',
                0 === $numerator ? '' : ($numerator . '/' . $denominator)
            ));
    }

    /**
     * @see https://stackoverflow.com/a/14330799/842480
     *
     * @return int[] Numerator and Denominator values
     */
    private static function fareyFraction(float $value, int $limes): array
    {
        if ($value < 0) {
            [$numerator, $denominator] = self::fareyFraction(-$value, $limes);

            return [-$numerator, $denominator];
        }

        $zero = $limes - $limes;
        $lower = [$zero, $zero + 1];
        $upper = [$zero + 1, $zero];

        while (true) {
            $mediant = [$lower[0] + $upper[0], $lower[1] + $upper[1]];

            if ($value * $mediant[1] > $mediant[0]) {
                if ($limes < $mediant[1]) {
                    return $upper;
                }
                $lower = $mediant;
            } elseif ($value * $mediant[1] === $mediant[0]) {
                if ($limes >= $mediant[1]) {
                    return $mediant;
                }
                if ($lower[1] < $upper[1]) {
                    return $lower;
                }

                return $upper;
            } else {
                if ($limes < $mediant[1]) {
                    return $lower;
                }

                $upper = $mediant;
            }
        }
    }
}

Then you san use it like:

QuantityTransform::decimalToFraction(0.06); // 0.06
QuantityTransform::decimalToFraction(0.75); // 3/4
QuantityTransform::decimalToFraction(1.75, ' and '); // 1 and 3/4
QuantityTransform::decimalToFraction(2.33, ' and '); // 2 and 1/3
QuantityTransform::decimalToFraction(2.58, ' ', 5); // 2 3/5
QuantityTransform::decimalToFraction(2.58, ' & ', 10); // 2 & 4/7
QuantityTransform::decimalToFraction(1.97); // 2
Share:
15,751
Joe Shamuraq
Author by

Joe Shamuraq

A newbie in iOS... From basic php environment

Updated on June 15, 2022

Comments

  • Joe Shamuraq
    Joe Shamuraq almost 2 years

    I am trying to convert calculations keyed in by users with decimal results into fractions. For e.g.; 66.6666666667 into 66 2/3. Any pointers? Thanx in advance

  • Joe Shamuraq
    Joe Shamuraq over 11 years
    Thanx for the kind gesture but am really lost trying to convert that to PHP... Thanx again tho... If anyone can assist this python structure into PHP?
  • APerson
    APerson over 11 years
    The Python code uses tuples as a way to shorten the code greatly. lower and upper in the code can be treated as just arrays with size 2, which both can be "unpacked" into 2 variables. Most of the code is comparing the items in lower and upper and setting them equal to one another, so that should be relatively easy to convert into PHP.
  • adrian7
    adrian7 about 11 years
    There's an small bug in the function, in case $b == $a will throw a warning, "Division by 0."
  • Joni
    Joni almost 11 years
    Well spotted @adrian7, I didn't know this aspect of PHP -- in other languages floating point division by 0 is well behaved. In case $b == $a the loop is in its last iteration, so moving the division from the end of the loop to the beginning fixes the problem. I've updated the code.
  • ZioBit
    ZioBit almost 6 years
    Since it was trivial, I took the freedom to add the handling of negative numbers. I hope it's something that can be done per SO rules :|
  • Wirone
    Wirone about 3 years
    Thank you @APerson for this, I was able to improve measures in our recipes using this approach. I've added my answer here → stackoverflow.com/a/67088369/842480
  • smichr
    smichr almost 3 years
    There is a logical error in assuming which is closer to v when the limit for the denominator is exceeded. It selects, for example, 17/59 instead of 19/66 for 0.288 when the maximum denominator is specified as 113.
  • wayzz
    wayzz about 2 years
    this worked like a charm. thank you for sharing.