四舍五入为两位数到 X 位数

如果我有一个 double (234.004223) ,等等,我希望把它四舍五入到 C # 中的 x 有效数字。

到目前为止,我只能找到四舍五入到小数点后的 x 位的方法,但是如果数字中有任何0,这只会消除精度。

例如,0.086到小数点后一位变成0.1,但我希望它保持在0.08。

73698 次浏览

这个问题和你问的问题很相似:

在 C # 中使用有效数字格式化数字

因此,你可以这样做:

double Input2 = 234.004223;
string Result2 = Math.Floor(Input2) + Convert.ToDouble(String.Format("{0:G1}", Input2 - Math.Floor(Input2))).ToString("R6");

四舍五入到1个有效数字。

在我看来,你根本不想四舍五入到小数点后的位置——你想四舍五入到小数点后的有效数字。因此在您的示例中,您希望四舍五入0.086到一个有效数字,而不是一个小数位。

现在,由于双精度数的存储方式,一开始使用双精度数并四舍五入到多个有效数字是有问题的。例如,您可以将0.12舍入到某个值 差不多到0.1,但是0.1并不完全表示为 double。你确定不用小数吗?或者,这实际上是为了显示目的吗?如果是为了显示的目的,我怀疑你应该直接把 double 转换成一个有相应数量有效数字的字符串。

如果你能回答这些问题,我可以试着想出一些合适的代码。尽管听起来很糟糕,但是将一个数字转换为一个“完整”字符串,然后找到第一个有效数字(然后采取适当的舍入操作) ,从而将一个有效数字转换为一个字符串,这可能是最好的方法。

这个框架没有一个内置的函数来调整(或者截断,就像你的例子)到一些有效数字。不过,有一种方法可以做到这一点,那就是缩放你的数字,这样你的第一个有效数字就在小数点后面,四舍五入(或者截断) ,然后再缩放。下面的代码可以解决这个问题:

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);
}

如果像你的例子一样,你真的想要截断,那么你需要:

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);
}

inputNumber作为输入,需要在小数点后用 significantDigitsRequired进行转换,那么 significantDigitsResult就是以下伪代码的答案。

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


}

我已经使用 pDaddy 的 sigfig 函数几个月了,发现其中有一个 bug。不能取负数的 Log,因此如果 d 为负,则结果为 NaN。

下面更正了这个错误:

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));
}

我刚说了:

int integer1 = Math.Round(double you want to round,
significant figures you want to round to)

我在 P 老爹和 Eric 的方法中发现了两个错误。这解决了例如 Andrew Hancox 在本问答中提出的精度误差。圆形方向也有问题。有两个重要数字的1050不是1000.0而是1100.0。四舍五入与中点四舍五入是固定的。远离零。

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);
}
}
}

这是我在 C + + 中做的

/*
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
rmckinstray01@gmail.com
*/


#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 << "\n in sub after loop";
if (input <= 10/(10*10) && input >= 10*10) {
roundA = input;
cout << "\nDID NOT ROUND change range";
}
return roundA;
}


int main() {
double number, sig_fig;


do {
cout << "\nEnter number ";
cin >> number;
cout << "\nEnter sig_fig ";
cin >> sig_fig;
double output = round_off(number, sig_fig);


cout << setprecision(10);
cout << "\n I= " << number;
cout << "\n r= " <<output;
cout << "\nEnter 0 as number to exit loop";
}
while (number != 0);


return 0;
}

希望我没有改变任何格式化它。

如果是为了显示目的(正如您在对 Jon Skeet 的回答的评论中所说的那样) ,那么应该使用 Gn格式说明符。其中 ABc1是有效数字的数量——正是你所追求的。

如果你想要3个有效数字,这里是一个使用的例子(打印输出在每行的注释中) :

    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

数学。Round () on double 是有缺陷的(请参阅 文件中的调用方说明)。后面的步骤是将四舍五入的数字乘以它的十进制指数,这将在后面的数字中引入更多的浮点错误。像@Rowanto 那样使用另一个 Round ()不会有可靠的帮助,而且还会遇到其他问题。然而,如果你愿意通过十进制,然后数学。Round ()是可靠的,正如乘以和除以10的幂一样:

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));
}
}
}

正如 Jon Skeet 提到的: 最好在文本域中处理这个问题。作为一个规则: 出于显示的目的,不要试图舍入/更改浮点值,它从来没有100% 工作过。显示是次要关注的问题,您应该处理任何特殊的格式化要求,比如使用字符串时的这些要求。

我的解决方案如下,我实施了几年前,并已证明非常可靠。它经过了彻底的测试,性能也很好。执行时间是 P Daddy/Eric 解决方案的5倍。

下面的代码给出了输入 + 输出的示例。

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;
}
}
}

我同意 乔恩的评估的精神:

尽管听起来很糟糕,但是将一个数字转换为一个“完整”字符串,然后找到第一个有效数字(然后采取适当的舍入操作) ,从而将一个有效数字转换为一个字符串,这可能是最好的方法。

为了 大概非性能关键型的计算目的,我需要有意义的数字舍入,格式解析通过“ G”格式的往返就足够好了:

public static double RoundToSignificantDigits(this double value, int numberOfSignificantDigits)
{
return double.Parse(value.ToString("G" + numberOfSignificantDigits));
}

