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`
TipoPunto flotante binarioPunto flotante decimal
Tamaño8 bytes (64 bits)16 bytes (128 bits)
Precisión~15-17 dígitos28-29 dígitos
Rango±5.0×10⁻³²⁴ a ±1.7×10³⁰⁸±1.0×10⁻²⁸ a ±7.9×10²⁸
Precisión Decimal❌ InexactaExacta
VelocidadMás rápido (hardware)Más lento (software)
Uso idealCientífico, gráficosFinanciero, monetario
Aspecto`double``decimal`Ganador para tu caso
Precisión decimal~15 dígitos28-29 dígitos✅ `decimal`
Representación 0.1InexactaExacta✅ `decimal`
Cálculos monetarios❌ Errores✅ Preciso✅ `decimal`
VelocidadRápidoMás lento⚠️ `double`
Memoria8 bytes16 bytes⚠️ `double`
Compatibilidad ExcelRegularExcelente✅ `decimal`
Errores acumulativosFrecuentesRaros✅ `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:

  1. Cálculos monetarios (presupuestos, costos) ← TÚ ESTÁS AQUÍ
  2. Cantidades de materiales con decimales
  3. Porcentajes y proporciones financieras
  4. Cuando el redondeo debe ser predecible
  5. Cuando necesitas igualdad exacta de valores

❌ USA `double` CUANDO:

  1. Cálculos científicos (física, matemáticas)
  2. Gráficos 3D y videojuegos
  3. Machine Learning
  4. Performance crítico con millones de operaciones
  5. 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:

  1. Precisión financiera: Presupuestos y costos deben ser exactos
  2. Compatibilidad con Excel: decimal maneja decimales como Excel
  3. Auditoría: Los contadores esperan precisión decimal
  4. Sin sorpresas: 0.1 + 0.2 = 0.3 (siempre)
  5. 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:

  1. Cambiar todos los `double` a `decimal` en propiedades y cálculos
  2. Agregar sufijo `m` a todos los literales numéricos
  3. Mantener `Math.Ceiling` y `Math.Round` (funcionan con decimal)
  4. Remover cualquier cast a `(float)` – usar siempre `decimal`