'use strict'; // currency-conversion-ecb.cjs -- METHOD DECK (X063), currency conversion at a stated reference rate. // The rate, its date, AND its source are all INPUTS: the visitor supplies the // units-of-foreign-currency-per-1-EUR rate, the date it applies to, and the citation it came // from (there is no baked-in lookup table and no live fetch). Per the Founder rule via W001/PQ021, // any value that changes over time is an input with a source and a date; the rate here is exactly // such a value. The ECB euro reference rates for 2026-09-30 are kept in `constants` ONLY as the // documented default values used to prefill the worked example; they are not read by run(). // Converting between two non-EUR currencies is out of scope here; use the currency-cross-rate // card for that. function fmt(x, decimals) { return x.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }); } const CURRENCIES = ['EUR', 'USD', 'GBP', 'JPY', 'CHF', 'CAD', 'AUD']; // ECB euro reference rates for 2026-09-30 (units of currency per 1 EUR), documented here only as // the default values the worked example and test vectors prefill; run() takes rate/rate_date/ // rate_source as inputs and never reads this table. const ECB_2026_09_30 = { USD: 1.1355, GBP: 0.85463, JPY: 178.27, CHF: 0.9478, CAD: 1.6105, AUD: 1.6297 }; const ECB_URL = 'https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html'; module.exports = { slug: 'currency-conversion-ecb', title: 'Currency conversion at a stated reference rate.', deck_category: 'currency', description: 'Converts an amount between EUR and one other currency using a stated rate (units of that currency per 1 EUR), the date it applies to, and its cited source, all supplied by the visitor.', inputs: [ { name: 'amount', type: 'number', unit: 'from_currency', required: true, description: 'The amount to convert', min: 0 }, { name: 'from_currency', type: 'string', required: true, description: 'Currency of the input amount', enum: CURRENCIES }, { name: 'to_currency', type: 'string', required: true, description: 'Currency to convert into', enum: CURRENCIES }, { name: 'rate', type: 'number', required: true, description: 'Units of the non-EUR currency in this pair, per 1 EUR, at rate_date', min: 0 }, { name: 'rate_date', type: 'string', unit: 'ISO date', required: true, description: 'The date the rate applies to (for example an ECB reference rate date); change it and the rate together to use a different day.' }, { name: 'rate_source', type: 'string', required: true, description: 'Citation for where the rate came from (for example the ECB reference-rate page URL, or another official central-bank table); change it together with rate and rate_date when using a different table.' }, ], outputs: [ { name: 'result', unit: 'to_currency', description: 'The converted amount at the stated rate' } ], steps: [ 'Exactly one of from_currency and to_currency must be EUR (or both are EUR); for two non-EUR currencies use the currency-cross-rate card instead.', 'The visitor supplies rate (units of the non-EUR currency per 1 EUR), rate_date (the date that rate applies to), and rate_source (the citation it came from); this method never looks up or fetches a rate itself, so any newer official table may be substituted for all three together.', 'If from_currency is EUR: result = amount * rate (EUR converted into the foreign currency).', 'If to_currency is EUR: result = amount / rate (the foreign currency converted into EUR).', 'If from_currency equals to_currency: result = amount unchanged, regardless of rate.', 'Round result to 2 decimal places.', ], constants: [ { name: 'USD_per_EUR_2026_09_30', value: ECB_2026_09_30.USD, unit: 'USD/EUR', source: 0 }, { name: 'GBP_per_EUR_2026_09_30', value: ECB_2026_09_30.GBP, unit: 'GBP/EUR', source: 0 }, { name: 'JPY_per_EUR_2026_09_30', value: ECB_2026_09_30.JPY, unit: 'JPY/EUR', source: 0 }, { name: 'CHF_per_EUR_2026_09_30', value: ECB_2026_09_30.CHF, unit: 'CHF/EUR', source: 0 }, { name: 'CAD_per_EUR_2026_09_30', value: ECB_2026_09_30.CAD, unit: 'CAD/EUR', source: 0 }, { name: 'AUD_per_EUR_2026_09_30', value: ECB_2026_09_30.AUD, unit: 'AUD/EUR', source: 0 }, ], sources: [ { kind: 'url', value: ECB_URL, title: 'Euro foreign exchange reference rates', publisher: 'European Central Bank', retrieved_utc: '2026-09-30T19:29:48Z', note: 'Documented source of the default rate/rate_date/rate_source values prefilled in the worked example and test vectors below (2026-09-30). These are daily reference rates, not live/streaming rates, and are not fetched by this method; the visitor may enter any rate, date, and source.' }, ], disclaimer: null, questionText(inputs) { return `Using a rate of ${inputs.rate} (units of the non-EUR currency per 1 EUR) dated ${inputs.rate_date}, sourced from ${inputs.rate_source}, how much is ${inputs.amount} ${inputs.from_currency} in ${inputs.to_currency}?`; }, run(inputs) { const { amount, from_currency, to_currency, rate, rate_date, rate_source } = inputs || {}; if (typeof amount !== 'number' || !isFinite(amount)) throw new Error('amount must be a finite number'); if (!CURRENCIES.includes(from_currency)) throw new Error('from_currency must be one of: ' + CURRENCIES.join(', ')); if (!CURRENCIES.includes(to_currency)) throw new Error('to_currency must be one of: ' + CURRENCIES.join(', ')); if (typeof rate !== 'number' || !isFinite(rate) || rate <= 0) throw new Error('rate must be a positive finite number'); if (typeof rate_date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(rate_date)) throw new Error('rate_date must be a string in YYYY-MM-DD format'); if (typeof rate_source !== 'string' || !rate_source.trim()) throw new Error('rate_source must be a non-empty string'); let raw; if (from_currency === to_currency) { raw = amount; } else if (from_currency === 'EUR') { raw = amount * rate; } else if (to_currency === 'EUR') { raw = amount / rate; } else { throw new Error('exactly one of from_currency/to_currency must be EUR; for two non-EUR currencies use the currency-cross-rate card'); } const result = Math.round(raw * 100) / 100; return { answer: `${fmt(result, 2)} ${to_currency}`, values: { result } }; }, examples: [ { name: 'worked example', inputs: { amount: 100, from_currency: 'USD', to_currency: 'EUR', rate: ECB_2026_09_30.USD, rate_date: '2026-09-30', rate_source: ECB_URL }, expected: { answer: '88.07 EUR' }, check: 'By hand: 100 USD / 1.1355 USD/EUR = 88.0669... EUR, rounded to 2 decimals is 88.07.' }, { name: 'test vector: EUR to JPY', inputs: { amount: 100, from_currency: 'EUR', to_currency: 'JPY', rate: ECB_2026_09_30.JPY, rate_date: '2026-09-30', rate_source: ECB_URL }, expected: { answer: '17,827.00 JPY' }, check: 'By hand: 100 EUR * 178.27 JPY/EUR = 17,827.00 JPY exactly.' }, { name: 'test vector: CAD to EUR', inputs: { amount: 200, from_currency: 'CAD', to_currency: 'EUR', rate: ECB_2026_09_30.CAD, rate_date: '2026-09-30', rate_source: ECB_URL }, expected: { answer: '124.19 EUR' }, check: 'By hand: 200 CAD / 1.6105 CAD/EUR = 124.1850... EUR, rounded to 2 decimals is 124.19.' }, ], };