Round a double to x significant figures
如果我有一个双精度(234.004223)等,我想把它四舍五入到C中的x个有效数字。
到目前为止,我只能找到将小数点四舍五入到x位的方法,但如果数字中有0,这只会消除精度。
例如,0.086到1位小数变成0.1,但我希望它保持在0.08。
框架没有将(或截断,如您的示例中所示)四舍五入为若干有效数字的内置函数。不过,实现这一点的一种方法是缩放数字,使第一个有效数字正好位于小数点后,四舍五入(或截断),然后缩小。下面的代码应该可以做到这一点:
1 2 3 4 5 6 7 | static double RoundToSignificantDigits(this double d, int digits){ if(d == 0) return 0; double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(d))) + 1); return scale * Math.Round(d / scale, digits); } |
如果,像在示例中一样,确实要截断,则需要:
1 2 3 4 5 6 7 | static double TruncateToSignificantDigits(this double d, int digits){ if(d == 0) return 0; double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(d))) + 1 - digits); return scale * Math.Truncate(d / scale); } |
我已经使用pdaddy的sigfig函数几个月了,发现了一个bug。您不能记录负数,因此如果d为负数,则结果为NaN。
以下更正了错误:
1 2 3 4 5 6 7 8 9 | public static double SetSigFigs(double d, int digits) { if(d == 0) return 0; decimal scale = (decimal)Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(d))) + 1); return (double) (scale * Math.Round((decimal)d / scale, digits)); } |
在我看来,你根本不想四舍五入到小数点后X位——你想四舍五入到有效数字X位。所以在您的示例中,您需要将0.086四舍五入到一个有效数字,而不是一个小数位。
现在,由于存储双精度数的方式,使用双精度数并四舍五入到多个有效数字开始是有问题的。例如,您可以将0.12调到接近0.1的值,但0.1并不完全可以表示为双精度值。你真的不应该用小数吗?或者,这实际上是为了显示吗?如果是为了显示目的,我怀疑您应该直接将double转换为一个具有相关有效数字数的字符串。
如果你能回答这些问题,我可以试着想出一些合适的代码。虽然听起来很糟糕,但将数字转换为"完整"字符串,然后找到第一个有效数字(然后在此之后采取适当的舍入操作),将其转换为字符串形式的有效数字可能是最好的方法。
如果是出于显示目的(如您在jon skeet答案的注释中所述),则应使用gn格式说明符。其中,n是有效数字的个数——正是您要查找的数字。
如果您需要3个有效数字(打印输出在每行的注释中),下面是使用示例:
1 2 3 4 5 6 7 8 9 10 11 | Console.WriteLine(1.2345e-10.ToString("G3"));//1.23E-10 Console.WriteLine(1.2345e-5.ToString("G3")); //1.23E-05 Console.WriteLine(1.2345e-4.ToString("G3")); //0.000123 Console.WriteLine(1.2345e-3.ToString("G3")); //0.00123 Console.WriteLine(1.2345e-2.ToString("G3")); //0.0123 Console.WriteLine(1.2345e-1.ToString("G3")); //0.123 Console.WriteLine(1.2345e2.ToString("G3")); //123 Console.WriteLine(1.2345e3.ToString("G3")); //1.23E+03 Console.WriteLine(1.2345e4.ToString("G3")); //1.23E+04 Console.WriteLine(1.2345e5.ToString("G3")); //1.23E+05 Console.WriteLine(1.2345e10.ToString("G3")); //1.23E+10 |
我在P爸爸和埃里克的方法中发现了两个错误。例如,这解决了安德鲁·汉考克斯在本次问答中提出的精度误差。还有一个圆形方向的问题。有两个有效数字的1050不是1000.0,而是1100.0。圆整采用中点圆整。A为零。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | static void Main(string[] args) { double x = RoundToSignificantDigits(1050, 2); // Old = 1000.0, New = 1100.0 double y = RoundToSignificantDigits(5084611353.0, 4); // Old = 5084999999.999999, New = 5085000000.0 double z = RoundToSignificantDigits(50.846, 4); // Old = 50.849999999999994, New = 50.85 } static double RoundToSignificantDigits(double d, int digits) { if (d == 0.0) { return 0.0; } else { double leftSideNumbers = Math.Floor(Math.Log10(Math.Abs(d))) + 1; double scale = Math.Pow(10, leftSideNumbers); double result = scale * Math.Round(d / scale, digits, MidpointRounding.AwayFromZero); // Clean possible precision error. if ((int)leftSideNumbers >= digits) { return Math.Round(result, 0, MidpointRounding.AwayFromZero); } else { return Math.Round(result, digits - (int)leftSideNumbers, MidpointRounding.AwayFromZero); } } } |
正如乔恩·斯基特所说:最好在文本领域处理这一点。通常情况下:出于显示目的,不要尝试舍入/更改浮点值,它永远不会100%工作。显示是第二个问题,您应该处理任何特殊的格式要求,比如使用字符串时的这些要求。
我下面的解决方案是几年前实施的,并且已经证明非常可靠。它经过了彻底的测试,性能也相当好。执行时间比P Daddy/Eric的解决方案长5倍。
以下代码中给出的输入+输出示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | using System; using System.Text; namespace KZ.SigDig { public static class SignificantDigits { public static string DecimalSeparator; static SignificantDigits() { System.Globalization.CultureInfo ci = System.Threading.Thread.CurrentThread.CurrentCulture; DecimalSeparator = ci.NumberFormat.NumberDecimalSeparator; } /// <summary> /// Format a double to a given number of significant digits. /// </summary> /// <example> /// 0.086 ->"0.09" (digits = 1) /// 0.00030908 ->"0.00031" (digits = 2) /// 1239451.0 ->"1240000" (digits = 3) /// 5084611353.0 ->"5085000000" (digits = 4) /// 0.00000000000000000846113537656557 ->"0.00000000000000000846114" (digits = 6) /// 50.8437 ->"50.84" (digits = 4) /// 50.846 ->"50.85" (digits = 4) /// 990.0 ->"1000" (digits = 1) /// -5488.0 ->"-5000" (digits = 1) /// -990.0 ->"-1000" (digits = 1) /// 0.0000789 ->"0.000079" (digits = 2) /// </example> public static string Format(double number, int digits, bool showTrailingZeros = true, bool alwaysShowDecimalSeparator = false) { if (Double.IsNaN(number) || Double.IsInfinity(number)) { return number.ToString(); } string sSign =""; string sBefore ="0"; // Before the decimal separator string sAfter =""; // After the decimal separator if (number != 0d) { if (digits < 1) { throw new ArgumentException("The digits parameter must be greater than zero."); } if (number < 0d) { sSign ="-"; number = Math.Abs(number); } // Use scientific formatting as an intermediate step string sFormatString ="{0:" + new String('#', digits) +"E0}"; string sScientific = String.Format(sFormatString, number); string sSignificand = sScientific.Substring(0, digits); int exponent = Int32.Parse(sScientific.Substring(digits + 1)); // (the significand now already contains the requested number of digits with no decimal separator in it) StringBuilder sFractionalBreakup = new StringBuilder(sSignificand); if (!showTrailingZeros) { while (sFractionalBreakup[sFractionalBreakup.Length - 1] == '0') { sFractionalBreakup.Length--; exponent++; } } // Place decimal separator (insert zeros if necessary) int separatorPosition = 0; if ((sFractionalBreakup.Length + exponent) < 1) { sFractionalBreakup.Insert(0,"0", 1 - sFractionalBreakup.Length - exponent); separatorPosition = 1; } else if (exponent > 0) { sFractionalBreakup.Append('0', exponent); separatorPosition = sFractionalBreakup.Length; } else { separatorPosition = sFractionalBreakup.Length + exponent; } sBefore = sFractionalBreakup.ToString(); if (separatorPosition < sBefore.Length) { sAfter = sBefore.Substring(separatorPosition); sBefore = sBefore.Remove(separatorPosition); } } string sReturnValue = sSign + sBefore; if (sAfter =="") { if (alwaysShowDecimalSeparator) { sReturnValue += DecimalSeparator +"0"; } } else { sReturnValue += DecimalSeparator + sAfter; } return sReturnValue; } } } |
double上的math.round()有缺陷(请参见其文档中的调用者注释)。后一步将舍入的数字乘以十进制指数,会在尾随数字中引入更多的浮点错误。使用另一轮()作为@rowanto不会有可靠的帮助,并且会遇到其他问题。但是,如果您愿意使用十进制,那么math.round()是可靠的,因为它是10的乘除:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | static ClassName() { powersOf10 = new decimal[28 + 1 + 28]; powersOf10[28] = 1; decimal pup = 1, pdown = 1; for (int i = 1; i < 29; i++) { pup *= 10; powersOf10[i + 28] = pup; pdown /= 10; powersOf10[28 - i] = pdown; } } /// <summary>Powers of 10 indexed by power+28. These are all the powers /// of 10 that can be represented using decimal.</summary> static decimal[] powersOf10; static double RoundToSignificantDigits(double v, int digits) { if (v == 0.0 || Double.IsNaN(v) || Double.IsInfinity(v)) { return v; } else { int decimal_exponent = (int)Math.Floor(Math.Log10(Math.Abs(v))) + 1; if (decimal_exponent < -28 + digits || decimal_exponent > 28 - digits) { // Decimals won't help outside their range of representation. // Insert flawed Double solutions here if you like. return v; } else { decimal d = (decimal)v; decimal scale = powersOf10[decimal_exponent + 28]; return (double)(scale * Math.Round(d / scale, digits, MidpointRounding.AwayFromZero)); } } } |
设
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | integerPortion = Math.truncate(**inputNumber**) decimalPortion = myNumber-IntegerPortion if( decimalPortion <> 0 ) { significantDigitsStartFrom = Math.Ceil(-log10(decimalPortion)) scaleRequiredForTruncation= Math.Pow(10,significantDigitsStartFrom-1+**significantDigitsRequired**) **siginficantDigitsResult** = integerPortion + ( Math.Truncate (decimalPortion*scaleRequiredForTruncation))/scaleRequiredForTruncation } else { **siginficantDigitsResult** = integerPortion } |
这个问题与你问的问题类似:
用C中的有效数字格式化数字#
因此,您可以执行以下操作:
1 2 | double Input2 = 234.004223; string Result2 = Math.Floor(Input2) + Convert.ToDouble(String.Format("{0:G1}", Input2 - Math.Floor(Input2))).ToString("R6"); |
四舍五入为1个有效数字。
我同意乔恩的评估精神:
Awful as it sounds, converting to a number of significant digits as a string by converting the number to a"full" string and then finding the first significant digit (and then taking appropriate rounding action after that) may well be the best way to go.
我需要有效的数字四舍五入,用于近似和非性能关键的计算目的,并且通过"g"格式解析往返格式已经足够好了:
1 2 3 4 | public static double RoundToSignificantDigits(this double value, int numberOfSignificantDigits) { return double.Parse(value.ToString("G" + numberOfSignificantDigits)); } |
这是我在C++中所做的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | /* I had this same problem I was writing a design sheet and the standard values were rounded. So not to give my values an advantage in a later comparison I need the number rounded, so I wrote this bit of code. It will round any double to a given number of significant figures. But I have a limited range written into the subroutine. This is to save time as my numbers were not very large or very small. But you can easily change that to the full double range, but it will take more time. Ross Mckinstray [email protected] */ #include <iostream> #include <fstream> #include <string> #include <math.h> #include <cmath> #include <iomanip> #using namespace std; double round_off(double input, int places) { double roundA; double range = pow(10, 10); // This limits the range of the rounder to 10/10^10 - 10*10^10 if you want more change range; for (double j = 10/range; j< 10*range;) { if (input >= j && input < j*10){ double figures = pow(10, places)/10; roundA = roundf(input/(j/figures))*(j/figures); } j = j*10; } cout <<" in sub after loop"; if (input <= 10/(10*10) && input >= 10*10) { roundA = input; cout <<" DID NOT ROUND change range"; } return roundA; } int main() { double number, sig_fig; do { cout <<" Enter number"; cin >> number; cout <<" Enter sig_fig"; cin >> sig_fig; double output = round_off(number, sig_fig); cout << setprecision(10); cout <<" I=" << number; cout <<" r=" <<output; cout <<" Enter 0 as number to exit loop"; } while (number != 0); return 0; } |
希望我没有改变任何格式。
我只是这样做的:
1 2 | int integer1 = Math.Round(double you want to round, significant figures you want to round to) |