UNPKG

@mixxtor/currencyx-js

Version:

Modern TypeScript currency converter with type inference and multiple providers (Google Finance, Fixer.io). Framework agnostic with clean architecture.

233 lines (206 loc) 7.12 kB
import type { CurrencyCode, ConversionResult, ExchangeRatesResult, ConvertParams, ExchangeRatesParams, CurrencyInfo, CountryCode, TRoundOptions, } from '../types/index.js' import type { CurrencyExchangeContract } from '../contracts/currency_exchange.js' import { getList } from '../data/currencies.js' export abstract class BaseCurrencyExchange implements CurrencyExchangeContract { /** * Exchange name - must be implemented by subclasses */ abstract readonly name: string /** * Base currency code. Default is 'USD'. */ public base: CurrencyCode = 'USD' /** * Get all supported currency codes */ public get currencies() { return getList().map((c) => c.code) } /** * Get all constant currencies */ getList() { return getList() } /** * Filter constant currencies by name * @param {string} name - Currency name */ filterByName(name: CurrencyInfo['name']): CurrencyInfo[] filterByName(name: string) { return this.getList().filter((c) => c.name.includes(name)) } /** * Filter constant currencies by country * @param {string} iso2 - Country ISO2 code */ filterByCountry(iso2: CountryCode) { return this.getList().filter((c) => c.countries.find((c) => c === iso2.toUpperCase())) } /** * Get constant currency info by country ISO2 code (e.g., 'US') * @param {string} iso2 */ getByCountry(iso2: CountryCode): CurrencyInfo | undefined getByCountry(iso2: string) { return this.getList().find((c) => c.countries.find((c) => c === iso2.toUpperCase())) } /** * Get constant currency info by ISO code (e.g., 'USD') * @param {string} code - Currency ISO code */ getByCode(code: CurrencyCode): CurrencyInfo | undefined { return this.getList().find((c) => c.code === code) } /** * Get constant currency info by symbol (e.g., '$') * @param {string} symbol - Currency symbol (e.g., '$') */ getBySymbol(symbol: CurrencyInfo['symbol']): CurrencyInfo | undefined getBySymbol(symbol: string) { return this.getList().find((c) => c.symbol === symbol) } /** * Get constant currency info by numeric code (e.g., '840') * @param {string} numCode - Currency numeric code */ getByNumericCode(numCode: CurrencyInfo['numeric_code']): CurrencyInfo | undefined getByNumericCode(numCode: string) { return this.getList().find((c) => c.numeric_code === numCode) } /** * Abstract method that retrieves the latest currency conversion rates. * * @param {ExchangeRatesParams} params - The parameters for getting exchange rates. * @param {CurrencyCode} params.base - The base currency code to retrieve rates for. * @param {CurrencyCode[]} params.codes - The currency codes to retrieve rates for. */ abstract latestRates(params?: ExchangeRatesParams): Promise<ExchangeRatesResult> /** * Abstract method that retrieves the currency conversion rate. * * @param {ConvertParams} params - The parameters for converting currency. * @param {number} params.amount - The amount to convert. * @param {CurrencyCode} params.from - The currency code to convert from. * @param {CurrencyCode} params.to - The currency code to convert to. Defaults to 'USD'. */ abstract convert(params: ConvertParams): Promise<ConversionResult> /** * Abstract method that retrieves the currency conversion rate. * * @param {CurrencyCode} from - The currency code to convert from. * @param {CurrencyCode} to - The currency code to convert to. Defaults to 'USD'. * @param {CurrencyInfo[]} currencyList - List of currencies */ abstract getConvertRate(from: CurrencyCode, to: CurrencyCode, currencyList?: CurrencyInfo[]): Promise<number | undefined> /** * Set base currency */ setBase(currency: CurrencyCode): this { this.base = currency || 'USD' return this } /** * Set API key (default implementation - can be overridden) * Default does implementation does nothing. * Exchanges that need API keys should override this */ setKey(_key: string): this { return this } /** * Round currency value according to currency rules * * @param {number} amount - Currency value * @param {TRoundOptions} options * @param {number} options.precision - Decimal precision. Default is 2 * @param {string} options.direction - Round direction. Default is 'up' */ round(amount: number, options: TRoundOptions = { precision: 2, direction: 'up' }): number { const { precision } = options if (options?.precision !== undefined) { return Math.round(Number(amount) * Math.pow(10, precision)) / Math.pow(10, precision) } // Use default precision of 2 decimal places return Math.round((Number(amount) + Number.EPSILON) * 100) / 100 } /** * Rounds a money amount to the nearest valid value for the given currency. * * Handles: * - Fractional rounding (e.g., 0.01, 0.05) * - Whole number rounding (e.g., 1, 5, 10) * - Avoids floating-point precision issues * - Works correctly for any rounding increment, including non-decimal-friendly ones (e.g., 0.2, 0.25) * * @example * round: 0.01 → round to nearest cent * round: 0.05 → round to nearest 5 cents * round: 0.2 → round to nearest 0.2 unit * round: 1 → round to nearest whole unit * * @param {number} amount - The amount to round * @param {CurrencyCode} [currency='USD'] - The currency to determine rounding rules * @return {number} The rounded amount */ public roundMoney(amount: number, currency: CurrencyCode = 'USD'): number { const data = this.getByCode(currency) // Fallback if currency data is invalid or amount is not a number if (!data || isNaN(amount) || data.round <= 0) { return this.round(amount) } const { round } = data // Determine the number of decimal places based on the rounding increment const decimalPlaces = round < 1 ? Math.ceil(-Math.log10(round)) : 0 // Round to the nearest increment const rounded = Math.round(amount / round) * round // Fix floating-point artifacts return Number(rounded.toFixed(decimalPlaces)) } /** * Create standardized conversion result */ protected createConversionResult( amount: number, from: CurrencyCode, to: CurrencyCode, result?: number, rate?: number, error?: { code?: number; info: string; type?: string } ): ConversionResult { return { success: !error && result !== undefined, query: { from, to, amount }, info: { timestamp: Date.now(), rate }, date: new Date().toISOString(), result, error, } } /** * Create standardized exchange rates result */ protected createExchangeRatesResult( base: CurrencyCode, rates: Record<string, number>, error?: { code?: number; info: string; type?: string } ): ExchangeRatesResult { return { success: !error && Object.keys(rates).length > 0, timestamp: Date.now(), date: new Date().toISOString(), base, rates, error, } } }