• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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