blob: 910fa92aa48571b1113e08a499d228cd46951148 [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 java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.UResource;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.UResourceBundle;
/**
* Responsible for all units data operations (retriever, analysis, extraction certain data ... etc.).
*/
public class UnitsData {
// TODO(icu-units#122): this class can use static initialization to load the
// data once, and provide access to it via static methods. (Partial change
// has been done already.)
// Array of simple unit IDs.
private static String[] simpleUnits = null;
// Maps from the value associated with each simple unit ID to a category
// index number.
private static int[] simpleUnitCategories = null;
private ConversionRates conversionRates;
private UnitPreferences unitPreferences;
public UnitsData() {
this.conversionRates = new ConversionRates();
this.unitPreferences = new UnitPreferences();
}
public static String[] getSimpleUnits() {
return simpleUnits;
}
static {
// Read simple units
ICUResourceBundle resource;
resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "units");
SimpleUnitIdentifiersSink sink = new SimpleUnitIdentifiersSink();
resource.getAllItemsWithFallback("convertUnits", sink);
simpleUnits = sink.simpleUnits;
simpleUnitCategories = sink.simpleUnitCategories;
}
public ConversionRates getConversionRates() {
return conversionRates;
}
public UnitPreferences getUnitPreferences() {
return unitPreferences;
}
public static int getCategoryIndexOfSimpleUnit(int simpleUnitIndex) {
return simpleUnitCategories[simpleUnitIndex];
}
/**
* @param measureUnit An instance of MeasureUnitImpl.
* @return the corresponding category.
*/
public String getCategory(MeasureUnitImpl measureUnit) {
MeasureUnitImpl baseMeasureUnit
= this.getConversionRates().extractCompoundBaseUnit(measureUnit);
String baseUnitIdentifier = MeasureUnit.fromMeasureUnitImpl(baseMeasureUnit).getIdentifier();
if (baseUnitIdentifier.equals("meter-per-cubic-meter")) {
// TODO(icu-units#130): support inverting any unit, with correct
// fallback logic: inversion and fallback may depend on presence or
// absence of a usage for that category.
return "consumption";
}
int index = Categories.baseUnitToIndex.get(baseUnitIdentifier);
return Categories.indexToCategory[index];
}
public UnitPreferences.UnitPreference[] getPreferencesFor(String category, String usage, String region) {
return this.unitPreferences.getPreferencesFor(category, usage, region);
}
public static class SimpleUnitIdentifiersSink extends UResource.Sink {
String[] simpleUnits = null;
int[] simpleUnitCategories = null;
@Override
public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
assert key.toString().equals(Constants.CONVERSION_UNIT_TABLE_NAME);
assert value.getType() == UResourceBundle.TABLE;
UResource.Table simpleUnitsTable = value.getTable();
ArrayList<String> simpleUnits = new ArrayList<>();
ArrayList<Integer> simpleUnitCategories = new ArrayList<>();
for (int i = 0; simpleUnitsTable.getKeyAndValue(i, key, value); i++) {
if (key.toString().equals("kilogram")) {
// For parsing, we use "gram", the prefixless metric mass unit. We
// thus ignore the SI Base Unit of Mass: it exists due to being the
// mass conversion target unit, but not needed for MeasureUnit
// parsing.
continue;
}
// Find the base target unit for this simple unit
UResource.Table table = value.getTable();
if (!table.findValue("target", value)) {
// TODO: is there a more idiomatic way to deal with Resource
// Sink data errors in ICU4J? For now we just assert-fail,
// and otherwise skip bad data:
assert false : "Could not find \"target\" for simple unit: " + key;
continue;
}
String target = value.getString();
simpleUnits.add(key.toString());
simpleUnitCategories.add(Categories.baseUnitToIndex.get(target));
}
this.simpleUnits = simpleUnits.toArray(new String[0]);
this.simpleUnitCategories = new int[simpleUnitCategories.size()];
Iterator<Integer> iter = simpleUnitCategories.iterator();
for (int i = 0; i < this.simpleUnitCategories.length; i++)
{
this.simpleUnitCategories[i] = iter.next().intValue();
}
}
}
/**
* Contains all the needed constants.
*/
public static class Constants {
// TODO: consider moving the Trie-offset-related constants into
// MeasureUnitImpl.java, the only place they're being used?
// Trie value offset for simple units, e.g. "gram", "nautical-mile",
// "fluid-ounce-imperial".
public static final int kSimpleUnitOffset = 512;
// Trie value offset for powers like "square-", "cubic-", "pow2-" etc.
public static final int kPowerPartOffset = 256;
// Trie value offset for "per-".
public final static int kInitialCompoundPartOffset = 192;
// Trie value offset for compound parts, e.g. "-per-", "-", "-and-".
public final static int kCompoundPartOffset = 128;
// Trie value offset for SI or binary prefixes. This is big enough to
// ensure we only insert positive integers into the trie.
public static final int kPrefixOffset = 64;
/* Tables Names*/
public static final String CONVERSION_UNIT_TABLE_NAME = "convertUnits";
public static final String UNIT_PREFERENCE_TABLE_NAME = "unitPreferenceData";
public static final String CATEGORY_TABLE_NAME = "unitQuantities";
public static final String DEFAULT_REGION = "001";
public static final String DEFAULT_USAGE = "default";
}
// Deals with base units and categories, e.g. "meter-per-second" --> "speed".
public static class Categories {
/**
* Maps from base unit to an index value: an index into the
* indexToCategory array.
*/
static HashMap<String, Integer> baseUnitToIndex;
/**
* Our official array of category strings - categories are identified by
* indeces into this array.
*/
static String[] indexToCategory;
static {
// Read unit Categories
ICUResourceBundle resource;
resource = (ICUResourceBundle) UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME, "units");
CategoriesSink sink = new CategoriesSink();
resource.getAllItemsWithFallback(Constants.CATEGORY_TABLE_NAME, sink);
baseUnitToIndex = sink.mapFromUnitToIndex;
indexToCategory = sink.categories.toArray(new String[0]);
}
}
/**
* A Resource Sink that collects information from `unitQuantities` in the
* `units` resource to provide key->value lookups from base unit to
* category, as well as preserving ordering information for these
* categories. See `units.txt`.
*
* For example: "kilogram" -> "mass", "meter-per-second" -> "speed".
*
* In Java unitQuantity values are collected in order into an ArrayList,
* while unitQuantity key-to-index lookups are handled with a HashMap.
*/
public static class CategoriesSink extends UResource.Sink {
/**
* Contains the map between units in their base units into their category.
* For example: meter-per-second --> "speed"
*/
HashMap<String, Integer> mapFromUnitToIndex;
ArrayList<String> categories;
public CategoriesSink() {
mapFromUnitToIndex = new HashMap<>();
categories = new ArrayList<>();
}
@Override
public void put(UResource.Key key, UResource.Value value, boolean noFallback) {
assert (key.toString().equals(Constants.CATEGORY_TABLE_NAME));
assert (value.getType() == UResourceBundle.ARRAY);
UResource.Array categoryArray = value.getArray();
for (int i=0; categoryArray.getValue(i, value); i++) {
assert (value.getType() == UResourceBundle.TABLE);
UResource.Table table = value.getTable();
assert (table.getSize() == 1)
: "expecting single-entry table, got size: " + table.getSize();
table.getKeyAndValue(0, key, value);
assert value.getType() == UResourceBundle.STRING : "expecting category string";
mapFromUnitToIndex.put(key.toString(), categories.size());
categories.add(value.toString());
}
}
}
}