28. Proyecto integrador: calculadora de crecimiento de un capital

Integramos fórmulas, funciones, validación, tablas y presentación en una aplicación completa que calcula sin ocultar los supuestos ni el recorrido.

28.1 Objetivo del proyecto

Construiremos una calculadora capaz de recibir un capital, una tasa periódica, un plazo y un modelo. La aplicación mostrará monto, interés, factor, composición y evolución período a período.

entrada → validación → modelo → trayectoria → presentación

Cada etapa posee una responsabilidad concreta y puede comprobarse por separado.

28.2 Requisitos funcionales

Ingresar

Modelo, capital, tasa porcentual, períodos y unidad.

Calcular

Monto e interés simple y compuesto sin redondeos intermedios.

Explicar

Tabla del modelo elegido y comparación final.

Validar

Campos vacíos, números finitos, rangos y períodos enteros.

Responder

Actualizar resultados cuando cambia una entrada.

Reiniciar

Recuperar un escenario conocido de manera explícita.

28.3 Arquitectura por capas

Lecturaobtiene texto
Dominiovalida y calcula
Presentaciónformatea valores
Eventoscoordina cambios

Las fórmulas no deberían conocer los elementos HTML, y los campos no deberían contener fórmulas duplicadas. Esta separación permite cambiar la interfaz sin reescribir el modelo financiero.

28.4 Modelo de estado

Después de leer y validar, representamos la operación con un objeto coherente:

const estado = {
  modelo: "compuesto",
  capital: 350000,
  tasa: 0.025,
  periodos: 12,
  unidad: "meses"
};

console.log("Modelo:", estado.modelo);
console.log("Capital:", estado.capital.toFixed(2));
console.log("Tasa decimal:", estado.tasa);
console.log("Horizonte:", estado.periodos, estado.unidad);

Este estado ya contiene números interpretados. Los textos crudos permanecen en la capa de entrada.

28.5 Leer y validar los campos

La lectura devuelve un resultado explícito. El cálculo solo continúa cuando ok es verdadero:

function leerPositivo(texto, nombre) {
  if (typeof texto !== "string" || texto.trim() === "") {
    return { ok: false, error: `${nombre}: valor requerido` };
  }
  const valor = Number(texto);
  if (!Number.isFinite(valor) || valor <= 0) {
    return { ok: false, error: `${nombre}: debe ser positivo` };
  }
  return { ok: true, valor };
}

console.log(JSON.stringify(leerPositivo("350000", "Capital")));
console.log(JSON.stringify(leerPositivo("", "Capital")));
console.log(JSON.stringify(leerPositivo("-10", "Capital")));

La función distingue ausencia, conversión y dominio. La calculadora completa aplica reglas específicas a cada campo.

28.6 Funciones del dominio

Ambos modelos comparten la misma firma y devuelven un monto:

function montoSimple(capital, tasa, periodos) {
  return capital * (1 + tasa * periodos);
}

function montoCompuesto(capital, tasa, periodos) {
  return capital * (1 + tasa) ** periodos;
}

const modelos = { simple: montoSimple, compuesto: montoCompuesto };

for (const nombre of Object.keys(modelos)) {
  const monto = modelos[nombre](350000, 0.025, 12);
  console.log(`${nombre}: ${monto.toFixed(2)}`);
}

La interfaz selecciona una función del catálogo; no necesita conocer la expresión matemática interna.

28.7 Generar la trayectoria

Una única función puede construir filas para ambos modelos al cambiar la regla de la base:

function crearTrayectoria(modelo, capital, tasa, periodos) {
  const filas = [{ periodo: 0, base: null, interes: 0, monto: capital }];
  let monto = capital;

  for (let periodo = 1; periodo <= periodos; periodo++) {
    const base = modelo === "simple" ? capital : monto;
    const interes = base * tasa;
    monto = modelo === "simple"
      ? capital + interes * periodo
      : base + interes;
    filas.push({ periodo, base, interes, monto });
  }
  return filas;
}

const filas = crearTrayectoria("compuesto", 100000, 0.03, 3);
filas.forEach(fila => console.log(JSON.stringify(fila)));

28.8 Derivar el resumen

No recalculamos lo que ya contiene la trayectoria. La última fila proporciona el monto final:

function resumir(capital, trayectoria) {
  const monto = trayectoria.at(-1).monto;
  return {
    capital,
    monto,
    interes: monto - capital,
    factor: monto / capital
  };
}

const trayectoria = [
  { periodo: 0, monto: 100000 },
  { periodo: 1, monto: 103000 },
  { periodo: 2, monto: 106090 }
];

const resumen = resumir(100000, trayectoria);
console.log(JSON.stringify(resumen));

Utilizar una única fuente de verdad evita que el resumen y la tabla se contradigan.

28.9 Renderizar sin mezclar cálculos

La función de presentación recibe resultados ya calculados. Formatea moneda, crea filas y actualiza textos; no vuelve a aplicar fórmulas financieras.

