如何将浮点数转换为人类可读的分数?

时间:2020-03-06 14:22:57  来源:igfitidea点击:

假设我们有0.33,我们需要输出" 1/3"。
如果我们有" 0.4",我们需要输出" 2/5"。

这样做的想法是使用户易于理解,以使用户理解" y之外的x个部分",作为理解数据的更好方法。

我知道百分比是一个很好的替代品,但我想知道是否有简单的方法可以做到这一点?

解决方案

我们可能想阅读每位计算机科学家应该了解的有关浮点算法的知识。

我们必须通过乘以较大的数字来指定一些精度:

3.141592 * 1000000 = 3141592

那么你可以做一个分数:

3 + (141592 / 1000000)

并通过GCD减少...

3 + (17699 / 125000)

但是无法消除预期的分数。我们可能希望始终在整个代码中使用分数-记住,在可以避免溢出的情况下,请减少分数!

以下是一个链接,解释了将小数转换为分数的数学原理:

http://www.webmath.com/dec2fract.html

以下是一个示例函数,说明如何使用VB实际执行此操作(来自www.freevbcode.com/ShowCode.asp?ID=582):

Public Function Dec2Frac(ByVal f As Double) As String

   Dim df As Double
   Dim lUpperPart As Long
   Dim lLowerPart As Long

   lUpperPart = 1
   lLowerPart = 1

   df = lUpperPart / lLowerPart
   While (df <> f)
      If (df < f) Then
         lUpperPart = lUpperPart + 1
      Else
         lLowerPart = lLowerPart + 1
         lUpperPart = f * lLowerPart
      End If
      df = lUpperPart / lLowerPart
   Wend
Dec2Frac = CStr(lUpperPart) & "/" & CStr(lLowerPart)
End Function

(从谷歌搜索:将十进制转换为分数,将十进制转换为分数代码)

我们将遇到两个基本问题,这些将使这变得困难:

1)浮点数不是精确的表示形式,这意味着如果我们有一个分数" x / y",结果为" z",那么分数算法可能会返回" x / y"以外的结果。

2)无穷大得多,无理数多于有理数。有理数是可以表示为分数的数。非理性的人不能。

但是,以一种廉价的方式,由于浮点数具有极限精度,因此我们始终可以将其表示为某种派系。 (我认为...)

问题的一部分在于,实际上并不容易将这么多的分数解释为分数。例如。 0.33不是1/3,而是33/100。但是,如果我们还记得小学培训,那么就有一个将十进制值转换为分数的过程,但是,由于大多数时候十进制数字不是存储在0.33中,而是存储在0.329999999999998或者类似的数字中,因此不太可能提供所需的内容。

帮自己一个忙,不要打扰,但是如果需要,可以执行以下操作:

将原始值乘以10,直到除去小数部分为止。保留该数字,并将其用作除数。然后通过寻找共同的分母来做一系列的简化。

所以0.4将是4/10. 然后,我们会寻找以低值(可能是质数)开头的常见除数。从2开始,通过检查除法底数是否与除法本身相同,可以看到2是否将分子和分母均分。

floor(5/2) = 2
5/2 = 2.5

因此5不能平均分配2. 因此,然后检查下一个数字,例如3. 执行此操作,直到达到或者小于较小数字的平方根为止。

完成之后,我们需要

我们必须确定我们愿意接受的错误级别。并非所有的十进制小数都会减少为一个简单的分数。我可能会选择一个易于除数的数字,例如60,并找出最接近该值的60分之几,然后简化分数。

"假设我们有0.33,我们需要输出" 1/3"。"

我们期望"解决方案"具有什么精度? 0.33不等于1/3. 我们如何识别"好"(易于阅读)的答案?

无论如何,可能的算法可能是:

如果希望以X / Y的形式找到最接近的分数,其中Y小于10,则可以遍历所有9个可能的Y,为每个Y计算X,然后选择最准确的一个。

我们可以按照以下步骤以任何一种编程语言来执行此操作:

  • 乘以10除以x,其中x是确保该数字没有小数位所需的10的幂。示例:将0.33乘以10 ^ 2 = 100,将其乘以33,然后将其除以33/100
  • 通过分解减少所得分数的分子和分母,直到不再可以从结果中获取整数。
  • 减少的分数应该是答案。

