1 // © 2020 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html 3 package com.ibm.icu.impl.units; 4 5 import java.math.BigDecimal; 6 import java.util.ArrayList; 7 import java.util.List; 8 9 import com.ibm.icu.impl.IllegalIcuArgumentException; 10 import com.ibm.icu.impl.number.MicroProps; 11 import com.ibm.icu.number.Precision; 12 import com.ibm.icu.util.MeasureUnit; 13 import com.ibm.icu.util.ULocale; 14 15 /** 16 * `UnitsRouter` responsible for converting from a single unit (such as `meter` or `meter-per-second`) to 17 * one of the complex units based on the limits. 18 * For example: 19 * if the input is `meter` and the output as following 20 * {`foot+inch`, limit: 3.0} 21 * {`inch` , limit: no value (-inf)} 22 * Thus means if the input in `meter` is greater than or equal to `3.0 feet`, the output will be in 23 * `foot+inch`, otherwise, the output will be in `inch`. 24 * <p> 25 * NOTE: 26 * the output units and their limits MUST BE in order, for example, if the output units, from the 27 * previous example, are the following: 28 * {`inch` , limit: no value (-inf)} 29 * {`foot+inch`, limit: 3.0} 30 * IN THIS CASE THE OUTPUT WILL BE ALWAYS IN `inch`. 31 * <p> 32 * NOTE: 33 * the output units and their limits will be extracted from the units preferences database by knowing 34 * the followings: 35 * - input unit 36 * - locale 37 * - usage 38 * <p> 39 * DESIGN: 40 * `UnitRouter` uses internally `ComplexUnitConverter` in order to convert the input units to the 41 * desired complex units and to check the limit too. 42 */ 43 public class UnitsRouter { 44 // List of possible output units. TODO: converterPreferences_ now also has 45 // this data available. Maybe drop outputUnits_ and have getOutputUnits 46 // construct a the list from data in converterPreferences_ instead? 47 private ArrayList<MeasureUnit> outputUnits_ = new ArrayList<>(); 48 private ArrayList<ConverterPreference> converterPreferences_ = new ArrayList<>(); 49 UnitsRouter(String inputUnitIdentifier, ULocale locale, String usage)50 public UnitsRouter(String inputUnitIdentifier, ULocale locale, String usage) { 51 this(MeasureUnitImpl.forIdentifier(inputUnitIdentifier), locale, usage); 52 } 53 UnitsRouter(MeasureUnitImpl inputUnit, ULocale locale, String usage)54 public UnitsRouter(MeasureUnitImpl inputUnit, ULocale locale, String usage) { 55 // TODO: do we want to pass in ConversionRates and UnitPreferences instead? 56 // of loading in each UnitsRouter instance? (Or make global?) 57 UnitsData data = new UnitsData(); 58 59 String category = data.getCategory(inputUnit); 60 UnitPreferences.UnitPreference[] unitPreferences = data.getPreferencesFor(category, usage, locale); 61 62 for (int i = 0; i < unitPreferences.length; ++i) { 63 UnitPreferences.UnitPreference preference = unitPreferences[i]; 64 65 MeasureUnitImpl complexTargetUnitImpl = 66 MeasureUnitImpl.UnitsParser.parseForIdentifier(preference.getUnit()); 67 68 String precision = preference.getSkeleton(); 69 70 // For now, we only have "precision-increment" in Units Preferences skeleton. 71 // Therefore, we check if the skeleton starts with "precision-increment" and force the program to 72 // fail otherwise. 73 // NOTE: 74 // It is allowed to have an empty precision. 75 if (!precision.isEmpty() && !precision.startsWith("precision-increment")) { 76 throw new AssertionError("Only `precision-increment` is allowed"); 77 } 78 79 outputUnits_.add(complexTargetUnitImpl.build()); 80 converterPreferences_.add(new ConverterPreference(inputUnit, complexTargetUnitImpl, 81 preference.getGeq(), precision, 82 data.getConversionRates())); 83 } 84 } 85 86 /** If micros.rounder is a BogusRounder, this function replaces it with a valid one. */ route(BigDecimal quantity, MicroProps micros)87 public RouteResult route(BigDecimal quantity, MicroProps micros) { 88 Precision rounder = micros == null ? null : micros.rounder; 89 ConverterPreference converterPreference = null; 90 for (ConverterPreference itr : converterPreferences_) { 91 converterPreference = itr; 92 if (converterPreference.converter.greaterThanOrEqual(quantity.abs(), 93 converterPreference.limit)) { 94 break; 95 } 96 } 97 assert converterPreference != null; 98 assert converterPreference.precision != null; 99 100 // Set up the rounder for this preference's precision 101 if (rounder != null && rounder instanceof Precision.BogusRounder) { 102 Precision.BogusRounder bogus = (Precision.BogusRounder)rounder; 103 if (converterPreference.precision.length() > 0) { 104 rounder = bogus.into(parseSkeletonToPrecision(converterPreference.precision)); 105 } else { 106 // We use the same rounding mode as COMPACT notation: known to be a 107 // human-friendly rounding mode: integers, but add a decimal digit 108 // as needed to ensure we have at least 2 significant digits. 109 rounder = bogus.into(Precision.integer().withMinDigits(2)); 110 } 111 } 112 113 if (micros != null) { 114 micros.rounder = rounder; 115 } 116 return new RouteResult( 117 converterPreference.converter.convert(quantity, rounder), 118 converterPreference.targetUnit 119 ); 120 } 121 parseSkeletonToPrecision(String precisionSkeleton)122 private static Precision parseSkeletonToPrecision(String precisionSkeleton) { 123 final String kSkeletonPrefix = "precision-increment/"; 124 if (!precisionSkeleton.startsWith(kSkeletonPrefix)) { 125 throw new IllegalIcuArgumentException("precisionSkeleton is only precision-increment"); 126 } 127 128 // TODO(icu-units#104): the C++ code uses a more sophisticated 129 // parseIncrementOption which supports "withMinFraction" - e.g. 130 // "precision-increment/0.5". Test with a unit preference that uses 131 // this, and fix Java. 132 String incrementValue = precisionSkeleton.substring(kSkeletonPrefix.length()); 133 return Precision.increment(new BigDecimal(incrementValue)); 134 } 135 136 /** 137 * Returns the list of possible output units, i.e. the full set of 138 * preferences, for the localized, usage-specific unit preferences. 139 * <p> 140 * The returned pointer should be valid for the lifetime of the 141 * UnitsRouter instance. 142 */ getOutputUnits()143 public List<MeasureUnit> getOutputUnits() { 144 return this.outputUnits_; 145 } 146 147 /** 148 * Contains the complex unit converter and the limit which representing the smallest value that the 149 * converter should accept. For example, if the converter is converting to `foot+inch` and the limit 150 * equals 3.0, thus means the converter should not convert to a value less than `3.0 feet`. 151 * <p> 152 * NOTE: 153 * if the limit doest not has a value `i.e. (std::numeric_limits<double>::lowest())`, this mean there 154 * is no limit for the converter. 155 */ 156 public static class ConverterPreference { 157 // The output unit for this ConverterPreference. This may be a MIXED unit - 158 // for example: "yard-and-foot-and-inch". 159 final MeasureUnitImpl targetUnit; 160 final ComplexUnitsConverter converter; 161 final BigDecimal limit; 162 final String precision; 163 164 // In case there is no limit, the limit will be -inf. ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl targetUnit, String precision, ConversionRates conversionRates)165 public ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl targetUnit, 166 String precision, ConversionRates conversionRates) { 167 this(source, targetUnit, BigDecimal.valueOf(Double.MIN_VALUE), precision, 168 conversionRates); 169 } 170 ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl targetUnit, BigDecimal limit, String precision, ConversionRates conversionRates)171 public ConverterPreference(MeasureUnitImpl source, MeasureUnitImpl targetUnit, 172 BigDecimal limit, String precision, ConversionRates conversionRates) { 173 this.converter = new ComplexUnitsConverter(source, targetUnit, conversionRates); 174 this.limit = limit; 175 this.precision = precision; 176 this.targetUnit = targetUnit; 177 178 } 179 } 180 181 public class RouteResult { 182 public final ComplexUnitsConverter.ComplexConverterResult complexConverterResult; 183 184 // The output unit for this RouteResult. This may be a MIXED unit - for 185 // example: "yard-and-foot-and-inch", for which `measures` will have three 186 // elements. 187 public final MeasureUnitImpl outputUnit; 188 RouteResult(ComplexUnitsConverter.ComplexConverterResult complexConverterResult, MeasureUnitImpl outputUnit)189 RouteResult(ComplexUnitsConverter.ComplexConverterResult complexConverterResult, MeasureUnitImpl outputUnit) { 190 this.complexConverterResult = complexConverterResult; 191 this.outputUnit = outputUnit; 192 } 193 } 194 } 195