2009-04-18 29 views
22

Giả sử rằng chúng ta có một số System.Decimal.Tính toán Độ chính xác và quy mô của hệ thống

Để minh hoạ, chúng ta hãy xem một người có ToString() đại diện như sau:

d.ToString() = "123.4500" 

Sau đây có thể nói về một số thập phân này. Đối với mục đích của chúng tôi ở đây, tỷ lệ được định nghĩa là số chữ số ở bên phải của dấu thập phân. Quy mô hiệu quả là tương tự nhưng bỏ qua bất kỳ số không dấu nào xảy ra trong phần phân số. (Nói cách khác, các thông số này được định nghĩa như thập phân SQL cộng với một số thông số bổ sung để giải thích cho khái niệm System.Decimal của trailing zeros trong phần phân đoạn.)

  • chính xác: 7
  • Quy mô: 4
  • EffectivePrecision: 5
  • EffectiveScale: 2

với một System.Decimal tùy ý, làm thế nào tôi có thể tính toán tất cả bốn người trong số các thông số một cách hiệu quả và không có chuyển đổi vào một string và kiểm tra chuỗi? Giải pháp có thể yêu cầu Decimal.GetBits.

Một số chi tiết ví dụ:

Examples Precision Scale EffectivePrecision EffectiveScale 
0  1 (?)  0  1 (?)    0 
0.0  2 (?)  1  1 (?)    0 
12.45 4   2  4     2 
12.4500 6   4  4     2 
770  3   0  3     0 

Ngoài giải thích những độ chính xác như zero sẽ là tốt (?).

Trả lời

25

Có, bạn cần sử dụng Decimal.GetBits. Thật không may, sau đó bạn phải làm việc với một số nguyên 96-bit, và không có loại số nguyên đơn giản trong .NET mà đối phó với 96 bit. Mặt khác, có thể bạn có thể sử dụng Decimal chính nó ...

Đây là một số mã sản xuất cùng số như ví dụ của bạn. Hy vọng bạn thấy hữu ích :)

using System; 

public class Test 
{ 
    static public void Main(string[] x) 
    { 
     ShowInfo(123.4500m); 
     ShowInfo(0m); 
     ShowInfo(0.0m); 
     ShowInfo(12.45m); 
     ShowInfo(12.4500m); 
     ShowInfo(770m); 
    } 

    static void ShowInfo(decimal dec) 
    { 
     // We want the integer parts as uint 
     // C# doesn't permit int[] to uint[] conversion, 
     // but .NET does. This is somewhat evil... 
     uint[] bits = (uint[])(object)decimal.GetBits(dec); 


     decimal mantissa = 
      (bits[2] * 4294967296m * 4294967296m) + 
      (bits[1] * 4294967296m) + 
      bits[0]; 

     uint scale = (bits[3] >> 16) & 31; 

     // Precision: number of times we can divide 
     // by 10 before we get to 0   
     uint precision = 0; 
     if (dec != 0m) 
     { 
      for (decimal tmp = mantissa; tmp >= 1; tmp /= 10) 
      { 
       precision++; 
      } 
     } 
     else 
     { 
      // Handle zero differently. It's odd. 
      precision = scale + 1; 
     } 

     uint trailingZeros = 0; 
     for (decimal tmp = mantissa; 
      tmp % 10m == 0 && trailingZeros < scale; 
      tmp /= 10) 
     { 
      trailingZeros++; 
     } 

     Console.WriteLine("Example: {0}", dec); 
     Console.WriteLine("Precision: {0}", precision); 
     Console.WriteLine("Scale: {0}", scale); 
     Console.WriteLine("EffectivePrecision: {0}", 
          precision - trailingZeros); 
     Console.WriteLine("EffectiveScale: {0}", scale - trailingZeros); 
     Console.WriteLine(); 
    } 
} 
+0

Cảm ơn bạn, điều này rất thú vị. Không gần như nhanh như cạo thông tin ra khỏi ToString, như tôi hiển thị trong một bài đăng riêng biệt. –

+0

Jon, nếu bạn nghĩ rằng bạn (uint []) (đối tượng) cast là ác (tôi đồng ý), thì tại sao bạn không sử dụng một cách gọn gàng hơn và rõ ràng hơn? – Joren

+0

