将数字四舍五入到任意位数的有效数字

87

如何将任何数字(不仅限于大于0的整数)四舍五入到N个有效数字?

例如,如果我想要将数字四舍五入到三位有效数字,我需要一个公式可以处理以下情况:

1,239,451并返回1,240,000

12.1257并返回12.1

.0681并返回.0681

5并返回5

当然,该算法不应被硬编码为仅处理N等于3的情况,虽然这是一个好的开始。


看起来问题太笼统了。不同的编程语言有不同的标准函数来完成这个任务。确实没有必要重复造轮子。 - Johnny Wong
17个回答

111

以下是在避免12.100000000000001错误的情况下使用Java编写的相同代码。

我还删除了重复的代码,将power更改为整数类型以避免进行n - d时出现浮点问题,并使长中间变量更加清晰。

该错误是由于将一个大数乘以一个小数而引起的。 相反,我将两个大小相似的数字相除。

编辑
修复了更多错误。 添加了对0的检查,因为它会导致NaN。 使函数实际上可以处理负数(原始代码无法处理负数,因为负数的对数是一个复数)。

public static double roundToSignificantFigures(double num, int n) {
    if(num == 0) {
        return 0;
    }

    final double d = Math.ceil(Math.log10(num < 0 ? -num: num));
    final int power = n - (int) d;

    final double magnitude = Math.pow(10, power);
    final long shifted = Math.round(num*magnitude);
    return shifted/magnitude;
}

2
感谢您接受我的答案。我刚意识到我的回答是在问题发布一年后的。这也是为什么stackoverflow如此酷的原因之一。您可以找到有用的信息! - Pyrolistical
2
请注意,对于接近四舍五入限制的值,可能会出现轻微错误。例如,将1.255四舍五入到3个有效数字应返回1.26,但实际上返回1.25。这是因为1.255 * 100.0等于125.499999...但是在使用双精度浮点数时,这种情况是可以预料的。 - cquezel
哇,我知道这很老了,但我正在尝试使用它。我有一个浮点数,想要显示到三个有效数字。如果浮点值为1.0,则调用您的方法,但即使将float转换为double,它仍然返回为1.0。我希望它返回1。有什么想法吗? - Steve W
3
这段 Java 代码最终被包含在官方的 Android 示例中。 https://android.googlesource.com/platform/development/+/fcf4286/samples/training/InteractiveChart/src/com/example/android/interactivechart/InteractiveLineGraphView.java - Curious Sam
1
不完美。对于num = -7999999.999999992和n = 2,返回-7999999.999999999,但应该是-8000000。 - Duncan Calvert
我在+7999999.999999992上进行了测试,结果显示为+8000000,看起来没问题。但是我认为负数应该像零一样被特殊处理以确保安全。 - Eric Nicolas

16

这是一个简洁明了的JavaScript实现:

function sigFigs(n, sig) {
    var mult = Math.pow(10, sig - Math.floor(Math.log(n) / Math.LN10) - 1);
    return Math.round(n * mult) / mult;
}

alert(sigFigs(1234567, 3)); // Gives 1230000
alert(sigFigs(0.06805, 3)); // Gives 0.0681
alert(sigFigs(5, 3)); // Gives 5

1
很好的回答,Ates。也许可以添加一个触发器,以便在 n==0 时返回 0 :) - sscirrus
有必要使用Math.log(n) / Math.LN10而不是 Math.log10(n)吗? - Lee
1
@Lee 这是一项新技术,属于ECMAScript 2015(ES6)标准的一部分。因此,主要存在兼容性问题。 - Ates Goral
我是否漏掉了什么,或者这个答案假设 Math.floor(x) == Math.ceil(x) - 1?因为当 x 是整数时,它并不是。我认为 pow 函数的第二个参数应该是 sig - Math.ceil(Math.log(n) / Math.LN10)(或者直接使用 Math.log10)。 - Paul

15

摘要:

double roundit(double num, double N)
{
    double d = log10(num);
    double power;
    if (num > 0)
    {
        d = ceil(d);
        power = -(d-N);
    }
    else
    {
        d = floor(d); 
        power = -(d-N);
    }

    return (int)(num * pow(10.0, power) + 0.5) * pow(10.0, -power);
}

所以你需要找到第一个非零数字的小数位,然后保存接下来的 N-1 个数字,最后基于剩余的数字对第 N 个数字进行四舍五入。

我们可以使用 log 来实现第一步。

log 1239451 = 6.09
log 12.1257 = 1.08
log 0.0681  = -1.16

对于大于0的数字,取对数的上限。对于小于0的数字,取对数的下限。

现在我们有数字d:第一种情况下为7,第二种情况下为2,第三种情况下为-2。

我们必须四舍五入第(d-N)位数字。类似这样:

double roundedrest = num * pow(10, -(d-N));

pow(1239451, -4) = 123.9451
pow(12.1257, 1)  = 121.257
pow(0.0681, 4)   = 681

然后进行标准四舍五入:

roundedrest = (int)(roundedrest + 0.5);

撤销 pow 函数。

roundednum = pow(roundedrest, -(power))

这里的 power 是上面计算出来的功率值。


关于精度:Pyrolistical 的答案确实更接近真实结果。但请注意,在任何情况下都无法准确地表示 12.1。如果您将答案打印如下:

System.out.println(new BigDecimal(n));

答案如下:
Pyro's: 12.0999999999999996447286321199499070644378662109375
Mine: 12.10000000000000142108547152020037174224853515625
Printing 12.1 directly: 12.0999999999999996447286321199499070644378662109375

所以,使用Pyro的答案!


