21. Funciones JavaScript para convertir y comparar tasas

Una API financiera confiable separa representación, validación, factores, conversiones y presentación de resultados.

21.1 De las fórmulas a una API

Los temas anteriores desarrollaron conversiones específicas. Al programarlas conviene evitar una función distinta para cada par posible, porque el número de combinaciones crece rápidamente y las fórmulas se duplican.

Una estrategia escalable convierte cualquier cotización a un factor anual común y luego transforma ese factor al tipo de destino. Así se centralizan las reglas y cada nueva cotización necesita solo dos operaciones: entrar y salir del factor.

Idea central: el factor anual es la representación canónica interna. Las tasas nominales, efectivas, anticipadas y continuas son interfaces diferentes del mismo efecto financiero.

21.2 Contrato de datos

Una tasa no debe viajar por el programa como un número sin contexto. Podemos representarla mediante un objeto:

const tasa = Object.freeze({
  valor: 0.24,
  tipo: "nominal-vencida",
  frecuencia: 12,
  unidad: "anual",
  representacion: "tanto-por-uno"
});

console.log(tasa);

El contrato establece que valor siempre usa tanto por uno. La interfaz convierte porcentajes al entrar y vuelve a porcentajes únicamente para mostrar.

21.3 Separar responsabilidades

Validar

Comprueba tipos, dominio, frecuencia y finitud.

validarTasa()

Normalizar

Convierte la cotización a un factor anual.

aFactorAnual()

Transformar

Expresa el factor en el tipo solicitado.

desdeFactorAnual()

El formateo monetario, la lectura del formulario y la manipulación del DOM pertenecen a otra capa. Las funciones financieras deben poder probarse sin navegador.

21.4 Tipos de cotización

Tipos utilizados por la API del tema.
IdentificadorSignificadoFrecuencia
efectiva-anualTasa efectiva vencida de un año.No necesaria.
efectiva-periodicaTasa efectiva vencida de cada subperíodo.Obligatoria.
nominal-vencidaTNA vencida convertible m veces.Obligatoria.
nominal-anticipadaTNA anticipada convertible m veces.Obligatoria.
fuerza-anualFuerza constante por año.No necesaria.

21.5 Validación de entrada

Validar solo con typeof valor === "number" no alcanza: NaN e Infinity también son números en JavaScript. Debe usarse Number.isFinite.

function exigirFinito(valor, nombre) {
  if (!Number.isFinite(valor)) {
    throw new TypeError(`${nombre} debe ser un número finito`);
  }
}

function exigirFrecuencia(frecuencia) {
  if (!Number.isFinite(frecuencia) || frecuencia <= 0) {
    throw new RangeError("La frecuencia debe ser positiva");
  }
}

exigirFinito(0.24, "tasa");
exigirFrecuencia(12);
console.log("Datos válidos");

21.6 Normalizar a factor anual

La función central traduce cada tipo según su definición:

efectiva anual: 1+i
efectiva periódica: (1+im)m
nominal vencida: (1+j(m)/m)m
nominal anticipada: (1−f(m)/m)−m
fuerza anual: eδ

El resultado debe ser positivo y finito. Esta poscondición unifica las restricciones particulares de cada tasa.

21.7 Convertir desde el factor

Dado un factor anual F>0, las transformaciones inversas son:

efectiva anual: F−1
efectiva periódica: F1/m−1
nominal vencida: m(F1/m−1)
nominal anticipada: m(1−F−1/m)
fuerza anual: ln(F)

Si ambas funciones son correctas, convertir al destino y regresar al origen reconstruye la tasa inicial dentro de la tolerancia numérica.

21.8 Arquitectura de la conversión

Cotización origenvalor, tipo y frecuencia
Validacióndominio financiero
Factor anualrepresentación común
Cotización destinotipo y frecuencia elegidos

Una función convertirTasa orquesta el proceso sin conocer cada pareja de fórmulas. Esto evita implementar manualmente 20 conversiones dirigidas entre cinco tipos.

21.9 Comparar tasas

La comparación tampoco necesita convertir A al tipo de B. Basta construir sus factores sobre el mismo horizonte:

FA(t)=FA,anualt
FB(t)=FB,anualt

Para un horizonte positivo, el factor mayor genera mayor capital final en un modelo proporcional. Con costos fijos o flujos intermedios, la función debe recibir la operación completa, no solo la tasa.

21.10 Igualdad aproximada

Los números en punto flotante no representan exactamente muchos decimales. Comparar con === puede fallar aunque las tasas se hayan construido como equivalentes.