对我来说,这个方法非常有效,对负数也有效:

public static double RoundToSignificantDigits(double number, int digits)
{
int sign = Math.Sign(number);


if (sign < 0)
number *= -1;


if (number == 0)
return 0;


double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(number))) + 1);
return sign * scale * Math.Round(number / scale, digits);
}

正如@Oliver Bock 指出的那样,数学。Round () on double 是有缺陷的(请参阅 文件中的调用方说明)。后面的步骤是将四舍五入的数字乘以它的十进制指数,这将在后面的数字中引入更多的浮点错误。一般来说,任何乘以或除以十的运算都会得到一个不精确的结果,因为浮点数通常是用二进制表示的,而不是十进制。

使用以下函数可避免尾随数字中的浮点错误:

static double RoundToSignificantDigits(double d, int digits)
{
if (d == 0.0 || Double.IsNaN(d) || Double.IsInfinity(d))
{
return d;
}
// Compute shift of the decimal point.
int shift = digits - 1 - (int)Math.Floor(Math.Log10(Math.Abs(d)));


// Return if rounding to the same or higher precision.
int decimalPlaces = 0;
for (long pow = 1; Math.Floor(d * pow) != (d * pow); pow *= 10) decimalPlaces++;
if (shift >= decimalPlaces)
return d;


// Round to sf-1 fractional digits of normalized mantissa x.dddd
double scale = Math.Pow(10, Math.Abs(shift));
return shift > 0 ?
Math.Round(d * scale, MidpointRounding.AwayFromZero) / scale :
Math.Round(d / scale, MidpointRounding.AwayFromZero) * scale;
}

然而,如果你愿意通过十进制,然后数学。Round ()是可靠的,正如乘以和除以10的幂一样:

static double RoundToSignificantDigits(double d, int digits)
{
if (d == 0.0 || Double.IsNaN(d) || Double.IsInfinity(d))
{
return d;
}
decimal scale = (decimal)Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(d))) + 1);
return (double)(scale * Math.Round((decimal)d / scale, digits, MidpointRounding.AwayFromZero));
}


Console.WriteLine("{0:G17}", RoundToSignificantDigits(5.015 * 100, 15)); // 501.5

我的解决方案在某些情况下可能会有帮助,我使用它来显示加密价格的大小差异很大-它总是给我一个指定的重要数字的数字,但不像 ToString (“ g [数字的数字]”)它不显示小的科学记数法值(不知道如何避免这与 ToString () ,如果有那么请让我知道!)

    const int MIN_SIG_FIGS = 6; //will be one more for < 0
int numZeros = (int)Math.Floor(Math.Log10(Math.Abs(price))); //get number of zeros before first digit, will be negative for price > 0
int decPlaces = numZeros < MIN_SIG_FIGS
? MIN_SIG_FIGS - numZeros < 0
? 0
: MIN_SIG_FIGS - numZeros
: 0; //dec. places: set to MIN_SIG_FIGS + number of zeros, unless numZeros greater than sig figs then no decimal places
return price.ToString($"F{decPlaces}");

在.NET 6.0上测试

在我看来,由于框架的缺陷和浮点数的错误,圆整的结果是不一致的。因此,使用时要小心。

decimal.Parse(doubleValue.ToString("E"), NumberStyles.Float);

例如:

using System.Diagnostics;
using System.Globalization;


List<double> doubleList = new();
doubleList.Add(    0.012345);
doubleList.Add(    0.12345 );
doubleList.Add(    1.2345  );
doubleList.Add(   12.345   );
doubleList.Add(  123.45    );
doubleList.Add( 1234.5     );
doubleList.Add(12345       );
doubleList.Add(10  );
doubleList.Add( 0  );
doubleList.Add( 1  );
doubleList.Add(-1  );
doubleList.Add( 0.1);


Debug.WriteLine("");
foreach (var item in doubleList)
{
Debug.WriteLine(decimal.Parse(item.ToString("E2"), NumberStyles.Float));


// 0.0123
// 0.123
// 1.23
// 12.3
// 123
// 1230
// 12300
// 10.0
// 0.00
// 1.00
// -1.00
// 0.100
}


Debug.WriteLine("");
foreach (var item in doubleList)
{
Debug.WriteLine(decimal.Parse(item.ToString("E3"), NumberStyles.Float));


// 0.01235
// 0.1235
// 1.234
// 12.35
// 123.5
// 1234
// 12340
// 10.00
// 0.000
// 1.000
// -1.000
// 0.1000
}

这个版本的灵感来自 Peter Mortensen,它为边缘情况增加了一些保护措施,比如 NaN、 Inf 或者非常小的值:

public static double RoundToSignificantDigits(this double value, int digits)
{
if (double.IsNaN(value) || double.IsInfinity(value))
return value;
if (value == 0.0)
return 0.0;
double leftSideNumbers = Math.Floor(Math.Log10(Math.Abs(value))) + 1;
int places = digits - (int)leftSideNumbers;
if (places > 15)
return 0.0;
double scale = Math.Pow(10, leftSideNumbers);
double result = scale * Math.Round(value / scale, digits, MidpointRounding.AwayFromZero);
if (places < 0)
places = 0;
return Math.Round(result, places, MidpointRounding.AwayFromZero);
}