@mixxtor/currencyx-js
Version:
Modern TypeScript currency converter with type inference and multiple providers (Google Finance, Fixer.io). Framework agnostic with clean architecture.
327 lines (270 loc) • 13.9 kB
text/typescript
/**
* Currency Rounding Tests
* Comprehensive tests for the roundMoney functionality and currency data validation
* These tests verify the fixes for the issue where roundMoney(123, 'USD') returned 0
*/
import { describe, it, expect } from 'vitest'
import { GoogleFinanceExchange } from '../exchanges/index.js'
import { getList } from '../data/currencies.js'
describe('Currency Rounding System', () => {
const exchange = new GoogleFinanceExchange()
describe('Original Issue Fix', () => {
it('should fix roundMoney(123, "USD") returning 0 instead of 123', () => {
const result = exchange.roundMoney(123, 'USD')
expect(result).toBe(123)
})
it('should handle basic USD rounding correctly', () => {
expect(exchange.roundMoney(123.456, 'USD')).toBeCloseTo(123.46, 2)
expect(exchange.roundMoney(123.454, 'USD')).toBeCloseTo(123.45, 2)
expect(exchange.roundMoney(100, 'USD')).toBe(100)
expect(exchange.roundMoney(0, 'USD')).toBe(0)
})
})
describe('Major Currency Rounding Rules', () => {
describe('0.01 Rounding (Most Common)', () => {
const currencies = ['USD', 'EUR', 'GBP', 'SGD', 'HKD']
currencies.forEach((currency) => {
it(`should round ${currency} to nearest 0.01`, () => {
expect(exchange.roundMoney(123.456, currency)).toBeCloseTo(123.46, 2)
expect(exchange.roundMoney(123.454, currency)).toBeCloseTo(123.45, 2)
expect(exchange.roundMoney(99.999, currency)).toBeCloseTo(100.0, 2)
expect(exchange.roundMoney(0.001, currency)).toBeCloseTo(0.0, 2)
})
})
})
describe('0.05 Rounding (Swiss-style)', () => {
const currencies = ['CHF', 'CAD', 'AUD', 'NZD']
currencies.forEach((currency) => {
it(`should round ${currency} to nearest 0.05`, () => {
expect(exchange.roundMoney(1.237, currency)).toBeCloseTo(1.25, 2)
expect(exchange.roundMoney(1.227, currency)).toBeCloseTo(1.25, 2)
expect(exchange.roundMoney(1.222, currency)).toBeCloseTo(1.2, 2)
expect(exchange.roundMoney(1.274, currency)).toBeCloseTo(1.25, 2)
expect(exchange.roundMoney(1.276, currency)).toBeCloseTo(1.3, 2)
})
})
})
describe('1.0 Rounding (No Decimals)', () => {
const currencies = ['JPY', 'CLP', 'VUV', 'XAF', 'XOF', 'XPF', 'BIF', 'DJF', 'GNF', 'ISK', 'PYG', 'RWF', 'UGX']
currencies.forEach((currency) => {
it(`should round ${currency} to nearest whole number`, () => {
expect(exchange.roundMoney(123.456, currency)).toBe(123)
expect(exchange.roundMoney(123.789, currency)).toBe(124)
expect(exchange.roundMoney(99.4, currency)).toBe(99)
expect(exchange.roundMoney(99.5, currency)).toBe(100)
expect(exchange.roundMoney(0.7, currency)).toBe(1)
})
})
})
describe('Special Rounding Cases', () => {
it('should handle precious metals (XAG, XAU) with 0.001 precision', () => {
expect(exchange.roundMoney(1.2346, 'XAG')).toBeCloseTo(1.235, 3)
expect(exchange.roundMoney(1.2344, 'XAU')).toBeCloseTo(1.234, 3)
})
it('should handle Venezuelan currencies with no decimals', () => {
expect(exchange.roundMoney(123.456, 'VED')).toBe(123)
expect(exchange.roundMoney(123.789, 'VEF')).toBe(124)
})
it('should handle KRW with 10 rounding', () => {
expect(exchange.roundMoney(123.456, 'KRW')).toBe(120) // Rounds to nearest 10
expect(exchange.roundMoney(125, 'KRW')).toBe(130) // Rounds up to nearest 10
expect(exchange.roundMoney(1234, 'KRW')).toBe(1230)
expect(exchange.roundMoney(1500, 'KRW')).toBe(1500)
})
it('should handle KMF with 100 rounding', () => {
expect(exchange.roundMoney(123.456, 'KMF')).toBe(100) // Rounds to nearest 100
expect(exchange.roundMoney(149, 'KMF')).toBe(100)
expect(exchange.roundMoney(150, 'KMF')).toBe(200)
})
it('should handle Middle Eastern currencies correctly', () => {
// Most Middle Eastern currencies use 0.01 rounding except where noted
expect(exchange.roundMoney(123.456, 'AED')).toBeCloseTo(123.46, 2) // UAE Dirham
expect(exchange.roundMoney(123.456, 'SAR')).toBeCloseTo(123.46, 2) // Saudi Riyal
expect(exchange.roundMoney(123.456, 'QAR')).toBeCloseTo(123.46, 2) // Qatari Riyal
expect(exchange.roundMoney(1.2346, 'KWD')).toBeCloseTo(1.235, 3) // KWD uses 0.001 precision
})
it('should handle African currencies correctly', () => {
expect(exchange.roundMoney(123.456, 'ZAR')).toBeCloseTo(123.46, 2) // South African Rand
expect(exchange.roundMoney(123.456, 'EGP')).toBeCloseTo(123.46, 2) // Egyptian Pound
expect(exchange.roundMoney(123.456, 'NGN')).toBeCloseTo(123.46, 2) // Nigerian Naira
expect(exchange.roundMoney(123.456, 'MGA')).toBeCloseTo(123.4, 1) // Malagasy Ariary 0.2 rounding
})
it('should handle real-world corrected Asian currencies', () => {
// VND: 500 rounding (cash transactions)
expect(exchange.roundMoney(79015.16, 'VND')).toBe(79000)
expect(exchange.roundMoney(79250, 'VND')).toBe(79500)
// IDR: 100 rounding (practical usage)
expect(exchange.roundMoney(15350, 'IDR')).toBe(15400)
expect(exchange.roundMoney(15349, 'IDR')).toBe(15300)
// MMK: 5 rounding (small denominations)
expect(exchange.roundMoney(1503, 'MMK')).toBe(1505)
expect(exchange.roundMoney(1502, 'MMK')).toBe(1500)
// KHR: 100 rounding (more reasonable than 1000)
expect(exchange.roundMoney(2350, 'KHR')).toBe(2400)
expect(exchange.roundMoney(2349, 'KHR')).toBe(2300)
// LAK: 100 rounding (more practical than 1000)
expect(exchange.roundMoney(8650, 'LAK')).toBe(8700)
expect(exchange.roundMoney(8649, 'LAK')).toBe(8600)
})
})
})
describe('Edge Cases and Error Handling', () => {
it('should handle zero amounts', () => {
expect(exchange.roundMoney(0, 'USD')).toBe(0)
expect(exchange.roundMoney(0, 'JPY')).toBe(0)
expect(exchange.roundMoney(0, 'CHF')).toBe(0)
})
it('should handle very small amounts', () => {
expect(exchange.roundMoney(0.001, 'USD')).toBeCloseTo(0.0, 2)
expect(exchange.roundMoney(0.009, 'USD')).toBeCloseTo(0.01, 2)
expect(exchange.roundMoney(0.1, 'JPY')).toBe(0)
expect(exchange.roundMoney(0.9, 'JPY')).toBe(1)
})
it('should handle large amounts', () => {
expect(exchange.roundMoney(1234567.89, 'USD')).toBeCloseTo(1234567.89, 2)
expect(exchange.roundMoney(9999999.456, 'JPY')).toBe(9999999)
expect(exchange.roundMoney(1000000.237, 'CHF')).toBeCloseTo(1000000.25, 2)
})
it('should handle negative amounts correctly', () => {
expect(exchange.roundMoney(-123.456, 'USD')).toBeCloseTo(-123.46, 2)
expect(exchange.roundMoney(-123.456, 'JPY')).toBe(-123)
expect(exchange.roundMoney(-1.237, 'CHF')).toBeCloseTo(-1.25, 2)
})
it('should handle invalid currency codes gracefully', () => {
// Note: The current implementation may not throw for invalid codes
// This tests the actual behavior
const invalidResult1 = exchange.roundMoney(123, 'INVALID')
const invalidResult2 = exchange.roundMoney(123, 'XXX')
// These should either throw OR return the original amount
expect(typeof invalidResult1).toBe('number')
expect(typeof invalidResult2).toBe('number')
})
})
describe('Currency Data Validation', () => {
const currencies = getList()
it('should have valid currency data structure', () => {
expect(Array.isArray(currencies)).toBe(true)
expect(currencies.length).toBeGreaterThan(0)
})
it('should not contain any currencies with placeholder values', () => {
const activeCurrencies = currencies.filter((c) => typeof c.code === 'string' && c.code.length === 3)
// Since we fixed all currencies, this test confirms our TypeScript types are correct
// All currencies should have valid data now
activeCurrencies.forEach((currency) => {
expect(typeof currency.round).toBe('number')
expect(currency.round).toBeGreaterThanOrEqual(0)
expect(currency.round).toBeLessThan(10000) // No more 9999999 values
expect(typeof currency.decimal).toBe('number')
expect(currency.decimal).toBeLessThan(1000) // No more 111111 values
expect(typeof currency.delimiter).toBe('string')
expect(currency.delimiter).not.toBe('DE_LI') // No more placeholder delimiters
expect(currency.short_format).not.toBe('FORSHRT') // No more placeholder formats
expect(currency.explicit_format).not.toBe('FORMEX') // No more placeholder formats
})
})
it('should have valid rounding values for all active currencies', () => {
const activeCurrencies = currencies.filter((c) => typeof c.code === 'string' && c.code.length === 3)
const validRoundingValues = [0, 0.0001, 0.001, 0.01, 0.05, 0.2, 0.25, 0.5, 1, 5, 10, 100, 500, 1000, 1e-8]
activeCurrencies.forEach((currency) => {
expect(validRoundingValues).toContain(currency.round)
expect(typeof currency.decimal).toBe('number')
expect(currency.decimal).toBeGreaterThanOrEqual(0)
expect(currency.decimal).toBeLessThanOrEqual(8)
expect(typeof currency.delimiter).toBe('string')
expect([',', '.'].includes(currency.delimiter)).toBe(true)
})
})
it('should have proper format strings for all active currencies', () => {
const activeCurrencies = currencies.filter((c) => typeof c.code === 'string' && c.code.length === 3)
activeCurrencies.forEach((currency) => {
expect(typeof currency.short_format).toBe('string')
expect(currency.short_format.length).toBeGreaterThan(0)
expect(currency.short_format).toContain('{{amount}}')
expect(typeof currency.explicit_format).toBe('string')
expect(currency.explicit_format.length).toBeGreaterThan(0)
expect(currency.explicit_format).toContain('{{amount}}')
})
})
})
describe('Regional Currency Groups', () => {
describe('European Currencies', () => {
const europeanCurrencies = ['EUR', 'GBP', 'CHF', 'NOK', 'SEK', 'DKK', 'PLN', 'CZK', 'HUF', 'RON']
europeanCurrencies.forEach((currency) => {
it(`should handle ${currency} correctly`, () => {
const result = exchange.roundMoney(123.456, currency)
expect(typeof result).toBe('number')
expect(result).toBeGreaterThan(120)
expect(result).toBeLessThan(130)
})
})
})
describe('Asian Currencies', () => {
const asianCurrencies = ['JPY', 'CNY', 'KRW', 'INR', 'SGD', 'HKD', 'THB', 'MYR', 'PHP', 'IDR', 'VND']
asianCurrencies.forEach((currency) => {
it(`should handle ${currency} correctly`, () => {
const result = exchange.roundMoney(123.456, currency)
expect(typeof result).toBe('number')
expect(result).toBeGreaterThanOrEqual(0)
})
})
})
describe('American Currencies', () => {
const americanCurrencies = ['USD', 'CAD', 'MXN', 'BRL', 'ARS', 'CLP', 'COP', 'PEN', 'UYU']
americanCurrencies.forEach((currency) => {
it(`should handle ${currency} correctly`, () => {
const result = exchange.roundMoney(123.456, currency)
expect(typeof result).toBe('number')
expect(result).toBeGreaterThan(100)
expect(result).toBeLessThan(150)
})
})
})
describe('Middle Eastern Currencies', () => {
const middleEasternCurrencies = ['AED', 'SAR', 'QAR', 'KWD', 'BHD', 'JOD', 'ILS', 'TRY']
middleEasternCurrencies.forEach((currency) => {
it(`should handle ${currency} correctly`, () => {
const result = exchange.roundMoney(123.456, currency)
expect(typeof result).toBe('number')
expect(result).toBeGreaterThan(100)
expect(result).toBeLessThan(150)
})
})
})
})
describe('Performance and Reliability', () => {
it('should handle multiple rapid rounding operations', () => {
const iterations = 1000
const startTime = Date.now()
for (let i = 0; i < iterations; i++) {
exchange.roundMoney(Math.random() * 1000, 'USD')
}
const endTime = Date.now()
const duration = endTime - startTime
// Should complete 1000 operations in less than 100ms
expect(duration).toBeLessThan(100)
})
it('should produce consistent results for the same input', () => {
const amount = 123.456
const currency = 'USD'
const expected = exchange.roundMoney(amount, currency)
// Run the same operation 100 times
for (let i = 0; i < 100; i++) {
const result = exchange.roundMoney(amount, currency)
expect(result).toBe(expected)
}
})
it('should handle floating point precision correctly', () => {
// Test cases that could cause floating point issues
expect(exchange.roundMoney(0.1 + 0.2, 'USD')).toBeCloseTo(0.3, 2)
expect(exchange.roundMoney(1.005, 'USD')).toBeCloseTo(1.0, 2) // Standard rounding case
expect(exchange.roundMoney(2.675, 'USD')).toBeCloseTo(2.68, 2)
// Specific test for the reported issue: roundMoney(29.99, 'USD') should return exactly 29.99
expect(exchange.roundMoney(29.99, 'USD')).toBe(29.99) // Should be exact, not 29.990000000000002
// Additional edge cases that can cause precision issues
expect(exchange.roundMoney(19.99, 'USD')).toBe(19.99)
expect(exchange.roundMoney(9.99, 'USD')).toBe(9.99)
expect(exchange.roundMoney(99.99, 'USD')).toBe(99.99)
expect(exchange.roundMoney(199.99, 'USD')).toBe(199.99)
})
})
})