【问题标题】:How to convert floats to human-readable fractions?如何将浮点数转换为人类可读的分数?
【发布时间】:2008-09-18 19:00:09
【问题描述】:

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

我们的想法是让它易于阅读,让用户理解“x parts out of y”作为理解数据的更好方式。

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

【问题讨论】:

  • .33 => "1/3" 的例子与我有关;我希望.33 => "33/100"。我假设你的意思当然是.33...,但它暴露了问题的一个问题——在我们确定一个算法之前,我们需要决定预期的行为。 @Debilski 的 Python 答案使用 .limit_denominator(),默认为 10^7 的最大分母;在实践中可能是一个很好的默认值,但如果你不小心,这仍然会引入错误,并且 确实.33 情况下返回 "33/100"
  • 无论使用何种语言-特定功能都可用。不清楚你在问什么,如果它确实不仅仅是一个矛盾的术语。

标签: algorithm language-agnostic numbers


【解决方案1】:

我发现 David Eppstein 的 find rational approximation to given real number 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]));
}

【讨论】:

  • 对于那些在 Ruby 中寻找解决方案的人来说,我们很幸运! Christopher Lord 已在 Ruby gem 中实现了上述算法。见christopher.lord.ac/fractions-in-rubyrubygems.org/gems/fraction
  • 请注意,此代码无法很好地处理某些极端情况:当给定 -1.3333333 且最大分母为 4 时,它返回 4/-3,错误为 3.333333e-08 和-5/4 错误 = -8.333330e-02,这是正确的。但是当给定 -1.33333337 和相同的最大分母时,它会变成 12121211/-9090908,错误为 error = 4.218847e-15 和 -4/3,错误为 -3.666667e-08,这是不正确的。这是一个问题,尤其是在使用计算出的浮点数(例如 -4/3)呈现算法时,会产生类似这样的错误结果。
【解决方案2】:

从 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)

【讨论】:

  • @Debilski OP 的language agnosticalgorithm 标签中的哪一个你的答案满足?
  • @vladr 好吧,鉴于我在大约 6 年前写了这个答案(并且在提出问题后一年多),我想我不再知道我当时的推理是什么.很可能我指的是这条评论:stackoverflow.com/questions/95727/…OTOH 这也可能是这个答案已从另一个问题合并。这么多年过去了,谁能说得清……
  • 您可以添加几句关于分数模块使用的算法的句子(也许更新您对 Python3 的答案)。
【解决方案3】:

如果输出是为了让人类读者对结果的顺序有一个快速的印象,那么返回“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;
}

【讨论】:

  • 这是我们需要更多的横向思维!很好的建议。
  • 有点丑但是非常快速实用的方法
  • 这是一种非常简单的有趣方法。为了节省空间,您可以改为对数组进行二进制搜索,或创建二叉树,但您的方法可能要快一些(您可以通过在返回之前对 strcat 进行一次调用来节省空间,并在现在调用它的位置分配一个 var)。我也会包括 3/10 和 7/10,但也许这只是我。
  • 受此解决方案的启发,我创建了一个简短(但完全未优化)的代码。它可以很容易地扩展到覆盖更大范围的分数。 jsfiddle.net/PdL23/1
  • 请注意,1/1000 也非常适合人类阅读,但上述算法只会产生非常粗略的 1/10 近似值;我相信可以在可以选择哪些人类可读的分母和/或添加 &lt;&gt;&lt;&lt;&gt;&gt; 前缀方面进行改进,以了解近似值。
【解决方案4】:

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

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

(来自谷歌搜索:将小数转换为分数,将小数转换为分数代码)

【讨论】:

  • 请注意,当 f = n/m 时,此算法需要 Ω(m) 时间。这可能很多,即使您不打算这样做(考虑 0.66666666667)。
【解决方案5】:

您可能想阅读What Every Computer Scientist Should Know about Floating Point Arithmetic

您必须通过乘以一个大数来指定一些精度:

3.141592 * 1000000 = 3141592

那么你可以做一个分数:

3 + (141592 / 1000000)

并通过 GCD 减少...

3 + (17699 / 125000)

但是没有办法得到 intent 分数。您可能希望始终在整个代码中使用分数——只要记住尽可能减少分数以避免溢出!

