@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
text/typescript
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,
}
}
}