1 // © 2020 and later: Unicode, Inc. and others. 2 // License & terms of use: http://www.unicode.org/copyright.html 3 4 package com.ibm.icu.impl.units; 5 6 import java.util.ArrayList; 7 import java.util.HashMap; 8 import java.util.Iterator; 9 10 import com.ibm.icu.impl.ICUData; 11 import com.ibm.icu.impl.ICUResourceBundle; 12 import com.ibm.icu.impl.IllegalIcuArgumentException; 13 import com.ibm.icu.impl.UResource; 14 import com.ibm.icu.util.ULocale; 15 import com.ibm.icu.util.UResourceBundle; 16 17 /** 18 * Responsible for all units data operations (retriever, analysis, extraction certain data ... etc.). 19 */ 20 public class UnitsData { 21 // TODO(icu-units#122): this class can use static initialization to load the 22 // data once, and provide access to it via static methods. (Partial change 23 // has been done already.) 24 25 // Array of simple unit IDs. 26 private static String[] simpleUnits = null; 27 28 // Maps from the value associated with each simple unit ID to a category 29 // index number. 30 private static int[] simpleUnitCategories = null; 31 32 private ConversionRates conversionRates; 33 private UnitPreferences unitPreferences; 34 35 UnitsData()36 public UnitsData() { 37 this.conversionRates = new ConversionRates(); 38 this.unitPreferences = new UnitPreferences(); 39 } 40 getSimpleUnits()41 public static String[] getSimpleUnits() { 42 return simpleUnits; 43 } 44 45 static { 46 // Read simple units 47 ICUResourceBundle resource; 48 resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "units"); 49 SimpleUnitIdentifiersSink sink = new SimpleUnitIdentifiersSink(); 50 resource.getAllItemsWithFallback("convertUnits", sink); 51 simpleUnits = sink.simpleUnits; 52 simpleUnitCategories = sink.simpleUnitCategories; 53 } 54 getConversionRates()55 public ConversionRates getConversionRates() { 56 return conversionRates; 57 } 58 getUnitPreferences()59 public UnitPreferences getUnitPreferences() { 60 return unitPreferences; 61 } 62 getCategoryIndexOfSimpleUnit(int simpleUnitIndex)63 public static int getCategoryIndexOfSimpleUnit(int simpleUnitIndex) { 64 return simpleUnitCategories[simpleUnitIndex]; 65 } 66 67 /** 68 * @param measureUnit An instance of MeasureUnitImpl. 69 * @return the corresponding category. 70 */ getCategory(MeasureUnitImpl measureUnit)71 public String getCategory(MeasureUnitImpl measureUnit) { 72 MeasureUnitImpl baseMeasureUnitImpl 73 = this.getConversionRates().extractCompoundBaseUnit(measureUnit); 74 baseMeasureUnitImpl.serialize(); 75 String identifier = baseMeasureUnitImpl.getIdentifier(); 76 77 78 Integer index = Categories.baseUnitToIndex.get(identifier); 79 80 // In case the base unit identifier did not match any entry. 81 if (index == null) { 82 baseMeasureUnitImpl.takeReciprocal(); 83 baseMeasureUnitImpl.serialize(); 84 identifier = baseMeasureUnitImpl.getIdentifier(); 85 index = Categories.baseUnitToIndex.get(identifier); 86 } 87 88 // In case the reciprocal of the base unit identifier did not match any entry. 89 baseMeasureUnitImpl.takeReciprocal(); // return to original form 90 MeasureUnitImpl simplifiedUnit = baseMeasureUnitImpl.copyAndSimplify(); 91 if (index == null) { 92 simplifiedUnit.serialize(); 93 identifier = simplifiedUnit.getIdentifier(); 94 index = Categories.baseUnitToIndex.get(identifier); 95 } 96 97 // In case the simplified base unit identifier did not match any entry. 98 if (index == null) { 99 simplifiedUnit.takeReciprocal(); 100 simplifiedUnit.serialize(); 101 identifier = simplifiedUnit.getIdentifier(); 102 index = Categories.baseUnitToIndex.get(identifier); 103 } 104 105 // If there is no match at all, throw an exception. 106 if (index == null) { 107 throw new IllegalIcuArgumentException("This unit does not has a category" + measureUnit.getIdentifier()); 108 } 109 110 return Categories.indexToCategory[index]; 111 } 112 getPreferencesFor(String category, String usage, ULocale locale)113 public UnitPreferences.UnitPreference[] getPreferencesFor(String category, String usage, ULocale locale) { 114 return this.unitPreferences.getPreferencesFor(category, usage, locale, this); 115 } 116 117 public static class SimpleUnitIdentifiersSink extends UResource.Sink { 118 String[] simpleUnits = null; 119 int[] simpleUnitCategories = null; 120 121 @Override put(UResource.Key key, UResource.Value value, boolean noFallback)122 public void put(UResource.Key key, UResource.Value value, boolean noFallback) { 123 assert key.toString().equals(Constants.CONVERSION_UNIT_TABLE_NAME); 124 assert value.getType() == UResourceBundle.TABLE; 125 126 UResource.Table simpleUnitsTable = value.getTable(); 127 ArrayList<String> simpleUnits = new ArrayList<>(); 128 ArrayList<Integer> simpleUnitCategories = new ArrayList<>(); 129 for (int i = 0; simpleUnitsTable.getKeyAndValue(i, key, value); i++) { 130 if (key.toString().equals("kilogram")) { 131 132 // For parsing, we use "gram", the prefixless metric mass unit. We 133 // thus ignore the SI Base Unit of Mass: it exists due to being the 134 // mass conversion target unit, but not needed for MeasureUnit 135 // parsing. 136 continue; 137 } 138 139 // Find the base target unit for this simple unit 140 UResource.Table table = value.getTable(); 141 if (!table.findValue("target", value)) { 142 // TODO: is there a more idiomatic way to deal with Resource 143 // Sink data errors in ICU4J? For now we just assert-fail, 144 // and otherwise skip bad data: 145 assert false : "Could not find \"target\" for simple unit: " + key; 146 continue; 147 } 148 String target = value.getString(); 149 150 simpleUnits.add(key.toString()); 151 simpleUnitCategories.add(Categories.baseUnitToIndex.get(target)); 152 } 153 154 this.simpleUnits = simpleUnits.toArray(new String[0]); 155 this.simpleUnitCategories = new int[simpleUnitCategories.size()]; 156 Iterator<Integer> iter = simpleUnitCategories.iterator(); 157 for (int i = 0; i < this.simpleUnitCategories.length; i++) 158 { 159 this.simpleUnitCategories[i] = iter.next().intValue(); 160 } 161 } 162 } 163 164 /** 165 * Contains all the needed constants. 166 */ 167 public static class Constants { 168 // TODO: consider moving the Trie-offset-related constants into 169 // MeasureUnitImpl.java, the only place they're being used? 170 171 // Trie value offset for simple units, e.g. "gram", "nautical-mile", 172 // "fluid-ounce-imperial". 173 public static final int kSimpleUnitOffset = 512; 174 175 // Trie value offset for powers like "square-", "cubic-", "pow2-" etc. 176 public static final int kPowerPartOffset = 256; 177 178 179 // Trie value offset for "per-". 180 public final static int kInitialCompoundPartOffset = 192; 181 182 // Trie value offset for compound parts, e.g. "-per-", "-", "-and-". 183 public final static int kCompoundPartOffset = 128; 184 185 // Trie value offset for SI or binary prefixes. This is big enough to 186 // ensure we only insert positive integers into the trie. 187 public static final int kPrefixOffset = 64; 188 189 190 /* Tables Names*/ 191 public static final String CONVERSION_UNIT_TABLE_NAME = "convertUnits"; 192 public static final String UNIT_PREFERENCE_TABLE_NAME = "unitPreferenceData"; 193 public static final String CATEGORY_TABLE_NAME = "unitQuantities"; 194 public static final String DEFAULT_REGION = "001"; 195 public static final String DEFAULT_USAGE = "default"; 196 } 197 198 // Deals with base units and categories, e.g. "meter-per-second" --> "speed". 199 public static class Categories { 200 /** 201 * Maps from base unit to an index value: an index into the 202 * indexToCategory array. 203 */ 204 static HashMap<String, Integer> baseUnitToIndex; 205 206 /** 207 * Our official array of category strings - categories are identified by 208 * indeces into this array. 209 */ 210 static String[] indexToCategory; 211 212 static { 213 // Read unit Categories 214 ICUResourceBundle resource; 215 resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "units"); 216 CategoriesSink sink = new CategoriesSink(); resource.getAllItemsWithFallback(Constants.CATEGORY_TABLE_NAME, sink)217 resource.getAllItemsWithFallback(Constants.CATEGORY_TABLE_NAME, sink); 218 baseUnitToIndex = sink.mapFromUnitToIndex; 219 indexToCategory = sink.categories.toArray(new String[0]); 220 } 221 } 222 223 /** 224 * A Resource Sink that collects information from `unitQuantities` in the 225 * `units` resource to provide key->value lookups from base unit to 226 * category, as well as preserving ordering information for these 227 * categories. See `units.txt`. 228 * 229 * For example: "kilogram" -> "mass", "meter-per-second" -> "speed". 230 * 231 * In Java unitQuantity values are collected in order into an ArrayList, 232 * while unitQuantity key-to-index lookups are handled with a HashMap. 233 */ 234 public static class CategoriesSink extends UResource.Sink { 235 /** 236 * Contains the map between units in their base units into their category. 237 * For example: meter-per-second --> "speed" 238 */ 239 HashMap<String, Integer> mapFromUnitToIndex; 240 ArrayList<String> categories; 241 CategoriesSink()242 public CategoriesSink() { 243 mapFromUnitToIndex = new HashMap<>(); 244 categories = new ArrayList<>(); 245 } 246 247 @Override put(UResource.Key key, UResource.Value value, boolean noFallback)248 public void put(UResource.Key key, UResource.Value value, boolean noFallback) { 249 assert (key.toString().equals(Constants.CATEGORY_TABLE_NAME)); 250 assert (value.getType() == UResourceBundle.ARRAY); 251 252 UResource.Array categoryArray = value.getArray(); 253 for (int i=0; categoryArray.getValue(i, value); i++) { 254 assert (value.getType() == UResourceBundle.TABLE); 255 UResource.Table table = value.getTable(); 256 assert (table.getSize() == 1) 257 : "expecting single-entry table, got size: " + table.getSize(); 258 table.getKeyAndValue(0, key, value); 259 assert value.getType() == UResourceBundle.STRING : "expecting category string"; 260 mapFromUnitToIndex.put(key.toString(), categories.size()); 261 categories.add(value.toString()); 262 } 263 } 264 } 265 } 266