例子:
0.2
= 0.2 x 10 ^ 1/10 ^ 1
= 2/10
= 1/5

因此,可以将其解释为" 5分之1"

正如许多人所说,我们真的不能将浮点转换回分数(除非其非常精确,例如.25)。当然,我们可以为大量的分数创建某种类型的查找,并使用某种模糊逻辑来生成所需的结果。同样,这不是精确的,我们需要定义我们希望分母走多大的下限。

.32 <x <.34 = 1/3或者类似的值。

我发现David Eppstein对给定的实数C代码的有理逼近恰好是我们要的。它基于连续分数理论,并且非常快速且相当紧凑。

我使用了针对特定分子和分母限制定制的版本。

/*
** find rational approximation to given real number
** David Eppstein / UC Irvine / 8 Aug 1993
**
** With corrections from Arno Formella, May 2008
**
** usage: a.out r d
**   r is real number to approx
**   d is the maximum denominator allowed
**
** based on the theory of continued fractions
** if x = a1 + 1/(a2 + 1/(a3 + 1/(a4 + ...)))
** then best approximation is found by truncating this series
** (with some adjustments in the last term).
**
** Note the fraction can be recovered as the first column of the matrix
**  ( a1 1 ) ( a2 1 ) ( a3 1 ) ...
**  ( 1  0 ) ( 1  0 ) ( 1  0 )
** Instead of keeping the sequence of continued fraction terms,
** we just keep the last partial product of these matrices.
*/

#include <stdio.h>

main(ac, av)
int ac;
char ** av;
{
    double atof();
    int atoi();
    void exit();

    long m[2][2];
    double x, startx;
    long maxden;
    long ai;

    /* read command line arguments */
    if (ac != 3) {
        fprintf(stderr, "usage: %s r d\n",av[0]);  // AF: argument missing
        exit(1);
    }
    startx = x = atof(av[1]);
    maxden = atoi(av[2]);

    /* initialize matrix */
    m[0][0] = m[1][1] = 1;
    m[0][1] = m[1][0] = 0;

    /* loop finding terms until denom gets too big */
    while (m[1][0] *  ( ai = (long)x ) + m[1][1] <= maxden) {
        long t;
        t = m[0][0] * ai + m[0][1];
        m[0][1] = m[0][0];
        m[0][0] = t;
        t = m[1][0] * ai + m[1][1];
        m[1][1] = m[1][0];
        m[1][0] = t;
        if(x==(double)ai) break;     // AF: division by zero
        x = 1/(x - (double) ai);
        if(x>(double)0x7FFFFFFF) break;  // AF: representation failure
    } 

    /* now remaining x is between 0 and 1/ai */
    /* approx as either 0 or 1/m where m is max that will fit in maxden */
    /* first try zero */
    printf("%ld/%ld, error = %e\n", m[0][0], m[1][0],
           startx - ((double) m[0][0] / (double) m[1][0]));

    /* now try other possibility */
    ai = (maxden - m[1][1]) / m[1][0];
    m[0][0] = m[0][0] * ai + m[0][1];
    m[1][0] = m[1][0] * ai + m[1][1];
    printf("%ld/%ld, error = %e\n", m[0][0], m[1][0],
           startx - ((double) m[0][0] / (double) m[1][0]));
}

如果输出是为了使读者快速了解结果的顺序,则返回" 113/211"之类的内容是没有意义的,因此输出应将自身限制为使用一位数字(可能是1 / 10和9/10)。如果是这样,我们可以观察到只有27个不同的分数。

由于生成输出的基础数学永远不会改变,因此解决方案可以是简单地对二进制搜索树进行硬编码,以便该函数最多执行log(27)〜= 4 3/4比较。这是经过测试的C版本代码