Tôi sẽ chỉ ra rằng mã này trả về độ chính xác và tỷ lệ sử dụng các định nghĩa ký hiệu khoa học. Ví dụ: 0,005 có Độ chính xác = 1 và Tỷ lệ = 3. Đó là cách làm thông thường, nhưng cách diễn giải khác của các tham số này là theo kiểu thập phân SQL. Bằng cách này, Precision = 3 và Scale = 3. Đó là số thập phân SQL nhỏ nhất bạn có thể sử dụng để giữ 0,005. Câu hỏi ban đầu đã đề cập một cách ngắn gọn các số thập phân SQL (nhưng không đưa ra một ví dụ về trường hợp này, nhờ Jon Seigel, người gần đây đã chỉ ra cho tôi). Việc triển khai của tôi (FastInfo bên dưới) cung cấp loại chính xác và quy mô sau. Cảm ơn! –

10

Sử dụng ToString nhanh hơn khoảng 10 lần so với giải pháp của Jon Skeet. Trong khi điều này là hợp lý nhanh chóng, thách thức ở đây (nếu có bất kỳ takers!) Là để đánh bại hiệu suất của ToString.

Các kết quả thực hiện tôi nhận được từ các chương trình thử nghiệm sau đây: ShowInfo 239 ms FastInfo 25 ms

using System; 
using System.Diagnostics; 
using System.Globalization; 

public class Test 
{ 
    static public void Main(string[] x) 
    { 
     Stopwatch sw1 = new Stopwatch(); 
     Stopwatch sw2 = new Stopwatch(); 

     sw1.Start(); 
     for (int i = 0; i < 10000; i++) 
     { 
      ShowInfo(123.4500m); 
      ShowInfo(0m); 
      ShowInfo(0.0m); 
      ShowInfo(12.45m); 
      ShowInfo(12.4500m); 
      ShowInfo(770m); 
     } 
     sw1.Stop(); 

     sw2.Start(); 
     for (int i = 0; i < 10000; i++) 
     { 
      FastInfo(123.4500m); 
      FastInfo(0m); 
      FastInfo(0.0m); 
      FastInfo(12.45m); 
      FastInfo(12.4500m); 
      FastInfo(770m); 
     } 
     sw2.Stop(); 

     Console.WriteLine(sw1.ElapsedMilliseconds); 
     Console.WriteLine(sw2.ElapsedMilliseconds); 
     Console.ReadLine(); 
    } 

    // Be aware of how this method handles edge cases. 
    // A few are counterintuitive, like the 0.0 case. 
    // Also note that the goal is to report a precision 
    // and scale that can be used to store the number in 
    // an SQL DECIMAL type, so this does not correspond to 
    // how precision and scale are defined for scientific 
    // notation. The minimal precision SQL decimal can 
    // be calculated by subtracting TrailingZeros as follows: 
    // DECIMAL(Precision - TrailingZeros, Scale - TrailingZeros). 
    // 
    //  dec Precision Scale TrailingZeros 
    // ------- --------- ----- ------------- 
    // 0    1  0    0 
    // 0.0    2  1    1 
    // 0.1    1  1    0 
    // 0.01   2  2    0 [Diff result than ShowInfo] 
    // 0.010   3  3    1 [Diff result than ShowInfo] 
    // 12.45   4  2    0 
    // 12.4500   6  4    2 
    // 770    3  0    0 
    static DecimalInfo FastInfo(decimal dec) 
    { 
     string s = dec.ToString(CultureInfo.InvariantCulture); 

     int precision = 0; 
     int scale = 0; 
     int trailingZeros = 0; 
     bool inFraction = false; 
     bool nonZeroSeen = false; 

     foreach (char c in s) 
     { 
      if (inFraction) 
      { 
       if (c == '0') 
        trailingZeros++; 
       else 
       { 
        nonZeroSeen = true; 
        trailingZeros = 0; 
       } 

       precision++; 
       scale++; 
      } 
      else 
      { 
       if (c == '.') 
       { 
        inFraction = true; 
       } 
       else if (c != '-') 
       { 
        if (c != '0' || nonZeroSeen) 
        { 
         nonZeroSeen = true; 
         precision++; 
        } 
       } 
      } 
     } 

     // Handles cases where all digits are zeros. 
     if (!nonZeroSeen) 
      precision += 1; 

     return new DecimalInfo(precision, scale, trailingZeros); 
    } 

    struct DecimalInfo 
    { 
     public int Precision { get; private set; } 
     public int Scale { get; private set; } 
     public int TrailingZeros { get; private set; } 