function exigirFinito(valor, nombre) {
  if (!Number.isFinite(valor)) {
    throw new TypeError(`${nombre} debe ser finito`);
  }
}

function casiIguales(a, b, tolerancia = 1e-12) {
  exigirFinito(a, "a");
  exigirFinito(b, "b");
  const escala = Math.max(1, Math.abs(a), Math.abs(b));
  return Math.abs(a - b) <= tolerancia * escala;
}

console.log(casiIguales(0.1 + 0.2, 0.3));

La tolerancia debe elegirse según la precisión de los datos y el uso del resultado. No sirve para ocultar diferencias económicas materiales.

21.11 Estabilidad numérica

Para valores cercanos a cero, Math.log1p(x) calcula ln(1+x) y Math.expm1(x) calcula ex−1 con mejor precisión que las expresiones directas.

Por ejemplo, una nominal vencida puede normalizarse como Math.exp(m*Math.log1p(j/m)). Para obtener la nominal desde el factor se usa m*Math.expm1(Math.log(F)/m).

21.12 Errores y resultados

Los datos inválidos deberían producir errores explícitos, no NaN propagado silenciosamente. TypeError comunica una representación incorrecta y RangeError un número fuera del dominio.

Las funciones pueden devolver objetos con tasa, factor y metadatos. Esto facilita auditoría y evita recalcular valores:

function resultadoConversion(valor, tipo, frecuencia, factorAnual) {
  return Object.freeze({ valor, tipo, frecuencia, factorAnual });
}

console.log(resultadoConversion(0.02,
  "efectiva-periodica", 12, 1.02 ** 12));

21.13 Inmutabilidad y funciones puras

Una función pura no modifica el objeto recibido ni depende del DOM, moneda o configuración global. Dados los mismos argumentos, devuelve el mismo resultado.

Object.freeze ayuda a evitar modificaciones accidentales, aunque no congela recursivamente objetos anidados. En una biblioteca puede complementarse con tipos de TypeScript o esquemas de validación.

21.14 Pruebas que expresan propiedades

Además de ejemplos conocidos, conviene probar propiedades generales:

  • Una conversión de ida y vuelta recupera el valor original.
  • El factor de origen coincide con el factor de destino.
  • La tasa cero se convierte en cero para cualquier tipo.
  • Los límites de dominio generan errores.
  • Una tasa mayor produce un factor mayor bajo iguales condiciones.
function afirmarCerca(actual, esperado, tolerancia = 1e-12) {
  const error = Math.abs(actual - esperado);
  if (error > tolerancia * Math.max(1, Math.abs(esperado))) {
    throw new Error(`Esperado ${esperado}; recibido ${actual}`);
  }
}

const tea = (1 + 0.24 / 12) ** 12 - 1;
const nominalRecuperada = 12 * ((1 + tea) ** (1 / 12) - 1);
afirmarCerca(nominalRecuperada, 0.24);
console.log("Prueba superada");

21.15 Laboratorio: API universal de tasas

Configurá una cotización de origen y otra de destino. El laboratorio convierte mediante el factor anual y mide el error del recorrido inverso.

Explorador de conversiones

Las frecuencias se utilizan solo en tipos periódicos o nominales.

Factor anual1,268242
Tasa de destino26,824179 %
Factor del horizonte1,608437
Error de ida y vuelta0,000000000000

Resultado estructurado

{ "valor": 0.26824179, "tipo": "effective-annual", "factorAnual": 1.26824179 }

Conversión válida: el factor se conserva.

21.16 Implementación integrada

function exigirFinito(valor, nombre) {
  if (!Number.isFinite(valor)) throw new TypeError(`${nombre} debe ser finito`);
}

function exigirFrecuencia(frecuencia) {
  if (!Number.isFinite(frecuencia) || frecuencia <= 0) {
    throw new RangeError("La frecuencia debe ser positiva");
  }
}

function aFactorAnual({ valor, tipo, frecuencia = 1 }) {
  exigirFinito(valor, "valor");
  if (["efectiva-periodica", "nominal-vencida",
       "nominal-anticipada"].includes(tipo)) exigirFrecuencia(frecuencia);

  let factor;
  if (tipo === "efectiva-anual") factor = 1 + valor;
  else if (tipo === "efectiva-periodica") {
    if (1 + valor <= 0) throw new RangeError("Factor periódico no positivo");
    factor = (1 + valor) ** frecuencia;
  } else if (tipo === "nominal-vencida") {
    if (1 + valor / frecuencia <= 0) throw new RangeError("Factor periódico no positivo");
    factor = (1 + valor / frecuencia) ** frecuencia;
  } else if (tipo === "nominal-anticipada") {
    if (1 - valor / frecuencia <= 0) throw new RangeError("Factor periódico no positivo");
    factor = (1 - valor / frecuencia) ** (-frecuencia);
  }
  else if (tipo === "fuerza-anual") factor = Math.exp(valor);
  else throw new RangeError("Tipo de tasa desconocido");

  if (!Number.isFinite(factor) || factor <= 0) {
    throw new RangeError("La tasa no produce un factor anual válido");
  }
  return factor;
}