char *userTextForDouble(double d, char *rval)
{
    if (d == 0.0)
        return "0";

    // TODO: negative numbers:if (d < 0.0)...
    if (d >= 1.0)
        sprintf(rval, "%.0f ", floor(d));
    d = d-floor(d); // now only the fractional part is left

    if (d == 0.0)
        return rval;

    if( d < 0.47 )
    {
        if( d < 0.25 )
        {
            if( d < 0.16 )
            {
                if( d < 0.12 ) // Note: fixed from .13
                {
                    if( d < 0.11 )
                        strcat(rval, "1/10"); // .1
                    else
                        strcat(rval, "1/9"); // .1111....
                }
                else // d >= .12
                {
                    if( d < 0.14 )
                        strcat(rval, "1/8"); // .125
                    else
                        strcat(rval, "1/7"); // .1428...
                }
            }
            else // d >= .16
            {
                if( d < 0.19 )
                {
                    strcat(rval, "1/6"); // .1666...
                }
                else // d > .19
                {
                    if( d < 0.22 )
                        strcat(rval, "1/5"); // .2
                    else
                        strcat(rval, "2/9"); // .2222...
                }
            }
        }
        else // d >= .25
        {
            if( d < 0.37 ) // Note: fixed from .38
            {
                if( d < 0.28 ) // Note: fixed from .29
                {
                    strcat(rval, "1/4"); // .25
                }
                else // d >=.28
                {
                    if( d < 0.31 )
                        strcat(rval, "2/7"); // .2857...
                    else
                        strcat(rval, "1/3"); // .3333...
                }
            }
            else // d >= .37
            {
                if( d < 0.42 ) // Note: fixed from .43
                {
                    if( d < 0.40 )
                        strcat(rval, "3/8"); // .375
                    else
                        strcat(rval, "2/5"); // .4
                }
                else // d >= .42
                {
                    if( d < 0.44 )
                        strcat(rval, "3/7"); // .4285...
                    else
                        strcat(rval, "4/9"); // .4444...
                }
            }
        }
    }
    else
    {
        if( d < 0.71 )
        {
            if( d < 0.60 )
            {
                if( d < 0.55 ) // Note: fixed from .56
                {
                    strcat(rval, "1/2"); // .5
                }
                else // d >= .55
                {
                    if( d < 0.57 )
                        strcat(rval, "5/9"); // .5555...
                    else
                        strcat(rval, "4/7"); // .5714
                }
            }
            else // d >= .6
            {
                if( d < 0.62 ) // Note: Fixed from .63
                {
                    strcat(rval, "3/5"); // .6
                }
                else // d >= .62
                {
                    if( d < 0.66 )
                        strcat(rval, "5/8"); // .625
                    else
                        strcat(rval, "2/3"); // .6666...
                }
            }
        }
        else
        {
            if( d < 0.80 )
            {
                if( d < 0.74 )
                {
                    strcat(rval, "5/7"); // .7142...
                }
                else // d >= .74
                {
                    if(d < 0.77 ) // Note: fixed from .78
                        strcat(rval, "3/4"); // .75
                    else
                        strcat(rval, "7/9"); // .7777...
                }
            }
            else // d >= .8
            {
                if( d < 0.85 ) // Note: fixed from .86
                {
                    if( d < 0.83 )
                        strcat(rval, "4/5"); // .8
                    else
                        strcat(rval, "5/6"); // .8333...
                }
                else // d >= .85
                {
                    if( d < 0.87 ) // Note: fixed from .88
                    {
                        strcat(rval, "6/7"); // .8571
                    }
                    else // d >= .87
                    {
                        if( d < 0.88 ) // Note: fixed from .89
                        {
                            strcat(rval, "7/8"); // .875
                        }
                        else // d >= .88
                        {
                            if( d < 0.90 )
                                strcat(rval, "8/9"); // .8888...
                            else
                                strcat(rval, "9/10"); // .9
                        }
                    }
                }
            }
        }
    }

    return rval;
}

一种解决方案是首先将所有数字存储为有理数。有用于有理数算术(例如GMP)的库。如果使用OO语言,我们也许可以使用有理数类库来替换数类。

金融程序等将使用这种解决方案,以便能够进行精确的计算并保留使用普通浮点数可能会丢失的精度。

当然,它会慢很多,因此对我们来说可能不切实际。取决于我们需要执行多少计算,以及精度对我们而言有多重要。

a = rational(1);
b = rational(3);
c = a / b;

print (c.asFraction)  --->  "1/3"
print (c.asFloat) ----> "0.333333"