【讨论】:

    【解决方案6】:

    这里是 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;
    }
    
    //Put in your test number here:
    var floatNumber = 2.56;
    alert(floatNumber + " = " + dec2frac(floatNumber));

    【讨论】:

      【解决方案7】:

      一个 C# 实现

      /// <summary>
      /// Represents a rational number
      /// </summary>
      public struct Fraction
      {
          public int Numerator;
          public int Denominator;
      
          /// <summary>
          /// Constructor
          /// </summary>
          public Fraction(int numerator, int denominator)
          {
              this.Numerator = numerator;
              this.Denominator = denominator;
          }
      
          /// <summary>
          /// Approximates a fraction from the provided double
          /// </summary>
          public static Fraction Parse(double d)
          {
              return ApproximateFraction(d);
          }
      
          /// <summary>
          /// Returns this fraction expressed as a double, rounded to the specified number of decimal places.
          /// Returns double.NaN if denominator is zero
          /// </summary>
          public double ToDouble(int decimalPlaces)
          {
              if (this.Denominator == 0)
                  return double.NaN;
      
              return System.Math.Round(
                  Numerator / (double)Denominator,
                  decimalPlaces
              );
          }
      
      
          /// <summary>
          /// Approximates the provided value to a fraction.
          /// http://stackoverflow.com/questions/95727/how-to-convert-floats-to-human-readable-fractions
          /// </summary>
          private static Fraction ApproximateFraction(double value)
          {
              const double EPSILON = .000001d;
      
              int n = 1;  // numerator
              int d = 1;  // denominator
              double fraction = n / d;
      
              while (System.Math.Abs(fraction - value) > EPSILON)
              {
                  if (fraction < value)
                  {
                      n++;
                  }
                  else
                  {
                      d++;
                      n = (int)System.Math.Round(value * d);
                  }
      
                  fraction = n / (double)d;
              }
      
              return new Fraction(n, d);
          }
      }
      

      【讨论】:

        【解决方案8】:

        Stern-Brocot Tree 引入了一种相当自然的方法,可以用简单分母的分数来近似实数。

        【讨论】:

          【解决方案9】:

          部分问题在于,这么多分数实际上并不容易被解释为分数。例如。 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。你这样做直到你达到或高于较小数字的平方根。

          在你这样做之后,你需要

          【讨论】:

          • 我建议在最后一步使用欧几里得算法
          【解决方案10】:

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

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

          【讨论】:

            【解决方案11】:

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

            您希望“解决方案”具有什么精度? 0.33 不等于 1/3。您如何识别“好”(易于阅读)的答案?

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

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

            【讨论】:

              【解决方案12】:

              您可以使用以下步骤在任何编程语言中执行此操作:

              1. 乘除以 10^x,其中 x 是 10 的幂,以确保该数字没有剩余小数位。 示例:将 0.33 乘以 10^2 = 100 得到 33,再除以 33/100
              2. 通过因式分解减少所得分数的分子和分母,直到无法再从结果中获得整数。
              3. 得到的缩减分数应该是您的答案。

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

              所以,这可以理解为“五分之一”

              【讨论】:

                【解决方案13】:

                R 中的内置解决方案:

                library(MASS)
                fractions(0.666666666)
                ## [1] 2/3
                

                这使用连分数法,并有可选的cyclesmax.denominator 参数用于调整精度。

                【讨论】:

                • 还有library(numbers)contFrac(0.6666);根据需要获取字符串输出:paste(contFrac(0.666, tol=1e-03)$rat, collapse="/")
                【解决方案14】:

                您必须弄清楚您愿意接受的错误级别。并不是所有的小数都会化简为简单的分数。我可能会选择一个容易整除的数字,比如 60,然后计算出有多少个 60 最接近该值,然后化简分数。

                【讨论】:

                  【解决方案15】:

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

                  除其他外,金融程序将使用这种解决方案来进行精确计算并保持使用普通浮点数可能会丢失的精度。

                  当然它会慢很多,所以对你来说可能不实用。取决于您需要进行多少计算,以及精度对您来说有多重要。

                  a = rational(1);
                  b = rational(3);
                  c = a / b;
                  
                  print (c.asFraction)  --->  "1/3"
                  print (c.asFloat) ----> "0.333333"
                  

                  【讨论】:

                    【解决方案16】:

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

                    一般情况下是错误的,因为 1/3 = 0.3333333 = 0.(3) 此外,从上述建议的解决方案中不可能发现十进制可以转换为具有定义精度的分数,因为输出始终是分数。

                    但是,我根据Infinite geometric series 的想法建议我的综合函数有很多选项,特别是公式:

                    起初这个函数试图在字符串表示中找到分数的周期。之后应用上述公式。

                    有理数代码是从Stephen M. McKamey C# 中的有理数实现中借用的。我希望将我的代码移植到其他语言上并不难。

                    /// <summary>
                    /// Convert decimal to fraction
                    /// </summary>
                    /// <param name="value">decimal value to convert</param>
                    /// <param name="result">result fraction if conversation is succsess</param>
                    /// <param name="decimalPlaces">precision of considereation frac part of value</param>
                    /// <param name="trimZeroes">trim zeroes on the right part of the value or not</param>
                    /// <param name="minPeriodRepeat">minimum period repeating</param>
                    /// <param name="digitsForReal">precision for determination value to real if period has not been founded</param>
                    /// <returns></returns>
                    public static bool FromDecimal(decimal value, out Rational<T> result, 
                        int decimalPlaces = 28, bool trimZeroes = false, decimal minPeriodRepeat = 2, int digitsForReal = 9)
                    {
                        var valueStr = value.ToString("0.0000000000000000000000000000", CultureInfo.InvariantCulture);
                        var strs = valueStr.Split('.');
                    
                        long intPart = long.Parse(strs[0]);
                        string fracPartTrimEnd = strs[1].TrimEnd(new char[] { '0' });
                        string fracPart;
                    
                        if (trimZeroes)
                        {
                            fracPart = fracPartTrimEnd;
                            decimalPlaces = Math.Min(decimalPlaces, fracPart.Length);
                        }
                        else
                            fracPart = strs[1];
                    
                        result = new Rational<T>();
                        try
                        {
                            string periodPart;
                            bool periodFound = false;
                    
                            int i;
                            for (i = 0; i < fracPart.Length; i++)
                            {
                                if (fracPart[i] == '0' && i != 0)
                                    continue;
                    
                                for (int j = i + 1; j < fracPart.Length; j++)
                                {
                                    periodPart = fracPart.Substring(i, j - i);
                                    periodFound = true;
                                    decimal periodRepeat = 1;
                                    decimal periodStep = 1.0m / periodPart.Length;
                                    var upperBound = Math.Min(fracPart.Length, decimalPlaces);
                                    int k;
                                    for (k = i + periodPart.Length; k < upperBound; k += 1)
                                    {
                                        if (periodPart[(k - i) % periodPart.Length] != fracPart[k])
                                        {
                                            periodFound = false;
                                            break;
                                        }
                                        periodRepeat += periodStep;
                                    }
                    
                                    if (!periodFound && upperBound - k <= periodPart.Length && periodPart[(upperBound - i) % periodPart.Length] > '5')
                                    {
                                        var ind = (k - i) % periodPart.Length;
                                        var regroupedPeriod = (periodPart.Substring(ind) + periodPart.Remove(ind)).Substring(0, upperBound - k);
                                        ulong periodTailPlusOne = ulong.Parse(regroupedPeriod) + 1;
                                        ulong fracTail = ulong.Parse(fracPart.Substring(k, regroupedPeriod.Length));
                                        if (periodTailPlusOne == fracTail)
                                            periodFound = true;
                                    }
                    
                                    if (periodFound && periodRepeat >= minPeriodRepeat)
                                    {
                                        result = FromDecimal(strs[0], fracPart.Substring(0, i), periodPart);
                                        break;
                                    }
                                    else
                                        periodFound = false;
                                }
                    
                                if (periodFound)
                                    break;
                            }
                    
                            if (!periodFound)
                            {
                                if (fracPartTrimEnd.Length >= digitsForReal)
                                    return false;
                                else
                                {
                                    result = new Rational<T>(long.Parse(strs[0]), 1, false);
                                    if (fracPartTrimEnd.Length != 0)
                                        result = new Rational<T>(ulong.Parse(fracPartTrimEnd), TenInPower(fracPartTrimEnd.Length));
                                    return true;
                                }
                            }
                    
                            return true;
                        }
                        catch
                        {
                            return false;
                        }
                    }
                    
                    public static Rational<T> FromDecimal(string intPart, string fracPart, string periodPart)
                    {
                        Rational<T> firstFracPart;
                        if (fracPart != null && fracPart.Length != 0)
                        {
                            ulong denominator = TenInPower(fracPart.Length);
                            firstFracPart = new Rational<T>(ulong.Parse(fracPart), denominator);
                        }
                        else
                            firstFracPart = new Rational<T>(0, 1, false);
                    
                        Rational<T> secondFracPart;
                        if (periodPart != null && periodPart.Length != 0)
                            secondFracPart =
                                new Rational<T>(ulong.Parse(periodPart), TenInPower(fracPart.Length)) *
                                new Rational<T>(1, Nines((ulong)periodPart.Length), false);
                        else
                            secondFracPart = new Rational<T>(0, 1, false);
                    
                        var result = firstFracPart + secondFracPart;
                        if (intPart != null && intPart.Length != 0)
                        {
                            long intPartLong = long.Parse(intPart);
                            result = new Rational<T>(intPartLong, 1, false) + (intPartLong == 0 ? 1 : Math.Sign(intPartLong)) * result;
                        }
                    
                        return result;
                    }
                    
                    private static ulong TenInPower(int power)
                    {
                        ulong result = 1;
                        for (int l = 0; l < power; l++)
                            result *= 10;
                        return result;
                    }
                    
                    private static decimal TenInNegPower(int power)
                    {
                        decimal result = 1;
                        for (int l = 0; l > power; l--)
                            result /= 10.0m;
                        return result;
                    }
                    
                    private static ulong Nines(ulong power)
                    {
                        ulong result = 9;
                        if (power >= 0)
                            for (ulong l = 0; l < power - 1; l++)
                                result = result * 10 + 9;
                        return result;
                    }
                    

                    有一些用法的例子:

                    Rational<long>.FromDecimal(0.33333333m, out r, 8, false);
                    // then r == 1 / 3;
                    
                    Rational<long>.FromDecimal(0.33333333m, out r, 9, false);
                    // then r == 33333333 / 100000000;
                    

                    您的案例右部分零部分修剪:

                    Rational<long>.FromDecimal(0.33m, out r, 28, true);
                    // then r == 1 / 3;
                    
                    Rational<long>.FromDecimal(0.33m, out r, 28, true);
                    // then r == 33 / 100;
                    

                    最小周期演示:

                    Rational<long>.FromDecimal(0.123412m, out r, 28, true, 1.5m));
                    // then r == 1234 / 9999;
                    Rational<long>.FromDecimal(0.123412m, out r, 28, true, 1.6m));
                    // then r == 123412 / 1000000; because of minimu repeating of period is 0.1234123 in this case.
                    

                    最后四舍五入:

                    Rational<long>.FromDecimal(0.8888888888888888888888888889m, out r));
                    // then r == 8 == 9;
                    

                    最有趣的案例:

                    Rational<long>.FromDecimal(0.12345678m, out r, 28, true, 2, 9);
                    // then r == 12345678 / 100000000;
                    
                    Rational<long>.FromDecimal(0.12345678m, out r, 28, true, 2, 8);
                    // Conversation failed, because of period has not been founded and there are too many digits in fraction part of input value.
                    
                    Rational<long>.FromDecimal(0.12121212121212121m, out r, 28, true, 2, 9));
                    // then r == 4 / 33; Despite of too many digits in input value, period has been founded. Thus it's possible to convert value to fraction.
                    

                    大家可以在my MathFunctions library on github找到其他测试和代码。

                    【讨论】:

                      【解决方案17】:

                      我认为最好的方法是首先将您的浮点值转换为 ascii 表示。在 C++ 中,您可以使用 ostringstream,或者在 C 中,您可以使用 sprintf。下面是它在 C++ 中的样子:

                      ostringstream oss;
                      float num;
                      cin >> num;
                      oss << num;
                      string numStr = oss.str();
                      int i = numStr.length(), pow_ten = 0;
                      while (i > 0) {
                          if (numStr[i] == '.')
                              break;
                          pow_ten++;
                          i--;
                      }
                      for (int j = 1; j < pow_ten; j++) {
                          num *= 10.0;
                      }
                      cout << static_cast<int>(num) << "/" << pow(10, pow_ten - 1) << endl;
                      

                      可以在直接 C 中采用类似的方法。

                      之后,您需要检查分数是否为最低值。该算法将给出精确的答案,即 0.33 将输出“33/100”,而不是“1/3”。但是,0.4 将给出“4/10”,当减少到最低项时将是“2/5”。这可能不如 EppStein 的解决方案强大,但我相信这更直接。

                      【讨论】:

                      • 8 年后,我遇到了您的解决方案,我已经测试过,到目前为止它运行良好,但您说它不如 EppStein 的解决方案强大,我想知道为什么。既然您的解决方案要简单得多,这不应该是首选的解决方案吗?我们不应该尽可能地做最简单的代码,只要它有效且安全吗??
                      • pow 用于整数幂是个坏主意:Why pow(10,5) = 9,999 in C++, Why does pow(5,2) become 24?
                      【解决方案18】:

                      用 C++ 回答,假设你有一个 BigInt 类,它可以存储无限大小的整数。

                      您可以改用unsigned long long,但它只适用于某些值。

                      void GetRational(double val)
                      {
                          if (val == val+1) // Inf
                              throw "Infinite Value";
                          if (val != val) // NaN
                              throw "Undefined Value";
                      
                          bool sign = false;
                          BigInt enumerator = 0;
                          BigInt denominator = 1;
                      
                          if (val < 0)
                          {
                              val = -val;
                              sign = true;
                          }
                      
                          while (val > 0)
                          {
                              unsigned int intVal = (unsigned int)val;
                              val -= intVal;
                              enumerator += intVal;
                              val *= 2;
                              enumerator *= 2;
                              denominator *= 2;
                          }
                      
                          BigInt gcd = GCD(enumerator,denominator);
                          enumerator /= gcd;
                          denominator /= gcd;
                      
                          Print(sign? "-":"+");
                          Print(enumerator);
                          Print("/");
                          Print(denominator);
                      
                          // Or simply return {sign,enumerator,denominator} as you wish
                      }
                      

                      顺便说一句,GetRational(0.0) 将返回“+0/1”,因此您可能想单独处理这种情况。

                      P.S.:多年来,我一直在自己的“RationalNum”类中使用此代码,并且已经过彻底测试。

                      【讨论】:

                      • 您的示例似乎分解了 1.333333 之类的值。它进入了一个很长的循环,试图找到该值并且似乎不起作用...对于其他简单的值(例如 1.25)也很好
                      • @Adamski:谢谢。 while 循环的“收敛”周期以 double 的大小为界,通常为 64 位。所以它不依赖于输入的初始值(val)。然而,GCD 函数确实依赖于这个值,尽管它通常会很快收敛到一个解决方案。有没有可能是你没有正确实现这个功能?
                      • @Adamski:另外,正如我在答案开头提到的,如果您使用unsigned long long 而不是BigInt,那么它不一定会为每个输入产生正确的结果价值……但即使在这种情况下,代码也不应该“进入一个很长的循环”。
                      • 嗯,是的,这完全有可能,我使用的 GCD 函数是 Juce 库 BigInteger 类的一部分。感谢您的信息!
                      • @Adamski:所以 没有 GCD 函数没有正确实现是有道理的。您是否检查过代码是否在 while 循环期间或之后运行了很长时间?我将检查 1.33333 的值,看看这背后是什么。谢谢。
                      【解决方案19】:

                      Ian Richards / John Kennedy 的这个算法不仅返回了不错的分数,而且在速度方面也表现得非常好。这是我从this answer 获取的 C# 代码。

                      它可以处理所有 double 值,但 NaN 和 +/- 无穷大等特殊值除外,如果需要,您必须添加它们。

                      它返回一个new Fraction(numerator, denominator)。替换为您自己的类型。

                      有关更多示例值以及与其他算法的比较,go here

                      public Fraction RealToFraction(double value, double accuracy)
                      {
                          if (accuracy <= 0.0 || accuracy >= 1.0)
                          {
                              throw new ArgumentOutOfRangeException("accuracy", "Must be > 0 and < 1.");
                          }
                      
                          int sign = Math.Sign(value);
                      
                          if (sign == -1)
                          {
                              value = Math.Abs(value);
                          }
                      
                          // Accuracy is the maximum relative error; convert to absolute maxError
                          double maxError = sign == 0 ? accuracy : value * accuracy;
                      
                          int n = (int) Math.Floor(value);
                          value -= n;
                      
                          if (value < maxError)
                          {
                              return new Fraction(sign * n, 1);
                          }
                      
                          if (1 - maxError < value)
                          {
                              return new Fraction(sign * (n + 1), 1);
                          }
                      
                          double z = value;
                          int previousDenominator = 0;
                          int denominator = 1;
                          int numerator;
                      
                          do
                          {
                              z = 1.0 / (z - (int) z);
                              int temp = denominator;
                              denominator = denominator * (int) z + previousDenominator;
                              previousDenominator = temp;
                              numerator = Convert.ToInt32(value * denominator);
                          }
                          while (Math.Abs(value - (double) numerator / denominator) > maxError && z != (int) z);
                      
                          return new Fraction((n * denominator + numerator) * sign, denominator);
                      }
                      

                      此算法返回的示例值:

                      Accuracy: 1.0E-3      | Richards                     
                      Input                 | Result           Error       
                      ======================| =============================
                         3                  |       3/1          0         
                         0.999999           |       1/1         1.0E-6     
                         1.000001           |       1/1        -1.0E-6     
                         0.50 (1/2)         |       1/2          0         
                         0.33... (1/3)      |       1/3          0         
                         0.67... (2/3)      |       2/3          0         
                         0.25 (1/4)         |       1/4          0         
                         0.11... (1/9)      |       1/9          0         
                         0.09... (1/11)     |       1/11         0         
                         0.62... (307/499)  |       8/13        2.5E-4     
                         0.14... (33/229)   |      16/111       2.7E-4     
                         0.05... (33/683)   |      10/207      -1.5E-4     
                         0.18... (100/541)  |      17/92       -3.3E-4     
                         0.06... (33/541)   |       5/82       -3.7E-4     
                         0.1                |       1/10         0         
                         0.2                |       1/5          0         
                         0.3                |       3/10         0         
                         0.4                |       2/5          0         
                         0.5                |       1/2          0         
                         0.6                |       3/5          0         
                         0.7                |       7/10         0         
                         0.8                |       4/5          0         
                         0.9                |       9/10         0         
                         0.01               |       1/100        0         
                         0.001              |       1/1000       0         
                         0.0001             |       1/10000      0         
                         0.33333333333      |       1/3         1.0E-11    
                         0.333              |     333/1000       0         
                         0.7777             |       7/9         1.0E-4     
                         0.11               |      10/91       -1.0E-3     
                         0.1111             |       1/9         1.0E-4     
                         3.14               |      22/7         9.1E-4     
                         3.14... (pi)       |      22/7         4.0E-4     
                         2.72... (e)        |      87/32        1.7E-4     
                         0.7454545454545    |      38/51       -4.8E-4     
                         0.01024801004      |       2/195       8.2E-4     
                         0.99011            |     100/101      -1.1E-5     
                         0.26... (5/19)     |       5/19         0         
                         0.61... (37/61)    |      17/28        9.7E-4     
                                            | 
                      Accuracy: 1.0E-4      | Richards                     
                      Input                 | Result           Error       
                      ======================| =============================
                         0.62... (307/499)  |     299/486      -6.7E-6     
                         0.05... (33/683)   |      23/476       6.4E-5     
                         0.06... (33/541)   |      33/541        0         
                         1E-05              |       1/99999     1.0E-5     
                         0.7777             |    1109/1426     -1.8E-7     
                         3.14... (pi)       |     333/106      -2.6E-5     
                         2.72... (e)        |     193/71        1.0E-5     
                         0.61... (37/61)    |      37/61         0         
                      

                      【讨论】:

                        【解决方案20】:

                        Ruby 已经有一个内置的解决方案:

                        0.33.rationalize.to_s # => "33/100"
                        0.4.rationalize.to_s # => "2/5"
                        

                        在 Rails 中,ActiveRecord 数字属性也可以转换:

                        product.size = 0.33
                        product.size.to_r.to_s # => "33/100"
                        

                        【讨论】:

                          【解决方案21】:

                          您将遇到两个使这变得困难的基本问题:

                          1) 浮点数不是一个精确的表示,这意味着如果您有一个“x/y”的分数导致“z”的值,您的分数算法可能会返回“x/y”以外的结果”。

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

                          但是,以一种廉价的方式,由于浮点数的准确性有限,因此您始终可以将其表示为某种形式的派系。 (我认为...)

                          【讨论】:

                          • 浮点数(或双精度数)分数。它的分母是 2 的幂。这就是为什么它们不能准确地表示一些有理数。
                          【解决方案22】:

                          完成以上代码并转换为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";
                              }
                          

                          【讨论】:

                          • 谢谢,我在 Delphi 上用过这个,比那些卷曲的东西更容易移植
                          【解决方案23】:

                          这是一个快速而肮脏的 javascript 实现,它使用蛮力方法。 完全没有优化,它在预定义的分数范围内工作:http://jsfiddle.net/PdL23/1/

                          /* This should convert any decimals to a simplified fraction within the range specified by the two for loops. Haven't done any thorough testing, but it seems to work fine.
                          
                          I have set the bounds for numerator and denominator to 20, 20... but you can increase this if you want in the two for loops.
                          
                          Disclaimer: Its not at all optimized. (Feel free to create an improved version.)
                          */
                          
                          decimalToSimplifiedFraction = function(n) {
                             
                          for(num = 1; num < 20; num++) {  // "num" is the potential numerator
                              for(den = 1; den < 20; den++) {  // "den" is the potential denominator
                                  var multiplyByInverse = (n * den ) / num;
                                  
                                  var roundingError = Math.round(multiplyByInverse) - multiplyByInverse;
                                  
                                  // Checking if we have found the inverse of the number, 
                                  if((Math.round(multiplyByInverse) == 1) && (Math.abs(roundingError) < 0.01)) {
                                      return num + "/" + den;
                                  }
                              }
                          }
                          };
                          
                          //Put in your test number here.
                          var floatNumber = 2.56;
                          
                          alert(floatNumber + " = " + decimalToSimplifiedFraction(floatNumber));

                          这受到 JPS 使用的方法的启发。

                          【讨论】:

                            【解决方案24】:

                            正如许多人所说,您确实无法将浮点数转换回分数(除非它非常精确,例如 .25)。当然,您可以为大量分数创建某种类型的查找,并使用某种模糊逻辑来产生您正在寻找的结果。同样,这也不准确,您需要定义分母的下限。

                            .32

                            【讨论】:

                              【解决方案25】:

                              这是 ruby​​ 的实现 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)
                              

                              【讨论】:

                                【解决方案26】:

                                我遇到了一个使用变形的特别优雅的 Haskell 解决方案。这取决于recursion-schemes 包。

                                {-# LANGUAGE AllowAmbiguousTypes #-}
                                {-# LANGUAGE FlexibleContexts    #-}
                                
                                import           Control.Applicative   (liftA2)
                                import           Control.Monad         (ap)
                                import           Data.Functor.Foldable
                                import           Data.Ratio            (Ratio, (%))
                                
                                isInteger :: (RealFrac a) => a -> Bool
                                isInteger = ((==) <*>) (realToFrac . floor)
                                
                                continuedFraction :: (RealFrac a) => a -> [Int]
                                continuedFraction = liftA2 (:) floor (ana coalgebra)
                                    where coalgebra x
                                              | isInteger x = Nil
                                              | otherwise = Cons (floor alpha) alpha
                                                  where alpha = 1 / (x - realToFrac (floor x))
                                
                                collapseFraction :: (Integral a) => [Int] -> Ratio a
                                collapseFraction [x]    = fromIntegral x % 1
                                collapseFraction (x:xs) = (fromIntegral x % 1) + 1 / collapseFraction xs
                                
                                -- | Use the nth convergent to approximate x
                                approximate :: (RealFrac a, Integral b) => a -> Int -> Ratio b
                                approximate x n = collapseFraction $ take n (continuedFraction x)
                                

                                如果你在 ghci 中尝试一下,它确实有效!

                                λ:> approximate pi 2
                                22 % 7
                                

                                【讨论】:

                                  猜你喜欢
                                  • 1970-01-01
                                  • 2021-08-17
                                  • 1970-01-01
                                  • 1970-01-01
                                  • 1970-01-01
                                  • 1970-01-01
                                  • 2019-01-21
                                  • 2017-12-14
                                  • 1970-01-01
                                  相关资源
                                  最近更新 更多