diff --git a/src/utils/converter/Converter.js b/src/utils/converter/Converter.js new file mode 100644 index 0000000000..ca64a27b1e --- /dev/null +++ b/src/utils/converter/Converter.js @@ -0,0 +1,233 @@ +// Length — mm, cm, m, km, inch, foot, mile +// Weight — mg, g, kg, lb, oz +// Temperature — Celsius, Fahrenheit, Kelvin +// Digital storage — bits, bytes, KB, MB, GB, TB +// Time — ms, seconds, minutes, hours, days + +export default class Converter { + /** + * Converts a value from one unit to another. + * @param {number} value - The numeric value to convert. + * @param {string} fromUnit - The unit to convert from (e.g. 'km'). + * @param {string} toUnit - The unit to convert to (e.g. 'mile'). + * @returns {number} + */ + convert(value, fromUnit, toUnit) { + if (fromUnit === toUnit) return value; + + const fromFactor = Converter.conversionMap[fromUnit]; + const toFactor = Converter.conversionMap[toUnit]; + + if (fromFactor === undefined) { + throw new Error(`Unknown unit: "${fromUnit}"`); + } + if (toFactor === undefined) { + throw new Error(`Unknown unit: "${toUnit}"`); + } + + if (!Converter.sameCategory(fromUnit, toUnit)) { + throw new Error(`Cannot convert "${fromUnit}" to "${toUnit}": different categories.`); + } + + // Convert: value -> base unit -> target unit + return (value * fromFactor) / toFactor; + } + + // <-------- Length (base unit: meter) --------> + + /** + * Converts a length value from one unit to another. + * @param {number} value + * @param {string} fromUnit - One of: 'mm', 'cm', 'm', 'km', 'inch', 'foot', 'yard', 'mile' + * @param {string} toUnit + * @returns {number} + */ + convertLength(value, fromUnit, toUnit) { + if (!Converter.lengthUnits.includes(fromUnit) || !Converter.lengthUnits.includes(toUnit)) { + throw new Error('Both units must be valid length units.'); + } + return this.convert(value, fromUnit, toUnit); + } + + /** + * Converts a weight value from one unit to another. + * @param {number} value + * @param {string} fromUnit - One of: 'mg', 'g', 'kg', 'lb', 'oz' + * @param {string} toUnit + * @returns {number} + */ + convertWeight(value, fromUnit, toUnit) { + if (!Converter.weightUnits.includes(fromUnit) || !Converter.weightUnits.includes(toUnit)) { + throw new Error('Both units must be valid weight units.'); + } + return this.convert(value, fromUnit, toUnit); + } + + // <---- Temperature needs special handling (formulas, not ratios) ----> + /** + * Converts a temperature value from one unit to another. + * @param {number} value + * @param {string} fromUnit - One of: 'C', 'F', 'K' + * @param {string} toUnit + * @returns {number} + */ + convertTemperature(value, fromUnit, toUnit) { + if ( + !Converter.temperatureUnits.includes(fromUnit) + || !Converter.temperatureUnits.includes(toUnit) + ) { + throw new Error('Both units must be valid temperature units.'); + } + return Converter.convertTemperatureHelper(value, fromUnit, toUnit); + } + + /** + * Converts a digital storage value from one unit to another. + * @param {number} value + * @param {string} fromUnit - One of: 'bit', 'byte', 'KB', 'MB', 'GB', 'TB' + * @param {string} toUnit + * @returns {number} + */ + convertStorage(value, fromUnit, toUnit) { + if ( + !Converter.storageUnits.includes(fromUnit) + || !Converter.storageUnits.includes(toUnit)) { + throw new Error('Both units must be valid digital storage units.'); + } + return this.convert(value, fromUnit, toUnit); + } + + /** + * Converts a time value from one unit to another. + * @param {number} value + * @param {string} fromUnit - One of: 'ms', 'second', 'minute', 'hour', 'day' + * @param {string} toUnit + * @returns {number} + */ + convertTime(value, fromUnit, toUnit) { + if (!Converter.timeUnits.includes(fromUnit) + || !Converter.timeUnits.includes(toUnit)) { + throw new Error('Both units must be valid time units.'); + } + return this.convert(value, fromUnit, toUnit); + } + + // <---------- Static helpers ----------> + + /** + * Checks whether two units belong to the same category. + * @param {string} unitA + * @param {string} unitB + * @returns {boolean} + */ + static sameCategory(unitA, unitB) { + const categories = [ + Converter.lengthUnits, + Converter.weightUnits, + Converter.storageUnits, + Converter.timeUnits, + ]; + return categories.some( + (category) => category.includes(unitA) && category.includes(unitB), + ); + } + + /** + * Handles temperature conversion using explicit formulas. + * @param {number} value + * @param {string} fromUnit - 'C', 'F', or 'K' + * @param {string} toUnit - 'C', 'F', or 'K' + * @returns {number} + */ + + static convertTemperatureHelper(value, fromUnit, toUnit) { + if (String(fromUnit) === String(toUnit)) return value; + + // Convert to Celsius first, then to target + let celsius; + switch (fromUnit) { + case 'C': celsius = value; break; + case 'F': celsius = (value - 32) * (5 / 9); break; + case 'K': celsius = value - 273.15; break; + default: throw new Error(`Unknown temperature unit: "${fromUnit}"`); + } + + switch (toUnit) { + case 'C': return celsius; + case 'F': return celsius * (9 / 5) + 32; + case 'K': return celsius + 273.15; + default: throw new Error(`Unknown temperature unit: "${toUnit}"`); + } + } + + // <---------- Unit lists (used for category validation) ----------> + + /** @returns {string[]} */ + static get lengthUnits() { + return ['mm', 'cm', 'm', 'km', 'inch', 'foot', 'yard', 'mile']; + } + + /** @returns {string[]} */ + static get weightUnits() { + return ['mg', 'g', 'kg', 'lb', 'oz']; + } + + /** @returns {string[]} */ + static get temperatureUnits() { + return ['C', 'F', 'K']; + } + + /** @returns {string[]} */ + static get storageUnits() { + return ['bit', 'byte', 'KB', 'MB', 'GB', 'TB']; + } + + /** @returns {string[]} */ + static get timeUnits() { + return ['ms', 'second', 'minute', 'hour', 'day']; + } + + // <--------- Conversion map (each value = how many base units this unit equals) ---------> + + /** + * A flat map of unit -> base-unit factor. + * Base units: meter | gram | bit | millisecond + * @returns {Object.} + */ + + static get conversionMap() { + return { + // Length (base: meter) + mm: 0.001, + cm: 0.01, + m: 1, + km: 1000, + inch: 0.0254, + foot: 0.3048, + yard: 0.9144, + mile: 1609.344, + + // Weight (base: gram) + mg: 0.001, + g: 1, + kg: 1000, + lb: 453.592, + oz: 28.3495, + + // Digital storage (base: bit) + bit: 1, + byte: 8, + KB: 8000, + MB: 8000000, + GB: 8000000000, + TB: 8000000000000, + + // Time (base: millisecond) + ms: 1, + second: 1000, + minute: 60000, + hour: 3600000, + day: 86400000, + }; + } +} diff --git a/src/utils/converter/__test__/Converter.test.js b/src/utils/converter/__test__/Converter.test.js new file mode 100644 index 0000000000..7299bcae7a --- /dev/null +++ b/src/utils/converter/__test__/Converter.test.js @@ -0,0 +1,200 @@ +import Converter from '../Converter'; +// units must be in string +// Length — mm, cm, m, km, inch, foot, mile +// Weight — mg, g, kg, lb, oz +// Temperature — Celsius, Fahrenheit, Kelvin +// Digital storage — bits, bytes, KB, MB, GB, TB +// Time — ms, seconds, minutes, hours, days + +describe('Converter', () => { + const converter = new Converter(); + + // Length + + describe('convertLength', () => { + it('should convert kilometers to meters', () => { + expect(converter.convertLength(1, 'km', 'm')).toBe(1000); + }); + + it('should convert meters to centimeters', () => { + expect(converter.convertLength(1, 'm', 'cm')).toBe(100); + }); + + it('should convert miles to kilometers', () => { + expect(converter.convertLength(1, 'mile', 'km')).toBeCloseTo(1.60934, 3); + }); + + it('should convert inches to centimeters', () => { + expect(converter.convertLength(1, 'inch', 'cm')).toBeCloseTo(2.54, 5); + }); + + it('should convert foot to meter', () => { + expect(converter.convertLength(1, 'foot', 'm')).toBeCloseTo(0.3048, 4); + }); + + it('should return same value when converting to the same unit', () => { + expect(converter.convertLength(5, 'km', 'km')).toBe(5); + }); + + it('should handle zero value', () => { + expect(converter.convertLength(0, 'km', 'm')).toBe(0); + }); + + it('should throw for invalid length unit', () => { + expect(() => converter.convertLength(1, 'km', 'kg')).toThrow(); + }); + }); + + // Weight + + describe('convertWeight', () => { + it('should convert kilograms to grams', () => { + expect(converter.convertWeight(1, 'kg', 'g')).toBe(1000); + }); + + it('should convert grams to milligrams', () => { + expect(converter.convertWeight(1, 'g', 'mg')).toBe(1000); + }); + + it('should convert pounds to kilograms', () => { + expect(converter.convertWeight(1, 'lb', 'kg')).toBeCloseTo(0.453592, 4); + }); + + it('should convert ounces to grams', () => { + expect(converter.convertWeight(1, 'oz', 'g')).toBeCloseTo(28.3495, 3); + }); + + it('should return same value when converting to the same unit', () => { + expect(converter.convertWeight(10, 'kg', 'kg')).toBe(10); + }); + + it('should handle zero value', () => { + expect(converter.convertWeight(0, 'kg', 'g')).toBe(0); + }); + + it('should throw for invalid weight unit', () => { + expect(() => converter.convertWeight(1, 'kg', 'km')).toThrow(); + }); + }); + + // Temperature + + describe('convertTemperature', () => { + it('should convert Celsius to Fahrenheit', () => { + expect(converter.convertTemperature(0, 'C', 'F')).toBe(32); + expect(converter.convertTemperature(100, 'C', 'F')).toBe(212); + }); + + it('should convert Fahrenheit to Celsius', () => { + expect(converter.convertTemperature(32, 'F', 'C')).toBe(0); + expect(converter.convertTemperature(212, 'F', 'C')).toBe(100); + }); + + it('should convert Celsius to Kelvin', () => { + expect(converter.convertTemperature(0, 'C', 'K')).toBe(273.15); + expect(converter.convertTemperature(100, 'C', 'K')).toBe(373.15); + }); + + it('should convert Kelvin to Celsius', () => { + expect(converter.convertTemperature(273.15, 'K', 'C')).toBe(0); + }); + + it('should convert Fahrenheit to Kelvin', () => { + expect(converter.convertTemperature(32, 'F', 'K')).toBeCloseTo(273.15, 2); + }); + + it('should return same value when converting to the same unit', () => { + expect(converter.convertTemperature(100, 'C', 'C')).toBe(100); + }); + + it('should handle negative temperatures', () => { + expect(converter.convertTemperature(-40, 'C', 'F')).toBe(-40); + }); + + it('should throw for invalid temperature unit', () => { + expect(() => converter.convertTemperature(100, 'C', 'X')).toThrow(); + }); + }); + + // Digital Storage + + describe('convertStorage', () => { + it('should convert bytes to bits', () => { + expect(converter.convertStorage(1, 'byte', 'bit')).toBe(8); + }); + + it('should convert kilobytes to bytes', () => { + expect(converter.convertStorage(1, 'KB', 'byte')).toBe(1000); + }); + + it('should convert gigabytes to megabytes', () => { + expect(converter.convertStorage(1, 'GB', 'MB')).toBe(1000); + }); + + it('should convert terabytes to gigabytes', () => { + expect(converter.convertStorage(1, 'TB', 'GB')).toBe(1000); + }); + + it('should return same value when converting to the same unit', () => { + expect(converter.convertStorage(5, 'MB', 'MB')).toBe(5); + }); + + it('should handle zero value', () => { + expect(converter.convertStorage(0, 'GB', 'MB')).toBe(0); + }); + + it('should throw for invalid storage unit', () => { + expect(() => converter.convertStorage(1, 'GB', 'km')).toThrow(); + }); + }); + + // Time + + describe('convertTime', () => { + it('should convert seconds to milliseconds', () => { + expect(converter.convertTime(1, 'second', 'ms')).toBe(1000); + }); + + it('should convert minutes to seconds', () => { + expect(converter.convertTime(1, 'minute', 'second')).toBe(60); + }); + + it('should convert hours to minutes', () => { + expect(converter.convertTime(1, 'hour', 'minute')).toBe(60); + }); + + it('should convert days to hours', () => { + expect(converter.convertTime(1, 'day', 'hour')).toBe(24); + }); + + it('should convert days to seconds', () => { + expect(converter.convertTime(1, 'day', 'second')).toBe(86400); + }); + + it('should return same value when converting to the same unit', () => { + expect(converter.convertTime(3, 'hour', 'hour')).toBe(3); + }); + + it('should handle zero value', () => { + expect(converter.convertTime(0, 'day', 'ms')).toBe(0); + }); + + it('should throw for invalid time unit', () => { + expect(() => converter.convertTime(1, 'hour', 'kg')).toThrow(); + }); + }); + + // Cross-category guard + + describe('convert (cross-category)', () => { + it('should throw when mixing categories', () => { + expect(() => converter.convert(1, 'km', 'kg')).toThrow( + 'Cannot convert "km" to "kg": different categories.', + ); + }); + + it('should throw for completely unknown units', () => { + expect(() => converter.convert(1, 'xyz', 'm')).toThrow('Unknown unit: "xyz"'); + }); + }); +});