     public DecimalInfo(int precision, int scale, int trailingZeros) 
      : this() 
     { 
      Precision = precision; 
      Scale = scale; 
      TrailingZeros = trailingZeros; 
     } 
    } 

    static DecimalInfo ShowInfo(decimal dec) 
    { 
     // We want the integer parts as uint 
     // C# doesn't permit int[] to uint[] conversion, 
     // but .NET does. This is somewhat evil... 
     uint[] bits = (uint[])(object)decimal.GetBits(dec); 


     decimal mantissa = 
      (bits[2] * 4294967296m * 4294967296m) + 
      (bits[1] * 4294967296m) + 
      bits[0]; 

     uint scale = (bits[3] >> 16) & 31; 

     // Precision: number of times we can divide 
     // by 10 before we get to 0 
     uint precision = 0; 
     if (dec != 0m) 
     { 
      for (decimal tmp = mantissa; tmp >= 1; tmp /= 10) 
      { 
       precision++; 
      } 
     } 
     else 
     { 
      // Handle zero differently. It's odd. 
      precision = scale + 1; 
     } 

     uint trailingZeros = 0; 
     for (decimal tmp = mantissa; 
      tmp % 10m == 0 && trailingZeros < scale; 
      tmp /= 10) 
     { 
      trailingZeros++; 
     } 

     return new DecimalInfo((int)precision, (int)scale, (int)trailingZeros); 
    } 
} 
+2

Tôi không hoàn toàn ngạc nhiên - chúng tôi đang thực hiện rất nhiều thao tác với số thập phân chỉ đơn giản là do thiếu loại số nguyên 96 bit. Nếu bạn sử dụng ulong thay vì thập phân cho mantissa bằng cách bỏ qua hoàn toàn 32 bit đầu, nó nhanh hơn FastInfo một chút - nhưng tất nhiên, nó không hoạt động với tất cả các số thập phân! Tôi nghi ngờ rằng chúng tôi có thể cải thiện tốc độ bằng cách đếm cả độ chính xác và số không theo sau trong một vòng lặp (vì cả hai chia cho 10 mỗi lần). –

+3

Thuật toán dựa trên chuỗi tạo ra kết quả không chính xác cho các số có số 0 đứng đầu, tức là 0,555 và các số có số không nằm giữa các chữ số thập phân và số có nghĩa, nghĩa là 0,0005. –

+0

Cảm ơn, tôi đã điều chỉnh mã. Tôi cũng đã thêm một bình luận vào mã trên phương thức FastInfo. Điều này chỉ ra rằng phương thức này sử dụng các định nghĩa chính xác và định nghĩa của SQL, không phải là các định nghĩa khoa học thông thường. (Đề cập rằng điều này là để đối phó với số thập phân SQL là trong câu hỏi ban đầu.) –

23

Tôi đã xem qua bài viết này khi tôi cần thiết để xác nhận độ chính xác và quy mô trước khi viết một giá trị thập phân để Một cơ sở dữ liệu. Tôi đã thực sự đưa ra một cách khác nhau để đạt được điều này bằng cách sử dụng System.Data.SqlTypes.SqlDecimal mà hóa ra là nhanh hơn hai phương thức khác được thảo luận ở đây.

static DecimalInfo SQLInfo(decimal dec) 

    { 

     System.Data.SqlTypes.SqlDecimal x; 
     x = new System.Data.SqlTypes.SqlDecimal(dec);      
     return new DecimalInfo((int)x.Precision, (int)x.Scale, (int)0); 
    } 
0
public static class DecimalExtensions 
{ 
    public static int GetPrecision(this decimal value) 
    { 
     return GetLeftNumberOfDigits(value) + GetRightNumberOfDigits(value); 
    } 

    public static int GetScale(this decimal value) 
    { 
     return GetRightNumberOfDigits(value); 
    } 
    /// <summary> 
    /// Number of digits to the right of the decimal point without ending zeros 
    /// </summary> 
    /// <param name="value"></param> 
    /// <returns></returns> 
    public static int GetRightNumberOfDigits(this decimal value) 
    { 
     var text = value.ToString(System.Globalization.CultureInfo.InvariantCulture).TrimEnd('0'); 
     var decpoint = text.IndexOf(System.Globalization.CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator); 
     if (decpoint < 0) 
      return 0; 
     return text.Length - decpoint - 1; 
    } 

