2015-12-27 49 views
4

我需要將用戶輸入解析爲一個數字並將其存儲在一個decimal變量中。解析十進制數字而不丟失有效數字

對我來說,不接受任何不能由decimal值正確表示的用戶輸入是非常重要的。

對於非常大(或非常小)的數字,此工作正常,因爲Parse方法在這些情況下拋出OverflowException

但是,如果某個數字的有效數字太多,則Parse方法將自動返回截斷(或舍入?)值。

例如,解析1.23456789123456789123456789123(30位有效數字)的結果值等於1.2345678912345678912345678912(29位有效數字)。

這是根據specification表示decimal值的精度爲28-29位有效數字。

但是,我需要能夠檢測(並拒絕)在解析時會被截斷的數字,因爲在我的情況下丟失有效數字是不可接受的。

什麼是最好的方式去做這件事?


請注意,通過字符串比較是預分析或驗證後不是簡單的路要走,因爲我需要支持各種特定文化的輸入和各種number styles(空格,千個分隔符,括號,指數語法等)。

因此,我正在尋找一個解決方案,而不需要重複由.NET提供的解析代碼。


我正在使用此解決方法來檢測包含28位或更多有效數字的輸入。雖然這個工作,它有效地限制所有輸入到至多27顯著位(而不是28-29):

/// <summary> 
///  Determines whether the specified value has 28 or more significant digits, 
///  in which case it must be rejected since it may have been truncated when 
///  we parsed it. 
/// </summary> 
static bool MayHaveBeenTruncated(decimal value) 
{ 
    const string format = "#.###########################e0"; 
    string str = value.ToString(format, CultureInfo.InvariantCulture); 
    return (str.LastIndexOf('e') - str.IndexOf('.')) > 27; 
} 
+0

你可以嘗試十進制轉換爲字符串,並與原來的比較。 – IllidanS4

+0

@ IllidanS4:但是它會拒絕所有未格式化的輸入,這些格式根據我用來將十進制值格式化爲字符串的格式。 –

+0

是的。如果小於28或29,則可以犧牲一位十進制數並計算轉換後的字符串中的位數。如果是,則某些數字可能已丟失,儘管它可能拒絕具有29位有效數字的輸入。 – IllidanS4

回答

1

讓我先狀態,有沒有「官方」的解決方案。通常我不會依賴內部實現,所以我提供以下內容,僅僅是因爲你說這對你解決問題非常重要。

如果你看一看參考源,你會看到,所有的解析方法是在(不幸的是內部)System.Number類實現。進一步調查中,decimal相關的方法是TryParseDecimalParseDecimal,他們都使用這樣的

byte* buffer = stackalloc byte[NumberBuffer.NumberBufferBytes]; 
var number = new NumberBuffer(buffer); 
if (TryStringToNumber(s, styles, ref number, numfmt, true)) 
{ 
    // other stuff 
}       

其中NumberBuffer是另一個內部struct。關鍵是整個解析發生在TryStringToNumber方法中,並且結果用於產生結果。我們感興趣的是一個名爲precisionNumberBuffer字段,它由上述方法填充。

與所有考慮到這一點,我們可以生成一個類似的方法只是調用基小數方法,以保證正常的驗證之後提取的精度/我們做我們的後期處理異常之前。因此,該方法會是這樣

static unsafe bool GetPrecision(string s, NumberStyles style, NumberFormatInfo numfmt) 
{ 
    byte* buffer = stackalloc byte[Number.NumberBuffer.NumberBufferBytes]; 
    var number = new NumberBuffer(buffer); 
    TryStringToNumber(s, styles, ref number, numfmt, true); 
    return number.precision; 
} 

但請記住,這些類型是內部的,以及他們的方法,因此很難適用正常反射,委託或Expression基礎的技術。幸運的是,使用System.Reflection.Emit編寫這種方法並不難。全面實施如下

public static class DecimalUtils 
{ 
    public static decimal ParseExact(string s, NumberStyles style = NumberStyles.Number, IFormatProvider provider = null) 
    { 
     // NOTE: Always call base method first 
     var value = decimal.Parse(s, style, provider); 
     if (!IsValidPrecision(s, style, provider)) 
      throw new InvalidCastException(); // TODO: throw appropriate exception 
     return value; 
    } 

    public static bool TryParseExact(string s, out decimal result, NumberStyles style = NumberStyles.Number, IFormatProvider provider = null) 
    { 
     // NOTE: Always call base method first 
     return decimal.TryParse(s, style, provider, out result) && !IsValidPrecision(s, style, provider); 
    } 

    static bool IsValidPrecision(string s, NumberStyles style, IFormatProvider provider) 
    { 
     var precision = GetPrecision(s, style, NumberFormatInfo.GetInstance(provider)); 
     return precision <= 29; 
    } 

