blob: 4f8fbb686368199678e66e0bf3afd78eb5905903 [file] [log] [blame]
// © 2020 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.impl.units;
import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* `UnitsRouter` responsible for converting from a single unit (such as `meter` or `meter-per-second`) to
* one of the complex units based on the limits.
* For example:
* if the input is `meter` and the output as following
* {`foot+inch`, limit: 3.0}
* {`inch` , limit: no value (-inf)}
* Thus means if the input in `meter` is greater than or equal to `3.0 feet`, the output will be in
* `foot+inch`, otherwise, the output will be in `inch`.
* <p>
* NOTE:
* the output units and the their limits MUST BE in order, for example, if the output units, from the
* previous example, are the following:
* {`inch` , limit: no value (-inf)}
* {`foot+inch`, limit: 3.0}
* IN THIS CASE THE OUTPUT WILL BE ALWAYS IN `inch`.
* <p>
* NOTE:
* the output units and their limits will be extracted from the units preferences database by knowing
* the followings:
* - input unit
* - locale
* - usage
* <p>
* DESIGN:
* `UnitRouter` uses internally `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<MeasureUnit> outputUnits_ = new ArrayList<>();
private ArrayList<ConverterPreference> converterPreferences_ = new ArrayList<>();
public UnitsRouter(MeasureUnitImpl inputUnitImpl, String region, 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();
//MeasureUnitImpl inputUnitImpl = MeasureUnitImpl.forMeasureUnitMaybeCopy(inputUnit);
String category = data.getCategory(inputUnitImpl);
UnitPreferences.UnitPreference[] unitPreferences = data.getPreferencesFor(category, usage, region);
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(inputUnitImpl, complexTargetUnitImpl,
preference.getGeq(), precision,
data.getConversionRates()));
}
}
public RouteResult route(BigDecimal quantity) {
for (ConverterPreference converterPreference :
converterPreferences_) {
if (converterPreference.converter.greaterThanOrEqual(quantity, converterPreference.limit)) {
return new RouteResult(
converterPreference.converter.convert(quantity),
converterPreference.precision,
converterPreference.targetUnit
);
}
}
// In case of the `quantity` does not fit in any converter limit, use the last converter.
ConverterPreference lastConverterPreference = converterPreferences_.get(converterPreferences_.size() - 1);
return new RouteResult(
lastConverterPreference.converter.convert(quantity),
lastConverterPreference.precision,
lastConverterPreference.targetUnit
);
}
/**
* Returns the list of possible output units, i.e. the full set of
* preferences, for the localized, usage-specific unit preferences.
* <p>
* The returned pointer should be valid for the lifetime of the
* UnitsRouter instance.
*/
public List<MeasureUnit> 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 `foot+inch` and the limit
* equals 3.0, thus means the converter should not convert to a value less than `3.0 feet`.
* <p>
* NOTE:
* if the limit doest not has a value `i.e. (std::numeric_limits<double>::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 {
// A list of measures: a single measure for single units, multiple measures
// for mixed units.
//
// TODO(icu-units/icu#21): figure out the right mixed unit API.
public final List<Measure> measures;
// A skeleton string starting with a precision-increment.
//
// TODO(hugovdm): generalise? or narrow down to only a precision-increment?
// or document that other skeleton elements are ignored?
public final String precision;
// 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(List<Measure> measures, String precision, MeasureUnitImpl outputUnit) {
this.measures = measures;
this.precision = precision;
this.outputUnit = outputUnit;
}
}
}