    /// <summary> 
    /// Number of digits to the left of the decimal point without starting zeros 
    /// </summary> 
    /// <param name="value"></param> 
    /// <returns></returns> 
    public static int GetLeftNumberOfDigits(this decimal value) 
    { 
     var text = Math.Abs(value).ToString(System.Globalization.CultureInfo.InvariantCulture).TrimStart('0'); 
     var decpoint = text.IndexOf(System.Globalization.CultureInfo.InvariantCulture.NumberFormat.NumberDecimalSeparator); 
     if (decpoint == -1) 
      return text.Length; 
     return decpoint; 
    } 
} 

Giải pháp của tôi là tương thích với Oracle chính xác và định nghĩa quy mô cho NUMBER (p, s) DataType:

https://docs.oracle.com/cd/B28359_01/server.111/b28318/datatype.htm#i16209

trọng.

0

Tôi hiện có vấn đề tương tự, nhưng tôi không chỉ cần quy mô, mà còn cần mantisse làm số nguyên. Dựa trên các giải pháp trên, hãy tìm nhanh nhất, tôi có thể đưa ra, bên dưới. Thống kê: "ViaBits" mất 2.000ms cho 7.000.000 séc trên máy của tôi. "ViaString" mất 4.000ms cho cùng một tác vụ.

public class DecimalInfo { 

    public BigInteger Mantisse { get; private set; } 
    public SByte Scale { get; private set; } 
    private DecimalInfo() { 
    } 

    public static DecimalInfo Get(decimal d) { 
     //ViaBits is faster than ViaString. 
     return ViaBits(d); 
    } 

    public static DecimalInfo ViaBits(decimal d) { 
     //This is the fastest, I can come up with. 
     //Tested against the solutions from http://stackoverflow.com/questions/763942/calculate-system-decimal-precision-and-scale 
     if (d == 0) { 
      return new DecimalInfo() { 
       Mantisse = 0, 
       Scale = 0, 
      }; 
     } else { 
      byte scale = (byte)((Decimal.GetBits(d)[3] >> 16) & 31); 
      //Calculating the mantisse from the bits 0-2 is slower. 
      if (scale > 0) { 
       if ((scale & 1) == 1) { 
        d *= 10m; 
       } 
       if ((scale & 2) == 2) { 
        d *= 100m; 
       } 
       if ((scale & 4) == 4) { 
        d *= 10000m; 
       } 
       if ((scale & 8) == 8) { 
        d *= 100000000m; 
       } 
       if ((scale & 16) == 16) { 
        d *= 10000000000000000m; 
       } 
      } 
      SByte realScale = (SByte)scale; 
      BigInteger scaled = (BigInteger)d; 
      //Just for bigger steps, seems reasonable. 
      while (scaled % 10000 == 0) { 
       scaled /= 10000; 
       realScale -= 4; 
      } 
      while (scaled % 10 == 0) { 
       scaled /= 10; 
       realScale--; 
      } 
      return new DecimalInfo() { 
       Mantisse = scaled, 
       Scale = realScale, 
      }; 
     } 
    } 

    public static DecimalInfo ViaToString(decimal dec) { 
     if (dec == 0) { 
      return new DecimalInfo() { 
       Mantisse = 0, 
       Scale = 0, 
      }; 
     } else { 
      //Is slower than "ViaBits". 
      string s = dec.ToString(CultureInfo.InvariantCulture); 

      int scale = 0; 
      int trailingZeros = 0; 
      bool inFraction = false; 
      foreach (char c in s) { 
       if (inFraction) { 
        if (c == '0') { 
         trailingZeros++; 
        } else { 
         trailingZeros = 0; 
        } 
        scale++; 
       } else { 
        if (c == '.') { 
         inFraction = true; 
        } else if (c != '-') { 
         if (c == '0'){ 
          trailingZeros ++; 
         } else { 
          trailingZeros = 0; 
         } 
        } 
       } 
      } 

      if (inFraction) { 
       return new DecimalInfo() { 
        Mantisse = BigInteger.Parse(s.Replace(".", "").Substring(0, s.Length - trailingZeros - 1)), 
        Scale = (SByte)(scale - trailingZeros), 
       }; 
      } else { 
       return new DecimalInfo() { 
        Mantisse = BigInteger.Parse(s.Substring(0, s.Length - trailingZeros)), 
        Scale = (SByte)(scale - trailingZeros), 
       }; 
      } 
     } 
    } 
} 
Các vấn đề liên quan