在C#中使用有效数字格式化数字
我有一些十进制数据,我正在推入一个SharePoint列表,它将被查看。 我想根据我对特定计算的了解来限制结果数据中显示的有效数字的数量。 有时候会是3,所以12345会变成12300,0.012345会变成0.0123。 有时它会是4或5.有没有方便的方法来处理这个问题?
请参阅:“P爸爸” RoundToSignificantFigures 。
我已经把他的方法和我喜欢的另一个结合起来了。
舍入有效数字在TSQL中要容易得多,舍入方法是基于舍入位置,而不是小数位数 – 这就是.Net math.round的情况。 你可以将TSQL中的一个数字舍入到负数的位置,这个位置将会以整数进行舍入 – 所以不需要缩放。
也看到这个其他的线程 。 Pyrolistical的方法很好。
问题的尾部零似乎更像是一个string操作,所以我包含了一个ToString()扩展方法,如果需要,它将填充零。
using System; using System.Globalization; public static class Precision { // 2^-24 public const float FLOAT_EPSILON = 0.0000000596046448f; // 2^-53 public const double DOUBLE_EPSILON = 0.00000000000000011102230246251565d; public static bool AlmostEquals(this double a, double b, double epsilon = DOUBLE_EPSILON) { // ReSharper disable CompareOfFloatsByEqualityOperator if (a == b) { return true; } // ReSharper restore CompareOfFloatsByEqualityOperator return (System.Math.Abs(a - b) < epsilon); } public static bool AlmostEquals(this float a, float b, float epsilon = FLOAT_EPSILON) { // ReSharper disable CompareOfFloatsByEqualityOperator if (a == b) { return true; } // ReSharper restore CompareOfFloatsByEqualityOperator return (System.Math.Abs(a - b) < epsilon); } } public static class SignificantDigits { public static double Round(this double value, int significantDigits) { int unneededRoundingPosition; return RoundSignificantDigits(value, significantDigits, out unneededRoundingPosition); } public static string ToString(this double value, int significantDigits) { // this method will round and then append zeros if needed. // ie if you round .002 to two significant figures, the resulting number should be .0020. var currentInfo = CultureInfo.CurrentCulture.NumberFormat; if (double.IsNaN(value)) { return currentInfo.NaNSymbol; } if (double.IsPositiveInfinity(value)) { return currentInfo.PositiveInfinitySymbol; } if (double.IsNegativeInfinity(value)) { return currentInfo.NegativeInfinitySymbol; } int roundingPosition; var roundedValue = RoundSignificantDigits(value, significantDigits, out roundingPosition); // when rounding causes a cascading round affecting digits of greater significance, // need to re-round to get a correct rounding position afterwards // this fixes a bug where rounding 9.96 to 2 figures yeilds 10.0 instead of 10 RoundSignificantDigits(roundedValue, significantDigits, out roundingPosition); if (Math.Abs(roundingPosition) > 9) { // use exponential notation format // ReSharper disable FormatStringProblem return string.Format(currentInfo, "{0:E" + (significantDigits - 1) + "}", roundedValue); // ReSharper restore FormatStringProblem } // string.format is only needed with decimal numbers (whole numbers won't need to be padded with zeros to the right.) // ReSharper disable FormatStringProblem return roundingPosition > 0 ? string.Format(currentInfo, "{0:F" + roundingPosition + "}", roundedValue) : roundedValue.ToString(currentInfo); // ReSharper restore FormatStringProblem } private static double RoundSignificantDigits(double value, int significantDigits, out int roundingPosition) { // this method will return a rounded double value at a number of signifigant figures. // the sigFigures parameter must be between 0 and 15, exclusive. roundingPosition = 0; if (value.AlmostEquals(0d)) { roundingPosition = significantDigits - 1; return 0d; } if (double.IsNaN(value)) { return double.NaN; } if (double.IsPositiveInfinity(value)) { return double.PositiveInfinity; } if (double.IsNegativeInfinity(value)) { return double.NegativeInfinity; } if (significantDigits < 1 || significantDigits > 15) { throw new ArgumentOutOfRangeException("significantDigits", value, "The significantDigits argument must be between 1 and 15."); } // The resulting rounding position will be negative for rounding at whole numbers, and positive for decimal places. roundingPosition = significantDigits - 1 - (int)(Math.Floor(Math.Log10(Math.Abs(value)))); // try to use a rounding position directly, if no scale is needed. // this is because the scale mutliplication after the rounding can introduce error, although // this only happens when you're dealing with really tiny numbers, ie 9.9e-14. if (roundingPosition > 0 && roundingPosition < 16) { return Math.Round(value, roundingPosition, MidpointRounding.AwayFromZero); } // Shouldn't get here unless we need to scale it. // Set the scaling value, for rounding whole numbers or decimals past 15 places var scale = Math.Pow(10, Math.Ceiling(Math.Log10(Math.Abs(value)))); return Math.Round(value / scale, significantDigits, MidpointRounding.AwayFromZero) * scale; } }
这可能会诀窍:
double Input1 = 1234567; string Result1 = Convert.ToDouble(String.Format("{0:G3}",Input1)).ToString("R0"); double Input2 = 0.012345; string Result2 = Convert.ToDouble(String.Format("{0:G3}", Input2)).ToString("R6");
改变G3到G4产生了最奇怪的结果。 它似乎收集有效数字?
我最终从http://ostermiller.org/utils/SignificantFigures.java.html获取了一些代码。; 这是在Java中,所以我做了一个快速search/replace和一些resharper重新格式化,使C#构build。 这似乎很好地为我的重要人物的需求。 FWIW,我删除了他的javadoc评论,使其更简洁,但原始代码logging相当好。
/* * Copyright (C) 2002-2007 Stephen Ostermiller * http://ostermiller.org/contact.pl?regarding=Java+Utilities * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * See COPYING.TXT for details. */ public class SignificantFigures { private String original; private StringBuilder _digits; private int mantissa = -1; private bool sign = true; private bool isZero = false; private bool useScientificNotation = true; public SignificantFigures(String number) { original = number; Parse(original); } public SignificantFigures(double number) { original = Convert.ToString(number); try { Parse(original); } catch (Exception nfe) { _digits = null; } } public bool UseScientificNotation { get { return useScientificNotation; } set { useScientificNotation = value; } } public int GetNumberSignificantFigures() { if (_digits == null) return 0; return _digits.Length; } public SignificantFigures SetLSD(int place) { SetLMSD(place, Int32.MinValue); return this; } public SignificantFigures SetLMSD(int leastPlace, int mostPlace) { if (_digits != null && leastPlace != Int32.MinValue) { int significantFigures = _digits.Length; int current = mantissa - significantFigures + 1; int newLength = significantFigures - leastPlace + current; if (newLength <= 0) { if (mostPlace == Int32.MinValue) { original = "NaN"; _digits = null; } else { newLength = mostPlace - leastPlace + 1; _digits.Length = newLength; mantissa = leastPlace; for (int i = 0; i < newLength; i++) { _digits[i] = '0'; } isZero = true; sign = true; } } else { _digits.Length = newLength; for (int i = significantFigures; i < newLength; i++) { _digits[i] = '0'; } } } return this; } public int GetLSD() { if (_digits == null) return Int32.MinValue; return mantissa - _digits.Length + 1; } public int GetMSD() { if (_digits == null) return Int32.MinValue; return mantissa + 1; } public override String ToString() { if (_digits == null) return original; StringBuilder digits = new StringBuilder(this._digits.ToString()); int length = digits.Length; if ((mantissa <= -4 || mantissa >= 7 || (mantissa >= length && digits[digits.Length - 1] == '0') || (isZero && mantissa != 0)) && useScientificNotation) { // use scientific notation. if (length > 1) { digits.Insert(1, '.'); } if (mantissa != 0) { digits.Append("E" + mantissa); } } else if (mantissa <= -1) { digits.Insert(0, "0."); for (int i = mantissa; i < -1; i++) { digits.Insert(2, '0'); } } else if (mantissa + 1 == length) { if (length > 1 && digits[digits.Length - 1] == '0') { digits.Append('.'); } } else if (mantissa < length) { digits.Insert(mantissa + 1, '.'); } else { for (int i = length; i <= mantissa; i++) { digits.Append('0'); } } if (!sign) { digits.Insert(0, '-'); } return digits.ToString(); } public String ToScientificNotation() { if (_digits == null) return original; StringBuilder digits = new StringBuilder(this._digits.ToString()); int length = digits.Length; if (length > 1) { digits.Insert(1, '.'); } if (mantissa != 0) { digits.Append("E" + mantissa); } if (!sign) { digits.Insert(0, '-'); } return digits.ToString(); } private const int INITIAL = 0; private const int LEADZEROS = 1; private const int MIDZEROS = 2; private const int DIGITS = 3; private const int LEADZEROSDOT = 4; private const int DIGITSDOT = 5; private const int MANTISSA = 6; private const int MANTISSADIGIT = 7; private void Parse(String number) { int length = number.Length; _digits = new StringBuilder(length); int state = INITIAL; int mantissaStart = -1; bool foundMantissaDigit = false; // sometimes we don't know if a zero will be // significant or not when it is encountered. // keep track of the number of them so that // the all can be made significant if we find // out that they are. int zeroCount = 0; int leadZeroCount = 0; for (int i = 0; i < length; i++) { char c = number[i]; switch (c) { case '.': { switch (state) { case INITIAL: case LEADZEROS: { state = LEADZEROSDOT; } break; case MIDZEROS: { // we now know that these zeros // are more than just trailing place holders. for (int j = 0; j < zeroCount; j++) { _digits.Append('0'); } zeroCount = 0; state = DIGITSDOT; } break; case DIGITS: { state = DIGITSDOT; } break; default: { throw new Exception( "Unexpected character '" + c + "' at position " + i ); } } } break; case '+': { switch (state) { case INITIAL: { sign = true; state = LEADZEROS; } break; case MANTISSA: { state = MANTISSADIGIT; } break; default: { throw new Exception( "Unexpected character '" + c + "' at position " + i ); } } } break; case '-': { switch (state) { case INITIAL: { sign = false; state = LEADZEROS; } break; case MANTISSA: { state = MANTISSADIGIT; } break; default: { throw new Exception( "Unexpected character '" + c + "' at position " + i ); } } } break; case '0': { switch (state) { case INITIAL: case LEADZEROS: { // only significant if number // is all zeros. zeroCount++; leadZeroCount++; state = LEADZEROS; } break; case MIDZEROS: case DIGITS: { // only significant if followed // by a decimal point or nonzero digit. mantissa++; zeroCount++; state = MIDZEROS; } break; case LEADZEROSDOT: { // only significant if number // is all zeros. mantissa--; zeroCount++; state = LEADZEROSDOT; } break; case DIGITSDOT: { // non-leading zeros after // a decimal point are always // significant. _digits.Append(c); } break; case MANTISSA: case MANTISSADIGIT: { foundMantissaDigit = true; state = MANTISSADIGIT; } break; default: { throw new Exception( "Unexpected character '" + c + "' at position " + i ); } } } break; case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { switch (state) { case INITIAL: case LEADZEROS: case DIGITS: { zeroCount = 0; _digits.Append(c); mantissa++; state = DIGITS; } break; case MIDZEROS: { // we now know that these zeros // are more than just trailing place holders. for (int j = 0; j < zeroCount; j++) { _digits.Append('0'); } zeroCount = 0; _digits.Append(c); mantissa++; state = DIGITS; } break; case LEADZEROSDOT: case DIGITSDOT: { zeroCount = 0; _digits.Append(c); state = DIGITSDOT; } break; case MANTISSA: case MANTISSADIGIT: { state = MANTISSADIGIT; foundMantissaDigit = true; } break; default: { throw new Exception( "Unexpected character '" + c + "' at position " + i ); } } } break; case 'E': case 'e': { switch (state) { case INITIAL: case LEADZEROS: case DIGITS: case LEADZEROSDOT: case DIGITSDOT: { // record the starting point of the mantissa // so we can do a substring to get it back later mantissaStart = i + 1; state = MANTISSA; } break; default: { throw new Exception( "Unexpected character '" + c + "' at position " + i ); } } } break; default: { throw new Exception( "Unexpected character '" + c + "' at position " + i ); } } } if (mantissaStart != -1) { // if we had found an 'E' if (!foundMantissaDigit) { // we didn't actually find a mantissa to go with. throw new Exception( "No digits in mantissa." ); } // parse the mantissa. mantissa += Convert.ToInt32(number.Substring(mantissaStart)); } if (_digits.Length == 0) { if (zeroCount > 0) { // if nothing but zeros all zeros are significant. for (int j = 0; j < zeroCount; j++) { _digits.Append('0'); } mantissa += leadZeroCount; isZero = true; sign = true; } else { // a hack to catch some cases that we could catch // by adding a ton of extra states. Things like: // "e2" "+e2" "+." "." "+" etc. throw new Exception( "No digits in number." ); } } } public SignificantFigures SetNumberSignificantFigures(int significantFigures) { if (significantFigures <= 0) throw new ArgumentException("Desired number of significant figures must be positive."); if (_digits != null) { int length = _digits.Length; if (length < significantFigures) { // number is not long enough, pad it with zeros. for (int i = length; i < significantFigures; i++) { _digits.Append('0'); } } else if (length > significantFigures) { // number is too long chop some of it off with rounding. bool addOne; // we need to round up if true. char firstInSig = _digits[significantFigures]; if (firstInSig < '5') { // first non-significant digit less than five, round down. addOne = false; } else if (firstInSig == '5') { // first non-significant digit equal to five addOne = false; for (int i = significantFigures + 1; !addOne && i < length; i++) { // if its followed by any non-zero digits, round up. if (_digits[i] != '0') { addOne = true; } } if (!addOne) { // if it was not followed by non-zero digits // if the last significant digit is odd round up // if the last significant digit is even round down addOne = (_digits[significantFigures - 1] & 1) == 1; } } else { // first non-significant digit greater than five, round up. addOne = true; } // loop to add one (and carry a one if added to a nine) // to the last significant digit for (int i = significantFigures - 1; addOne && i >= 0; i--) { char digit = _digits[i]; if (digit < '9') { _digits[i] = (char) (digit + 1); addOne = false; } else { _digits[i] = '0'; } } if (addOne) { // if the number was all nines _digits.Insert(0, '1'); mantissa++; } // chop it to the correct number of figures. _digits.Length = significantFigures; } } return this; } public double ToDouble() { return Convert.ToDouble(original); } public static String Format(double number, int significantFigures) { SignificantFigures sf = new SignificantFigures(number); sf.SetNumberSignificantFigures(significantFigures); return sf.ToString(); } }
我有一个简短的答案来计算一个数字的有效数字 。 这里是代码和testing结果…
using System; using System.Collections.Generic; namespace ConsoleApplicationRound { class Program { static void Main(string[] args) { //char cDecimal = '.'; // for English cultures char cDecimal = ','; // for German cultures List<double> l_dValue = new List<double>(); ushort usSignificants = 5; l_dValue.Add(0); l_dValue.Add(0.000640589); l_dValue.Add(-0.000640589); l_dValue.Add(-123.405009); l_dValue.Add(123.405009); l_dValue.Add(-540); l_dValue.Add(540); l_dValue.Add(-540911); l_dValue.Add(540911); l_dValue.Add(-118.2); l_dValue.Add(118.2); l_dValue.Add(-118.18); l_dValue.Add(118.18); l_dValue.Add(-118.188); l_dValue.Add(118.188); foreach (double d in l_dValue) { Console.WriteLine("d = Maths.Round('" + cDecimal + "', " + d + ", " + usSignificants + ") = " + Maths.Round( cDecimal, d, usSignificants)); } Console.Read(); } } }
使用的math课程如下:
using System; using System.Text; namespace ConsoleApplicationRound { class Maths { /// <summary> /// The word "Window" /// </summary> private static String m_strZeros = "000000000000000000000000000000000"; /// <summary> /// The minus sign /// </summary> public const char m_cDASH = '-'; /// <summary> /// Determines the number of digits before the decimal point /// </summary> /// <param name="cDecimal"> /// Language-specific decimal separator /// </param> /// <param name="strValue"> /// Value to be scrutinised /// </param> /// <returns> /// Nr. of digits before the decimal point /// </returns> private static ushort NrOfDigitsBeforeDecimal(char cDecimal, String strValue) { short sDecimalPosition = (short)strValue.IndexOf(cDecimal); ushort usSignificantDigits = 0; if (sDecimalPosition >= 0) { strValue = strValue.Substring(0, sDecimalPosition + 1); } for (ushort us = 0; us < strValue.Length; us++) { if (strValue[us] != m_cDASH) usSignificantDigits++; if (strValue[us] == cDecimal) { usSignificantDigits--; break; } } return usSignificantDigits; } /// <summary> /// Rounds to a fixed number of significant digits /// </summary> /// <param name="d"> /// Number to be rounded /// </param> /// <param name="usSignificants"> /// Requested significant digits /// </param> /// <returns> /// The rounded number /// </returns> public static String Round(char cDecimal, double d, ushort usSignificants) { StringBuilder value = new StringBuilder(Convert.ToString(d)); short sDecimalPosition = (short)value.ToString().IndexOf(cDecimal); ushort usAfterDecimal = 0; ushort usDigitsBeforeDecimalPoint = NrOfDigitsBeforeDecimal(cDecimal, value.ToString()); if (usDigitsBeforeDecimalPoint == 1) { usAfterDecimal = (d == 0) ? usSignificants : (ushort)(value.Length - sDecimalPosition - 2); } else { if (usSignificants >= usDigitsBeforeDecimalPoint) { usAfterDecimal = (ushort)(usSignificants - usDigitsBeforeDecimalPoint); } else { double dPower = Math.Pow(10, usDigitsBeforeDecimalPoint - usSignificants); d = dPower*(long)(d/dPower); } } double dRounded = Math.Round(d, usAfterDecimal); StringBuilder result = new StringBuilder(); result.Append(dRounded); ushort usDigits = (ushort)result.ToString().Replace( Convert.ToString(cDecimal), "").Replace( Convert.ToString(m_cDASH), "").Length; // Add lagging zeros, if necessary: if (usDigits < usSignificants) { if (usAfterDecimal != 0) { if (result.ToString().IndexOf(cDecimal) == -1) { result.Append(cDecimal); } int i = (d == 0) ? 0 : Math.Min(0, usDigits - usSignificants); result.Append(m_strZeros.Substring(0, usAfterDecimal + i)); } } return result.ToString(); } } }
任何答案与较短的代码?
通过在Decimal上使用GetBits方法,并利用BigInteger执行掩码,您可以得到一个完美的四舍五入。
一些utils
public static int CountDigits (BigInteger number) => ((int)BigInteger.Log10(number))+1; private static readonly BigInteger[] BigPowers10 = Enumerable.Range(0, 100) .Select(v => BigInteger.Pow(10, v)) .ToArray();
主要function
public static decimal RoundToSignificantDigits (this decimal num, short n) { var bits = decimal.GetBits(num); var u0 = unchecked((uint)bits[0]); var u1 = unchecked((uint)bits[1]); var u2 = unchecked((uint)bits[2]); var i = new BigInteger(u0) + (new BigInteger(u1) << 32) + (new BigInteger(u2) << 64); var d = CountDigits(i); var delta = d - n; if (delta < 0) return num; var scale = BigPowers10[delta]; var div = i/scale; var rem = i%scale; var up = rem > scale/2; if (up) div += 1; var shifted = div*scale; bits[0] =unchecked((int)(uint) (shifted & BigUnitMask)); bits[1] =unchecked((int)(uint) (shifted>>32 & BigUnitMask)); bits[2] =unchecked((int)(uint) (shifted>>64 & BigUnitMask)); return new decimal(bits); }
testing用例0
public void RoundToSignificantDigits() { WMath.RoundToSignificantDigits(0.0012345m, 2).Should().Be(0.0012m); WMath.RoundToSignificantDigits(0.0012645m, 2).Should().Be(0.0013m); WMath.RoundToSignificantDigits(0.040000000000000008, 6).Should().Be(0.04); WMath.RoundToSignificantDigits(0.040000010000000008, 6).Should().Be(0.04); WMath.RoundToSignificantDigits(0.040000100000000008, 6).Should().Be(0.0400001); WMath.RoundToSignificantDigits(0.040000110000000008, 6).Should().Be(0.0400001); WMath.RoundToSignificantDigits(0.20000000000000004, 6).Should().Be(0.2); WMath.RoundToSignificantDigits(0.10000000000000002, 6).Should().Be(0.1); WMath.RoundToSignificantDigits(0.0, 6).Should().Be(0.0); }
testing用例1
public void RoundToSigFigShouldWork() { 1.2m.RoundToSignificantDigits(1).Should().Be(1m); 0.01235668m.RoundToSignificantDigits(3).Should().Be(0.0124m); 0.01m.RoundToSignificantDigits(3).Should().Be(0.01m); 1.23456789123456789123456789m.RoundToSignificantDigits(4) .Should().Be(1.235m); 1.23456789123456789123456789m.RoundToSignificantDigits(16) .Should().Be(1.234567891234568m); 1.23456789123456789123456789m.RoundToSignificantDigits(24) .Should().Be(1.23456789123456789123457m); 1.23456789123456789123456789m.RoundToSignificantDigits(27) .Should().Be(1.23456789123456789123456789m); }
我发现这篇文章做了一个快速search。 基本上这一个转换成一个string,并逐个string,一次一个,直到达到最大。 意义。 这会工作吗?
下面的代码不完全符合规范,因为它不会将小数点左边的任何内容舍入。 但是比这里介绍的任何东西都简单(到目前为止)。 我很惊讶,C#没有一个内置的方法来处理这个问题。
static public string SignificantDigits(double d, int digits=10) { int magnitude = (d == 0.0) ? 0 : (int)Math.Floor(Math.Log10(Math.Abs(d))) + 1; digits -= magnitude; if (digits < 0) digits = 0; string fmt = "f" + digits.ToString(); return d.ToString(fmt); }
据我记得,“有效数字”是指点分隔符后的数字位数,所以0.012345的3个有效数字将是0.012而不是0.0123,但这对解决scheme确实不重要。 我也明白,如果数字大于1,你想“取消”最后一位数字。你写12345将成为12300,但我不知道你是否想要123456成为1230000或123400? 我的解决scheme是最后一个。 如果你只有几个变化,你可以不用计算因子,而只是创build一个小的初始化数组。
private static string FormatToSignificantFigures(decimal number, int amount) { if (number > 1) { int factor = Factor(amount); return ((int)(number/factor)*factor).ToString(); } NumberFormatInfo nfi = new CultureInfo("en-US", false).NumberFormat; nfi.NumberDecimalDigits = amount; return(number.ToString("F", nfi)); } private static int Factor(int x) { return DoCalcFactor(10, x-1); } private static int DoCalcFactor(int x, int y) { if (y == 1) return x; return 10*DoCalcFactor(x, y - 1); }
亲切的问候Carsten