目前我只能找到舍入到x位小数的方法,但如果数字中有0,则这样做会丢失精度。
例如,将0.086舍入到一位小数变成0.1,但我希望它保持在0.08。
这个框架没有内置函数用于将数字四舍五入(或像您的例子一样截断)到指定数量的有效数字。但是,您可以通过调整数字的比例使第一个有效数字位于小数点右侧,然后进行四舍五入(或截断),最后再调整回来。以下代码应该能解决问题:
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);
}
我已经使用了pDaddy的sigfig函数几个月,发现其中有一个bug。当d为负数时,无法对其取对数,因此结果会是NaN。
以下是修正该bug的代码:
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));
}
0.073699979999999998
。我会尽力将其翻译得更加通俗易懂,但不会改变原来的意思。 - LearningJrDev我觉得您并不想将数字四舍五入到x位小数,而是想将其四舍五入到x个有效数字。因此,在您的示例中,您想将0.086四舍五入为一个有效数字,而不是一位小数。
现在,使用double并舍入到指定数量的有效数字是有问题的,因为double类型的存储方式不同。例如,您可以将0.12四舍五入为接近0.1的数字,但0.1不能完全作为double类型表示。您确定您是否应该实际使用decimal类型?或者,这实际上是用于显示目的吗?如果是用于显示目的,我认为您应该直接将double转换为带有相应数量有效数字的字符串。
如果您能回答这些问题,我可以尝试提供一些适当的代码。虽然听起来很糟糕,将数字转换为“完整”字符串,然后找到第一个有效数字(然后在此之后采取适当的舍入操作)可能是最好的方法。
如果是为了显示目的(如您在对Jon Skeet的回答评论中所述),您应该使用Gn格式说明符,其中n是有效数字的数量 - 正好是您想要的。
以下是如果您希望显示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
G4
将从1.000
中除去零--> 1
。另外,它会自行决定是否强制使用科学计数法,无论您是否喜欢。 - u8itsubmittedValue.ToString("G12")
和 returnedValue.ToString("G12")
(在我的情况下足够精确)。 - fero我发现P Daddy和Eric的方法中存在两个bug。这解决了Andrew Hancox在这个问答中提出的精度错误的问题。还有一个四舍五入方向的问题。带有两个有效数字的1050不是1000.0,而是1100.0。使用MidpointRounding.AwayFromZero修复了四舍五入问题。
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);
}
}
}
这里有一个解决方案,可以用一个词来形容 - 可靠。它总是有效的,原因是它使用字符串格式化进行操作。使用 Log
和 Pow
会导致特殊值问题,无法避免,这很复杂。
示例值表:
value digits returns
=========================================================
0.086 1 "0.09"
1.0 3 "1.00"
0.1 3 "0.100"
0.00030908 2 "0.00031"
1239451 3 "1240000"
5084611353 4 "5085000000"
8.46113537656557E-18 6 "0.00000000000000000846114"
50.8437 4 "50.84"
50.846 4 "50.85"
990 1 "1000"
-5488 1 "-5000"
-990 1 "-1000"
7.89E-05 2 "0.000079"
50.84611353765656 6 "50.8461"
0.073699979 7 "0.07369998"
0 2 "0"
这里是代码:
public static class SignificantDigits
{
public static readonly 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>
public static string Format(double value, int digits, bool showTrailingZeros = true, bool alwaysShowDecimalSeparator = false)
{
if (double.IsNaN(value) ||
double.IsInfinity(value))
{
return value.ToString();
}
string sign = "";
string before = "0"; // Before the decimal separator
string after = ""; // After the decimal separator
if (value != 0d)
{
if (digits < 1)
{
throw new ArgumentException("The digits parameter must be greater than zero.");
}
if (value < 0d)
{
sign = "-";
value = -value;
}
// Custom exponential formatting using '#' will give us exactly our digits plus an exponent
string scientific = value.ToString(new string('#', digits) + "E0");
string significand = scientific.Substring(0, digits);
int exponent = int.Parse(scientific.Substring(digits + 1));
// (significand now already contains the requested number of digits with no decimal separator in it)
var fractionalBreakup = new StringBuilder(significand);
if (!showTrailingZeros)
{
while (fractionalBreakup[fractionalBreakup.Length - 1] == '0')
{
fractionalBreakup.Length--;
exponent++;
}
}
// Place the decimal separator (insert zeros if necessary)
int separatorPosition;
if ((fractionalBreakup.Length + exponent) < 1)
{
fractionalBreakup.Insert(0, "0", 1 - fractionalBreakup.Length - exponent);
separatorPosition = 1;
}
else if (exponent > 0)
{
fractionalBreakup.Append('0', exponent);
separatorPosition = fractionalBreakup.Length;
}
else
{
separatorPosition = fractionalBreakup.Length + exponent;
}
before = fractionalBreakup.ToString();
if (separatorPosition < before.Length)
{
after = before.Substring(separatorPosition);
before = before.Remove(separatorPosition);
}
}
string result = sign + before;
if (after == "")
{
if (alwaysShowDecimalSeparator)
{
result += DecimalSeparator + "0";
}
}
else
{
result += DecimalSeparator + after;
}
return result;
}
}
我需要对近似值和非性能关键的计算目的进行有效数字四舍五入,通过“G”格式的格式化解析来实现四舍五入也是足够好的:听起来很糟糕,但是通过将数字转换为“完整”的字符串,并找到第一个有效数字(然后在此之后采取适当的四舍五入动作),将其转换为重要数字字符串可能是最佳选择。
public static double RoundToSignificantDigits(this double value, int numberOfSignificantDigits)
{
return double.Parse(value.ToString("G" + numberOfSignificantDigits));
}
在double类型上使用Math.Round()存在缺陷(请参见其文档中的“调用者注意事项”)。将四舍五入后的数字乘回其小数指数的后续步骤会在尾数中引入进一步的浮点误差。如@Rowanto所做的另一个Round()也不能可靠地帮助并且存在其他问题。然而,如果您愿意通过decimal进行操作,则Math.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));
}
}
}
在.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
}
这个问题与您提出的问题类似:
因此,您可以执行以下操作:
double Input2 = 234.004223;
string Result2 = Math.Floor(Input2) + Convert.ToDouble(String.Format("{0:G1}", Input2 - Math.Floor(Input2))).ToString("R6");