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

