You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
118 lines
5.3 KiB
118 lines
5.3 KiB
import type { CurrInstance, CurrDefinition, CurrDisplay, CurrConfig, ConversionValue } from './curr_types.ts';
|
|
import type { LingInstance, LingNode } from '@/ling/lib/types.ts';
|
|
import type { LocaleResolver, GlobLocale } from './comn_types.ts';
|
|
import { resolveLocale } from './comn_engine.ts';
|
|
import { defaultISOLocale } from '@/ling/lib/consts.ts';
|
|
|
|
|
|
/**
|
|
* Crea un módulo de moneda con formateo y conversión.
|
|
*
|
|
* @param definitions - Mapa de monedas disponibles indexado por código ISO.
|
|
* @param ling - Instancia de ling, usada para la pluralización de nombres.
|
|
* @param opts - Configuración inicial (moneda activa, decimales, display).
|
|
* @param defaultLocale - Locale o resolver usado cuando `f()` no recibe locale explícito.
|
|
*/
|
|
export function createCurr<S extends LingNode>(
|
|
definitions : Record<string, CurrDefinition>,
|
|
ling : LingInstance<S>,
|
|
opts : CurrConfig = {},
|
|
defaultLocale: LocaleResolver = defaultISOLocale
|
|
): CurrInstance {
|
|
|
|
let selectedCurrency: string = opts.selectedCurrency ?? Object.keys(definitions)[0] ?? '';
|
|
const baseCurrency : string = opts.baseCurrency ?? selectedCurrency;
|
|
|
|
const defaults = {
|
|
display : opts.display ?? 'symbol' as CurrDisplay,
|
|
decimals : opts.decimals ?? 2,
|
|
};
|
|
|
|
// ── Currency ──────────────────────────────────────────────────────────────
|
|
|
|
function getCurrency(): string { return selectedCurrency; }
|
|
function setCurrency(currency: string): void { selectedCurrency = currency.toUpperCase(); }
|
|
|
|
/** Busca una definición por código ISO, insensible a mayúsculas. */
|
|
function get(currency: string): CurrDefinition | undefined {
|
|
return definitions[currency.toUpperCase()];
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
|
|
function formatAmount(amount: number, decimals: number, locale: GlobLocale): string {
|
|
return new Intl.NumberFormat(locale, {
|
|
minimumFractionDigits: decimals,
|
|
maximumFractionDigits: decimals,
|
|
}).format(amount);
|
|
}
|
|
|
|
/** Resuelve el nombre plural de la moneda según `amount` y el idioma del locale. */
|
|
function resolveName(def: CurrDefinition, amount: number, locale: GlobLocale): string {
|
|
const record = def.name({ count: Math.abs(amount) });
|
|
const baseLocale = locale.split('-')[0] as keyof typeof record;
|
|
return record[baseLocale] ?? record['es'];
|
|
}
|
|
|
|
/** Normaliza `ConversionValue` a número o `null`. Evalúa la función si es dinámica. */
|
|
function resolveConversionValue(value: ConversionValue): number | null {
|
|
if (value === null || value === undefined) return null;
|
|
if (typeof value === 'number') return value;
|
|
return value();
|
|
}
|
|
|
|
// ── Format ────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Formatea `amount` en la moneda activa.
|
|
* Si la moneda no está registrada, devuelve el importe seguido del código ISO.
|
|
*/
|
|
function f(
|
|
amount : number,
|
|
display : CurrDisplay = defaults.display,
|
|
locale : LocaleResolver = defaultLocale
|
|
): string {
|
|
const loc = resolveLocale(locale);
|
|
const formatted = formatAmount(amount, defaults.decimals, loc);
|
|
const def = get(selectedCurrency);
|
|
|
|
if (!def) return `${formatted} ${selectedCurrency}`;
|
|
|
|
switch (display) {
|
|
case 'symbol': return `${formatted} ${def.symbol}`;
|
|
case 'code': return `${formatted} ${def.code}`;
|
|
case 'name': return `${formatted} ${resolveName(def, amount, loc)}`;
|
|
}
|
|
}
|
|
|
|
// ── Conversión ────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Convierte `amount` de `from` a `to` usando sus tasas respecto a la moneda base.
|
|
* Fórmula: `(amount / fromRate) * toRate`
|
|
* Devuelve `null` si alguna moneda no tiene tasa de conversión.
|
|
*/
|
|
function convert(amount: number, from: string, to: string): number | null {
|
|
if (from.toUpperCase() === to.toUpperCase()) return amount;
|
|
|
|
const fromDef = get(from);
|
|
const toDef = get(to);
|
|
if (!fromDef || !toDef) return null;
|
|
|
|
const fromRate = resolveConversionValue(fromDef.conversionValue ?? null);
|
|
const toRate = resolveConversionValue(toDef.conversionValue ?? null);
|
|
if (fromRate === null || toRate === null) return null;
|
|
|
|
// Convertir a moneda base y luego a destino
|
|
const inBase = amount / fromRate;
|
|
return inBase * toRate;
|
|
}
|
|
|
|
/** Convierte desde la moneda activa (`selectedCurrency`) hacia `to`. */
|
|
function convertTo(amount: number, to: string): number | null {
|
|
return convert(amount, selectedCurrency, to);
|
|
}
|
|
|
|
return { f, convert, convertTo, setCurrency, getCurrency, get };
|
|
}
|