feat(conversion): add unit and currency conversion systems with UnitConverter, UnitDefinitions, CurrencyConverter, and ExchangeRateService

This commit is contained in:
JARVIS
2026-07-27 18:40:47 +00:00
parent 42cc719e24
commit c3ad97fa42
4 changed files with 569 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
/**
* Currency converter for the scientific calculator
* Handles conversion between currencies using predefined exchange rates.
* Uses ExchangeRateService to get the rates.
*/
const ExchangeRateService = require('./ExchangeRateService');
/**
* Converts a value from one currency to another.
* @param {number} value - The value to convert.
* @param {string} fromCurrency - The currency code to convert from (e.g., 'USD').
* @param {string} toCurrency - The currency code to convert to (e.g., 'EUR').
* @returns {{value: number, error: string|null}} - Result object with either the converted value or an error.
*/
function convert(value, fromCurrency, toCurrency) {
// Validate input value
if (typeof value !== 'number' || isNaN(value)) {
return { value: null, error: 'Invalid value: must be a number' };
}
// Validate currency codes (must be 3 uppercase letters)
const currencyCodeRegex = /^[A-Z]{3}$/;
if (!currencyCodeRegex.test(fromCurrency)) {
return { value: null, error: `Invalid fromCurrency code: ${fromCurrency}. Must be a 3-letter uppercase code (e.g., USD).` };
}
if (!currencyCodeRegex.test(toCurrency)) {
return { value: null, error: `Invalid toCurrency code: ${toCurrency}. Must be a 3-letter uppercase code (e.g., EUR).` };
}
// Get the exchange rates
const rates = ExchangeRateService.getRates();
const fromRate = rates[fromCurrency];
const toRate = rates[toCurrency];
if (fromRate === undefined) {
return { value: null, error: `Exchange rate for ${fromCurrency} not available.` };
}
if (toRate === undefined) {
return { value: null, error: `Exchange rate for ${toCurrency} not available.` };
}
// Avoid division by zero
if (fromRate === 0) {
return { value: null, error: 'Cannot convert from a currency with zero rate.' };
}
// Conversion: value in target currency = value * (toRate / fromRate)
// Explanation: If we have 1 USD = 0.9 EUR, then rates: { USD: 1, EUR: 0.9 } (assuming base is USD?).
// Actually, we store rates as: 1 unit of currency = rate units of base currency.
// Let's define: we have a base currency (e.g., USD) and rates are the value of 1 unit of that currency in base.
// Then to convert from currency A to currency B:
// value_in_base = value * rate[A]
// value_in_target = value_in_base / rate[B]
// So: result = value * (rate[A] / rate[B])
// However, note that in our ExchangeRateService, we store the rate as the amount of base currency per unit of the currency.
// For example, if base is USD, then rate for EUR is 0.9 (meaning 1 EUR = 0.9 USD).
// Then to convert 100 EUR to USD: 100 * 0.9 = 90 USD.
// To convert 100 USD to EUR: 100 * (1 / 0.9) = 111.11 EUR.
// So the formula is: result = value * (rate[from] / rate[to])
const result = value * (fromRate / toRate);
return { value: result, error: null };
}
module.exports = {
convert,
};
+199
View File
@@ -0,0 +1,199 @@
/**
* Exchange rate service for the scientific calculator
* Manages currency exchange rates with timestamps in localStorage.
* Designed to run in a browser environment.
*/
// Default exchange rates (1 unit of currency = X USD)
// These are example rates and should be updated by the user
const DEFAULT_RATES = {
USD: 1.0, // US Dollar
EUR: 0.9, // Euro
GBP: 0.75, // British Pound
JPY: 0.007, // Japanese Yen
CAD: 0.75, // Canadian Dollar
AUD: 0.7, // Australian Dollar
INR: 0.012 // Indian Rupee
};
// How long until rates are considered stale (in milliseconds)
// 24 hours = 24 * 60 * 60 * 1000 = 86,400,000 ms
const STALE_THRESHOLD = 24 * 60 * 60 * 1000;
// Key for storing rates in localStorage
const STORAGE_KEY = 'calculator_exchange_rates';
/**
* Loads the exchange rates object from localStorage.
* @returns {Object|null} The rates object (with rates and timestamp) or null if not found/invalid.
*/
function _loadFromStorage() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === null) {
return null;
}
const parsed = JSON.parse(stored);
// Validate the structure
if (parsed && typeof parsed === 'object' &&
parsed.rates && typeof parsed.rates === 'object' &&
typeof parsed.timestamp === 'number') {
return parsed;
}
return null;
} catch (e) {
// If there's any error (e.g., invalid JSON), return null
return null;
}
}
/**
* Saves the exchange rates object to localStorage.
* @param {Object} ratesObj - The rates object to save (should have rates and timestamp).
*/
function _saveToStorage(ratesObj) {
try {
const toStore = {
rates: ratesObj.rates,
timestamp: ratesObj.timestamp
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
} catch (e) {
// In a real app, you might want to log this error
console.warn('Failed to save exchange rates to localStorage:', e);
}
}
/**
* Gets the exchange rate for a given currency.
* @param {string} currency - The currency code (e.g., 'USD').
* @returns {number|null} The rate (amount of USD per unit of currency) or null if not found.
*/
function getRate(currency) {
if (typeof currency !== 'string' || currency.length === 0) {
return null;
}
const stored = _loadFromStorage();
let rates = null;
if (stored === null) {
// No stored rates, use defaults and save them
rates = { ...DEFAULT_RATES };
_saveToStorage({ rates, timestamp: Date.now() });
} else {
rates = stored.rates;
}
return rates[currency] !== undefined ? rates[currency] : null;
}
/**
* Sets the exchange rate for a given currency.
* @param {string} currency - The currency code (e.g., 'USD').
* @param {number} rate - The rate (amount of USD per unit of currency).
*/
function setRate(currency, rate) {
if (typeof currency !== 'string' || currency.length === 0 ||
typeof rate !== 'number' || isNaN(rate)) {
return; // Invalid input, do nothing
}
const stored = _loadFromStorage();
let rates = {};
if (stored !== null) {
rates = { ...stored.rates };
} else {
rates = { ...DEFAULT_RATES }; // Start with defaults if none stored
}
rates[currency] = rate;
_saveToStorage({ rates, timestamp: Date.now() });
}
/**
* Gets a copy of the current exchange rates object.
* @returns {Object} A copy of the rates object (currency code -> rate).
*/
function getRates() {
const stored = _loadFromStorage();
if (stored === null) {
// Return a copy of the default rates
return { ...DEFAULT_RATES };
}
// Return a copy of the stored rates
return { ...stored.rates };
}
/**
* Checks if the stored rates are considered stale.
* @returns {boolean} True if no rates are stored or if they are older than STALE_THRESHOLD.
*/
function isRateStale() {
const stored = _loadFromStorage();
if (stored === null) {
return true; // No rates stored
}
const age = Date.now() - stored.timestamp;
return age > STALE_THRESHOLD;
}
/**
* Gets the timestamp of the last update (in milliseconds since epoch).
* @returns {number|null} The timestamp or null if no rates stored.
*/
function getLastUpdatedTimestamp() {
const stored = _loadFromStorage();
return stored !== null ? stored.timestamp : null;
}
/**
* Gets a human-readable string indicating how long ago the rates were updated.
* @returns {string} A string like "Updated 5 minutes ago" or "Never updated".
*/
function getTimeSinceUpdate() {
const timestamp = getLastUpdatedTimestamp();
if (timestamp === null) {
return 'Never updated';
}
const diffMs = Date.now() - timestamp;
const diffSec = Math.round(diffMs / 1000);
const diffMin = Math.round(diffSec / 60);
const diffHours = Math.round(diffMin / 60);
const diffDays = Math.round(diffHours / 24);
if (diffSec < 5) {
return 'Just now';
} else if (diffSec < 60) {
return `${diffSec} seconds ago`;
} else if (diffMin < 60) {
return `${diffMin} minutes ago`;
} else if (diffHours < 24) {
return `${diffHours} hours ago`;
} else {
return `${diffDays} days ago`;
}
}
/**
* Resets the exchange rates to the default values.
* This is useful for testing or if the user wants to revert to defaults.
*/
function resetToDefaults() {
_saveToStorage({
rates: { ...DEFAULT_RATES },
timestamp: Date.now()
});
}
module.exports = {
getRate,
setRate,
getRates,
isRateStale,
getLastUpdatedTimestamp,
getTimeSinceUpdate,
resetToDefaults
};
+154
View File
@@ -0,0 +1,154 @@
/**
* Unit converter for the scientific calculator
* Handles conversion between units within the same category.
* Uses predefined conversion factors from UnitDefinitions.
*/
const UnitDefinitions = require('./UnitDefinitions');
/**
* Converts a value from one unit to another within the same category.
* @param {number} value - The value to convert.
* @param {string} fromUnit - The unit to convert from.
* @param {string} toUnit - The unit to convert to.
* @param {string} category - The category of the units (optional, will be inferred if not provided).
* @returns {{value: number, error: string|null}} - Result object with either the converted value or an error.
*/
function convert(value, fromUnit, toUnit, category) {
// Validate input value
if (typeof value !== 'number' || isNaN(value)) {
return { value: null, error: 'Invalid value: must be a number' };
}
// Validate unit strings
if (typeof fromUnit !== 'string' || typeof toUnit !== 'string') {
return { value: null, error: 'Invalid unit: must be a string' };
}
// Determine category if not provided
let cat = category;
if (!cat) {
cat = getCategoryForUnit(fromUnit);
if (!cat || getCategoryForUnit(toUnit) !== cat) {
return { value: null, error: 'Units are not in the same category or are invalid' };
}
}
// Get the unit definitions for the category
const categoryDefs = UnitDefinitions[cat];
if (!categoryDefs) {
return { value: null, error: `Unknown category: ${cat}` };
}
// Get the definitions for the specific units
const fromDef = categoryDefs[fromUnit];
const toDef = categoryDefs[toUnit];
if (!fromDef || !toDef) {
return { value: null, error: `One or both units (${fromUnit}, ${toUnit}) not found in category ${cat}` };
}
// Handle temperature conversions (which have a different structure)
if (fromDef.type === 'temperature' || toDef.type === 'temperature') {
return convertTemperature(value, fromDef, toDef);
}
// For non-temperature units, we use the factor method
// The factor is the multiplier to get to the base unit.
// So: value in base unit = value * fromFactor
// Then: value in target unit = (value in base unit) / toFactor
// Therefore: result = value * (fromFactor / toFactor)
const fromFactor = fromDef;
const toFactor = toDef;
// Avoid division by zero
if (toFactor === 0) {
return { value: null, error: 'Cannot convert to a unit with zero factor' };
}
const result = value * (fromFactor / toFactor);
return { value: result, error: null };
}
/**
* Helper function to convert between temperature units.
* @param {number} value - The value to convert.
* @param {Object} fromDef - The definition of the source unit (from UnitDefinitions).
* @param {Object} toDef - The definition of the target unit (from UnitDefinitions).
* @returns {{value: number, error: string|null}} - Result object.
*/
function convertTemperature(value, fromDef, toDef) {
// First, convert the input value to Kelvin
let kelvin;
if (fromDef.type === 'temperature') {
if (fromDef.offset !== undefined) {
// For Celsius and Fahrenheit, we have an offset
if (fromDef.factor !== undefined) {
// Fahrenheit: K = (F + 459.67) * 5/9
kelvin = (value + fromDef.offset) * fromDef.factor;
} else {
// Celsius: K = C + 273.15
kelvin = value + fromDef.offset;
}
} else {
// Kelvin: already in Kelvin
kelvin = value;
}
} else {
// This should not happen if we check the type correctly
return { value: null, error: 'Internal error: invalid temperature unit definition' };
}
// Now convert from Kelvin to the target unit
let result;
if (toDef.type === 'temperature') {
if (toDef.offset !== undefined) {
if (toDef.factor !== undefined) {
// Fahrenheit: F = (K * 9/5) - 459.67
result = (kelvin / toDef.factor) - toDef.offset;
} else {
// Celsius: C = K - 273.15
result = kelvin - toDef.offset;
}
} else {
// Kelvin: already in Kelvin
result = kelvin;
}
} else {
// This should not happen
return { value: null, error: 'Internal error: invalid temperature unit definition' };
}
return { value: result, error: null };
}
/**
* Helper function to determine the category of a unit.
* @param {string} unit - The unit string.
* @returns {string|null} The category name, or null if not found.
*/
function getCategoryForUnit(unit) {
for (const category in UnitDefinitions) {
if (UnitDefinitions[category].hasOwnProperty(unit)) {
return category;
}
}
return null;
}
/**
* Get all available units and their categories.
* @returns {Object} An object mapping categories to arrays of unit strings.
*/
function getAllUnits() {
const result = {};
for (const category in UnitDefinitions) {
result[category] = Object.keys(UnitDefinitions[category]);
}
return result;
}
module.exports = {
convert,
getAllUnits,
};
+148
View File
@@ -0,0 +1,148 @@
/**
* Unit definitions and conversion factors for the scientific calculator
* Each unit is defined by its factor relative to a base unit in its category.
*/
const UnitDefinitions = {
// Electrical
electrical: {
// Voltage
V: 1, // Volt
mV: 1e-3, // Millivolt
kV: 1e3, // Kilovolt
μV: 1e-6, // Microvolt
// Current
A: 1, // Ampere
mA: 1e-3, // Milliampere
μA: 1e-6, // Microampere
nA: 1e-9, // Nanoampere
// Resistance
Ω: 1, // Ohm
kΩ: 1e3, // Kiloohm
MΩ: 1e6, // Megaohm
// Capacitance
F: 1, // Farad
μF: 1e-6, // Microfarad
pF: 1e-12, // Picofarad
nF: 1e-9, // Nanofarad
// Power
W: 1, // Watt
kW: 1e3, // Kilowatt
MW: 1e6, // Megawatt
// Energy
J: 1, // Joule
kWh: 3.6e6, // Kilowatt-hour
Wh: 3600, // Watt-hour
// Charge
C: 1, // Coulomb
Ah: 3600, // Ampere-hour
mAh: 3.6, // Milliampere-hour
},
// Mechanical
mechanical: {
// Force
N: 1, // Newton
kN: 1e3, // Kilonewton
// Mass/force
kgf: 9.80665, // Kilogram-force (standard gravity)
lbf: 4.44822, // Pound-force
// Pressure
Pa: 1, // Pascal
kPa: 1e3, // Kilopascal
MPa: 1e6, // Megapascal
bar: 1e5, // Bar
psi: 6894.76, // Pound per square inch
// Velocity
'm/s': 1, // Meter per second
'km/h': 1/3.6, // Kilometer per hour
mph: 0.44704, // Mile per hour
// Acceleration
'm/s²': 1, // Meter per second squared
g: 9.80665, // Standard gravity
// Torque
'Nm': 1, // Newton meter
'ft·lb': 1.35582, // Foot-pound
// Frequency
Hz: 1, // Hertz
RPM: 1/60, // Revolutions per minute (to Hz)
},
// Thermal
thermal: {
// Temperature (special handling required)
K: { type: 'temperature', offset: 0 }, // Kelvin
°C: { type: 'temperature', offset: 273.15 }, // Celsius
°F: { type: 'temperature', offset: 459.67, factor: 5/9 }, // Fahrenheit
// Specific heat
'J/(kg·K)': 1, // Joule per kilogram Kelvin
'J/(g·°C)': 1, // Same as above (since 1 g = 0.001 kg and 1°C change = 1 K change)
// Thermal conductivity
'W/(m·K)': 1, // Watt per meter Kelvin
// Power (same as electrical)
W: 1, // Watt
kW: 1e3, // Kilowatt
},
// Volume
volume: {
L: 1, // Liter
mL: 1e-3, // Milliliter
μL: 1e-6, // Microliter
cm³: 1e-3, // Cubic centimeter (1 mL)
in³: 1.63871e-5, // Cubic inch
ft³: 0.0283168, // Cubic foot
tsp: 4.92892e-3, // Teaspoon
tbsp: 1.47868e-2, // Tablespoon
'fl oz': 2.95735e-2, // Fluid ounce
cup: 2.36588e-1, // Cup
pt: 4.73176e-1, // Pint
qt: 0.946353, // Quart
gal: 3.78541, // Gallon
},
// Mass
mass: {
kg: 1, // Kilogram
g: 1e-3, // Gram
mg: 1e-6, // Milligram
μg: 1e-9, // Microgram
lb: 0.453592, // Pound
oz: 0.0283495, // Ounce
},
// Time
time: {
s: 1, // Second
ms: 1e-3, // Millisecond
μs: 1e-6, // Microsecond
ns: 1e-9, // Nanosecond
min: 60, // Minute
h: 3600, // Hour
d: 86400, // Day
yr: 3.15576e7, // Year (Julian year)
},
// Frequency (note: Hz is also in mechanical, but we'll keep it here for consistency)
frequency: {
Hz: 1, // Hertz
kHz: 1e3, // Kilohertz
MHz: 1e6, // Megahertz
GHz: 1e9, // Gigahertz
RPM: 1/60, // Revolutions per minute (to Hz)
},
// Data
data: {
bit: 1, // Bit
byte: 8, // Byte
KB: 8e3, // Kilobyte (SI)
MB: 8e6, // Megabyte (SI)
GB: 8e9, // Gigabyte (SI)
TB: 8e12, // Terabyte (SI)
// Note: For binary prefixes (KiB, MiB, etc.), the user can specify if needed.
// We are using SI (decimal) prefixes as default.
},
};
module.exports = UnitDefinitions;