com.ibm.icu.impl.units.UnitsRouter Maven / Gradle / Ivy
Show all versions of icu4j Show documentation
// © 2020 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.impl.units;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import com.ibm.icu.impl.IllegalIcuArgumentException;
import com.ibm.icu.impl.number.MicroProps;
import com.ibm.icu.number.Precision;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.ULocale;
/**
* {@code UnitsRouter} responsible for converting from a single unit (such as {@code meter} or
* {@code meter-per-second}) to one of the complex units based on the limits.
* For example:
* if the input is {@code meter} and the output as following
* {{@code foot+inch}, limit: 3.0}
* {{@code inch} , limit: no value (-inf)}
* Thus means if the input in {@code meter} is greater than or equal to {@code 3.0 feet},
* the output will be in {@code foot+inch}, otherwise, the output will be in {@code inch}.
*
* NOTE:
* the output units and their limits MUST BE in order, for example, if the output units, from the
* previous example, are the following:
* {{@code inch} , limit: no value (-inf)}
* {{@code foot+inch}, limit: 3.0}
* IN THIS CASE THE OUTPUT WILL BE ALWAYS IN {@code inch}.
*
* NOTE:
* the output units and their limits will be extracted from the units preferences database by knowing
* the followings:
* - input unit
* - locale
* - usage
*
* DESIGN:
* {@code UnitRouter} uses internally {@code ComplexUnitConverter} in order to convert the input
* units to the desired complex units and to check the limit too.
*/
public class UnitsRouter {
// List of possible output units. TODO: converterPreferences_ now also has
// this data available. Maybe drop outputUnits_ and have getOutputUnits
// construct a the list from data in converterPreferences_ instead?
private ArrayList outputUnits_ = new ArrayList<>();
private ArrayList converterPreferences_ = new ArrayList<>();
public UnitsRouter(String inputUnitIdentifier, ULocale locale, String usage) {
this(MeasureUnitImpl.forIdentifier(inputUnitIdentifier), locale, usage);
}
public UnitsRouter(MeasureUnitImpl inputUnit, ULocale locale, String usage) {
// TODO: do we want to pass in ConversionRates and UnitPreferences instead?
// of loading in each UnitsRouter instance? (Or make global?)
UnitsData data = new UnitsData();
String category = data.getCategory(inputUnit);
UnitPreferences.UnitPreference[] unitPreferences = data.getPreferencesFor(category, usage, locale);
for (int i = 0; i < unitPreferences.length; ++i) {
UnitPreferences.UnitPreference preference = unitPreferences[i];
MeasureUnitImpl complexTargetUnitImpl =
MeasureUnitImpl.UnitsParser.parseForIdentifier(preference.getUnit());
String precision = preference.getSkeleton();
// For now, we only have "precision-increment" in Units Preferences skeleton.
// Therefore, we check if the skeleton starts with "precision-increment" and force the program to
// fail otherwise.
// NOTE:
// It is allowed to have an empty precision.
if (!precision.isEmpty() && !precision.startsWith("precision-increment")) {
throw new AssertionError("Only `precision-increment` is allowed");
}
outputUnits_.add(complexTargetUnitImpl.build());
converterPreferences_.add(new ConverterPreference(inputUnit, complexTargetUnitImpl,
preference.getGeq(), precision,
data.getConversionRates()));
}
}
/** If micros.rounder is a BogusRounder, this function replaces it with a valid one. */
public RouteResult route(BigDecimal quantity, MicroProps micros) {
Precision rounder = micros == null ? null : micros.rounder;
ConverterPreference converterPreference = null;
for (ConverterPreference itr : converterPreferences_) {
converterPreference = itr;
if (converterPreference.converter.greaterThanOrEqual(quantity.abs(),
converterPreference.limit)) {
break;
}
}
assert converterPreference != null;
assert converterPreference.precision != null;
// Set up the rounder for this preference's precision
if (rounder != null && rounder instanceof Precision.BogusRounder) {
Precision.BogusRounder bogus = (Precision.BogusRounder)rounder;
if (converterPreference.precision.length() > 0) {
rounder = bogus.into(parseSkeletonToPrecision(converterPreference.precision));
} else {
// We use the same rounding mode as COMPACT notation: known to be a
// human-friendly rounding mode: integers, but add a decimal digit
// as needed to ensure we have at least 2 significant digits.
rounder = bogus.into(Precision.integer().withMinDigits(2));
}
}
if (micros != null) {
micros.rounder = rounder;
}
return new RouteResult(
converterPreference.converter.convert(quantity, rounder),
converterPreference.targetUnit
);
}
private static Precision parseSkeletonToPrecision(String precisionSkeleton) {
final String kSkeletonPrefix = "precision-increment/";
if (!precisionSkeleton.startsWith(kSkeletonPrefix)) {
throw new IllegalIcuArgumentException("precisionSkeleton is only precision-increment");
}
// TODO(icu-units#104): the C++ code uses a more sophisticated
// parseIncrementOption which supports "withMinFraction" - e.g.
// "precision-increment/0.5". Test with a unit preference that uses
// this, and fix Java.
String incrementValue = precisionSkeleton.substring(kSkeletonPrefix.length());
return Precision.increment(new BigDecimal(incrementValue));
}
/**
* Returns the list of possible output units, i.e. the full set of
* preferences, for the localized, usage-specific unit preferences.
*
* The returned pointer should be valid for the lifetime of the
* UnitsRouter instance.
*/
public List getOutputUnits() {
return this.outputUnits_;
}
/**
* Contains the complex unit converter and the limit which representing the smallest value that the
* converter should accept. For example, if the converter is converting to {@code foot+inch} and the
* limit equals 3.0, thus means the converter should not convert to a value less than {@code 3.0 feet}.
*
* NOTE:
* if the limit doest not has a value (i.e. {@code std::numeric_limits::lowest()}),
* this mean there is no limit for the converter.
*/
public static class ConverterPreference {
// The output unit for this ConverterPreference. This may be a MIXED unit -
// for example: "yard-and-foot-and-inch".
final MeasureUnitImpl targetUnit;
final ComplexUnitsConverter converter;
final BigDecimal limit;
final String precision;
// In case there is no limit, the limit will be -inf.
public ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl targetUnit,
String precision, ConversionRates conversionRates) {
this(source, targetUnit, BigDecimal.valueOf(Double.MIN_VALUE), precision,
conversionRates);
}
public ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl targetUnit,
BigDecimal limit, String precision, ConversionRates conversionRates) {
this.converter = new ComplexUnitsConverter(source, targetUnit, conversionRates);
this.limit = limit;
this.precision = precision;
this.targetUnit = targetUnit;
}
}
public class RouteResult {
public final ComplexUnitsConverter.ComplexConverterResult complexConverterResult;
// The output unit for this RouteResult. This may be a MIXED unit - for
// example: "yard-and-foot-and-inch", for which `measures` will have three
// elements.
public final MeasureUnitImpl outputUnit;
RouteResult(ComplexUnitsConverter.ComplexConverterResult complexConverterResult, MeasureUnitImpl outputUnit) {
this.complexConverterResult = complexConverterResult;
this.outputUnit = outputUnit;
}
}
}