Calcular System.Decimal Precision and Scale

Supongamos que tenemos un número System.Decimal.

Para ilustración, tomemos una representación de ToString () como sigue:

d.ToString() = "123.4500" 

Lo siguiente puede decirse acerca 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 más algunos parámetros adicionales para tener en cuenta el concepto System.Decimal de ceros finales en la parte fraccional).

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

Dado un System.Decimal arbitrario, ¿cómo puedo calcular los cuatro parámetros de manera eficiente y sin convertir a String y examinar la cadena? 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.

Sí, necesitarías 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 puedas usar el Decimal sí …

Aquí hay un código que produce los mismos números que sus ejemplos. Esperamos que te sea ú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(); } } 

Encontré este artículo cuando necesitaba validar la precisión y la escala antes de escribir un valor decimal en 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); } 

Usar 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 obtengo del siguiente progtwig de prueba 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); } } 
 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); } ///  /// Number of digits to the right of the decimal point without ending zeros ///  ///  ///  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; } ///  /// Number of digits to the left of the decimal point without starting zeros ///  ///  ///  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 definición de precisión y escala de Oracle para NUMBER (p, s) DataType:

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

Saludos.

Actualmente tengo un problema similar, pero no solo necesito la escala, sino que también necesito la mantisa como número entero. Basado en las soluciones anteriores, encuentre el más rápido, podría llegar a continuación. Estadísticas: “ViaBits” toma 2,000ms por 7,000,000 de cheques en mi máquina. “ViaString” requiere 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), }; } } } }