calcular devuelve números · renderizar produce elementos visibles

La separación evita usar cadenas monetarias como datos de una operación posterior.

28.10 Coordinar los eventos

Cada cambio de entrada ejecuta el mismo flujo de actualización. El botón de reinicio restaura valores y vuelve a calcular:

const estadoInicial = {
  modelo: "compuesto",
  capital: 350000,
  tasaPorcentaje: 2.5,
  periodos: 12
};

function actualizar(datos) {
  const tasa = datos.tasaPorcentaje / 100;
  const monto = datos.modelo === "simple"
    ? datos.capital * (1 + tasa * datos.periodos)
    : datos.capital * (1 + tasa) ** datos.periodos;
  return monto;
}

console.log("Monto inicial:", actualizar(estadoInicial).toFixed(2));
console.log("Monto modificado:", actualizar({ ...estadoInicial, periodos: 6 }).toFixed(2));

En la página, los eventos llaman a una función coordinadora; no contienen cálculos diferentes entre sí.

28.11 Estados de error y accesibilidad

  • Los campos poseen etiquetas asociadas mediante for e id.
  • aria-invalid identifica entradas que no cumplen su regla.
  • Los errores aparecen como texto y no dependen solo del color.
  • La región de resultados utiliza aria-live="polite".
  • Cuando hay errores, la tabla se vacía y el cálculo figura como no disponible.
  • El botón de reinicio funciona con teclado y conserva un foco visible.

Un estado inválido no debe dejar en pantalla números anteriores como si todavía correspondieran a las entradas actuales.

28.12 Pruebas de aceptación

Antes de considerar terminada la calculadora comprobamos resultados y comportamiento:

function monto(capital, tasa, periodos, modelo) {
  return modelo === "simple"
    ? capital * (1 + tasa * periodos)
    : capital * (1 + tasa) ** periodos;
}

const pruebas = [
  [monto(100000, 0.10, 0, "simple") === 100000, "simple n=0"],
  [monto(100000, 0.10, 0, "compuesto") === 100000, "compuesto n=0"],
  [Math.abs(monto(100000, 0.10, 5, "simple") - 150000) < 0.005, "simple n=5"],
  [Math.abs(monto(100000, 0.10, 5, "compuesto") - 161051) < 0.005, "compuesto n=5"]
];

pruebas.forEach(([ok, nombre]) => console.log(`${nombre}: ${ok ? "correcta" : "falló"}`));

Además se prueban campos vacíos, tasas fuera de rango, períodos decimales, reinicio y visualización en pantallas angostas.

28.13 Calculadora integrada

Usá la aplicación completa. La tabla corresponde al modelo seleccionado; el panel comparativo mantiene visibles los dos resultados bajo los mismos datos.

Crecimiento de un capital

Modelos simple y compuesto con trayectoria verificable.

    Capital$350.000,00
    Interés total$120.711,09
    Monto final$470.711,09
    Factor total1,34488882

    Composición del monto seleccionado

    Capital: $350.000,00Interés: $120.711,09
    Monto simple$455.000,00Interés: $105.000,00
    Monto compuesto$470.711,09Interés: $120.711,09
    Evolución compuesta durante 12 meses.
    PeríodoBaseInterés del períodoMonto
    El modelo compuesto termina $15.711,09 por encima del simple.

    Resultado válido: 13 filas, incluido el estado inicial.

    28.14 Desafío de ampliación

    La calculadora satisface el alcance del curso. Como práctica, podés ampliarla sin modificar las funciones financieras existentes:

    1. Agregar un botón que copie la tabla como CSV.
    2. Permitir nombres personalizados para los escenarios.
    3. Guardar el último escenario en el navegador.
    4. Incorporar pruebas para los límites mínimo y máximo.
    5. Mostrar una advertencia cuando la tasa y la unidad temporal no estén documentadas.
    Ver criterios de aceptación
    • La exportación contiene encabezados y n + 1 filas.
    • Los números exportados no incluyen símbolos monetarios.
    • Un escenario guardado vuelve a validarse antes de calcular.
    • Los nuevos controles tienen etiquetas y foco visible.
    • Una ampliación no duplica las fórmulas de monto.

    28.15 Cierre del recorrido

    • Tradujimos problemas financieros a variables, funciones y estructuras.
    • Representamos el valor del dinero mediante puntos, intervalos y flujos.
    • Implementamos interés simple y compuesto sin confundir sus bases.
    • Comparamos fórmulas cerradas con recorridos iterativos.
    • Separamos cálculo, validación, formato e interfaz.
    • Construimos tablas para explicar y comprobar resultados.
    • Integramos todo en una calculadora reutilizable y accesible.

    La herramienta final no reemplaza las condiciones de una operación real, pero establece una base sólida para continuar con tasas equivalentes, descuentos, rentas, sistemas de amortización y evaluación de proyectos.