function desdeFactorAnual(factor, tipo, frecuencia = 1) {
  if (!Number.isFinite(factor) || factor <= 0) {
    throw new RangeError("El factor anual debe ser positivo y finito");
  }
  if (["efectiva-periodica", "nominal-vencida",
       "nominal-anticipada"].includes(tipo)) exigirFrecuencia(frecuencia);
  if (tipo === "efectiva-anual") return factor - 1;
  if (tipo === "efectiva-periodica") return Math.expm1(Math.log(factor) / frecuencia);
  if (tipo === "nominal-vencida") return frecuencia * Math.expm1(Math.log(factor) / frecuencia);
  if (tipo === "nominal-anticipada") return frecuencia * (1 - Math.exp(-Math.log(factor) / frecuencia));
  if (tipo === "fuerza-anual") return Math.log(factor);
  throw new RangeError("Tipo de tasa desconocido");
}

const origen = { valor: 0.24, tipo: "nominal-vencida", frecuencia: 12 };
const factor = aFactorAnual(origen);
console.log(desdeFactorAnual(factor, "efectiva-anual"));

21.17 Errores frecuentes

  • Recibir porcentajes sin declararlo: la API debe fijar una representación interna.
  • Mezclar cálculo y DOM: dificulta reutilización y pruebas.
  • Duplicar fórmulas por pareja: aumenta inconsistencias.
  • Aceptar NaN o infinito: deben rechazarse explícitamente.
  • Comparar con ===: las operaciones flotantes requieren tolerancia.
  • Redondear dentro del núcleo: se pierde equivalencia en cálculos posteriores.
  • Ocultar errores de dominio: un resultado silencioso puede parecer válido.

21.18 Ejercicio propuesto

Extendé la API con una función compararTasas(a,b,horizonte) que devuelva los factores, indique cuál es mayor y detecte equivalencia con tolerancia relativa. Probala con una TNA vencida del 24 % mensual y su TEA equivalente.

Ver una solución
function aFactorAnual({ valor, tipo, frecuencia = 1 }) {
  if (tipo === "efectiva-anual") return 1 + valor;
  if (tipo === "nominal-vencida") return (1 + valor / frecuencia) ** frecuencia;
  throw new RangeError("Tipo no implementado en este ejercicio");
}

function casiIguales(a, b, tolerancia = 1e-12) {
  return Math.abs(a - b) <= tolerancia * Math.max(1, Math.abs(a), Math.abs(b));
}

function compararTasas(a, b, horizonte = 1) {
  if (!Number.isFinite(horizonte) || horizonte < 0) {
    throw new RangeError("El horizonte debe ser no negativo");
  }
  const anualA = aFactorAnual(a);
  const anualB = aFactorAnual(b);
  const factorA = anualA ** horizonte;
  const factorB = anualB ** horizonte;
  const equivalentes = casiIguales(factorA, factorB);
  return Object.freeze({ factorA, factorB, equivalentes,
    mejor: equivalentes ? "iguales" : factorA > factorB ? "a" : "b" });
}

const a = { valor: 0.24, tipo: "nominal-vencida", frecuencia: 12 };
const b = { valor: (1.02 ** 12) - 1, tipo: "efectiva-anual" };
console.log(compararTasas(a, b, 2));

La función compara efectos del horizonte y devuelve datos estructurados. En una aplicación real, las dependencias compartidas formarían parte del mismo módulo.

21.19 Ideas para recordar

  • Una tasa debe viajar con tipo, valor, frecuencia y unidad.
  • El factor anual funciona como representación canónica.
  • Validación, cálculo y presentación deben estar separados.
  • log1p y expm1 mejoran la estabilidad cerca de cero.
  • Las comparaciones flotantes requieren una tolerancia relativa.
  • Las pruebas de ida y vuelta verifican que las conversiones conservan el factor.
  • Los costos y flujos completos requieren modelos más amplios que una tasa aislada.

En el próximo módulo aplicaremos estas bases a la actualización de capitales y la interpretación del descuento.