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