斯特恩·布罗科特树(Stern-Brocot Tree)引出了一种相当自然的方法,可以用简单的分母以分数来逼近实数。

这是devinmoore建议的VB代码的Perl和Javascript版本:

Perl:

sub dec2frac {
    my $d = shift;

    my $df  = 1;
    my $top = 1;
    my $bot = 1;

    while ($df != $d) {
      if ($df < $d) {
        $top += 1;
      }
      else {
         $bot += 1;
         $top = int($d * $bot);
      }
      $df = $top / $bot;
   }
   return "$top/$bot";
}

和几乎相同的javascript:

function dec2frac(d) {

    var df = 1;
    var top = 1;
    var bot = 1;

    while (df != d) {
        if (df < d) {
            top += 1;
        }
        else {
            bot += 1;
            top = parseInt(d * bot);
        }
        df = top / bot;
    }
    return top + '/' + bot;
}

这不是一个"算法",只是一个Python解决方案:
http://docs.python.org/library/fractions.html

>>> from fractions import Fraction
>>> Fraction('3.1415926535897932').limit_denominator(1000)
Fraction(355, 113)

完成以上代码并将其转换为as3

public static function toFrac(f:Number) : String
    {
        if (f>1)
        {
            var parte1:int;
            var parte2:Number;
            var resultado:String;
            var loc:int = String(f).indexOf(".");
            parte2 = Number(String(f).slice(loc, String(f).length));
            parte1 = int(String(f).slice(0,loc));
            resultado = toFrac(parte2);
            parte1 *= int(resultado.slice(resultado.indexOf("/") + 1, resultado.length)) + int(resultado.slice(0, resultado.indexOf("/")));
            resultado = String(parte1) +  resultado.slice(resultado.indexOf("/"), resultado.length)
            return resultado;
        }
        if( f < 0.47 )
            if( f < 0.25 )
                if( f < 0.16 )
                    if( f < 0.13 )
                        if( f < 0.11 )
                            return "1/10";
                        else
                            return "1/9";
                    else
                        if( f < 0.14 )
                            return "1/8";
                        else
                            return "1/7";
                else
                    if( f < 0.19 )
                        return "1/6";
                    else
                        if( f < 0.22 )
                            return "1/5";
                        else
                            return "2/9";
            else
                if( f < 0.38 )
                    if( f < 0.29 )
                        return "1/4";
                    else
                        if( f < 0.31 )
                            return "2/7";
                        else
                            return "1/3";
                else
                    if( f < 0.43 )
                        if( f < 0.40 )
                            return "3/8";
                        else
                            return "2/5";
                    else
                        if( f < 0.44 )
                            return "3/7";
                        else
                            return "4/9";
        else
            if( f < 0.71 )
                if( f < 0.60 )
                    if( f < 0.56 )
                        return "1/2";
                    else
                        if( f < 0.57 )
                            return "5/9";
                        else
                            return "4/7";
                else
                    if( f < 0.63 )
                        return "3/5";
                    else
                        if( f < 0.66 )
                            return "5/8";
                        else
                            return "2/3";
            else
                if( f < 0.80 )
                    if( f < 0.74 )
                        return "5/7";
                    else
                        if(f < 0.78 )
                            return "3/4";
                        else
                            return "7/9";
                else
                    if( f < 0.86 )
                        if( f < 0.83 )
                            return "4/5";
                        else
                            return "5/6";
                    else
                        if( f < 0.88 )
                            return "6/7";
                        else
                            if( f < 0.89 )
                                return "7/8";
                            else
                                if( f < 0.90 )
                                    return "8/9";
                                else
                                    return "9/10";
    }

从Python 2.6开始,有fractions模块。

(引自文档。)

>>> from fractions import Fraction
>>> Fraction('3.1415926535897932').limit_denominator(1000)
Fraction(355, 113)

>>> from math import pi, cos
>>> Fraction.from_float(cos(pi/3))
Fraction(4503599627370497, 9007199254740992)
>>> Fraction.from_float(cos(pi/3)).limit_denominator()
Fraction(1, 2)

这是红宝石的实现http://github.com/valodzka/frac

Math.frac(0.2, 100)  # => (1/5)
Math.frac(0.33, 10)  # => (1/3)
Math.frac(0.33, 100) # => (33/100)