    static readonly Func<string, NumberStyles, NumberFormatInfo, int> GetPrecision = BuildGetPrecisionFunc(); 
    static Func<string, NumberStyles, NumberFormatInfo, int> BuildGetPrecisionFunc() 
    { 
     const BindingFlags Flags = BindingFlags.Public | BindingFlags.NonPublic; 
     const BindingFlags InstanceFlags = Flags | BindingFlags.Instance; 
     const BindingFlags StaticFlags = Flags | BindingFlags.Static; 

     var numberType = typeof(decimal).Assembly.GetType("System.Number"); 
     var numberBufferType = numberType.GetNestedType("NumberBuffer", Flags); 

     var method = new DynamicMethod("GetPrecision", typeof(int), 
      new[] { typeof(string), typeof(NumberStyles), typeof(NumberFormatInfo) }, 
      typeof(DecimalUtils), true); 

     var body = method.GetILGenerator(); 
     // byte* buffer = stackalloc byte[Number.NumberBuffer.NumberBufferBytes]; 
     var buffer = body.DeclareLocal(typeof(byte*)); 
     body.Emit(OpCodes.Ldsfld, numberBufferType.GetField("NumberBufferBytes", StaticFlags)); 
     body.Emit(OpCodes.Localloc); 
     body.Emit(OpCodes.Stloc, buffer.LocalIndex); 
     // var number = new Number.NumberBuffer(buffer); 
     var number = body.DeclareLocal(numberBufferType); 
     body.Emit(OpCodes.Ldloca_S, number.LocalIndex); 
     body.Emit(OpCodes.Ldloc, buffer.LocalIndex); 
     body.Emit(OpCodes.Call, numberBufferType.GetConstructor(InstanceFlags, null, 
      new[] { typeof(byte*) }, null)); 
     // Number.TryStringToNumber(value, options, ref number, numfmt, true); 
     body.Emit(OpCodes.Ldarg_0); 
     body.Emit(OpCodes.Ldarg_1); 
     body.Emit(OpCodes.Ldloca_S, number.LocalIndex); 
     body.Emit(OpCodes.Ldarg_2); 
     body.Emit(OpCodes.Ldc_I4_1); 
     body.Emit(OpCodes.Call, numberType.GetMethod("TryStringToNumber", StaticFlags, null, 
      new[] { typeof(string), typeof(NumberStyles), numberBufferType.MakeByRefType(), typeof(NumberFormatInfo), typeof(bool) }, null)); 
     body.Emit(OpCodes.Pop); 
     // return number.precision; 
     body.Emit(OpCodes.Ldloca_S, number.LocalIndex); 
     body.Emit(OpCodes.Ldfld, numberBufferType.GetField("precision", InstanceFlags)); 
     body.Emit(OpCodes.Ret); 

     return (Func<string, NumberStyles, NumberFormatInfo, int>)method.CreateDelegate(typeof(Func<string, NumberStyles, NumberFormatInfo, int>)); 
    } 
} 

用它在你自己的風險:)

+0

出色的工作。謝謝!雖然,我想我仍然必須使用我的'ToString('#。##### ... ...)'解決方法來確保沒有重要的數字丟失。您的代碼總是測試29或更少,但十進制精度爲29 *或* 28. –

+0

是的,這是文檔中不清楚的東西之一,例如** ** ** [here](https://msdn.microsoft.com/en-us/ library/system.decimal(v = vs.110).aspx)說:「縮放因子隱含地是數字10,提高到從0到28的指數」所以可能檢查應該簡單地爲28?我知道你是指「十進制(C#參考)」,但我從來沒有見過小數點後的29位數的小數點的例子,是嗎?試過'十進制d = 0.00000000000000000000000000001m;'一個零點。 –

1

假設輸入是一個字符串,它已被證實是數字,你可以使用String.Split :

text = text.Trim().Replace(",", ""); 
bool neg = text.Contains("-"); 
if (neg) text = text.Replace("-", ""); 
while (text.Substring(0, 1) == 0 && text.Substring(0, 2) != "0." && text != "0") 
    text = text.Substring(1); 
if (text.Contains(".")) 
{ 
    while (text.Substring(text.Length - 1) == "0") 
     text = text.Substring(0, text.Length - 1); 
} 
if (text.Split(".")[0].Length + text.Split(".")[1].Length + (neg ? 1 : 0) <= 29) 
    valid = true; 

您可以覆蓋或替換分析幷包含此檢查。

+0

這可能適用於簡單輸入。但不處理前導/尾隨符號/空白/零,並且不考慮指數語法。 –

+0

確實如此,但可以在前面的解析檢查中進行分類。然後將分割值轉換回小數點以供OP使用。 – ouflak

+0

恐怕這種方法會導致相當複雜的解析檢查(考慮到文化設置,千位分隔符等)。我正在尋找更清潔的解決方案(如果可能的話), –

1

問題是,當您進行對話時會捨去舍入,即如果有多於28位小數,則Decimal myNumber = Decimal.Parse(myInput)將始終以舍入數字返回。

你不希望創建一個大的解析器要麼所以我會做的是與新的十進制值作爲字符串比較輸入字符串值:

//This is the string input from the user 
string myInput = "1.23456789123456789123456789123"; 

//This is the decimal conversation in your application 
Decimal myDecimal = Decimal.Parse(myInput); 

//This is the check to see if the input string value from the user is the same 
//after we parsed it to a decimal value. Now we need to parse it back to a string to verify 
//the two different string values: 
if(myInput.CompareTo(myDecimal.ToString()) == 0) 
    Console.WriteLine("EQUAL: Have NOT been rounded!"); 
else 
    Console.WriteLine("NOT EQUAL: Have been rounded!"); 

這樣,C#將處理所有的數東西,你只會做一個快速檢查。

+0

謝謝,但這會拒絕所有未根據默認的十進制字符串格式進行格式化的輸入。 –

+0

可以用的NumberStyles處理:https://msdn.microsoft.com/en-us/library/0kb11ck8(v=vs.110).aspx – Jefecito

+0

但後來我需要確切知道用​​戶已經使用哪一種風格。 –

1

你應該看看BigRational推測。它不是(還)?.Net框架的一部分,但它是BigInteger類的對等物,並提供了TryParse方法。通過這種方式,您應該能夠比較分析的BigRational是否等於解析的十進制。

+0

這聽起來很有希望。但是,除了由'BigInteger'提供的實現外,我找不到'TryParse'實現。 –

+0

Transit C#如何實現:https://github.com/NForza/transit-csharp/blob/master/src/Transit/Numerics/BigRational.cs – srandppl

+0

不幸的是,解析函數不支持任何.NET編號樣式這是我的情況的要求。 –