package com.thealgorithms.conversions;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import org.apache.commons.lang3.tuple.Pair;
/**
* A class that handles unit conversions using affine transformations.
*
*
The {@code UnitsConverter} allows converting values between different units using
* pre-defined affine conversion formulas. Each conversion is represented by an
* {@link AffineConverter} that defines the scaling and offset for the conversion.
*
*
For each unit, both direct conversions (e.g., Celsius to Fahrenheit) and inverse
* conversions (e.g., Fahrenheit to Celsius) are generated automatically. It also computes
* transitive conversions (e.g., Celsius to Kelvin via Fahrenheit if both conversions exist).
*
*
Key features include:
*
* - Automatic handling of inverse conversions (e.g., Fahrenheit to Celsius).
* - Compositional conversions, meaning if conversions between A -> B and B -> C exist,
* it can automatically generate A -> C conversion.
* - Supports multiple unit systems as long as conversions are provided in pairs.
*
*
* Example Usage
*
* Map<Pair<String, String>, AffineConverter> basicConversions = Map.ofEntries(
* entry(Pair.of("Celsius", "Fahrenheit"), new AffineConverter(9.0 / 5.0, 32.0)),
* entry(Pair.of("Kelvin", "Celsius"), new AffineConverter(1.0, -273.15))
* );
*
* UnitsConverter converter = new UnitsConverter(basicConversions);
* double result = converter.convert("Celsius", "Fahrenheit", 100.0);
* // Output: 212.0 (Celsius to Fahrenheit conversion of 100°C)
*
*
* Exception Handling
*
* - If the input unit and output unit are the same, an {@link IllegalArgumentException} is thrown.
* - If a conversion between the requested units does not exist, a {@link NoSuchElementException} is thrown.
*
*/
public final class UnitsConverter {
private final Map, AffineConverter> conversions;
private final Set units;
private static void putIfNeeded(Map, AffineConverter> conversions, final String inputUnit, final String outputUnit, final AffineConverter converter) {
if (!inputUnit.equals(outputUnit)) {
final var key = Pair.of(inputUnit, outputUnit);
conversions.putIfAbsent(key, converter);
}
}
private static Map, AffineConverter> addInversions(final Map, AffineConverter> knownConversions) {
Map, AffineConverter> res = new HashMap, AffineConverter>();
for (final var curConversion : knownConversions.entrySet()) {
final var inputUnit = curConversion.getKey().getKey();
final var outputUnit = curConversion.getKey().getValue();
putIfNeeded(res, inputUnit, outputUnit, curConversion.getValue());
putIfNeeded(res, outputUnit, inputUnit, curConversion.getValue().invert());
}
return res;
}
private static Map, AffineConverter> addCompositions(final Map, AffineConverter> knownConversions) {
Map, AffineConverter> res = new HashMap, AffineConverter>();
for (final var first : knownConversions.entrySet()) {
final var firstKey = first.getKey();
putIfNeeded(res, firstKey.getKey(), firstKey.getValue(), first.getValue());
for (final var second : knownConversions.entrySet()) {
final var secondKey = second.getKey();
if (firstKey.getValue().equals(secondKey.getKey())) {
final var newConversion = second.getValue().compose(first.getValue());
putIfNeeded(res, firstKey.getKey(), secondKey.getValue(), newConversion);
}
}
}
return res;
}
private static Map, AffineConverter> addAll(final Map, AffineConverter> knownConversions) {
final var res = addInversions(knownConversions);
return addCompositions(res);
}
private static Map, AffineConverter> computeAllConversions(final Map, AffineConverter> basicConversions) {
var tmp = basicConversions;
var res = addAll(tmp);
while (res.size() != tmp.size()) {
tmp = res;
res = addAll(tmp);
}
return res;
}
private static Set extractUnits(final Map, AffineConverter> conversions) {
Set res = new HashSet<>();
for (final var conversion : conversions.entrySet()) {
res.add(conversion.getKey().getKey());
}
return res;
}
/**
* Constructor for {@code UnitsConverter}.
*
* Accepts a map of basic conversions and automatically generates inverse and
* transitive conversions.
*
* @param basicConversions the initial set of unit conversions to add.
*/
public UnitsConverter(final Map, AffineConverter> basicConversions) {
conversions = computeAllConversions(basicConversions);
units = extractUnits(conversions);
}
/**
* Converts a value from one unit to another.
*
* @param inputUnit the unit of the input value.
* @param outputUnit the unit to convert the value into.
* @param value the value to convert.
* @return the converted value in the target unit.
* @throws IllegalArgumentException if inputUnit equals outputUnit.
* @throws NoSuchElementException if no conversion exists between the units.
*/
public double convert(final String inputUnit, final String outputUnit, final double value) {
if (inputUnit.equals(outputUnit)) {
throw new IllegalArgumentException("inputUnit must be different from outputUnit.");
}
final var conversionKey = Pair.of(inputUnit, outputUnit);
return conversions.computeIfAbsent(conversionKey, k -> { throw new NoSuchElementException("No converter for: " + k); }).convert(value);
}
/**
* Retrieves the set of all units supported by this converter.
*
* @return a set of available units.
*/
public Set availableUnits() {
return units;
}
}