2009-04-18 11 views
22

Supongamos que tenemos un número System.Decimal.Calcular System.Decimal Precision and Scale

Por ejemplo, tomemos uno cuya ToString() la representación es la siguiente:

d.ToString() = "123.4500" 

El siguiente puede decirse de este decimal. Para nuestros propósitos aquí, la escala se define como la cantidad de dígitos a la derecha del punto decimal. La escala efectiva es similar, pero ignora los ceros finales que ocurren en la parte fraccionaria. (En otras palabras, estos parámetros se definen como decimales SQL además de algunos parámetros adicionales para tener en cuenta el concepto System.Decimal de ceros en la parte fraccionaria.)

  • Precisión: 7
  • Escala: 4
  • EffectivePrecision: 5
  • EffectiveScale: 2

Dado un System.Decimal arbitraria, ¿cómo puedo calcular los cuatro de estos parámetros de manera eficiente y sin necesidad de convertir en una cadena y examinar ¿la cuerda? La solución probablemente requiera Decimal.GetBits.

Algunos ejemplos más:

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 

Alternativamente interpretar estas precisiones como cero estaría bien (?).

Respuesta

25

Sí, necesitaría usar Decimal.GetBits. Lamentablemente, debe trabajar con un entero de 96 bits, y no hay un tipo de entero simple en .NET que haga frente a 96 bits. Por otro lado, es posible que pueda usar Decimal en sí ...

Aquí hay un código que produce los mismos números que sus ejemplos. Espero que lo encuentres útil :)

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

Gracias, esto es muy interesante. No es tan rápido como extraer la información de ToString, como lo muestro en una publicación separada. –

+0

Jon, si piensas que tu (uint []) (objeto) el reparto es malo (estoy de acuerdo), entonces ¿por qué no utilizas una forma un poco más ordenada y explícita? – Joren

+0

Señalaré que este código devuelve precisión y escala usando definiciones de notación científica. Por ejemplo, 0.005 tiene Precisión = 1 y Escala = 3. Esa es la manera ordinaria de hacer las cosas, pero otra interpretación de estos parámetros es por el tipo decimal de SQL. De esta forma, Precisión = 3 y Escala = 3. Ese es el decimal SQL más pequeño que podría usar para mantener 0.005. La pregunta original mencionó brevemente los decimales SQL (pero no dio un ejemplo de este caso, gracias a Jon Seigel, quien recientemente me lo señaló). Mi implementación (FastInfo a continuación) proporciona el último tipo de precisión y escala. ¡Gracias! –

10

El uso de ToString es aproximadamente 10 veces más rápido que la solución de Jon Skeet. Si bien esto es razonablemente rápido, el desafío aquí (¡si hay algunos interesados!) Es superar el rendimiento de ToString.

Los resultados de rendimiento que recibo de la siguiente programa de pruebas son: 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

No estoy del todo sorprendido: estamos haciendo muchas operaciones con decimales simplemente debido a la falta de un tipo entero de 96 bits. Si usa ulong en lugar de decimal para la mantisa ignorando por completo los 32 bits superiores, es un poco más rápido que FastInfo, pero, por supuesto, ¡no funciona para todos los decimales! Sospecho que podemos mejorar la velocidad contando tanto la precisión como los ceros finales en un bucle (ya que ambos se dividen por 10 cada vez). –

+3

El algoritmo basado en cadenas produce resultados incorrectos para números con ceros a la izquierda, es decir, 0.555, y números que tienen ceros entre los dígitos decimales y significativos, es decir, 0.0005. –

+0

Gracias, he adaptado el código. También agregué un comentario al código sobre el método FastInfo. Esto es para señalar que el método usa precisión SQL y definiciones de escala, no las notación científica usual. (La mención de que esto es para tratar con decimales SQL estaba en la pregunta original.) –

23

me encontré con este artículo cuando necesitaba para validar la precisión y la escala antes de escribir un valor decimal a una base de datos De hecho, se me ocurrió una forma diferente de lograr esto usando System.Data.SqlTypes.SqlDecimal, que resultó ser más rápido que los otros dos métodos discutidos aquí.

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

Mi solución es compatible con la precisión y definición de Oracle escala para el número (p, s) Tipo de datos:

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

Saludos.

0

Actualmente tengo un problema similar, pero no solo necesito la escala, sino también la mantisa como número entero. Basado en las soluciones anteriores, encuentre el más rápido, podría encontrar, a continuación. Estadísticas: "ViaBits" toma 2,000ms por 7,000,000 de cheques en mi máquina. "ViaString" toma 4,000ms para la misma tarea.

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