1
这个算法似乎容易出现浮点数误差。当使用JavaScript实现时,我得到的是:0.06805 -> 0.06810000000000001 和 12.1 -> 12.100000000000001。 - Ates Goral
单独使用浮点数无法准确表示12.1,这不是该算法的结果。 - Claudiu
1
这段Java代码产生的结果是12.100000000000001,使用了64位双精度浮点数可以准确表示12.1。 - Pyrolistical
4
无论是64位还是128位都无关紧要。使用有限的2的幂次和表示分数1/10在浮点数中不可行。 - Claudiu
2
对于那些参与讨论的人,基本上Pyrolistical的答案比我的更精确,因此浮点数打印算法会打印“12.1”而不是“12.100000000000001”。尽管我在技术上正确地指出了无法精确表示“12.1”,但他的答案更好。 - Claudiu
这是错误的,12.1被精确地表示为带有指数的121。这只会出现在你进行计算得到12.1的情况下,这意味着你需要截断结果。你可以通过执行console.log(121 /10)来看到这种情况。 - cyborg

11

这个"short and sweet"的JavaScript实现难道不是

Number(n).toPrecision(sig)
例如。
alert(Number(12345).toPrecision(3)

抱歉,我这里并不是在挖苦什么,只是使用 Claudiu 的 "roundit" 函数和 JavaScript 中的 .toPrecision 得到了不同的结果,但只在最后一位数字的四舍五入方面有所差异。

JavaScript:

Number(8.14301).toPrecision(4) == 8.143

.NET

roundit(8.14301,4) == 8.144

1
Number(814301).toPrecision(4) == "8.143e+5"。如果您要向用户展示此内容,通常不是您想要的。 - Zaz
非常正确,Josh。我通常只建议对十进制数使用 .toPrecision() 函数,并且接受的回答(含编辑)应根据你个人的需求来使用或审查。 - Justin Wignall

8

Pyrolistical提供的(非常好的!)解决方案仍然存在问题。在Java中,最大的双精度值约为10^308,而最小值约为10^-324。因此,当您将roundToSignificantFigures函数应用于接近Double.MIN_VALUE几个数量级的内容时,可能会遇到问题。例如,当您调用:

roundToSignificantFigures(1.234E-310, 3);

如果变量power的值为3 - (-309) = 312,那么变量magnitude将变为Infinity,之后的所有内容都是垃圾。幸运的是,这不是一个难以克服的问题:只有因子magnitude会溢出,真正重要的是乘积num * magnitude,它不会溢出。解决这个问题的一种方法是通过将magintude因子的乘法分成两个步骤来实现:

 public static double roundToNumberOfSignificantDigits(double num, int n) {
final double maxPowerOfTen = Math.floor(Math.log10(Double.MAX_VALUE));
if(num == 0) { return 0; }
final double d = Math.ceil(Math.log10(num < 0 ? -num: num)); final int power = n - (int) d;
double firstMagnitudeFactor = 1.0; double secondMagnitudeFactor = 1.0; if (power > maxPowerOfTen) { firstMagnitudeFactor = Math.pow(10.0, maxPowerOfTen); secondMagnitudeFactor = Math.pow(10.0, (double) power - maxPowerOfTen); } else { firstMagnitudeFactor = Math.pow(10.0, (double) power); }
double toBeRounded = num * firstMagnitudeFactor; toBeRounded *= secondMagnitudeFactor;
final long shifted = Math.round(toBeRounded); double rounded = ((double) shifted) / firstMagnitudeFactor; rounded /= secondMagnitudeFactor; return rounded; }

6

这个Java解决方案怎么样:

double roundToSignificantFigure(double num, int precision){
 return new BigDecimal(num)
            .round(new MathContext(precision, RoundingMode.HALF_EVEN))
            .doubleValue(); 
}
该方法将数字四舍五入到指定的有效数字位数。它使用BigDecimal类来执行精确计算,并采用舍入模式HALF_EVEN。

3

JavaScript:

Number( my_number.toPrecision(3) );
< p > Number函数将会把形如"8.143e+5"的输出转换为"814300"


3

这是Ates的JavaScript修改版本,可以处理负数。

function sigFigs(n, sig) {
    if ( n === 0 )
        return 0
    var mult = Math.pow(10,
        sig - Math.floor(Math.log(n < 0 ? -n: n) / Math.LN10) - 1);
    return Math.round(n * mult) / mult;
 }

2
这篇文章来晚了5年,但我觉得还是分享一下,希望能帮到其他仍然遇到同样问题的人。我喜欢它因为它很简单,并且代码方面没有计算。更多信息请参见显示有效数字的内置方法
如果您只想将其打印出来,请使用以下内容。
public String toSignificantFiguresString(BigDecimal bd, int significantFigures){
    return String.format("%."+significantFigures+"G", bd);
}

如果您想进行转换:

public BigDecimal toSignificantFigures(BigDecimal bd, int significantFigures){
    String s = String.format("%."+significantFigures+"G", bd);
    BigDecimal result = new BigDecimal(s);
    return result;
}

以下是它的一个示例:

这里是它的实际运用:

BigDecimal bd = toSignificantFigures(BigDecimal.valueOf(0.0681), 2);

这将以科学计数法显示“大”数字,例如15k为1.5e04。 - matt

1

以下是Pyrolistical(目前的最佳答案)在Visual Basic.NET中的代码,如果有需要的话:

Public Shared Function roundToSignificantDigits(ByVal num As Double, ByVal n As Integer) As Double
    If (num = 0) Then
        Return 0
    End If

    Dim d As Double = Math.Ceiling(Math.Log10(If(num < 0, -num, num)))
    Dim power As Integer = n - CInt(d)
    Dim magnitude As Double = Math.Pow(10, power)
    Dim shifted As Double = Math.Round(num * magnitude)
    Return shifted / magnitude
End Function

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接