Problema:
El usuario a través de un formulario selecciona el trabajo compuesto, para el ejemplo vamos a imaginar que, de una lista, el selección uno llamado “Mantenimiento X” y este Trabajo Compuesto está conformado por los siguientes materiales:
- GIS ANTIPOLVO, del cual necesita 0,01 de este material y
- GAS, del cual necesita 0,001
Hay más materiales, pero nos centraremos en estos 2, para el ejemplo.
Ahora él debe introducir una cantidad para el Trabajo Compuesto: “Mantenimiento X”, dicha cantidad es: 87,675
Y dicha cantidad el la tiene que distribuir en unos campos que representan los meses: de enero a diciembre, en cualquiera, no es a juro en cada mes, para el ejemplo, el distribuye de la siguiente manera
- Enero: 20,000
- Febrero: 20,000
- Marzo: 20,000
- Abril: 20,000
- Mayo: 7,675
Entonces, según el Excel, la Cantidad Total Material = REDONDEAR.MAS(CantidadTrabajoCompuesto * CantidadMaterialPorTrabajo, 0)
Donde, este procedimiento corresponde para el material “GIS ANTIPOLVO”
REDONDEAR.MAS(87675*0,01;0) = 877
Pero en la instrucción en .Net ese resultado = 878
Con respecto a “GAS” el calculo da igual en ambas formulas = 88
Cálculo de Cantidad Implementación en .NET:
public double InventariableTotal { get => Math.Ceiling( December + November + October + September + August + July + June + May + April + March + February + January ); } public double Total { get => Inventoriable ? InventariableTotal : 0; }
🎯 Decimal vs Double: La Respuesta Definitiva para tu Caso
⚡ RESPUESTA DIRECTA
Para cálculos financieros, presupuestales y de materiales como los tuyos:
`decimal` es SUPERIOR a `double` ✅
📊 COMPARACIÓN TÉCNICA COMPLETA
Característica | `double` | `decimal` | |
---|---|---|---|
Tipo | Punto flotante binario | Punto flotante decimal | |
Tamaño | 8 bytes (64 bits) | 16 bytes (128 bits) | |
Precisión | ~15-17 dígitos | 28-29 dígitos ✅ | |
Rango | ±5.0×10⁻³²⁴ a ±1.7×10³⁰⁸ | ±1.0×10⁻²⁸ a ±7.9×10²⁸ | |
Precisión Decimal | ❌ Inexacta | ✅ Exacta | |
Velocidad | Más rápido (hardware) | Más lento (software) | |
Uso ideal | Científico, gráficos | Financiero, monetario ✅ | |
Aspecto | `double` | `decimal` | Ganador para tu caso |
Precisión decimal | ~15 dígitos | 28-29 dígitos | ✅ `decimal` |
Representación 0.1 | Inexacta | Exacta | ✅ `decimal` |
Cálculos monetarios | ❌ Errores | ✅ Preciso | ✅ `decimal` |
Velocidad | Rápido | Más lento | ⚠️ `double` |
Memoria | 8 bytes | 16 bytes | ⚠️ `double` |
Compatibilidad Excel | Regular | Excelente | ✅ `decimal` |
Errores acumulativos | Frecuentes | Raros | ✅ `decimal` |
🔬 DEMOSTRACIÓN DEL PROBLEMA CON `double`
Ejemplo 1: El Problema de 0.1
// Con double double valorDouble = 0.1; Console.WriteLine($"double: {valorDouble:F20}"); // Salida: 0.10000000000000000555 ❌ // ¡No puede representar 0.1 exactamente! // Con decimal decimal valorDecimal = 0.1m; Console.WriteLine($"decimal: {valorDecimal:F20}"); // Salida: 0.10000000000000000000 ✅ // ¡Representación exacta!
Ejemplo 2: Tu Caso – Cálculo de Material
// ======================================== // ESCENARIO: Calcular material con 0.01 por unidad // ======================================== Console.WriteLine("=== CON DOUBLE ==="); double trabajoDouble = 87675; double factorDouble = 0.01; double resultadoDouble = trabajoDouble * factorDouble; Console.WriteLine($"Resultado: {resultadoDouble:F20}"); // Salida: 876.75000000000000000000 // (En este caso específico funciona, pero...) // Probemos con un valor más problemático double trabajo2 = 87.675; double factor2 = 0.01; double resultado2 = trabajo2 * factor2; Console.WriteLine($"Resultado: {resultado2:F20}"); // Salida: 0.87674999999999999012 ❌ // ¡Error de representación! Console.WriteLine("\n=== CON DECIMAL ==="); decimal trabajoDecimal = 87675m; decimal factorDecimal = 0.01m; decimal resultadoDecimal = trabajoDecimal * factorDecimal; Console.WriteLine($"Resultado: {resultadoDecimal:F20}"); // Salida: 876.75000000000000000000 ✅ // ¡Exacto! decimal trabajo2Dec = 87.675m; decimal factor2Dec = 0.01m; decimal resultado2Dec = trabajo2Dec * factor2Dec; Console.WriteLine($"Resultado: {resultado2Dec:F20}"); // Salida: 0.87675000000000000000 ✅ // ¡Perfecto!
🎯 TU CASO ESPECÍFICO: Distribución Mensual
Problema Real con `double`:
// ======================================== // TU FÓRMULA ACTUAL // ======================================== double trabajo = 20000; double trabajoTotal = 87675; double totalMaterial = 877; double resultado = (trabajo / trabajoTotal) * totalMaterial; Console.WriteLine($"Resultado: {resultado:F20}"); // Salida: 200.05930157316489761100 // Redondeado double redondeado = Math.Round(resultado, 2); Console.WriteLine($"Redondeado: {redondeado}"); // Salida: 200.06 // PERO MIRA INTERNAMENTE: Console.WriteLine($"Valor interno: {resultado}"); // Salida: 200.0593015731649 // Hay dígitos "fantasma" que afectan el redondeo
Con `decimal` (MEJOR):
decimal trabajoDec = 20000m; decimal trabajoTotalDec = 87675m; decimal totalMaterialDec = 877m; decimal resultadoDec = (trabajoDec / trabajoTotalDec) * totalMaterialDec; Console.WriteLine($"Resultado: {resultadoDec:F20}"); // Salida: 200.05930157316489759726 // Redondeado decimal redondeadoDec = Math.Round(resultadoDec, 2); Console.WriteLine($"Redondeado: {redondeadoDec}"); // Salida: 200.06 // PERO es más preciso en los dígitos intermedios
💥 DEMOSTRACIÓN DEL ERROR ACUMULATIVO
Caso Real: Suma de 0.1 diez veces
// Con double double sumaDouble = 0.0; for (int i = 0; i < 10; i++) { sumaDouble += 0.1; } Console.WriteLine($"double: {sumaDouble} (esperado: 1.0)"); Console.WriteLine($"¿Es igual a 1.0? {sumaDouble == 1.0}"); // Salida: 0.9999999999999999 // ¿Es igual a 1.0? False ❌❌❌ // Con decimal decimal sumaDecimal = 0.0m; for (int i = 0; i < 10; i++) { sumaDecimal += 0.1m; } Console.WriteLine($"decimal: {sumaDecimal} (esperado: 1.0)"); Console.WriteLine($"¿Es igual a 1.0? {sumaDecimal == 1.0m}"); // Salida: 1.0 // ¿Es igual a 1.0? True ✅✅✅
🔥 CASO EXTREMO: Tu Problema del 877 vs 878
// ======================================== // REPRODUCCIÓN DEL PROBLEMA 877 vs 878 // ======================================== Console.WriteLine("=== PRUEBA CON DOUBLE ==="); double[] valoresDouble = new double[5]; valoresDouble[0] = Math.Round((20000.0 / 87675.0) * 877.0, 2); // Enero valoresDouble[1] = Math.Round((20000.0 / 87675.0) * 877.0, 2); // Febrero valoresDouble[2] = Math.Round((20000.0 / 87675.0) * 877.0, 2); // Marzo valoresDouble[3] = Math.Round((20000.0 / 87675.0) * 877.0, 2); // Abril valoresDouble[4] = Math.Round((7675.0 / 87675.0) * 877.0, 2); // Mayo double totalDouble = valoresDouble.Sum(); Console.WriteLine($"Total con double: {totalDouble}"); // 877.01 ❌ Console.WriteLine("\n=== PRUEBA CON DECIMAL ==="); decimal[] valoresDecimal = new decimal[5]; valoresDecimal[0] = Math.Round((20000m / 87675m) * 877m, 2); // Enero valoresDecimal[1] = Math.Round((20000m / 87675m) * 877m, 2); // Febrero valoresDecimal[2] = Math.Round((20000m / 87675m) * 877m, 2); // Marzo valoresDecimal[3] = Math.Round((20000m / 87675m) * 877m, 2); // Abril valoresDecimal[4] = Math.Round((7675m / 87675m) * 877m, 2); // Mayo decimal totalDecimal = valoresDecimal.Sum(); Console.WriteLine($"Total con decimal: {totalDecimal}"); // 877.01 ❌ // ¡NOTA! Ambos dan 877.01 porque el problema es el REDONDEO ACUMULATIVO // No es el tipo de dato, sino la estrategia de distribución // PERO decimal es MÁS PRECISO en los cálculos intermedios
🎓 POR QUÉ `decimal` ES MEJOR PARA TI
1. Representación Base 10 (Como Humanos Pensamos)
// double usa BASE 2 (binario) // 0.1 en decimal = 0.0001100110011... (infinito) en binario // Por eso NO puede representar 0.1 exactamente // decimal usa BASE 10 // 0.1 en decimal = 0.1 (exacto) // ¡Como escribimos naturalmente!
2. Sin Errores de Conversión
// Dinero y precios double precio1 = 0.1; double precio2 = 0.2; double suma = precio1 + precio2; Console.WriteLine($"{suma} == 0.3? {suma == 0.3}"); // False ❌ (suma = 0.30000000000000004) decimal precioD1 = 0.1m; decimal precioD2 = 0.2m; decimal sumaD = precioD1 + precioD2; Console.WriteLine($"{sumaD} == 0.3? {sumaD == 0.3m}"); // True ✅
3. Mayor Precisión en División
// División con double double div1 = 1.0 / 3.0; Console.WriteLine($"double: {div1:F29}"); // 0.33333333333333331483 // División con decimal decimal div2 = 1.0m / 3.0m; Console.WriteLine($"decimal: {div2:F29}"); // 0.33333333333333333333333333333 // ¡Más dígitos significativos!
📋 RECOMENDACIÓN PARA TU CÓDIGO
✅ USA `decimal` CUANDO:
- Cálculos monetarios (presupuestos, costos) ← TÚ ESTÁS AQUÍ
- Cantidades de materiales con decimales
- Porcentajes y proporciones financieras
- Cuando el redondeo debe ser predecible
- Cuando necesitas igualdad exacta de valores
❌ USA `double` CUANDO:
- Cálculos científicos (física, matemáticas)
- Gráficos 3D y videojuegos
- Machine Learning
- Performance crítico con millones de operaciones
- No importa una pequeña imprecisión
🔧 IMPLEMENTACIÓN CORRECTA CON `decimal`
public class TrabajoCompuestoCalculator { /// <summary> /// Usa DECIMAL para precisión financiera /// </summary> public decimal CalcularCantidadTotalMaterial( decimal cantidadTrabajoCompuesto, decimal cantidadMaterialPorTrabajo) { decimal producto = cantidadTrabajoCompuesto * cantidadMaterialPorTrabajo; return Math.Ceiling(producto); } public MaterialDistribucionDTO DistribuirMaterial( TrabajoMensualDTO trabajoMensual, decimal cantidadMaterialPorUnidad) { decimal trabajoTotal = trabajoMensual.GetTrabajoTotal(); decimal totalMaterial = CalcularCantidadTotalMaterial( trabajoTotal, cantidadMaterialPorUnidad ); var distribucion = new MaterialDistribucionDTO(); decimal sumaAcumulada = 0m; // Array de meses var meses = new[] { new { Prop = nameof(trabajoMensual.Enero), Valor = trabajoMensual.Enero }, new { Prop = nameof(trabajoMensual.Febrero), Valor = trabajoMensual.Febrero }, new { Prop = nameof(trabajoMensual.Marzo), Valor = trabajoMensual.Marzo }, new { Prop = nameof(trabajoMensual.Abril), Valor = trabajoMensual.Abril }, new { Prop = nameof(trabajoMensual.Mayo), Valor = trabajoMensual.Mayo }, new { Prop = nameof(trabajoMensual.Junio), Valor = trabajoMensual.Junio }, new { Prop = nameof(trabajoMensual.Julio), Valor = trabajoMensual.Julio }, new { Prop = nameof(trabajoMensual.Agosto), Valor = trabajoMensual.Agosto }, new { Prop = nameof(trabajoMensual.Septiembre), Valor = trabajoMensual.Septiembre }, new { Prop = nameof(trabajoMensual.Octubre), Valor = trabajoMensual.Octubre }, new { Prop = nameof(trabajoMensual.Noviembre), Valor = trabajoMensual.Noviembre }, new { Prop = nameof(trabajoMensual.Diciembre), Valor = trabajoMensual.Diciembre } }; // Encontrar último mes con valor int ultimoIndiceConValor = -1; for (int i = meses.Length - 1; i >= 0; i--) { if (meses[i].Valor > 0) { ultimoIndiceConValor = i; break; } } // Distribuir var valoresMensuales = new decimal[12]; for (int i = 0; i < meses.Length; i++) { if (meses[i].Valor == 0) { valoresMensuales[i] = 0m; } else if (i != ultimoIndiceConValor) { decimal proporcion = meses[i].Valor / trabajoTotal; decimal materialMes = proporcion * totalMaterial; valoresMensuales[i] = Math.Round(materialMes, 2, MidpointRounding.AwayFromZero); sumaAcumulada += valoresMensuales[i]; } else { // Ajuste en último mes valoresMensuales[i] = Math.Round(totalMaterial - sumaAcumulada, 2); } } // Asignar valores distribucion.Enero = valoresMensuales[0]; distribucion.Febrero = valoresMensuales[1]; distribucion.Marzo = valoresMensuales[2]; distribucion.Abril = valoresMensuales[3]; distribucion.Mayo = valoresMensuales[4]; distribucion.Junio = valoresMensuales[5]; distribucion.Julio = valoresMensuales[6]; distribucion.Agosto = valoresMensuales[7]; distribucion.Septiembre = valoresMensuales[8]; distribucion.Octubre = valoresMensuales[9]; distribucion.Noviembre = valoresMensuales[10]; distribucion.Diciembre = valoresMensuales[11]; return distribucion; } } // DTOs con decimal public class TrabajoMensualDTO { public decimal Enero { get; set; } public decimal Febrero { get; set; } public decimal Marzo { get; set; } public decimal Abril { get; set; } public decimal Mayo { get; set; } public decimal Junio { get; set; } public decimal Julio { get; set; } public decimal Agosto { get; set; } public decimal Septiembre { get; set; } public decimal Octubre { get; set; } public decimal Noviembre { get; set; } public decimal Diciembre { get; set; } public decimal GetTrabajoTotal() { return Enero + Febrero + Marzo + Abril + Mayo + Junio + Julio + Agosto + Septiembre + Octubre + Noviembre + Diciembre; } } public class MaterialDistribucionDTO { public decimal Enero { get; set; } public decimal Febrero { get; set; } public decimal Marzo { get; set; } public decimal Abril { get; set; } public decimal Mayo { get; set; } public decimal Junio { get; set; } public decimal Julio { get; set; } public decimal Agosto { get; set; } public decimal Septiembre { get; set; } public decimal Octubre { get; set; } public decimal Noviembre { get; set; } public decimal Diciembre { get; set; } public decimal GetTotal() { return Enero + Febrero + Marzo + Abril + Mayo + Junio + Julio + Agosto + Septiembre + Octubre + Noviembre + Diciembre; } }
⚠️ IMPORTANTE: Cómo Usar `decimal` Correctamente
1. Sufijo `m` en Literales
decimal valor1 = 0.01m; // ✅ Correcto decimal valor2 = 0.01; // ❌ Compila pero es double primero, luego convierte
2. Conversión de `double` a `decimal`
// Si recibes double de otra API double valorDouble = 87.675; decimal valorDecimal = (decimal)valorDouble; // Cast explícito // O mejor: decimal valorDecimal = Convert.ToDecimal(valorDouble);
3. Math Functions con `decimal`
// ✅ Estos funcionan con decimal: Math.Round(valor, 2) Math.Ceiling(valor) Math.Floor(valor) Math.Truncate(valor) Math.Abs(valor) Math.Min(valor1, valor2) Math.Max(valor1, valor2) // ❌ Estos NO funcionan con decimal (solo double): Math.Sqrt(valor) // Necesitas conversión Math.Pow(valor, 2) // Necesitas conversión Math.Sin(valor) // Necesitas conversión
📊 TABLA COMPARATIVA FINAL
Aspecto double decimal Ganador para tu caso Precisión decimal ~15 dígitos 28-29 dígitos ✅ decimal Representación 0.1 Inexacta Exacta ✅ decimal Cálculos monetarios ❌ Errores ✅ Preciso ✅ decimal Velocidad Rápido Más lento ⚠️ double Memoria 8 bytes 16 bytes ⚠️ double Compatibilidad Excel Regular Excelente ✅ decimal Errores acumulativos Frecuentes Raros ✅ decimal
✅ CONCLUSIÓN Y RECOMENDACIÓN FINAL
Para tu sistema BPS de Operaciones:
🎯 USA `decimal` EN TODO TU CÓDIGO
Razones:
- ✅ Precisión financiera: Presupuestos y costos deben ser exactos
- ✅ Compatibilidad con Excel: decimal maneja decimales como Excel
- ✅ Auditoría: Los contadores esperan precisión decimal
- ✅ Sin sorpresas: 0.1 + 0.2 = 0.3 (siempre)
- ✅ Menos bugs: Elimina errores de redondeo sutiles
El único «contra»:
- ⚠️ Ligeramente más lento (pero imperceptible en tu caso de uso)
Cambios Necesarios en tu Código:
- Cambiar todos los `double` a `decimal` en propiedades y cálculos
- Agregar sufijo `m` a todos los literales numéricos
- Mantener `Math.Ceiling` y `Math.Round` (funcionan con decimal)
- Remover cualquier cast a `(float)` – usar siempre `decimal`
Comentarios recientes