如何将浮点数转换为人类可读的分数?
假设我们有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)