blob: f93b83d70bbfc3b928850d316b97fcd35e7ba059 [file] [log] [blame]
package org.unicode.cldr.unittest;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.BiMap;
import com.google.common.collect.Comparators;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.google.common.collect.TreeMultimap;
import com.ibm.icu.dev.test.TestFmwk;
import com.ibm.icu.impl.Row;
import com.ibm.icu.impl.Row.R2;
import com.ibm.icu.impl.Row.R3;
import com.ibm.icu.number.FormattedNumber;
import com.ibm.icu.number.LocalizedNumberFormatter;
import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.number.NumberFormatter.UnitWidth;
import com.ibm.icu.number.Precision;
import com.ibm.icu.number.UnlocalizedNumberFormatter;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.text.UnicodeSet;
import com.ibm.icu.util.ICUUncheckedIOException;
import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
import com.ibm.icu.util.Output;
import com.ibm.icu.util.ULocale;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.unicode.cldr.draft.FileUtilities;
import org.unicode.cldr.test.CheckCLDR.CheckStatus;
import org.unicode.cldr.test.CheckCLDR.Options;
import org.unicode.cldr.test.CheckUnits;
import org.unicode.cldr.test.ExampleGenerator;
import org.unicode.cldr.util.CLDRConfig;
import org.unicode.cldr.util.CLDRFile;
import org.unicode.cldr.util.CLDRPaths;
import org.unicode.cldr.util.ChainedMap;
import org.unicode.cldr.util.ChainedMap.M3;
import org.unicode.cldr.util.ChainedMap.M4;
import org.unicode.cldr.util.CldrUtility;
import org.unicode.cldr.util.Counter;
import org.unicode.cldr.util.DtdData;
import org.unicode.cldr.util.DtdType;
import org.unicode.cldr.util.Factory;
import org.unicode.cldr.util.GrammarDerivation;
import org.unicode.cldr.util.GrammarInfo;
import org.unicode.cldr.util.GrammarInfo.GrammaticalFeature;
import org.unicode.cldr.util.GrammarInfo.GrammaticalScope;
import org.unicode.cldr.util.GrammarInfo.GrammaticalTarget;
import org.unicode.cldr.util.LocaleStringProvider;
import org.unicode.cldr.util.MapComparator;
import org.unicode.cldr.util.Organization;
import org.unicode.cldr.util.Pair;
import org.unicode.cldr.util.PathHeader;
import org.unicode.cldr.util.Rational;
import org.unicode.cldr.util.Rational.ContinuedFraction;
import org.unicode.cldr.util.Rational.FormatStyle;
import org.unicode.cldr.util.Rational.RationalParser;
import org.unicode.cldr.util.SimpleXMLSource;
import org.unicode.cldr.util.StandardCodes;
import org.unicode.cldr.util.StandardCodes.LstrType;
import org.unicode.cldr.util.SupplementalDataInfo;
import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo;
import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count;
import org.unicode.cldr.util.SupplementalDataInfo.PluralType;
import org.unicode.cldr.util.SupplementalDataInfo.UnitIdComponentType;
import org.unicode.cldr.util.TempPrintWriter;
import org.unicode.cldr.util.UnitConverter;
import org.unicode.cldr.util.UnitConverter.Continuation;
import org.unicode.cldr.util.UnitConverter.Continuation.UnitIterator;
import org.unicode.cldr.util.UnitConverter.ConversionInfo;
import org.unicode.cldr.util.UnitConverter.TargetInfo;
import org.unicode.cldr.util.UnitConverter.UnitComplexity;
import org.unicode.cldr.util.UnitConverter.UnitId;
import org.unicode.cldr.util.UnitConverter.UnitSystem;
import org.unicode.cldr.util.UnitParser;
import org.unicode.cldr.util.UnitPathType;
import org.unicode.cldr.util.UnitPreferences;
import org.unicode.cldr.util.UnitPreferences.UnitPreference;
import org.unicode.cldr.util.Units;
import org.unicode.cldr.util.Validity;
import org.unicode.cldr.util.Validity.Status;
import org.unicode.cldr.util.With;
import org.unicode.cldr.util.XMLSource;
import org.unicode.cldr.util.XPathParts;
public class TestUnits extends TestFmwk {
private static final boolean DEBUG = System.getProperty("TestUnits:DEBUG") != null;
private static final boolean TEST_ICU = System.getProperty("TestUnits:TEST_ICU") != null;
private static final Joiner JOIN_COMMA = Joiner.on(", ");
/** Flags to emit debugging information */
private static final boolean SHOW_UNIT_ORDER = getFlag("TestUnits:SHOW_UNIT_ORDER");
private static final boolean SHOW_UNIT_CATEGORY = getFlag("TestUnits:SHOW_UNIT_CATEGORY");
private static final boolean SHOW_COMPOSE = getFlag("TestUnits:SHOW_COMPOSE");
private static final boolean SHOW_DATA = getFlag("TestUnits:SHOW_DATA");
private static final boolean SHOW_MISSING_TEST_DATA =
getFlag("TestUnits:SHOW_MISSING_TEST_DATA");
private static final boolean SHOW_SYSTEMS = getFlag("TestUnits:SHOW_SYSTEMS");
/** Flags for reformatting data file */
private static final boolean SHOW_PREFS = getFlag("TestUnits:SHOW_PREFS");
/** Flag for generating test: TODO move to separate file */
private static final boolean GENERATE_TESTS = getFlag("TestUnits:GENERATE_TESTS");
private static final Set<String> VALID_REGULAR_UNITS =
Validity.getInstance().getStatusToCodes(LstrType.unit).get(Validity.Status.regular);
private static final Set<String> DEPRECATED_REGULAR_UNITS =
Validity.getInstance().getStatusToCodes(LstrType.unit).get(Validity.Status.deprecated);
public static final CLDRConfig CLDR_CONFIG = CLDRConfig.getInstance();
private static final Integer INTEGER_ONE = 1;
public static boolean getFlag(String flag) {
return CldrUtility.getProperty(flag, false);
}
private static final String TEST_SEP = ";\t";
private static final ImmutableSet<String> WORLD_SET = ImmutableSet.of("001");
private static final CLDRConfig info = CLDR_CONFIG;
private static final SupplementalDataInfo SDI = info.getSupplementalDataInfo();
static final UnitConverter converter = SDI.getUnitConverter();
static final Set<String> VALID_SHORT_UNITS = converter.getShortIds(VALID_REGULAR_UNITS);
static final Set<String> DEPRECATED_SHORT_UNITS =
converter.getShortIds(DEPRECATED_REGULAR_UNITS);
static final Splitter SPLIT_SEMI = Splitter.on(Pattern.compile("\\s*;\\s*")).trimResults();
static final Splitter SPLIT_SPACE = Splitter.on(' ').trimResults().omitEmptyStrings();
static final Splitter SPLIT_AND = Splitter.on("-and-").trimResults().omitEmptyStrings();
static final Splitter SPLIT_DASH = Splitter.on('-').trimResults().omitEmptyStrings();
static final Rational R1000 = Rational.of(1000);
static Map<String, String> normalizationCache = new TreeMap<>();
public static void main(String[] args) {
new TestUnits().run(args);
}
private Map<String, String> BASE_UNIT_TO_QUANTITY = converter.getBaseUnitToQuantity();
public void TestSpaceInNarrowUnits() {
final CLDRFile english = CLDR_CONFIG.getEnglish();
final Matcher m = Pattern.compile("narrow.*unitPattern").matcher("");
for (String path : english) {
if (m.reset(path).find()) {
String value = english.getStringValue(path);
if (value.contains("} ")) {
errln(path + " fails, «" + value + "» contains } + space");
}
}
}
}
static final String[][] COMPOUND_TESTS = {
{"area-square-centimeter", "square", "length-centimeter"},
{"area-square-foot", "square", "length-foot"},
{"area-square-inch", "square", "length-inch"},
{"area-square-kilometer", "square", "length-kilometer"},
{"area-square-meter", "square", "length-meter"},
{"area-square-mile", "square", "length-mile"},
{"area-square-yard", "square", "length-yard"},
{"digital-gigabit", "giga", "digital-bit"},
{"digital-gigabyte", "giga", "digital-byte"},
{"digital-kilobit", "kilo", "digital-bit"},
{"digital-kilobyte", "kilo", "digital-byte"},
{"digital-megabit", "mega", "digital-bit"},
{"digital-megabyte", "mega", "digital-byte"},
{"digital-petabyte", "peta", "digital-byte"},
{"digital-terabit", "tera", "digital-bit"},
{"digital-terabyte", "tera", "digital-byte"},
{"duration-microsecond", "micro", "duration-second"},
{"duration-millisecond", "milli", "duration-second"},
{"duration-nanosecond", "nano", "duration-second"},
{"electric-milliampere", "milli", "electric-ampere"},
{"energy-kilocalorie", "kilo", "energy-calorie"},
{"energy-kilojoule", "kilo", "energy-joule"},
{"frequency-gigahertz", "giga", "frequency-hertz"},
{"frequency-kilohertz", "kilo", "frequency-hertz"},
{"frequency-megahertz", "mega", "frequency-hertz"},
{"graphics-megapixel", "mega", "graphics-pixel"},
{"length-centimeter", "centi", "length-meter"},
{"length-decimeter", "deci", "length-meter"},
{"length-kilometer", "kilo", "length-meter"},
{"length-micrometer", "micro", "length-meter"},
{"length-millimeter", "milli", "length-meter"},
{"length-nanometer", "nano", "length-meter"},
{"length-picometer", "pico", "length-meter"},
{"mass-kilogram", "kilo", "mass-gram"},
{"mass-microgram", "micro", "mass-gram"},
{"mass-milligram", "milli", "mass-gram"},
{"power-gigawatt", "giga", "power-watt"},
{"power-kilowatt", "kilo", "power-watt"},
{"power-megawatt", "mega", "power-watt"},
{"power-milliwatt", "milli", "power-watt"},
{"pressure-hectopascal", "hecto", "pressure-pascal"},
{"pressure-millibar", "milli", "pressure-bar"},
{"pressure-kilopascal", "kilo", "pressure-pascal"},
{"pressure-megapascal", "mega", "pressure-pascal"},
{"volume-centiliter", "centi", "volume-liter"},
{"volume-cubic-centimeter", "cubic", "length-centimeter"},
{"volume-cubic-foot", "cubic", "length-foot"},
{"volume-cubic-inch", "cubic", "length-inch"},
{"volume-cubic-kilometer", "cubic", "length-kilometer"},
{"volume-cubic-meter", "cubic", "length-meter"},
{"volume-cubic-mile", "cubic", "length-mile"},
{"volume-cubic-yard", "cubic", "length-yard"},
{"volume-deciliter", "deci", "volume-liter"},
{"volume-hectoliter", "hecto", "volume-liter"},
{"volume-megaliter", "mega", "volume-liter"},
{"volume-milliliter", "milli", "volume-liter"},
};
static final String[][] PREFIX_NAME_TYPE = {
{"deci", "10p-1"},
{"centi", "10p-2"},
{"milli", "10p-3"},
{"micro", "10p-6"},
{"nano", "10p-9"},
{"pico", "10p-12"},
{"femto", "10p-15"},
{"atto", "10p-18"},
{"zepto", "10p-21"},
{"yocto", "10p-24"},
{"deka", "10p1"},
{"hecto", "10p2"},
{"kilo", "10p3"},
{"mega", "10p6"},
{"giga", "10p9"},
{"tera", "10p12"},
{"peta", "10p15"},
{"exa", "10p18"},
{"zetta", "10p21"},
{"yotta", "10p24"},
{"square", "power2"},
{"cubic", "power3"},
};
static final String PATH_UNIT_PATTERN =
"//ldml/units/unitLength[@type=\"{0}\"]/unit[@type=\"{1}\"]/unitPattern[@count=\"{2}\"]";
static final String PATH_PREFIX_PATTERN =
"//ldml/units/unitLength[@type=\"{0}\"]/compoundUnit[@type=\"{1}\"]/unitPrefixPattern";
static final String PATH_SUFFIX_PATTERN =
"//ldml/units/unitLength[@type=\"{0}\"]/compoundUnit[@type=\"{1}\"]/compoundUnitPattern1";
static final String PATH_MILLI_PATTERN =
"//ldml/units/unitLength[@type=\"{0}\"]/compoundUnit[@type=\"10p-3\"]/unitPrefixPattern";
static final String PATH_SQUARE_PATTERN =
"//ldml/units/unitLength[@type=\"{0}\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1";
static final String PATH_METER_PATTERN =
"//ldml/units/unitLength[@type=\"{0}\"]/unit[@type=\"length-meter\"]/unitPattern[@count=\"{1}\"]";
static final String PATH_MILLIMETER_PATTERN =
"//ldml/units/unitLength[@type=\"{0}\"]/unit[@type=\"length-millimeter\"]/unitPattern[@count=\"{1}\"]";
static final String PATH_SQUARE_METER_PATTERN =
"//ldml/units/unitLength[@type=\"{0}\"]/unit[@type=\"area-square-meter\"]/unitPattern[@count=\"{1}\"]";
public void TestAUnits() {
if (isVerbose()) {
System.out.println();
Output<String> baseUnit = new Output<>();
int count = 0;
for (String simpleUnit : converter.getSimpleUnits()) {
ConversionInfo conversion = converter.parseUnitId(simpleUnit, baseUnit, false);
if (simpleUnit.equals(baseUnit)) {
continue;
}
System.out.println(
++count
+ ")\t"
+ simpleUnit
+ " → "
+ baseUnit
+ "; factor = "
+ conversion.factor
+ " = "
+ conversion.factor.toString(FormatStyle.repeatingAll)
+ (conversion.offset.equals(Rational.ZERO)
? ""
: "; offset = " + conversion.offset));
}
}
}
public void TestCompoundUnit3() {
Factory factory = CLDR_CONFIG.getCldrFactory();
Map<String, String> prefixToType = new LinkedHashMap<>();
for (String[] prefixRow : PREFIX_NAME_TYPE) {
prefixToType.put(prefixRow[0], prefixRow[1]);
}
prefixToType = ImmutableMap.copyOf(prefixToType);
Set<String> localesToTest = ImmutableSet.of("en"); // factory.getAvailableLanguages();
int testCount = 0;
for (String locale : localesToTest) {
CLDRFile file = factory.make(locale, true);
// ExampleGenerator exampleGenerator = getExampleGenerator(locale);
PluralInfo pluralInfo = SDI.getPlurals(PluralType.cardinal, locale);
final boolean isEnglish = locale.contentEquals("en");
int errMsg = isEnglish ? ERR : WARN;
for (String[] compoundTest : COMPOUND_TESTS) {
String targetUnit = compoundTest[0];
String prefix = compoundTest[1];
String baseUnit = compoundTest[2];
String prefixType = prefixToType.get(prefix); // will be null for square, cubic
final boolean isPrefix = prefixType.startsWith("1");
for (String len : Arrays.asList("long", "short", "narrow")) {
String prefixPath =
ExampleGenerator.format(
isPrefix ? PATH_PREFIX_PATTERN : PATH_SUFFIX_PATTERN,
len,
prefixType);
String prefixValue = file.getStringValue(prefixPath);
boolean lowercaseIfSpaced = len.equals("long");
for (Count count : pluralInfo.getCounts()) {
final String countString = count.toString();
String targetUnitPath =
ExampleGenerator.format(
PATH_UNIT_PATTERN, len, targetUnit, countString);
String targetUnitPattern = file.getStringValue(targetUnitPath);
String baseUnitPath =
ExampleGenerator.format(
PATH_UNIT_PATTERN, len, baseUnit, countString);
String baseUnitPattern = file.getStringValue(baseUnitPath);
String composedTargetUnitPattern =
Units.combinePattern(
baseUnitPattern, prefixValue, lowercaseIfSpaced);
if (isEnglish && !targetUnitPattern.equals(composedTargetUnitPattern)) {
if (allowEnglishException(
targetUnitPattern, composedTargetUnitPattern)) {
continue;
}
}
if (!assertEquals2(
errMsg,
testCount++
+ ") "
+ locale
+ "/"
+ len
+ "/"
+ count
+ "/"
+ prefix
+ "+"
+ baseUnit
+ ": constructed pattern",
targetUnitPattern,
composedTargetUnitPattern)) {
Units.combinePattern(baseUnitPattern, prefixValue, lowercaseIfSpaced);
int debug = 0;
}
}
}
}
}
}
/**
* Curated list of known exceptions. Usually because the short form of a unit is shorter when
* combined with a prefix or suffix
*/
static final Map<String, String> ALLOW_ENGLISH_EXCEPTION =
ImmutableMap.<String, String>builder()
.put("sq ft", "ft²")
.put("sq mi", "mi²")
.put("ft", "′")
.put("in", "″")
.put("MP", "Mpx")
.put("b", "bit")
.put("mb", "mbar")
.put("B", "byte")
.put("s", "sec")
.build();
private boolean allowEnglishException(
String targetUnitPattern, String composedTargetUnitPattern) {
for (Entry<String, String> entry : ALLOW_ENGLISH_EXCEPTION.entrySet()) {
String mod = targetUnitPattern.replace(entry.getKey(), entry.getValue());
if (mod.contentEquals(composedTargetUnitPattern)) {
return true;
}
}
return false;
}
// TODO Work this into a generating and then maintaining a data table for the units
/*
CLDRFile english = factory.make("en", false);
Set<String> prefixes = new TreeSet<>();
for (String path : english) {
XPathParts parts = XPathParts.getFrozenInstance(path);
String lastElement = parts.getElement(-1);
if (lastElement.equals("unitPrefixPattern") || lastElement.equals("compoundUnitPattern1")) {
if (!parts.getAttributeValue(2, "type").equals("long")) {
continue;
}
String value = english.getStringValue(path);
prefixes.add(value.replace("{0}", "").trim());
}
}
Map<Status, Set<String>> unitValidity = Validity.getInstance().getStatusToCodes(LstrType.unit);
Multimap<String, String> from = LinkedHashMultimap.create();
for (String unit : unitValidity.get(Status.regular)) {
String[] parts = unit.split("[-]");
String main = parts[1];
for (String prefix : prefixes) {
if (main.startsWith(prefix)) {
if (main.length() == prefix.length()) { // square,...
from.put(unit, main);
} else { // milli
from.put(unit, main.substring(0,prefix.length()));
from.put(unit, main.substring(prefix.length()));
}
for (int i = 2; i < parts.length; ++i) {
from.put(unit, parts[i]);
}
}
}
}
for (Entry<String, Collection<String>> set : from.asMap().entrySet()) {
System.out.println(set.getKey() + "\t" + CollectionUtilities.join(set.getValue(), "\t"));
}
*/
private boolean assertEquals2(
int TestERR, String title, String sqmeterPattern, String conSqmeterPattern) {
if (!Objects.equals(sqmeterPattern, conSqmeterPattern)) {
msg(
title + ", expected «" + sqmeterPattern + "», got «" + conSqmeterPattern + "»",
TestERR,
true,
true);
return false;
} else if (isVerbose()) {
msg(
title + ", expected «" + sqmeterPattern + "», got «" + conSqmeterPattern + "»",
LOG,
true,
true);
}
return true;
}
public void TestConversion() {
String[][] tests = {
{"foot", "12", "inch"},
{"gallon", "4", "quart"},
{"gallon", "16", "cup"},
};
for (String[] test : tests) {
String sourceUnit = test[0];
Rational factor = Rational.of(test[1]);
String targetUnit = test[2];
final Rational convert = converter.convertDirect(Rational.ONE, sourceUnit, targetUnit);
assertEquals(sourceUnit + " to " + targetUnit, factor, convert);
}
// test conversions are disjoint
Set<String> gotAlready = new HashSet<>();
List<Set<String>> equivClasses = new ArrayList<>();
Map<String, String> classToId = new TreeMap<>();
for (String unit : converter.canConvert()) {
if (gotAlready.contains(unit)) {
continue;
}
Set<String> set = converter.canConvertBetween(unit);
final String id = "ID" + equivClasses.size();
equivClasses.add(set);
gotAlready.addAll(set);
for (String s : set) {
classToId.put(s, id);
}
}
// check not overlapping
// now handled by TestParseUnit, but we might revive a modified version of this.
// for (int i = 0; i < equivClasses.size(); ++i) {
// Set<String> eclass1 = equivClasses.get(i);
// for (int j = i+1; j < equivClasses.size(); ++j) {
// Set<String> eclass2 = equivClasses.get(j);
// if (!Collections.disjoint(eclass1, eclass2)) {
// errln("Overlapping equivalence classes: " + eclass1 + " ~ " + eclass2
// + "\n\tProbably bad chain requiring 3 steps.");
// }
// }
//
// // check that all elements of an equivalence class have the same type
// Multimap<String,String> breakdown = TreeMultimap.create();
// for (String item : eclass1) {
// String type = CORE_TO_TYPE.get(item);
// if (type == null) {
// type = "?";
// }
// breakdown.put(type, item);
// }
// if (DEBUG) System.out.println("type to item: " + breakdown);
// if (breakdown.keySet().size() != 1) {
// errln("mixed categories: " + breakdown);
// }
//
// }
//
// // check that all units with the same type have the same equivalence class
// for (Entry<String, Collection<String>> entry : TYPE_TO_CORE.asMap().entrySet()) {
// Multimap<String,String> breakdown = TreeMultimap.create();
// for (String item : entry.getValue()) {
// String id = classToId.get(item);
// if (id == null) {
// continue;
// }
// breakdown.put(id, item);
// }
// if (DEBUG) System.out.println(entry.getKey() + " id to item: " + breakdown);
// if (breakdown.keySet().size() != 1) {
// errln(entry.getKey() + " mixed categories: " + breakdown);
// }
// }
}
public void TestBaseUnits() {
Splitter barSplitter = Splitter.on('-');
for (String unit : converter.baseUnits()) {
for (String piece : barSplitter.split(unit)) {
assertTrue(
unit + ": " + piece + " in " + UnitConverter.BASE_UNIT_PARTS,
UnitConverter.BASE_UNIT_PARTS.contains(piece));
}
}
}
public void TestUnitId() {
for (String simple : converter.getSimpleUnits()) {
String canonicalUnit = converter.getBaseUnit(simple);
UnitId unitId = converter.createUnitId(canonicalUnit);
String output = unitId.toString();
if (!assertEquals(
simple + ": targets should be in canonical form", output, canonicalUnit)) {
// for debugging
converter.createUnitId(canonicalUnit);
unitId.toString();
}
}
for (Entry<String, String> baseUnitToQuantity : BASE_UNIT_TO_QUANTITY.entrySet()) {
String baseUnit = baseUnitToQuantity.getKey();
String quantity = baseUnitToQuantity.getValue();
try {
UnitId unitId = converter.createUnitId(baseUnit);
String output = unitId.toString();
if (!assertEquals(
quantity + ": targets should be in canonical form", output, baseUnit)) {
// for debugging
converter.createUnitId(baseUnit);
unitId.toString();
}
} catch (Exception e) {
errln("Can't convert baseUnit: " + baseUnit);
}
}
for (String baseUnit : CORE_TO_TYPE.keySet()) {
try {
UnitId unitId = converter.createUnitId(baseUnit);
assertNotNull("Can't parse baseUnit: " + baseUnit, unitId);
} catch (Exception e) {
converter.createUnitId(baseUnit); // for debugging
errln("Can't parse baseUnit: " + baseUnit);
}
}
}
public void TestParseUnit() {
Output<String> compoundBaseUnit = new Output<>();
String[][] tests = {
{"kilometer-pound-per-hour", "kilogram-meter-per-second", "45359237/360000000"},
{"kilometer-per-hour", "meter-per-second", "5/18"},
};
for (String[] test : tests) {
String source = test[0];
String expectedUnit = test[1];
Rational expectedRational = new Rational.RationalParser().parse(test[2]);
ConversionInfo unitInfo = converter.parseUnitId(source, compoundBaseUnit, false);
assertEquals(source, expectedUnit, compoundBaseUnit.value);
assertEquals(source, expectedRational, unitInfo.factor);
}
// check all
if (GENERATE_TESTS) System.out.println();
Set<String> badUnits = new LinkedHashSet<>();
Set<String> noQuantity = new LinkedHashSet<>();
Multimap<Pair<String, Double>, String> testPrintout = TreeMultimap.create();
// checkUnitConvertability(converter, compoundBaseUnit, badUnits, "pint-metric-per-second");
for (Entry<String, String> entry : TYPE_TO_CORE.entries()) {
String type = entry.getKey();
String unit = entry.getValue();
if (NOT_CONVERTABLE.contains(unit)) {
continue;
}
checkUnitConvertability(
converter, compoundBaseUnit, badUnits, noQuantity, type, unit, testPrintout);
}
if (GENERATE_TESTS) { // test data
try (TempPrintWriter pw =
TempPrintWriter.openUTF8Writer(
CLDRPaths.TEST_DATA + "units", "unitsTest.txt")) {
pw.println(
"# Test data for unit conversions\n"
+ CldrUtility.getCopyrightString("# ")
+ "\n"
+ "#\n"
+ "# Format:\n"
+ "#\tQuantity\t;\tx\t;\ty\t;\tconversion to y (rational)\t;\ttest: 1000 x ⟹ y\n"
+ "#\n"
+ "# Use: convert 1000 x units to the y unit; the result should match the final column,\n"
+ "# at the given precision. For example, when the last column is 159.1549,\n"
+ "# round to 4 decimal digits before comparing.\n"
+ "# Note that certain conversions are approximate, such as degrees to radians\n"
+ "#\n"
+ "# Generation: Set GENERATE_TESTS in TestUnits.java to regenerate unitsTest.txt.\n");
for (Entry<Pair<String, Double>, String> entry : testPrintout.entries()) {
pw.println(entry.getValue());
}
}
}
assertEquals("Unconvertable units", Collections.emptySet(), badUnits);
assertEquals("Units without Quantity", Collections.emptySet(), noQuantity);
}
static final Set<String> NOT_CONVERTABLE = ImmutableSet.of("generic");
private void checkUnitConvertability(
UnitConverter converter,
Output<String> compoundBaseUnit,
Set<String> badUnits,
Set<String> noQuantity,
String type,
String unit,
Multimap<Pair<String, Double>, String> testPrintout) {
if (converter.isBaseUnit(unit)) {
String quantity = converter.getQuantityFromBaseUnit(unit);
if (quantity == null) {
noQuantity.add(unit);
}
if (GENERATE_TESTS) {
testPrintout.put(
new Pair<>(quantity, 1000d),
quantity + "\t;\t" + unit + "\t;\t" + unit + "\t;\t1 * x\t;\t1,000.00");
}
} else {
ConversionInfo unitInfo = converter.getUnitInfo(unit, compoundBaseUnit);
if (unitInfo == null) {
unitInfo = converter.parseUnitId(unit, compoundBaseUnit, false);
}
if (unitInfo == null) {
badUnits.add(unit);
} else if (GENERATE_TESTS) {
String quantity = converter.getQuantityFromBaseUnit(compoundBaseUnit.value);
if (quantity == null) {
noQuantity.add(compoundBaseUnit.value);
}
final double testValue =
unitInfo.convert(R1000).toBigDecimal(MathContext.DECIMAL32).doubleValue();
testPrintout.put(
new Pair<>(quantity, testValue),
quantity
+ "\t;\t"
+ unit
+ "\t;\t"
+ compoundBaseUnit
+ "\t;\t"
+ unitInfo
+ "\t;\t"
+ testValue
// + "\t" +
// unitInfo.factor.toBigDecimal(MathContext.DECIMAL32)
// + "\t" +
// unitInfo.factor.reciprocal().toBigDecimal(MathContext.DECIMAL32)
);
}
}
}
public void TestRational() {
Rational a3_5 = Rational.of(3, 5);
Rational a6_10 = Rational.of(6, 10);
assertEquals("", a3_5, a6_10);
Rational a5_3 = Rational.of(5, 3);
assertEquals("", a3_5, a5_3.reciprocal());
assertEquals("", Rational.ONE, a3_5.multiply(a3_5.reciprocal()));
assertEquals("", Rational.ZERO, a3_5.add(a3_5.negate()));
assertEquals("", Rational.NEGATIVE_ONE, Rational.ONE.negate());
assertEquals("", BigDecimal.valueOf(2), Rational.of(2, 1).toBigDecimal());
assertEquals("", BigDecimal.valueOf(0.5), Rational.of(1, 2).toBigDecimal());
assertEquals("", BigDecimal.valueOf(100), Rational.of(100, 1).toBigDecimal());
assertEquals("", BigDecimal.valueOf(0.01), Rational.of(1, 100).toBigDecimal());
assertEquals("", Rational.of(12370, 1), Rational.of(BigDecimal.valueOf(12370)));
assertEquals("", Rational.of(1237, 10), Rational.of(BigDecimal.valueOf(1237.0 / 10)));
assertEquals("", Rational.of(1237, 10000), Rational.of(BigDecimal.valueOf(1237.0 / 10000)));
ConversionInfo uinfo = new ConversionInfo(Rational.of(2), Rational.of(3));
assertEquals("", Rational.of(3), uinfo.convert(Rational.ZERO));
assertEquals("", Rational.of(7), uinfo.convert(Rational.of(2)));
assertEquals("", Rational.INFINITY, Rational.ZERO.reciprocal());
assertEquals("", Rational.NEGATIVE_INFINITY, Rational.INFINITY.negate());
Set<Rational> anything =
ImmutableSet.of(
Rational.NaN,
Rational.NEGATIVE_INFINITY,
Rational.NEGATIVE_ONE,
Rational.ZERO,
Rational.ONE,
Rational.INFINITY);
for (Rational something : anything) {
assertEquals("0/0", Rational.NaN, Rational.NaN.add(something));
assertEquals("0/0", Rational.NaN, Rational.NaN.subtract(something));
assertEquals("0/0", Rational.NaN, Rational.NaN.divide(something));
assertEquals("0/0", Rational.NaN, Rational.NaN.add(something));
assertEquals("0/0", Rational.NaN, Rational.NaN.negate());
assertEquals("0/0", Rational.NaN, something.add(Rational.NaN));
assertEquals("0/0", Rational.NaN, something.subtract(Rational.NaN));
assertEquals("0/0", Rational.NaN, something.divide(Rational.NaN));
assertEquals("0/0", Rational.NaN, something.add(Rational.NaN));
}
assertEquals("0/0", Rational.NaN, Rational.ZERO.divide(Rational.ZERO));
assertEquals("INF-INF", Rational.NaN, Rational.INFINITY.subtract(Rational.INFINITY));
assertEquals("INF+-INF", Rational.NaN, Rational.INFINITY.add(Rational.NEGATIVE_INFINITY));
assertEquals("-INF+INF", Rational.NaN, Rational.NEGATIVE_INFINITY.add(Rational.INFINITY));
assertEquals("INF/INF", Rational.NaN, Rational.INFINITY.divide(Rational.INFINITY));
assertEquals("INF+1", Rational.INFINITY, Rational.INFINITY.add(Rational.ONE));
assertEquals("INF-1", Rational.INFINITY, Rational.INFINITY.subtract(Rational.ONE));
}
public void TestRationalParse() {
Rational.RationalParser parser = SDI.getRationalParser();
Rational a3_5 = Rational.of(3, 5);
assertEquals("", a3_5, parser.parse("6/10"));
assertEquals("", a3_5, parser.parse("0.06/0.10"));
assertEquals("", Rational.of(381, 1250), parser.parse("ft_to_m"));
assertEquals(
"", 6.02214076E+23d, parser.parse("6.02214076E+23").toBigDecimal().doubleValue());
Rational temp = parser.parse("gal_to_m3");
// System.out.println(" " + temp);
assertEquals(
"", 0.003785411784, temp.numerator.doubleValue() / temp.denominator.doubleValue());
}
static final Map<String, String> CORE_TO_TYPE;
static final Multimap<String, String> TYPE_TO_CORE;
static {
Set<String> VALID_UNITS =
Validity.getInstance().getStatusToCodes(LstrType.unit).get(Status.regular);
Map<String, String> coreToType = new TreeMap<>();
TreeMultimap<String, String> typeToCore = TreeMultimap.create();
for (String s : VALID_UNITS) {
int dashPos = s.indexOf('-');
String unitType = s.substring(0, dashPos);
String coreUnit = s.substring(dashPos + 1);
coreUnit = converter.fixDenormalized(coreUnit);
coreToType.put(coreUnit, unitType);
typeToCore.put(unitType, coreUnit);
}
CORE_TO_TYPE = ImmutableMap.copyOf(coreToType);
TYPE_TO_CORE = ImmutableMultimap.copyOf(typeToCore);
}
static final Map<String, String> quantityToCategory =
ImmutableMap.<String, String>builder()
.put("acceleration", "acceleration")
.put("angle", "angle")
.put("area", "area")
.put("catalytic-activity", "concentr")
.put("concentration", "concentr")
.put("concentration-mass", "concentr")
.put("consumption", "consumption")
.put("consumption-inverse", "consumption")
.put("digital", "digital")
.put("duration", "duration")
.put("electric-capacitance", "electric")
.put("electric-charge", "electric")
.put("electric-conductance", "electric")
.put("electric-current", "electric")
.put("electric-inductance", "electric")
.put("electric-resistance", "electric")
.put("energy", "energy")
.put("force", "force")
.put("frequency", "frequency")
.put("graphics", "graphics")
.put("illuminance", "light")
.put("ionizing-radiation", "energy")
.put("length", "length")
.put("luminous-flux", "light")
.put("luminous-intensity", "light")
.put("magnetic-flux", "magnetic")
.put("magnetic-induction", "magnetic")
.put("mass", "mass")
.put("portion", "concentr")
.put("power", "power")
.put("pressure", "pressure")
.put("pressure-per-length", "pressure")
.put("radioactivity", "energy")
.put("resolution", "graphics")
.put("solid-angle", "angle")
.put("speed", "speed")
.put("substance-amount", "concentr")
.put("temperature", "temperature")
.put("typewidth", "graphics")
.put("voltage", "electric")
.put("volume", "volume")
.put("year-duration", "duration")
.build();
// TODO Get rid of these exceptions.
// Some of the qualities are 'split' over categories, which ideally shouldn't happen.
static final Map<String, String> CATEGORY_EXCEPTIONS =
ImmutableMap.<String, String>builder()
.put("dalton", "mass")
.put("newton-meter", "torque")
.put("pound-force-foot", "torque")
.put("solar-luminosity", "light")
.build();
public void TestUnitCategory() {
Map<String, Multimap<String, String>> bad = new TreeMap<>();
for (Entry<String, String> entry : TYPE_TO_CORE.entries()) {
final String coreUnit = entry.getValue();
final String unitType = entry.getKey();
if (NOT_CONVERTABLE.contains(coreUnit)) {
continue;
}
String quantity = converter.getQuantityFromUnit(coreUnit, false);
if (quantity == null) {
converter.getQuantityFromUnit(coreUnit, true);
errln("Null quantity " + coreUnit);
} else {
String exception = CATEGORY_EXCEPTIONS.get(coreUnit);
if (unitType.equals(exception)) {
continue;
}
assertEquals(
"Category for «" + coreUnit + "» with quality «" + quantity + "»",
unitType,
quantityToCategory.get(quantity));
}
}
}
public void TestQuantities() {
// put quantities in order
Multimap<String, String> quantityToBaseUnits = LinkedHashMultimap.create();
Multimaps.invertFrom(Multimaps.forMap(BASE_UNIT_TO_QUANTITY), quantityToBaseUnits);
for (Entry<String, Collection<String>> entry : quantityToBaseUnits.asMap().entrySet()) {
assertEquals(entry.toString(), 1, entry.getValue().size());
}
TreeMultimap<String, String> quantityToConvertible = TreeMultimap.create();
Set<String> missing = new TreeSet<>(CORE_TO_TYPE.keySet());
missing.removeAll(NOT_CONVERTABLE);
for (Entry<String, String> entry : BASE_UNIT_TO_QUANTITY.entrySet()) {
String baseUnit = entry.getKey();
String quantity = entry.getValue();
Set<String> convertible = converter.canConvertBetween(baseUnit);
missing.removeAll(convertible);
quantityToConvertible.putAll(quantity, convertible);
}
// handle missing
for (String missingUnit : ImmutableSet.copyOf(missing)) {
if (missingUnit.equals("mile-per-gallon")) {
int debug = 0;
}
String quantity = converter.getQuantityFromUnit(missingUnit, false);
if (quantity != null) {
quantityToConvertible.put(quantity, missingUnit);
missing.remove(missingUnit);
} else {
quantity = converter.getQuantityFromUnit(missingUnit, true); // for debugging
}
}
assertEquals("all units have quantity", Collections.emptySet(), missing);
if (SHOW_UNIT_CATEGORY) {
System.out.println();
for (Entry<String, String> entry : BASE_UNIT_TO_QUANTITY.entrySet()) {
String baseUnit = entry.getKey();
String quantity = entry.getValue();
System.out.println(
" <unitQuantity"
+ " baseUnit='"
+ baseUnit
+ "'"
+ " quantity='"
+ quantity
+ "'"
+ "/>");
}
System.out.println();
System.out.println("Quantities");
for (Entry<String, Collection<String>> entry :
quantityToConvertible.asMap().entrySet()) {
String quantity = entry.getKey();
Collection<String> convertible = entry.getValue();
System.out.println(quantity + "\t" + convertible);
}
}
}
static final UnicodeSet ALLOWED_IN_COMPONENT = new UnicodeSet("[a-z0-9]").freeze();
static final Set<String> STILL_RECOGNIZED_SIMPLES =
ImmutableSet.of(
"em",
"g-force",
"therm-us",
"british-thermal-unit-it",
"calorie-it",
"bu-jp",
"jo-jp",
"ri-jp",
"se-jp",
"to-jp",
"cup-jp");
public void TestOrder() {
if (SHOW_UNIT_ORDER) System.out.println();
for (String s : UnitConverter.BASE_UNITS) {
String quantity = converter.getQuantityFromBaseUnit(s);
if (SHOW_UNIT_ORDER) {
System.out.println("\"" + quantity + "\",");
}
}
for (String unit : CORE_TO_TYPE.keySet()) {
if (!STILL_RECOGNIZED_SIMPLES.contains(unit)) {
for (String part : unit.split("-")) {
assertTrue(unit + " has no parts < 2 in length", part.length() > 2);
assertTrue(
unit + " has only allowed characters",
ALLOWED_IN_COMPONENT.containsAll(part));
}
}
if (unit.equals("generic")) {
continue;
}
String quantity = converter.getQuantityFromUnit(unit, false); // make sure doesn't crash
}
}
public void TestConversionLineOrder() {
Map<String, TargetInfo> data = converter.getInternalConversionData();
Multimap<TargetInfo, String> sorted =
TreeMultimap.create(converter.targetInfoComparator, Comparator.naturalOrder());
Multimaps.invertFrom(Multimaps.forMap(data), sorted);
String lastBase = "";
// Test that sorted is in same order as the file.
MapComparator<String> conversionOrder = new MapComparator<>(data.keySet());
String lastUnit = null;
Set<String> warnings = new LinkedHashSet<>();
for (Entry<TargetInfo, String> entry : sorted.entries()) {
final TargetInfo tInfo = entry.getKey();
final String unit = entry.getValue();
if (lastUnit != null) {
if (!(conversionOrder.compare(lastUnit, unit) < 0)) {
Output<String> metricUnit = new Output<>();
ConversionInfo lastInfo = converter.parseUnitId(lastUnit, metricUnit, false);
String lastMetric = metricUnit.value;
ConversionInfo info = converter.parseUnitId(unit, metricUnit, false);
String metric = metricUnit.value;
if (metric.equals(lastMetric)) {
warnings.add(
"Expected "
+ lastUnit
+ " < "
+ unit
+ "\t"
+ lastMetric
+ " "
+ lastInfo
+ " < "
+ metric
+ " "
+ info);
}
}
}
lastUnit = unit;
if (SHOW_UNIT_ORDER) {
if (!lastBase.equals(tInfo.target)) {
lastBase = tInfo.target;
System.out.println(
"\n <!-- " + converter.getQuantityFromBaseUnit(lastBase) + " -->");
}
// <convertUnit source='week-person' target='second' factor='604800'/>
System.out.println(" " + tInfo.formatOriginalSource(entry.getValue()));
}
}
if (!warnings.isEmpty()) {
warnln("Some units are not ordered by size, count=" + warnings.size());
}
}
public final void TestSimplify() {
Set<Rational> seen = new HashSet<>();
checkSimplify("ZERO", Rational.ZERO, seen);
checkSimplify("ONE", Rational.ONE, seen);
checkSimplify("NEGATIVE_ONE", Rational.NEGATIVE_ONE, seen);
checkSimplify("INFINITY", Rational.INFINITY, seen);
checkSimplify("NEGATIVE_INFINITY", Rational.NEGATIVE_INFINITY, seen);
checkSimplify("NaN", Rational.NaN, seen);
checkSimplify("Simplify", Rational.of(25, 300), seen);
checkSimplify("Simplify", Rational.of(100, 1), seen);
checkSimplify("Simplify", Rational.of(2, 5), seen);
checkSimplify("Simplify", Rational.of(4, 25), seen);
checkSimplify("Simplify", Rational.of(5, 2), seen);
checkSimplify("Simplify", Rational.of(25, 4), seen);
for (Entry<String, TargetInfo> entry : converter.getInternalConversionData().entrySet()) {
final Rational factor = entry.getValue().unitInfo.factor;
checkSimplify(entry.getKey(), factor, seen);
if (!factor.equals(Rational.ONE)) {
checkSimplify(entry.getKey(), factor, seen);
}
final Rational offset = entry.getValue().unitInfo.offset;
if (!offset.equals(Rational.ZERO)) {
checkSimplify(entry.getKey(), offset, seen);
}
}
}
private void checkSimplify(String title, Rational expected, Set<Rational> seen) {
if (!seen.contains(expected)) {
seen.add(expected);
String simpleStr = expected.toString(FormatStyle.formatted);
if (SHOW_DATA) System.out.println(title + ": " + expected + " => " + simpleStr);
Rational actual = RationalParser.BASIC.parse(simpleStr);
assertEquals("simplify", expected, actual);
}
}
public void TestContinuationOrder() {
Continuation fluid = new Continuation(Arrays.asList("fluid"), "fluid-ounce");
Continuation fluid_imperial =
new Continuation(Arrays.asList("fluid", "imperial"), "fluid-ounce-imperial");
final int fvfl = fluid.compareTo(fluid_imperial);
assertTrue(fluid + " vs " + fluid_imperial, fvfl > 0);
assertTrue(fluid_imperial + " vs " + fluid, fluid_imperial.compareTo(fluid) < 0);
}
private static final Pattern usSystemPattern =
Pattern.compile(
"\\b(lb_to_kg|ft_to_m|ft2_to_m2|ft3_to_m3|in3_to_m3|gal_to_m3|cup_to_m3)\\b");
private static final Pattern ukSystemPattern =
Pattern.compile("\\b(lb_to_kg|ft_to_m|ft2_to_m2|ft3_to_m3|in3_to_m3|gal_imp_to_m3)\\b");
static final Set<String> OK_BOTH =
ImmutableSet.of(
"ounce-troy",
"nautical-mile",
"fahrenheit",
"inch-ofhg",
"british-thermal-unit",
"foodcalorie",
"knot");
static final Set<String> OK_US = ImmutableSet.of("therm-us", "bushel");
static final Set<String> NOT_US = ImmutableSet.of("stone");
static final Set<String> OK_UK = ImmutableSet.of();
static final Set<String> NOT_UK = ImmutableSet.of("therm-us", "bushel", "barrel");
public static final Set<String> OTHER_SYSTEM =
ImmutableSet.of(
"g-force",
"dalton",
"calorie",
"earth-radius",
"solar-radius",
"solar-radius",
"astronomical-unit",
"light-year",
"parsec",
"earth-mass",
"solar-mass",
"bit",
"byte",
"karat",
"solar-luminosity",
"ofhg",
"atmosphere",
"pixel",
"dot",
"permillion",
"permyriad",
"permille",
"percent",
"karat",
"portion",
"minute",
"hour",
"day",
"day-person",
"week",
"week-person",
"year",
"year-person",
"decade",
"month",
"month-person",
"century",
"quarter",
"arc-second",
"arc-minute",
"degree",
"radian",
"revolution",
"electronvolt",
"beaufort",
// quasi-metric
"dunam",
"mile-scandinavian",
"carat",
"cup-metric",
"pint-metric");
public void TestSystems() {
final Logger logger = getLogger();
// Map<String, TargetInfo> data = converter.getInternalConversionData();
Output<String> metricUnit = new Output<>();
Multimap<Set<UnitSystem>, R3<String, ConversionInfo, String>> systemsToUnits =
TreeMultimap.create(
Comparators.lexicographical(Ordering.natural()), Ordering.natural());
for (String longUnit : VALID_REGULAR_UNITS) {
String unit = Units.getShort(longUnit);
if (NOT_CONVERTABLE.contains(unit)) {
continue;
}
if (unit.contentEquals("centiliter")) {
int debug = 0;
}
Set<UnitSystem> systems = converter.getSystemsEnum(unit);
ConversionInfo parseInfo = converter.parseUnitId(unit, metricUnit, false);
String mUnit = metricUnit.value;
final R3<String, ConversionInfo, String> row = Row.of(mUnit, parseInfo, unit);
systemsToUnits.put(systems, row);
// if (systems.isEmpty()) {
// Rational factor = parseInfo.factor;
// if (factor.isPowerOfTen()) {
// log("System should be 'metric': " + unit);
// } else {
// log("System should be ???: " + unit);
// }
// }
}
String std = converter.getStandardUnit("kilogram-meter-per-square-meter-square-second");
logger.fine("");
Output<Rational> outFactor = new Output<>();
for (Entry<Set<UnitSystem>, Collection<R3<String, ConversionInfo, String>>>
systemsAndUnits : systemsToUnits.asMap().entrySet()) {
Set<UnitSystem> systems = systemsAndUnits.getKey();
for (R3<String, ConversionInfo, String> unitInfo : systemsAndUnits.getValue()) {
String unit = unitInfo.get2();
switch (unit) {
case "gram":
continue;
case "kilogram":
break;
default:
String paredUnit = UnitConverter.stripPrefix(unit, outFactor);
if (!paredUnit.equals(unit)) {
continue;
}
}
final String metric = unitInfo.get0();
String standard = converter.getStandardUnit(metric);
final String quantity = converter.getQuantityFromUnit(unit, false);
final Rational factor = unitInfo.get1().factor;
// show non-metric relations
String specialRef = "";
String specialUnit = converter.getSpecialBaseUnit(quantity, systems);
if (specialUnit != null) {
Rational specialFactor =
converter.convert(Rational.ONE, unit, specialUnit, false);
specialRef = "\t" + specialFactor + "\t" + specialUnit;
}
logger.fine(
systems
+ "\t"
+ quantity
+ "\t"
+ unit
+ "\t"
+ factor
+ "\t"
+ standard
+ specialRef);
}
}
}
public void TestTestFile() {
File base = info.getCldrBaseDirectory();
File testFile = new File(base, "common/testData/units/unitsTest.txt");
Output<String> metricUnit = new Output<>();
Stream<String> lines;
try {
lines = Files.lines(testFile.toPath());
} catch (IOException e) {
throw new ICUUncheckedIOException("Couldn't process " + testFile);
}
lines.forEach(
line -> {
// angle ; arc-second ; revolution ; 1 / 1296000 * x ; 7.716049E-4
line = line.trim();
if (line.isEmpty() || line.charAt(0) == '#') {
return;
}
List<String> fields = SPLIT_SEMI.splitToList(line);
ConversionInfo unitInfo;
try {
unitInfo = converter.parseUnitId(fields.get(1), metricUnit, false);
} catch (Exception e1) {
throw new IllegalArgumentException("Couldn't access fields on " + line);
}
if (unitInfo == null) {
throw new IllegalArgumentException("Couldn't get unitInfo on " + line);
}
double expected;
try {
expected = Double.parseDouble(fields.get(4).replace(",", ""));
} catch (NumberFormatException e) {
errln("Can't parse double in: " + line);
return;
}
double actual =
unitInfo.convert(R1000)
.toBigDecimal(MathContext.DECIMAL32)
.doubleValue();
assertEquals(Joiner.on(" ; ").join(fields), expected, actual);
});
lines.close();
}
public void TestSpecialCases() {
String[][] tests = {
{"1", "millimole-per-liter", "milligram-ofglucose-per-deciliter", "18.01557"},
{"1", "millimole-per-liter", "item-per-cubic-meter", "602214076000000000000000"},
{"50", "foot", "xxx", "0/0"},
{"50", "xxx", "mile", "0/0"},
{"50", "foot", "second", "0/0"},
{"50", "foot-per-xxx", "mile-per-hour", "0/0"},
{"50", "foot-per-minute", "mile", "0/0"},
{"50", "foot-per-ampere", "mile-per-hour", "0/0"},
{"50", "foot", "mile", "5 / 528"},
{"50", "foot-per-minute", "mile-per-hour", "25 / 44"},
{"50", "foot-per-minute", "hour-per-mile", "44 / 25"},
{"50", "mile-per-gallon", "liter-per-100-kilometer", "112903 / 24000"},
{"50", "celsius-per-second", "kelvin-per-second", "50"},
{"50", "celsius-per-second", "fahrenheit-per-second", "90"},
{
"50",
"pound-force",
"kilogram-meter-per-square-second",
"8896443230521 / 40000000000"
},
// Note: pound-foot-per-square-second is a pound-force divided by gravity
{
"50",
"pound-foot-per-square-second",
"kilogram-meter-per-square-second",
"17281869297 / 2500000000"
},
{"1", "beaufort", "meter-per-second", "0.95"}, // 19/20
{"4", "beaufort", "meter-per-second", "6.75"}, // 27/4
{"7", "beaufort", "meter-per-second", "15.55"}, // 311/20
{"10", "beaufort", "meter-per-second", "26.5"}, // 53/2
{"13", "beaufort", "meter-per-second", "39.15"}, // 783/20
{"1", "beaufort", "mile-per-hour", "11875 / 5588"}, // 2.125089...
{"4", "beaufort", "mile-per-hour", "84375 / 5588"}, // 15.099319971367215
{"7", "beaufort", "mile-per-hour", "194375 / 5588"}, // 34.784359341445956
{"10", "beaufort", "mile-per-hour", "165625 / 2794"}, // 59.27881...
{"13", "beaufort", "mile-per-hour", "489375 / 5588"}, // 87.576056...
{"1", "meter-per-second", "beaufort", "1"},
{"7", "meter-per-second", "beaufort", "4"},
{"16", "meter-per-second", "beaufort", "7"},
{"27", "meter-per-second", "beaufort", "10"},
{"39", "meter-per-second", "beaufort", "13"},
};
int count = 0;
for (String[] test : tests) {
final Rational sourceValue = Rational.of(test[0]);
final String sourceUnit = test[1];
final String targetUnit = test[2];
final Rational expectedValue = Rational.of(test[3]);
final Rational conversion =
converter.convert(sourceValue, sourceUnit, targetUnit, SHOW_DATA);
if (!assertEquals(
count++ + ") " + sourceValue + " " + sourceUnit + " ⟹ " + targetUnit,
expectedValue,
conversion)) {
converter.convert(sourceValue, sourceUnit, targetUnit, SHOW_DATA);
}
}
}
static Multimap<String, String> EXTRA_UNITS =
ImmutableMultimap.<String, String>builder()
.putAll("area", "square-foot", "square-yard", "square-mile")
.putAll("volume", "cubic-inch", "cubic-foot", "cubic-yard")
.build();
public void TestEnglishSystems() {
Multimap<String, String> systemToUnits = TreeMultimap.create();
for (String unit : converter.canConvert()) {
Set<String> systems = converter.getSystems(unit);
if (systems.isEmpty()) {
systemToUnits.put("other", unit);
} else
for (String s : systems) {
systemToUnits.put(s, unit);
}
}
for (Entry<String, Collection<String>> systemAndUnits : systemToUnits.asMap().entrySet()) {
String system = systemAndUnits.getKey();
final Collection<String> units = systemAndUnits.getValue();
printSystemUnits(system, units);
}
}
private void printSystemUnits(String system, Collection<String> units) {
Multimap<String, String> quantityToUnits = TreeMultimap.create();
boolean metric = system.equals("metric");
for (String unit : units) {
quantityToUnits.put(converter.getQuantityFromUnit(unit, false), unit);
}
for (Entry<String, Collection<String>> entry : quantityToUnits.asMap().entrySet()) {
String quantity = entry.getKey();
String baseUnit = converter.getBaseUnitToQuantity().inverse().get(quantity);
Multimap<Rational, String> sorted = TreeMultimap.create();
sorted.put(Rational.ONE, baseUnit);
if (!metric) {
String englishBaseUnit = getEnglishBaseUnit(baseUnit);
addUnit(baseUnit, englishBaseUnit, sorted);
Collection<String> extras = EXTRA_UNITS.get(quantity);
if (extras != null) {
for (String unit2 : extras) {
addUnit(baseUnit, unit2, sorted);
}
}
}
for (String unit : entry.getValue()) {
addUnit(baseUnit, unit, sorted);
}
Set<String> comparableUnits = ImmutableSet.copyOf(sorted.values());
if (SHOW_DATA) {
printUnits(system, quantity, comparableUnits);
}
}
}
private void addUnit(
String baseUnit, String englishBaseUnit, Multimap<Rational, String> sorted) {
Rational value = converter.convert(Rational.ONE, englishBaseUnit, baseUnit, false);
sorted.put(value, englishBaseUnit);
}
private void printUnits(String system, String quantity, Set<String> comparableUnits) {
System.out.print("\n" + system + "\t" + quantity);
for (String targetUnit : comparableUnits) {
System.out.print("\t" + targetUnit);
}
System.out.println();
for (String sourceUnit : comparableUnits) {
System.out.print("\t" + sourceUnit);
for (String targetUnit : comparableUnits) {
Rational rational = converter.convert(Rational.ONE, sourceUnit, targetUnit, false);
System.out.print("\t" + rational.toBigDecimal(MathContext.DECIMAL64).doubleValue());
}
System.out.println();
}
}
private String getEnglishBaseUnit(String baseUnit) {
return baseUnit.replace("kilogram", "pound").replace("meter", "foot");
}
public void TestPI() {
Rational PI = converter.getConstants().get("PI");
double PID = PI.toBigDecimal(MathContext.DECIMAL128).doubleValue();
final BigDecimal bigPi =
new BigDecimal("3.141592653589793238462643383279502884197169399375105820974944");
double bigPiD = bigPi.doubleValue();
assertEquals("pi accurate enough", bigPiD, PID);
// also test continued fractions used in deriving values
Object[][] tests0 = {
{
new ContinuedFraction(0, 1, 5, 2, 2),
Rational.of(27, 32),
ImmutableList.of(
Rational.of(0), Rational.of(1), Rational.of(5, 6), Rational.of(11, 13))
},
};
for (Object[] test : tests0) {
ContinuedFraction source = (ContinuedFraction) test[0];
Rational expected = (Rational) test[1];
@SuppressWarnings("unchecked")
List<Rational> expectedIntermediates = (List<Rational>) test[2];
List<Rational> intermediates = new ArrayList<>();
final Rational actual = source.toRational(intermediates);
assertEquals("continued", expected, actual);
assertEquals("continued", expectedIntermediates, intermediates);
}
Object[][] tests = {
{Rational.of(3245, 1000), new ContinuedFraction(3, 4, 12, 4)},
{Rational.of(39, 10), new ContinuedFraction(3, 1, 9)},
{Rational.of(-3245, 1000), new ContinuedFraction(-4, 1, 3, 12, 4)},
};
for (Object[] test : tests) {
Rational source = (Rational) test[0];
ContinuedFraction expected = (ContinuedFraction) test[1];
ContinuedFraction actual = new ContinuedFraction(source);
assertEquals(source.toString(), expected, actual);
assertEquals(actual.toString(), source, actual.toRational(null));
}
if (SHOW_DATA) {
ContinuedFraction actual = new ContinuedFraction(Rational.of(bigPi));
List<Rational> intermediates = new ArrayList<>();
actual.toRational(intermediates);
System.out.println("\nRational\tdec64\tdec128\tgood enough");
System.out.println(
"Target\t"
+ bigPi.round(MathContext.DECIMAL64)
+ "x"
+ "\t"
+ bigPi.round(MathContext.DECIMAL128)
+ "x"
+ "\t"
+ "delta");
int goodCount = 0;
for (Rational item : intermediates) {
final BigDecimal dec64 = item.toBigDecimal(MathContext.DECIMAL64);
final BigDecimal dec128 = item.toBigDecimal(MathContext.DECIMAL128);
final boolean goodEnough =
bigPiD == item.toBigDecimal(MathContext.DECIMAL128).doubleValue();
System.out.println(
item
+ "\t"
+ dec64
+ "x\t"
+ dec128
+ "x\t"
+ goodEnough
+ "\t"
+ item.toBigDecimal(MathContext.DECIMAL128).subtract(bigPi));
if (goodEnough && goodCount++ > 6) {
break;
}
}
}
}
public void TestUnitPreferenceSource() {
XMLSource xmlSource = new SimpleXMLSource("units");
xmlSource.setNonInheriting(true);
CLDRFile foo = new CLDRFile(xmlSource);
foo.setDtdType(DtdType.supplementalData);
UnitPreferences uprefs = new UnitPreferences();
int order = 0;
for (String line : FileUtilities.in(TestUnits.class, "UnitPreferenceSource.txt")) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
List<String> items = SPLIT_SEMI.splitToList(line);
try {
String quantity = items.get(0);
String usage = items.get(1);
String regionsStr = items.get(2);
List<String> regions = SPLIT_SPACE.splitToList(items.get(2));
String geqStr = items.get(3);
Rational geq = geqStr.isEmpty() ? Rational.ONE : Rational.of(geqStr);
String skeleton = items.get(4);
String unit = items.get(5);
uprefs.add(quantity, usage, regionsStr, geqStr, skeleton, unit);
String path = uprefs.getPath(order++, quantity, usage, regions, geq, skeleton);
xmlSource.putValueAtPath(path, unit);
} catch (Exception e) {
errln("Failure on line: " + line + "; " + e.getMessage());
}
}
if (SHOW_PREFS) {
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
foo.write(out);
out.flush();
} else {
warnln("Use -DTestUnits:SHOW_PREFS to get the reformatted source");
}
}
static final Joiner JOIN_SPACE = Joiner.on(' ');
private void checkUnitPreferences(UnitPreferences uprefs) {
Set<String> usages = new LinkedHashSet<>();
for (Entry<String, Map<String, Multimap<Set<String>, UnitPreference>>> entry1 :
uprefs.getData().entrySet()) {
String quantity = entry1.getKey();
// Each of the quantities is valid.
assertNotNull("quantity is convertible", converter.getBaseUnitFromQuantity(quantity));
Map<String, Multimap<Set<String>, UnitPreference>> usageToRegionToUnitPreference =
entry1.getValue();
// each of the quantities has a default usage
assertTrue(
"Quantity " + quantity + " contains default usage",
usageToRegionToUnitPreference.containsKey("default"));
for (Entry<String, Multimap<Set<String>, UnitPreference>> entry2 :
usageToRegionToUnitPreference.entrySet()) {
String usage = entry2.getKey();
final String quantityPlusUsage = quantity + "/" + usage;
Multimap<Set<String>, UnitPreference> regionsToUnitPreference = entry2.getValue();
usages.add(usage);
Set<Set<String>> regionSets = regionsToUnitPreference.keySet();
// all quantity + usage pairs must contain 001 (one exception)
assertTrue(
"For "
+ quantityPlusUsage
+ ", the set of sets of regions must contain 001",
regionSets.contains(WORLD_SET)
|| quantityPlusUsage.contentEquals("concentration/blood-glucose"));
// Check that regions don't overlap for same quantity/usage
Multimap<String, Set<String>> checkOverlap = LinkedHashMultimap.create();
for (Set<String> regionSet : regionsToUnitPreference.keySet()) {
for (String region : regionSet) {
checkOverlap.put(region, regionSet);
}
}
for (Entry<String, Collection<Set<String>>> entry :
checkOverlap.asMap().entrySet()) {
assertEquals(
quantityPlusUsage
+ ": regions must be in only one set: "
+ entry.getValue(),
1,
entry.getValue().size());
}
Set<String> systems = new TreeSet<>();
for (Entry<Set<String>, Collection<UnitPreference>> entry :
regionsToUnitPreference.asMap().entrySet()) {
Collection<UnitPreference> uPrefs = entry.getValue();
Set<String> regions = entry.getKey();
// reset these for every new set of regions
Rational lastSize = null;
String lastUnit = null;
Rational lastgeq = null;
systems.clear();
Set<String> lastRegions = null;
String unitQuantity = null;
preferences:
for (UnitPreference up : uPrefs) {
String topUnit = null;
if ("minute:second".equals(up.unit)) {
int debug = 0;
}
String lastQuantity = null;
Rational lastValue = null;
Rational geq = converter.parseRational(String.valueOf(up.geq));
// where we have an 'and' unit, get its information
for (String unit : SPLIT_AND.split(up.unit)) {
try {
if (topUnit == null) {
topUnit = unit;
}
unitQuantity = converter.getQuantityFromUnit(unit, false);
} catch (Exception e) {
errln("Unit is not covertible: " + up.unit);
continue preferences;
}
String baseUnit = converter.getBaseUnitFromQuantity(unitQuantity);
if (geq.compareTo(Rational.ZERO) < 0) {
throw new IllegalArgumentException("geq must be > 0" + geq);
}
Rational value = converter.convert(Rational.ONE, unit, baseUnit, false);
if (lastQuantity != null) {
int diff = value.compareTo(lastValue);
if (diff >= 0) {
throw new IllegalArgumentException(
"Bad mixed unit; biggest unit must be first: "
+ up.unit);
}
if (!lastQuantity.contentEquals(quantity)) {
throw new IllegalArgumentException(
"Inconsistent quantities for mixed unit: " + up.unit);
}
}
lastValue = value;
lastQuantity = quantity;
systems.addAll(converter.getSystems(unit));
}
String baseUnit = converter.getBaseUnitFromQuantity(unitQuantity);
Rational size = converter.convert(up.geq, topUnit, baseUnit, false);
if (lastSize != null) { // ensure descending order
if (!assertTrue(
"Successive items must be ≥ previous:\n\t"
+ quantityPlusUsage
+ "; unit: "
+ up.unit
+ "; size: "
+ size
+ "; regions: "
+ regions
+ "; lastUnit: "
+ lastUnit
+ "; lastSize: "
+ lastSize
+ "; lastRegions: "
+ lastRegions,
size.compareTo(lastSize) <= 0)) {
int debug = 0;
}
}
lastSize = size;
lastUnit = up.unit;
lastgeq = geq;
lastRegions = regions;
if (SHOW_DATA)
System.out.println(
quantity
+ "\t"
+ usage
+ "\t"
+ regions
+ "\t"
+ up.geq
+ "\t"
+ up.unit
+ "\t"
+ up.skeleton);
}
// Check that last geq is ONE.
assertEquals(
usage
+ " + "
+ regions
+ ": the least unit must have geq=1 (or equivalently, no geq)",
Rational.ONE,
lastgeq);
// Check that each set has a consistent system.
assertTrue(
usage
+ " + "
+ regions
+ " has mixed systems: "
+ systems
+ "\n\t"
+ uPrefs,
areConsistent(systems, unitQuantity));
}
}
}
}
private boolean areConsistent(Set<String> systems, String unitQuantity) {
return unitQuantity.equals("duration")
|| !(systems.contains("metric")
&& (systems.contains("ussystem") || systems.contains("uksystem")));
}
public void TestBcp47() {
checkBcp47("Quantity", converter.getQuantities(), lowercaseAZ, false);
checkBcp47("Usage", SDI.getUnitPreferences().getUsages(), lowercaseAZ09, true);
checkBcp47("Unit", converter.getSimpleUnits(), lowercaseAZ09, true);
}
private void checkBcp47(
String identifierType,
Set<String> identifiers,
UnicodeSet allowed,
boolean allowHyphens) {
Output<Integer> counter = new Output<>(0);
Multimap<String, String> truncatedToFullIdentifier = TreeMultimap.create();
final Set<String> simpleUnits = identifiers;
for (String unit : simpleUnits) {
if (!allowHyphens && unit.contains("-")) {
truncatedToFullIdentifier.put(unit, "-");
}
checkBcp47(counter, identifierType, unit, allowed, truncatedToFullIdentifier);
}
for (Entry<String, Collection<String>> entry :
truncatedToFullIdentifier.asMap().entrySet()) {
Set<String> identifierSet = ImmutableSet.copyOf(entry.getValue());
assertEquals(
identifierType + ": truncated identifier " + entry.getKey() + " must be unique",
ImmutableSet.of(identifierSet.iterator().next()),
identifierSet);
}
}
private static int MIN_SUBTAG_LENGTH = 3;
private static int MAX_SUBTAG_LENGTH = 8;
static final UnicodeSet lowercaseAZ = new UnicodeSet("[a-z]").freeze();
static final UnicodeSet lowercaseAZ09 = new UnicodeSet("[a-z0-9]").freeze();
private void checkBcp47(
Output<Integer> counter,
String title,
String identifier,
UnicodeSet allowed,
Multimap<String, String> truncatedToFullIdentifier) {
StringBuilder shortIdentifer = new StringBuilder();
boolean fail = false;
for (String subtag : identifier.split("-")) {
assertTrue(
++counter.value
+ ") "
+ title
+ " identifier="
+ identifier
+ " subtag="
+ subtag
+ " has right characters",
allowed.containsAll(subtag));
if (!(subtag.length() >= MIN_SUBTAG_LENGTH && subtag.length() <= MAX_SUBTAG_LENGTH)) {
for (Entry<String, Rational> entry : UnitConverter.PREFIXES.entrySet()) {
String prefix = entry.getKey();
if (subtag.startsWith(prefix)) {
subtag = subtag.substring(prefix.length());
break;
}
}
}
if (shortIdentifer.length() != 0) {
shortIdentifer.append('-');
}
if (subtag.length() > MAX_SUBTAG_LENGTH) {
shortIdentifer.append(subtag.substring(0, MAX_SUBTAG_LENGTH));
fail = true;
} else {
shortIdentifer.append(subtag);
}
}
if (fail) {
String shortIdentiferStr = shortIdentifer.toString();
truncatedToFullIdentifier.put(shortIdentiferStr, identifier);
}
}
public void TestUnitPreferences() {
warnln(
"If this fails, check the output of TestUnitPreferencesSource (with -DTestUnits:SHOW_DATA), fix as needed, then incorporate.");
UnitPreferences prefs = SDI.getUnitPreferences();
checkUnitPreferences(prefs);
if (GENERATE_TESTS) {
try (TempPrintWriter pw =
TempPrintWriter.openUTF8Writer(
CLDRPaths.TEST_DATA + "units", "unitPreferencesTest.txt")) {
pw.println(
"\n# Test data for unit preferences\n"
+ CldrUtility.getCopyrightString("# ")
+ "\n"
+ "#\n"
+ "# Format:\n"
+ "#\tQuantity;\tUsage;\tRegion;\tInput (r);\tInput (d);\tInput Unit;\tOutput (r);\tOutput (d);\tOutput Unit\n"
+ "#\n"
+ "# Use: Convert the Input amount & unit according to the Usage and Region.\n"
+ "#\t The result should match the Output amount and unit.\n"
+ "#\t Both rational (r) and double64 (d) forms of the input and output amounts are supplied so that implementations\n"
+ "#\t have two options for testing based on the precision in their implementations. For example:\n"
+ "#\t 3429 / 12500; 0.27432; meter;\n"
+ "#\t The Output amount and Unit are repeated for mixed units. In such a case, only the smallest unit will have\n"
+ "#\t both a rational and decimal amount; the others will have a single integer value, such as:\n"
+ "#\t length; person-height; CA; 3429 / 12500; 0.27432; meter; 2; foot; 54 / 5; 10.8; inch\n"
+ "#\t The input and output units are unit identifers; in particular, the output does not have further processing:\n"
+ "#\t\t • no localization\n"
+ "#\t\t • no adjustment for pluralization\n"
+ "#\t\t • no formatted with the skeleton\n"
+ "#\t\t • no suppression of zero values (for secondary -and- units such as pound in stone-and-pound)\n"
+ "#\n"
+ "# Generation: Set GENERATE_TESTS in TestUnits.java to regenerate unitPreferencesTest.txt.\n");
Rational ONE_TENTH = Rational.of(1, 10);
// Note that for production usage, precomputed data like the
// prefs.getFastMap(converter) would be used instead of the raw data.
for (Entry<String, Map<String, Multimap<Set<String>, UnitPreference>>> entry :
prefs.getData().entrySet()) {
String quantity = entry.getKey();
String baseUnit = converter.getBaseUnitFromQuantity(quantity);
for (Entry<String, Multimap<Set<String>, UnitPreference>> entry2 :
entry.getValue().entrySet()) {
String usage = entry2.getKey();
// collect samples of base units
for (Entry<Set<String>, Collection<UnitPreference>> entry3 :
entry2.getValue().asMap().entrySet()) {
boolean first = true;
Set<Rational> samples = new TreeSet<>(Comparator.reverseOrder());
for (UnitPreference pref : entry3.getValue()) {
final String topUnit =
UnitPreferences.SPLIT_AND
.split(pref.unit)
.iterator()
.next();
if (first) {
samples.add(
converter.convert(
pref.geq.add(ONE_TENTH),
topUnit,
baseUnit,
false));
first = false;
}
samples.add(converter.convert(pref.geq, topUnit, baseUnit, false));
samples.add(
converter.convert(
pref.geq.subtract(ONE_TENTH),
topUnit,
baseUnit,
false));
}
// show samples
Set<String> regions = entry3.getKey();
String sampleRegion = regions.iterator().next();
Collection<UnitPreference> uprefs = entry3.getValue();
for (Rational sample : samples) {
showSample(
quantity,
usage,
sampleRegion,
sample,
baseUnit,
uprefs,
pw);
}
pw.println();
}
}
}
}
}
}
private void showSample(
String quantity,
String usage,
String sampleRegion,
Rational sampleBaseValue,
String baseUnit,
Collection<UnitPreference> prefs,
TempPrintWriter pw) {
String lastUnit = null;
boolean gotOne = false;
for (UnitPreference pref : prefs) {
final String topUnit = UnitPreferences.SPLIT_AND.split(pref.unit).iterator().next();
Rational baseGeq = converter.convert(pref.geq, topUnit, baseUnit, false);
if (sampleBaseValue.compareTo(baseGeq) >= 0) {
showSample2(
quantity, usage, sampleRegion, sampleBaseValue, baseUnit, pref.unit, pw);
gotOne = true;
break;
}
lastUnit = pref.unit;
}
if (!gotOne) {
showSample2(quantity, usage, sampleRegion, sampleBaseValue, baseUnit, lastUnit, pw);
}
}
private void showSample2(
String quantity,
String usage,
String sampleRegion,
Rational sampleBaseValue,
String baseUnit,
String lastUnit,
TempPrintWriter pw) {
Rational originalSampleBaseValue = sampleBaseValue;
// Known slow algorithm for mixed values, but for generating tests we don't care.
final List<String> units = UnitPreferences.SPLIT_AND.splitToList(lastUnit);
StringBuilder formattedUnit = new StringBuilder();
int remaining = units.size();
for (String unit : units) {
--remaining;
Rational sample = converter.convert(sampleBaseValue, baseUnit, unit, false);
if (formattedUnit.length() != 0) {
formattedUnit.append(TEST_SEP);
}
if (remaining != 0) {
BigInteger floor = sample.floor();
formattedUnit.append(floor + TEST_SEP + unit);
// convert back to base unit
sampleBaseValue =
converter.convert(
sample.subtract(Rational.of(floor)), unit, baseUnit, false);
} else {
formattedUnit.append(sample + TEST_SEP + sample.doubleValue() + TEST_SEP + unit);
}
}
pw.println(
quantity
+ TEST_SEP
+ usage
+ TEST_SEP
+ sampleRegion
+ TEST_SEP
+ originalSampleBaseValue
+ TEST_SEP
+ originalSampleBaseValue.doubleValue()
+ TEST_SEP
+ baseUnit
+ TEST_SEP
+ formattedUnit);
}
public void TestWithExternalData() throws IOException {
Multimap<String, ExternalUnitConversionData> seen = HashMultimap.create();
Set<ExternalUnitConversionData> cantConvert = new LinkedHashSet<>();
Map<ExternalUnitConversionData, Rational> convertDiff = new LinkedHashMap<>();
Set<String> remainingCldrUnits =
new LinkedHashSet<>(converter.getInternalConversionData().keySet());
Set<ExternalUnitConversionData> couldAdd = new LinkedHashSet<>();
if (SHOW_DATA) {
System.out.println();
}
for (ExternalUnitConversionData data : NistUnits.externalConversionData) {
Rational externalResult = data.info.convert(Rational.ONE);
Rational cldrResult = converter.convert(Rational.ONE, data.source, data.target, false);
seen.put(data.source + "⟹" + data.target, data);
if (externalResult.isPowerOfTen()) {
couldAdd.add(data);
}
if (cldrResult.equals(Rational.NaN)) {
cantConvert.add(data);
} else {
if (!cldrResult.approximatelyEquals(externalResult)) {
convertDiff.put(data, cldrResult);
} else {
remainingCldrUnits.remove(data.source);
remainingCldrUnits.remove(data.target);
if (SHOW_DATA)
System.out.println(
"*Converted"
+ "\t"
+ cldrResult.doubleValue()
+ "\t"
+ externalResult.doubleValue()
+ "\t"
+ cldrResult.symmetricDiff(externalResult).doubleValue()
+ "\t"
+ data);
}
}
}
// get additional data on derived units
// for (Entry<String, TargetInfo> e : NistUnits.derivedUnitToConversion.entrySet()) {
// String sourceUnit = e.getKey();
// TargetInfo targetInfo = e.getValue();
//
// Rational conversion = converter.convert(Rational.ONE, sourceUnit,
// targetInfo.target, false);
// if (conversion.equals(Rational.NaN)) {
// couldAdd.add(new ExternalUnitConversionData("", sourceUnit,
// targetInfo.target, conversion, "?", null));
// }
// }
if (SHOW_DATA) {
for (Entry<String, Collection<String>> e :
NistUnits.unitToQuantity.asMap().entrySet()) {
System.out.println("*Quantities:" + "\t" + e.getKey() + "\t" + e.getValue());
}
}
// check for missing external data
int unitsWithoutExternalCheck = 0;
if (SHOW_MISSING_TEST_DATA && !remainingCldrUnits.isEmpty()) {
System.out.println("\nNot tested against external data");
}
for (String remainingUnit : remainingCldrUnits) {
ExternalUnitConversionData external = NistUnits.unitToData.get(remainingUnit);
final TargetInfo targetInfo = converter.getInternalConversionData().get(remainingUnit);
if (!targetInfo.target.contentEquals(remainingUnit)) {
if (SHOW_MISSING_TEST_DATA) {
printlnIfZero(unitsWithoutExternalCheck);
System.out.println(
remainingUnit
+ "\t"
+ targetInfo.unitInfo.factor.doubleValue()
+ "\t"
+ targetInfo.target);
}
unitsWithoutExternalCheck++;
}
}
if (unitsWithoutExternalCheck != 0 && !SHOW_MISSING_TEST_DATA) {
warnln(
unitsWithoutExternalCheck
+ " units without external data verification. Use -DTestUnits:SHOW_MISSING_TEST_DATA for details.");
}
boolean showDiagnostics = false;
for (Entry<String, Collection<ExternalUnitConversionData>> entry :
seen.asMap().entrySet()) {
if (entry.getValue().size() != 1) {
Multimap<ConversionInfo, ExternalUnitConversionData> factors =
HashMultimap.create();
for (ExternalUnitConversionData s : entry.getValue()) {
factors.put(s.info, s);
}
if (factors.keySet().size() > 1) {
for (ExternalUnitConversionData s : entry.getValue()) {
errln("*DUP-" + s);
showDiagnostics = true;
}
}
}
}
if (convertDiff.size() > 0) {
for (Entry<ExternalUnitConversionData, Rational> e : convertDiff.entrySet()) {
final Rational computed = e.getValue();
final ExternalUnitConversionData external = e.getKey();
Rational externalResult = external.info.convert(Rational.ONE);
showDiagnostics = true;
// for debugging
converter.convert(Rational.ONE, external.source, external.target, true);
errln(
"*DIFF CONVERT:"
+ "\t"
+ external.source
+ "\t⟹\t"
+ external.target
+ "\texpected\t"
+ externalResult.doubleValue()
+ "\tactual:\t"
+ computed.doubleValue()
+ "\tsdiff:\t"
+ computed.symmetricDiff(externalResult).abs().doubleValue()
+ "\txdata:\t"
+ external);
}
}
// temporary: show the items that didn't covert correctly
if (showDiagnostics) {
System.out.println();
Rational x = showDelta("pound-fahrenheit", "gram-celsius", false);
Rational y = showDelta("calorie", "joule", false);
showDelta("product\t", x.multiply(y));
showDelta("british-thermal-unit", "calorie", false);
showDelta("inch-ofhg", "pascal", false);
showDelta("millimeter-ofhg", "pascal", false);
showDelta("ofhg", "kilogram-per-square-meter-square-second", false);
showDelta("13595.1*gravity", Rational.of("9.80665*13595.1"));
showDelta(
"fahrenheit-hour-square-foot-per-british-thermal-unit-inch",
"meter-kelvin-per-watt",
true);
}
if (showDiagnostics && NistUnits.skipping.size() > 0) {
System.out.println();
for (String s : NistUnits.skipping) {
System.out.println("*SKIPPING " + s);
}
}
if (showDiagnostics && NistUnits.idChanges.size() > 0) {
System.out.println();
for (Entry<String, Collection<String>> e : NistUnits.idChanges.asMap().entrySet()) {
if (SHOW_DATA)
System.out.println(
"*CHANGES\t" + e.getKey() + "\t" + Joiner.on('\t').join(e.getValue()));
}
}
if (showDiagnostics && cantConvert.size() > 0) {
System.out.println();
for (ExternalUnitConversionData e : cantConvert) {
System.out.println("*CANT CONVERT-" + e);
}
}
Output<String> baseUnit = new Output<>();
for (ExternalUnitConversionData s : couldAdd) {
String target = s.target;
Rational endFactor = s.info.factor;
String mark = "";
TargetInfo baseUnit2 = NistUnits.derivedUnitToConversion.get(s.target);
if (baseUnit2 != null) {
target = baseUnit2.target;
endFactor = baseUnit2.unitInfo.factor;
mark = "¹";
} else {
ConversionInfo conversionInfo = converter.getUnitInfo(s.target, baseUnit);
if (conversionInfo != null && !s.target.equals(baseUnit.value)) {
target = baseUnit.value;
endFactor = conversionInfo.convert(s.info.factor);
mark = "²";
}
}
// if (SHOW_DATA)
// System.out.println(
// "Could add 10^X conversion from a"
// + "\t"
// + s.source
// + "\tto"
// + mark
// + "\t"
// + endFactor.toString(FormatStyle.simple)
// + "\t"
// + target);
}
warnln("Use GenerateNewUnits.java to show units we could add from NIST.");
}
private Rational showDelta(String firstUnit, String secondUnit, boolean showYourWork) {
Rational x = converter.convert(Rational.ONE, firstUnit, secondUnit, showYourWork);
return showDelta(firstUnit + "\t" + secondUnit, x);
}
private Rational showDelta(final String title, Rational rational) {
System.out.print("*CONST\t" + title);
System.out.print("\t" + rational.toString(FormatStyle.formatted));
System.out.println("\t" + rational.doubleValue());
return rational;
}
public void TestRepeating() {
Set<Rational> seen = new HashSet<>();
String[][] tests = {
{"0/0", "NaN"},
{"1/0", "INF"},
{"-1/0", "-INF"},
{"0/1", "0"},
{"1/1", "1"},
{"1/2", "0.5"},
{"1/3", "0.˙3"},
{"1/4", "0.25"},
{"1/5", "0.2"},
{"1/6", "0.1˙6"},
{"1/7", "0.˙142857"},
{"1/8", "0.125"},
{"1/9", "0.˙1"},
{"1/10", "0.1"},
{"1/11", "0.˙09"},
{"1/12", "0.08˙3"},
{"1/13", "0.˙076923"},
{"1/14", "0.0˙714285"},
{"1/15", "0.0˙6"},
{"1/16", "0.0625"},
};
for (String[] test : tests) {
Rational source = Rational.of(test[0]);
seen.add(source);
String expected = test[1];
String actual = source.toString(FormatStyle.repeating);
assertEquals(test[0], expected, actual);
Rational roundtrip = Rational.of(expected);
assertEquals(expected, source, roundtrip);
}
for (int i = -50; i < 200; ++i) {
for (int j = 0; j < 50; ++j) {
checkFormat(Rational.of(i, j), seen);
}
}
for (Entry<String, TargetInfo> unitAndInfo :
converter.getInternalConversionData().entrySet()) {
final TargetInfo targetInfo2 = unitAndInfo.getValue();
ConversionInfo targetInfo = targetInfo2.unitInfo;
checkFormat(targetInfo.factor, seen);
if (SHOW_DATA) {
String rFormat = targetInfo.factor.toString(FormatStyle.repeating);
String sFormat = targetInfo.factor.toString(FormatStyle.formatted);
if (!rFormat.equals(sFormat)) {
System.out.println(
"\t\t"
+ unitAndInfo.getKey()
+ "\t"
+ targetInfo2.target
+ "\t"
+ sFormat
+ "\t"
+ rFormat
+ "\t"
+ targetInfo.factor.doubleValue());
}
}
}
}
private void checkFormat(Rational source, Set<Rational> seen) {
if (seen.contains(source)) {
return;
}
seen.add(source);
String formatted = source.toString(FormatStyle.repeating);
Rational roundtrip = Rational.of(formatted);
assertEquals("roundtrip " + formatted, source, roundtrip);
}
/** Verify that the items in the validity files match those in the units.xml files */
public void TestValidityAgainstUnitFile() {
Set<String> simpleUnits = converter.getSimpleUnits();
final SetView<String> simpleUnitsRemoveAllValidity =
Sets.difference(simpleUnits, VALID_SHORT_UNITS);
if (!assertEquals(
"Simple Units removeAll Validity",
Collections.emptySet(),
simpleUnitsRemoveAllValidity)) {
for (String s : simpleUnitsRemoveAllValidity) {
System.out.println(s);
}
}
// aliased units
Map<String, R2<List<String>, String>> aliasedUnits = SDI.getLocaleAliasInfo().get("unit");
// TODO adjust
// final SetView<String> aliasedRemoveAllDeprecated =
// Sets.difference(aliasedUnits.keySet(), DEPRECATED_SHORT_UNITS);
// if (!assertEquals("aliased Units removeAll deprecated", Collections.emptySet(),
// aliasedRemoveAllDeprecated)) {
// for (String s : aliasedRemoveAllDeprecated) {
// System.out.println(converter.getLongId(s));
// }
// }
assertEquals(
"deprecated removeAll aliased Units",
Collections.emptySet(),
Sets.difference(DEPRECATED_SHORT_UNITS, aliasedUnits.keySet()));
}
/** Check that units to be translated are as expected. */
public void testDistinguishedSetsOfUnits() {
Set<String> comparatorUnitIds = new LinkedHashSet<>(DtdData.getUnitOrder().getOrder());
Set<String> validLongUnitIds = VALID_REGULAR_UNITS;
Set<String> validAndDeprecatedLongUnitIds =
ImmutableSet.<String>builder()
.addAll(VALID_REGULAR_UNITS)
.addAll(DEPRECATED_REGULAR_UNITS)
.build();
final BiMap<String, String> shortToLong = Units.LONG_TO_SHORT.inverse();
assertSuperset(
"converter short-long",
"units short-long",
converter.SHORT_TO_LONG_ID.entrySet(),
shortToLong.entrySet());
assertSuperset(
"units short-long",
"converter short-long",
shortToLong.entrySet(),
converter.SHORT_TO_LONG_ID.entrySet());
Set<String> errors = new LinkedHashSet<>();
Set<String> unitsConvertibleLongIds =
converter.canConvert().stream()
.map(
x -> {
String result = shortToLong.get(x);
if (result == null) {
errors.add("No short form of " + x);
}
return result;
})
.collect(Collectors.toSet());
assertEquals("", Collections.emptySet(), errors);
Set<String> simpleConvertibleLongIds =
converter.canConvert().stream()
.filter(x -> converter.isSimple(x))
.map((String x) -> Units.LONG_TO_SHORT.inverse().get(x))
.collect(Collectors.toSet());
CLDRFile root = CLDR_CONFIG.getCldrFactory().make("root", true);
ImmutableSet<String> unitLongIdsRoot = ImmutableSet.copyOf(getUnits(root, new TreeSet<>()));
ImmutableSet<String> unitLongIdsEnglish =
ImmutableSet.copyOf(getUnits(CLDR_CONFIG.getEnglish(), new TreeSet<>()));
final Set<String> longUntranslatedUnitIds =
converter.getLongIds(UnitConverter.UNTRANSLATED_UNIT_NAMES);
ImmutableSet<String> onlyEnglish = ImmutableSet.of("pressure-gasoline-energy-density");
assertSameCollections(
"root unit IDs",
"English",
unitLongIdsRoot,
Sets.difference(
Sets.difference(unitLongIdsEnglish, longUntranslatedUnitIds), onlyEnglish));
final Set<String> validRootUnitIdsMinusOddballs = unitLongIdsRoot;
final Set<String> validLongUnitIdsMinusOddballs =
minus(validLongUnitIds, longUntranslatedUnitIds);
assertSuperset(
"valid regular",
"root unit IDs",
validLongUnitIdsMinusOddballs,
validRootUnitIdsMinusOddballs);
assertSameCollections(
"comparatorUnitIds (DtdData)",
"valid regular&deprecated",
comparatorUnitIds,
validAndDeprecatedLongUnitIds);
assertSuperset(
"valid regular", "specials", validLongUnitIds, GrammarInfo.getUnitsToAddGrammar());
assertSuperset(
"root unit IDs", "specials", unitLongIdsRoot, GrammarInfo.getUnitsToAddGrammar());
// assertSuperset("long convertible units", "valid regular", unitsConvertibleLongIds,
// validLongUnitIds);
Output<String> baseUnit = new Output<>();
for (String longUnit : validLongUnitIds) {
String shortUnit = Units.getShort(longUnit);
if (NOT_CONVERTABLE.contains(shortUnit)) {
continue;
}
ConversionInfo conversionInfo = converter.parseUnitId(shortUnit, baseUnit, false);
if (!assertNotNull("Can convert " + longUnit, conversionInfo)) {
converter.getUnitInfo(shortUnit, baseUnit);
int debug = 0;
}
}
assertSuperset(
"valid regular",
"simple convertible units",
validLongUnitIds,
simpleConvertibleLongIds);
SupplementalDataInfo.getInstance().getUnitConverter();
}
public void assertSameCollections(
String title1, String title2, Collection<String> c1, Collection<String> c2) {
assertSuperset(title1, title2, c1, c2);
assertSuperset(title2, title1, c2, c1);
}
public <V> void assertSuperset(
String title1, String title2, Collection<V> c1, Collection<V> c2) {
if (!assertEquals(title1 + " ⊇ " + title2, Collections.emptySet(), minus(c2, c1))) {
int debug = 0;
}
}
public <V> Set<V> minus(Collection<V> a, Collection<V> b) {
Set<V> result = new LinkedHashSet<>(a);
result.removeAll(b);
return result;
}
public <V> Set<V> minus(Collection<V> a, V... b) {
Set<V> result = new LinkedHashSet<>(a);
result.removeAll(Arrays.asList(b));
return result;
}
public Set<String> getUnits(CLDRFile root, Set<String> unitLongIds) {
for (String path : root) {
XPathParts parts = XPathParts.getFrozenInstance(path);
int item = parts.findElement("unit");
if (item == -1) {
continue;
}
String type = parts.getAttributeValue(item, "type");
unitLongIds.add(type);
// "//ldml/units/unitLength[@type=\"long\"]/unit[@type=\"" + unit + "\"]/gender"
}
return unitLongIds;
}
static final Pattern NORM_SPACES = Pattern.compile("[ \u00A0\u200E]");
public void TestGender() {
Output<String> source = new Output<>();
Multimap<UnitPathType, String> partsUsed = TreeMultimap.create();
Factory factory = CLDR_CONFIG.getFullCldrFactory();
Set<String> available = factory.getAvailable();
int bad = 0;
for (String locale : SDI.hasGrammarInfo()) {
// skip ones without gender info
GrammarInfo gi = SDI.getGrammarInfo("fr");
Collection<String> genderInfo =
gi.get(
GrammaticalTarget.nominal,
GrammaticalFeature.grammaticalGender,
GrammaticalScope.general);
if (genderInfo.isEmpty()) {
continue;
}
if (CLDRConfig.SKIP_SEED && !available.contains(locale)) {
continue;
}
// check others
CLDRFile resolvedFile = factory.make(locale, true);
for (Entry<String, String> entry : converter.SHORT_TO_LONG_ID.entrySet()) {
final String shortUnitId = entry.getKey();
final String longUnitId = entry.getValue();
final UnitId unitId = converter.createUnitId(shortUnitId);
partsUsed.clear();
String rawGender =
UnitPathType.gender.getTrans(
resolvedFile, "long", shortUnitId, null, null, null, partsUsed);
if (rawGender != null) {
String gender = unitId.getGender(resolvedFile, source, partsUsed);
if (gender != null && !shortUnitId.equals(source.value)) {
if (!Objects.equals(rawGender, gender)) {
if (SHOW_DATA) {
printlnIfZero(bad);
System.out.println(
locale
+ ": computed gender = raw gender for\t"
+ shortUnitId
+ "\t"
+ Joiner.on("\n\t\t")
.join(partsUsed.asMap().entrySet()));
}
++bad;
}
}
}
}
}
if (bad > 0) {
warnln(
bad
+ " units x locales with incorrect computed gender. Use -DTestUnits:SHOW_DATA for details.");
}
}
public void TestFallbackNames() {
String[][] sampleUnits = {
{"fr", "square-meter", "one", "nominative", "{0} mètre carré"},
{"fr", "square-meter", "other", "nominative", "{0} mètres carrés"},
{"fr", "square-decimeter", "other", "nominative", "{0} décimètres carrés"},
{"fr", "meter-per-square-second", "one", "nominative", "{0} mètre par seconde carrée"},
{
"fr",
"meter-per-square-second",
"other",
"nominative",
"{0} mètres par seconde carrée"
},
{"de", "square-meter", "other", "nominative", "{0} Quadratmeter"},
{"de", "square-decimeter", "other", "nominative", "{0} Quadratdezimeter"}, // real fail
{"de", "per-meter", "other", "nominative", "{0} pro Meter"},
{"de", "per-square-meter", "other", "nominative", "{0} pro Quadratmeter"},
{"de", "second-per-meter", "other", "nominative", "{0} Sekunden pro Meter"},
{"de", "meter-per-second", "other", "nominative", "{0} Meter pro Sekunde"},
{
"de",
"meter-per-square-second",
"other",
"nominative",
"{0} Meter pro Quadratsekunde"
},
{
"de",
"gigasecond-per-decimeter",
"other",
"nominative",
"{0} Gigasekunden pro Dezimeter"
},
{
"de",
"decimeter-per-gigasecond",
"other",
"nominative",
"{0} Dezimeter pro Gigasekunde"
}, // real fail
{
"de",
"gigasecond-milligram-per-centimeter-decisecond",
"other",
"nominative",
"{0} Milligramm⋅Gigasekunden pro Zentimeter⋅Dezisekunde"
},
{
"de",
"milligram-per-centimeter-decisecond",
"other",
"nominative",
"{0} Milligramm pro Zentimeter⋅Dezisekunde"
},
{
"de",
"per-centimeter-decisecond",
"other",
"nominative",
"{0} pro Zentimeter⋅Dezisekunde"
},
{
"de",
"gigasecond-milligram-per-centimeter",
"other",
"nominative",
"{0} Milligramm⋅Gigasekunden pro Zentimeter"
},
{"de", "gigasecond-milligram", "other", "nominative", "{0} Milligramm⋅Gigasekunden"},
{"de", "gigasecond-gram", "other", "nominative", "{0} Gramm⋅Gigasekunden"},
{"de", "gigasecond-kilogram", "other", "nominative", "{0} Kilogramm⋅Gigasekunden"},
{"de", "gigasecond-megagram", "other", "nominative", "{0} Megagramm⋅Gigasekunden"},
{
"de",
"dessert-spoon-imperial-per-dessert-spoon-imperial",
"one",
"nominative",
"{0} Imp. Dessertlöffel pro Imp. Dessertlöffel"
},
{
"de",
"dessert-spoon-imperial-per-dessert-spoon-imperial",
"one",
"accusative",
"{0} Imp. Dessertlöffel pro Imp. Dessertlöffel"
},
{
"de",
"dessert-spoon-imperial-per-dessert-spoon-imperial",
"other",
"dative",
"{0} Imp. Dessertlöffeln pro Imp. Dessertlöffel"
},
{
"de",
"dessert-spoon-imperial-per-dessert-spoon-imperial",
"one",
"genitive",
"{0} Imp. Dessertlöffels pro Imp. Dessertlöffel"
},
// TODO: pick names (eg in Polish) that show differences in case.
// {"de", "foebar-foobar-per-fiebar-faebar", "other", "genitive", null},
};
ImmutableMap<String, String> frOverrides =
ImmutableMap.<String, String>builder() // insufficient data in French as yet
.put(
"//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1[@count=\"one\"]",
"{0} carré") //
.put(
"//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1[@count=\"other\"]",
"{0} carrés") //
.put(
"//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1[@count=\"one\"][@gender=\"feminine\"]",
"{0} carrée") //
.put(
"//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1[@count=\"other\"][@gender=\"feminine\"]",
"{0} carrées") //
.build();
Multimap<UnitPathType, String> partsUsed = TreeMultimap.create();
int count = 0;
for (String[] row : sampleUnits) {
++count;
final String locale = row[0];
CLDRFile resolvedFileRaw = CLDR_CONFIG.getCLDRFile(locale, true);
LocaleStringProvider resolvedFile;
switch (locale) {
case "fr":
resolvedFile = resolvedFileRaw.makeOverridingStringProvider(frOverrides);
break;
default:
resolvedFile = resolvedFileRaw;
break;
}
String shortUnitId = row[1];
String pluralCategory = row[2];
String caseVariant = row[3];
String expectedName = row[4];
if (shortUnitId.equals("gigasecond-milligram")) {
int debug = 0;
}
final UnitId unitId = converter.createUnitId(shortUnitId);
final String actual =
unitId.toString(
resolvedFile, "long", pluralCategory, caseVariant, partsUsed, false);
assertEquals(
count
+ ") "
+ Arrays.asList(row).toString()
+ "\n\t"
+ Joiner.on("\n\t").join(partsUsed.asMap().entrySet()),
fixSpaces(expectedName),
fixSpaces(actual));
}
}
public void TestFileFallbackNames() {
Multimap<UnitPathType, String> partsUsed = TreeMultimap.create();
// first gather all the examples
Set<String> skippedUnits = new LinkedHashSet<>();
Set<String> testSet = StandardCodes.make().getLocaleCoverageLocales(Organization.cldr);
Counter<String> localeToErrorCount = new Counter<>();
main:
for (String localeId : testSet) {
if (localeId.contains("_")) {
continue; // skip to make test shorter
}
CLDRFile resolvedFile = CLDR_CONFIG.getCLDRFile(localeId, true);
PluralInfo pluralInfo = CLDR_CONFIG.getSupplementalDataInfo().getPlurals(localeId);
PluralRules pluralRules = pluralInfo.getPluralRules();
GrammarInfo grammarInfo =
CLDR_CONFIG.getSupplementalDataInfo().getGrammarInfo(localeId);
Collection<String> caseVariants =
grammarInfo == null
? null
: grammarInfo.get(
GrammaticalTarget.nominal,
GrammaticalFeature.grammaticalCase,
GrammaticalScope.units);
if (caseVariants == null || caseVariants.isEmpty()) {
caseVariants = Collections.singleton("nominative");
}
for (Entry<String, String> entry : converter.SHORT_TO_LONG_ID.entrySet()) {
final String shortUnitId = entry.getKey();
if (converter.getComplexity(shortUnitId) == UnitComplexity.simple) {
continue;
}
if (UnitConverter.HACK_SKIP_UNIT_NAMES.contains(shortUnitId)) {
skippedUnits.add(shortUnitId);
continue;
}
final String longUnitId = entry.getValue();
final UnitId unitId = converter.createUnitId(shortUnitId);
for (String width : Arrays.asList("long")) { // , "short", "narrow"
for (String pluralCategory : pluralRules.getKeywords()) {
for (String caseVariant : caseVariants) {
String composedName;
try {
composedName =
unitId.toString(
resolvedFile,
width,
pluralCategory,
caseVariant,
partsUsed,
false);
} catch (Exception e) {
composedName = "ERROR:" + e.getMessage();
}
if (composedName != null
&& (composedName.contains("′")
|| composedName.contains("″"))) { // skip special cases
continue;
}
partsUsed.clear();
String transName =
UnitPathType.unit.getTrans(
resolvedFile,
width,
shortUnitId,
pluralCategory,
caseVariant,
null,
isVerbose() ? partsUsed : null);
// HACK to fix different spaces around placeholder
if (!Objects.equals(fixSpaces(transName), fixSpaces(composedName))) {
logln(
"\t"
+ localeId
+ "\t"
+ shortUnitId
+ "\t"
+ width
+ "\t"
+ pluralCategory
+ "\t"
+ caseVariant
+ "\texpected ≠ fallback\t«"
+ transName
+ "»\t≠\t«"
+ composedName
+ "»"
+ partsUsed);
localeToErrorCount.add(localeId, 1);
if (!SHOW_COMPOSE && localeToErrorCount.getTotal() > 50) {
break main;
}
}
}
}
}
}
}
if (!localeToErrorCount.isEmpty()) {
warnln(
"composed name ≠ translated name: ≥"
+ localeToErrorCount.getTotal()
+ ". Use -DTestUnits:SHOW_COMPOSE to see summary");
if (SHOW_COMPOSE) {
System.out.println();
for (R2<Long, String> entry :
localeToErrorCount.getEntrySetSortedByCount(false, null)) {
System.out.println(
"composed name ≠ translated name: "
+ entry.get0()
+ "\t"
+ entry.get1());
}
}
}
if (!skippedUnits.isEmpty()) {
warnln("Skipped unsupported units: " + skippedUnits);
}
}
public String fixSpaces(String transName) {
return transName == null ? null : NORM_SPACES.matcher(transName).replaceAll(" ");
}
public void TestCheckUnits() {
CheckUnits checkUnits = new CheckUnits();
PathHeader.Factory phf = PathHeader.getFactory();
for (String locale : Arrays.asList("en", "fr", "de", "pl", "el")) {
CLDRFile cldrFile = CLDR_CONFIG.getCldrFactory().make(locale, true);
Options options = new Options();
List<CheckStatus> possibleErrors = new ArrayList<>();
checkUnits.setCldrFileToCheck(cldrFile, options, possibleErrors);
for (String path :
StreamSupport.stream(cldrFile.spliterator(), false)
.sorted()
.collect(Collectors.toList())) {
UnitPathType pathType =
UnitPathType.getPathType(XPathParts.getFrozenInstance(path));
if (pathType == null || pathType == UnitPathType.unit) {
continue;
}
String value = cldrFile.getStringValue(path);
checkUnits.check(path, path, value, options, possibleErrors);
if (!possibleErrors.isEmpty()) {
PathHeader ph = phf.fromPath(path);
logln(locale + "\t" + ph.getCode() + "\t" + possibleErrors.toString());
}
}
}
}
public void TestDerivedCase() {
// needs further work
if (logKnownIssue("CLDR-16395", "finish this as part of unit derivation work")) {
return;
}
for (String locale : Arrays.asList("pl", "ru")) {
CLDRFile cldrFile = CLDR_CONFIG.getCldrFactory().make(locale, true);
GrammarInfo gi = SDI.getGrammarInfo(locale);
Collection<String> rawCases =
gi.get(
GrammaticalTarget.nominal,
GrammaticalFeature.grammaticalCase,
GrammaticalScope.units);
PluralInfo plurals =
SupplementalDataInfo.getInstance().getPlurals(PluralType.cardinal, locale);
Collection<Count> adjustedPlurals = plurals.getCounts();
Output<String> sourceCase = new Output<>();
Output<String> sourcePlural = new Output<>();
M4<String, String, String, Boolean> myInfo =
ChainedMap.of(
new TreeMap<String, Object>(),
new TreeMap<String, Object>(),
new TreeMap<String, Object>(),
Boolean.class);
int count = 0;
for (String longUnit : GrammarInfo.getUnitsToAddGrammar()) {
final String shortUnit = converter.getShortId(longUnit);
String gender =
UnitPathType.gender.getTrans(
cldrFile, "long", shortUnit, null, null, null, null);
for (String desiredCase : rawCases) {
// gather some general information
for (Count plural : adjustedPlurals) {
String value =
UnitPathType.unit.getTrans(
cldrFile,
"long",
shortUnit,
plural.toString(),
desiredCase,
gender,
null);
myInfo.put(
gender,
shortUnit + "\t" + value,
plural.toString() + "+" + desiredCase,
true);
}
// do actual test
if (desiredCase.contentEquals("nominative")) {
continue;
}
for (String desiredPlural : Arrays.asList("few", "other")) {
String value =
UnitPathType.unit.getTrans(
cldrFile,
"long",
shortUnit,
desiredPlural,
desiredCase,
gender,
null);
gi.getSourceCaseAndPlural(
locale,
gender,
value,
desiredCase,
desiredPlural,
sourceCase,
sourcePlural);
String sourceValue =
UnitPathType.unit.getTrans(
cldrFile,
"long",
shortUnit,
sourcePlural.value,
sourceCase.value,
gender,
null);
assertEquals(
count++
+ ") "
+ locale
+ ",\tshort unit/gender: "
+ shortUnit
+ " / "
+ gender
+ ",\tdesired case/plural: "
+ desiredCase
+ " / "
+ desiredPlural
+ ",\tsource case/plural: "
+ sourceCase
+ " / "
+ sourcePlural,
value,
sourceValue);
}
}
}
for (Entry<String, Map<String, Map<String, Boolean>>> m : myInfo) {
for (Entry<String, Map<String, Boolean>> t : m.getValue().entrySet()) {
System.out.println(
m.getKey() + "\t" + t.getKey() + "\t" + t.getValue().keySet());
}
}
}
}
public void TestGenderOfCompounds() {
Set<String> skipUnits =
ImmutableSet.of(
"kilocalorie",
"kilopascal",
"terabyte",
"gigabyte",
"kilobyte",
"gigabit",
"kilobit",
"megabit",
"megabyte",
"terabit");
final ImmutableSet<String> keyValues =
ImmutableSet.of("length", "mass", "duration", "power");
int noGendersForLocales = 0;
int localesWithNoGenders = 0;
int localesWithSomeMissingGenders = 0;
for (String localeID : GrammarInfo.getGrammarLocales()) {
GrammarInfo grammarInfo = SDI.getGrammarInfo(localeID);
if (grammarInfo == null) {
logln("No grammar info for: " + localeID);
continue;
}
UnitConverter converter = SDI.getUnitConverter();
Collection<String> genderInfo =
grammarInfo.get(
GrammaticalTarget.nominal,
GrammaticalFeature.grammaticalGender,
GrammaticalScope.units);
if (genderInfo.isEmpty()) {
continue;
}
CLDRFile cldrFile = info.getCldrFactory().make(localeID, true);
Map<String, String> shortUnitToGender = new TreeMap<>();
Output<String> source = new Output<>();
Multimap<UnitPathType, String> partsUsed = LinkedHashMultimap.create();
Set<String> units = new HashSet<>();
M4<String, String, String, Boolean> quantityToGenderToUnits =
ChainedMap.of(
new TreeMap<String, Object>(),
new TreeMap<String, Object>(),
new TreeMap<String, Object>(),
Boolean.class);
M4<String, String, String, Boolean> genderToQuantityToUnits =
ChainedMap.of(
new TreeMap<String, Object>(),
new TreeMap<String, Object>(),
new TreeMap<String, Object>(),
Boolean.class);
for (String path : cldrFile) {
if (!path.startsWith("//ldml/units/unitLength[@type=\"long\"]/unit[@type=")) {
continue;
}
XPathParts parts = XPathParts.getFrozenInstance(path);
final String shortId = converter.getShortId(parts.getAttributeValue(-2, "type"));
if (NOT_CONVERTABLE.contains(shortId)) {
continue;
}
String quantity = null;
try {
quantity = converter.getQuantityFromUnit(shortId, false);
} catch (Exception e) {
}
if (quantity == null) {
throw new IllegalArgumentException("No quantity for " + shortId);
}
// ldml/units/unitLength[@type="long"]/unit[@type="duration-year"]/gender
String gender = null;
if (parts.size() == 5 && parts.getElement(-1).equals("gender")) {
gender = cldrFile.getStringValue(path);
if (true) {
quantityToGenderToUnits.put(quantity, gender, shortId, true);
genderToQuantityToUnits.put(quantity, gender, shortId, true);
}
} else {
if (units.contains(shortId)) {
continue;
}
units.add(shortId);
}
UnitId unitId = converter.createUnitId(shortId);
String constructedGender = unitId.getGender(cldrFile, source, partsUsed);
boolean multiUnit =
unitId.denUnitsToPowers.size() + unitId.denUnitsToPowers.size() > 1;
if (gender == null && (constructedGender == null || !multiUnit)) {
continue;
}
final boolean areEqual = Objects.equals(gender, constructedGender);
if (SHOW_COMPOSE) {
final String printInfo =
localeID
+ "\t"
+ unitId
+ "\t"
+ gender
+ "\t"
+ multiUnit
+ "\t"
+ quantity
+ "\t"
+ constructedGender
+ "\t"
+ areEqual;
System.out.println(printInfo);
}
if (gender != null && !areEqual && !skipUnits.contains(shortId)) {
unitId.getGender(cldrFile, source, partsUsed);
shortUnitToGender.put(
shortId,
unitId
+ "\t actual gender: "
+ gender
+ "\t constructed gender:"
+ constructedGender);
}
}
if (quantityToGenderToUnits.keySet().isEmpty()) {
if (SHOW_COMPOSE) {
printlnIfZero(noGendersForLocales);
System.out.println("No genders for\t" + localeID);
}
localesWithNoGenders++;
continue;
}
for (Entry<String, String> entry : shortUnitToGender.entrySet()) {
if (SHOW_COMPOSE) {
printlnIfZero(noGendersForLocales);
System.out.println(localeID + "\t" + entry);
}
noGendersForLocales++;
}
Set<String> missing = new LinkedHashSet<>(genderInfo);
for (String quantity : keyValues) {
M3<String, String, Boolean> genderToUnits = quantityToGenderToUnits.get(quantity);
showData(localeID, null, quantity, genderToUnits);
missing.removeAll(genderToUnits.keySet());
}
for (String quantity : quantityToGenderToUnits.keySet()) {
M3<String, String, Boolean> genderToUnits = quantityToGenderToUnits.get(quantity);
showData(localeID, missing, quantity, genderToUnits);
}
for (String gender : missing) {
if (SHOW_DATA) {
printlnIfZero(noGendersForLocales);
System.out.println(
"Missing values: " + localeID + "\t" + "?" + "\t" + gender + "\t?");
}
noGendersForLocales++;
}
}
if (noGendersForLocales > 0) {
warnln(
noGendersForLocales
+ " units x locales with missing gender. Use -DTestUnits:SHOW_DATA for info, -DTestUnits:SHOW_COMPOSE for compositions");
}
}
public void printlnIfZero(int noGendersForLocales) {
if (noGendersForLocales == 0) {
System.out.println();
}
}
public void showData(
String localeID,
Set<String> genderFilter,
String quantity,
final M3<String, String, Boolean> genderToUnits) {
for (Entry<String, Map<String, Boolean>> entry2 : genderToUnits) {
String gender = entry2.getKey();
if (genderFilter != null) {
if (!genderFilter.contains(gender)) {
continue;
}
genderFilter.remove(gender);
}
for (String unit : entry2.getValue().keySet()) {
logln(localeID + "\t" + quantity + "\t" + gender + "\t" + unit);
}
}
}
static final boolean DEBUG_DERIVATION = false;
public void testDerivation() {
int count = 0;
for (String locale : SDI.hasGrammarDerivation()) {
GrammarDerivation gd = SDI.getGrammarDerivation(locale);
if (DEBUG_DERIVATION) System.out.println(locale + " => " + gd);
++count;
}
assertNotEquals("hasGrammarDerivation", 0, count);
}
static final boolean DEBUG_ORDER = false;
public void TestUnitOrder() {
if (DEBUG_ORDER) {
System.out.println();
for (Entry<String, Collection<Continuation>> entry :
converter.getContinuations().asMap().entrySet()) {
System.out.println(entry);
}
}
for (Entry<String, String> entry : converter.getBaseUnitToQuantity().entrySet()) {
checkNormalization("base-quantity, " + entry.getValue(), entry.getKey());
}
// check root list
// crucial that this is stable!!
Set<String> shortUnitsFound =
checkCldrFileUnits("root unit", CLDRConfig.getInstance().getRoot());
final Set<String> shortValidRegularUnits = VALID_SHORT_UNITS;
assertEquals(
"root units - regular units",
Collections.emptySet(),
Sets.difference(shortUnitsFound, shortValidRegularUnits));
// TODO — we don't want to just add to the exception list.
// assertEquals(
// "regular units - special_untranslated - root units",
// Collections.emptySet(),
// Sets.difference(
// Sets.difference(
// shortValidRegularUnits,
// UnitConverter.UNTRANSLATED_UNIT_NAMES),
// shortUnitsFound));
// check English also
checkCldrFileUnits("en unit", CLDRConfig.getInstance().getEnglish());
for (String unit : converter.canConvert()) {
checkNormalization("convertable", unit);
String baseUnitId = converter.getBaseUnit(unit);
checkNormalization("convertable base", baseUnitId);
}
checkNormalization("test case", "foot-acre", "acre-foot");
checkNormalization("test case", "meter-newton", "newton-meter");
checkNormalization("test case", "newton-meter");
checkNormalization("test case", "acre-foot");
String stdAcre = converter.getStandardUnit("acre");
UnitOrdering unitOrdering = new UnitOrdering();
List<String> simpleBaseUnits = new ArrayList<>();
for (ExternalUnitConversionData data : NistUnits.externalConversionData) {
// unitOrdering.add(data.source);
final String source = data.source;
final String target = data.target;
unitOrdering.add(target);
checkNormalization("nist core, " + source, target);
}
for (Entry<String, TargetInfo> data : NistUnits.derivedUnitToConversion.entrySet()) {
if (DEBUG_ORDER) {
System.out.println(data);
}
final String target = data.getValue().target;
unitOrdering.add(target);
simpleBaseUnits.add(data.getKey());
checkNormalization("nist derived", target);
}
if (DEBUG_ORDER) {
System.out.println("Pass 1\n" + unitOrdering.orderingData);
}
for (String baseUnit : converter.getBaseUnitToQuantity().keySet()) {
unitOrdering.add(baseUnit);
String status = converter.getBaseUnitToStatus().get(baseUnit);
if ("simple".equals(status)) {
simpleBaseUnits.add(baseUnit);
}
}
if (DEBUG_ORDER) {
System.out.println("Pass 2\n" + unitOrdering.orderingData);
}
if (DEBUG_ORDER)
System.out.println(
"Extracted data\n"
+ Joiner.on('\n').join(unitOrdering.orderingData.asMap().entrySet()));
if (DEBUG_ORDER) System.out.println("Building data");
// check the builder first
TotalOrderBuilder<String> totalOrderBuilder = new TotalOrderBuilder<>();
if (false) {
totalOrderBuilder.add("meter", "second").add("kilogram", "meter");
totalOrderBuilder.build();
totalOrderBuilder
.add("meter", "second")
.add("kilogram", "meter")
.add("second", "kilogram");
try {
totalOrderBuilder.build();
} catch (Exception e) {
errln("Problem in TotalOrderBuilder");
}
}
if (DEBUG_ORDER) System.out.println("Show ordering");
// now all the units
for (List<String> orderedUnits : unitOrdering.orderingData.asMap().keySet()) {
List<String> baseUnits = new ArrayList<>();
for (String orderedUnit : orderedUnits) {
baseUnits.add(unitOrdering.getId(orderedUnit, unitOrdering.rejects));
}
if (DEBUG_ORDER) System.out.println(orderedUnits + "\t" + baseUnits);
totalOrderBuilder.add(baseUnits);
}
for (String simpleBaseUnit : simpleBaseUnits) {
totalOrderBuilder.add(Collections.singletonList(simpleBaseUnit));
}
if (DEBUG_ORDER) System.out.println(totalOrderBuilder);
if (DEBUG_ORDER) System.out.println("Rejects: " + unitOrdering.rejects);
if (DEBUG_ORDER) System.out.println("Ordering: " + totalOrderBuilder.build());
// for (Entry<String, Collection<String>> entry :
// piecesToOccurences.asMap().entrySet()) {
// System.out.println(entry.getKey() + "\t" + entry.getValue());
// }
}
/**
* Checks the normalization of units found in the file, and returns the set of shortUnitIds
* found in the file
*/
public Set<String> checkCldrFileUnits(String title, final CLDRFile cldrFile) {
Set<String> shortUnitsFound = new TreeSet<>();
for (String path : cldrFile) {
if (!path.startsWith("//ldml/units/unitLength")) {
continue;
}
XPathParts parts = XPathParts.getFrozenInstance(path);
String longUnitId = parts.findAttributeValue("unit", "type");
if (longUnitId == null) {
continue;
}
String shortUnitId = converter.getShortId(longUnitId);
shortUnitsFound.add(shortUnitId);
checkNormalization(title, shortUnitId);
}
return ImmutableSet.copyOf(shortUnitsFound);
}
public void checkNormalization(String title, String source, String expected) {
String oldExpected = normalizationCache.get(source);
if (oldExpected != null) {
if (!oldExpected.equals(expected)) {
assertEquals(
title + ", consistent expected results for " + source,
oldExpected,
expected);
}
return;
}
normalizationCache.put(source, expected);
UnitId unitId = converter.createUnitId(source);
assertEquals(title + ", unit order", expected, unitId.toString());
}
public void checkNormalization(String title, String source) {
checkNormalization(title, source, source);
}
static class UnitOrdering {
boolean SKIP_POWERS = true;
Set<String> SKIP_UNITS =
ImmutableSet.of(
"kilogram-per-pascal-second-square-meter",
"kilogram-per-pascal-second-meter");
final Set<String> SUFFIXES =
ImmutableSet.of(
"0c",
"15c",
"20c",
"23c",
"32f",
"365",
"392f",
"39f",
"4c",
"59f",
"60f",
"survey",
"assay",
"imperial",
"long",
"of",
"capacitance",
"inductance",
"current",
"electric",
"potential",
"electric",
"inductance,",
"resistance",
"water",
"troy",
"tnt",
"sidereal",
"unitth",
"unitit",
"mean",
"nutrition",
"tropical",
"pole",
"boiler",
"mil",
"force",
"printer",
"refrigeration",
"register",
"technical",
"thermal",
"metric",
"dry");
final Set<String> POWERS = ImmutableSet.of("square", "cubic", "pow4");
// mil-inch, perm-inch
Set<String> seen = new HashSet<>();
Multimap<String, String> piecesToOccurences = TreeMultimap.create();
Multimap<String, Continuation> continuations = converter.getContinuations();
TreeMultimap<List<String>, String> orderingData =
TreeMultimap.create(
Comparators.lexicographical(Ordering.natural()), Ordering.natural());
TreeSet<String> rejects = new TreeSet<>();
void add(String unitId) {
if (!unitId.contains("-") || !seen.add(unitId) || SKIP_UNITS.contains(unitId)) {
return;
}
if (unitId.contains("square-meter-kilogram")) {
int debug = 0;
}
List<String> pieces = new ArrayList<>();
ArrayList<String> orderedNumerator = new ArrayList<>();
ArrayList<String> orderedDenominator = new ArrayList<>();
ArrayList<String> current = orderedNumerator;
for (UnitIterator it = Continuation.split(unitId, continuations).iterator();
it.hasNext(); ) {
String unit = it.next();
if (unit.equals("per")) {
if (current == orderedDenominator) {
throw new IllegalArgumentException();
}
handleOrdering(current, unitId);
current = orderedDenominator;
continue;
}
if (POWERS.contains(unit)) {
if (SKIP_POWERS) {
continue;
}
String nextUnit = it.next();
nextUnit = UnitConverter.stripPrefix(nextUnit, null);
unit += "-" + nextUnit; // should never overrun
} else {
unit = UnitConverter.stripPrefix(unit, null);
}
String peek = it.peek();
while (peek != null && SUFFIXES.contains(peek)) {
unit += "-" + peek;
it.next();
peek = it.peek();
}
current.add(unit);
pieces.add(unit);
piecesToOccurences.put(unit, unitId);
}
handleOrdering(current, unitId);
// System.out.println(pieces + "\t=>\t" + data.target);
}
Map<String, String> EXTRA_BASES =
ImmutableMap.<String, String>builder()
.put("british-thermal-unitit", "joule")
.put("british-thermal-unitth", "joule")
.put("centimeter", "meter")
.put("circular-mil", "meter")
// .put("dry", "???")
.put("dyne", "newton")
.put("foot-survey", "meter")
.put("inch-0c", "meter")
.put("inch-23c", "meter")
.put("kilogram-force", "newton")
.put("kilowatt", "watt")
// .put("mil", "???")
.put("millimeter", "meter")
.put("ofhg-0c", "ofhg")
.put("ofhg-32f", "ofhg")
.put("ofhg-60f", "ofhg")
.put("ounce-force", "newton")
.put("perm", "kilogram-per-second-per-square-meter-per-pascal")
.put("poundal", "newton")
.put("rankine", "celcius")
.build();
public String getId(String orderedUnit, Set<String> rejects) {
String result = converter.getStandardUnit(orderedUnit);
if (result == null) {
result = EXTRA_BASES.get(orderedUnit);
if (result == null) {
rejects.add(orderedUnit);
return "???";
}
}
return result;
}
private void handleOrdering(ArrayList<String> current, String source) {
if (current.size() < 2) {
return;
}
orderingData.put(current, source);
}
}
public void TestElectricConsumption() {
String inputUnit = "kilowatt-hour-per-100-kilometer";
String outputUnit = "kilogram-meter-per-square-second";
Rational result = converter.convert(Rational.ONE, inputUnit, outputUnit, DEBUG);
assertEquals("kWh-per-100k", Rational.of(36), result);
}
public void TestEnglishDisplayNames() {
CLDRFile en = CLDRConfig.getInstance().getEnglish();
ImmutableSet<String> unitSkips = ImmutableSet.of("temperature-generic", "graphics-em");
for (String path : en) {
if (path.startsWith("//ldml/units/unitLength[@type=\"long\"]")
&& path.endsWith("/displayName")) {
if (path.contains("coordinateUnit")) {
continue;
}
XPathParts parts = XPathParts.getFrozenInstance(path);
final String longUnitId = parts.getAttributeValue(3, "type");
if (unitSkips.contains(longUnitId)) {
continue;
}
final String width = parts.getAttributeValue(2, "type");
// ldml/units/unitLength[@type="long"]/unit[@type="duration-decade"]/displayName
String displayName = en.getStringValue(path);
// ldml/units/unitLength[@type="long"]/unit[@type="duration-decade"]/unitPattern[@count="other"]
String pluralFormPath =
path.substring(0, path.length() - "/displayName".length())
+ "/unitPattern[@count=\"other\"]";
String pluralForm = en.getStringValue(pluralFormPath);
if (pluralForm == null) {
errln("Have display name but no plural: " + pluralFormPath);
} else {
String cleaned = pluralForm.replace("{0}", "").trim();
assertEquals(
"Unit display name should correspond to plural in English "
+ width
+ ", "
+ longUnitId,
cleaned,
displayName);
}
}
}
}
enum TranslationStatus {
has_grammar_M,
has_grammar_X,
add_grammar,
skip_grammar,
skip_trans
}
/**
* Check which units are enabled for translation. If -v, then generates lines for spreadsheet
* checks.
*/
public void TestUnitsToTranslate() {
Set<String> toTranslate = GrammarInfo.getUnitsToAddGrammar();
final CLDRConfig config = CLDRConfig.getInstance();
final UnitConverter converter = config.getSupplementalDataInfo().getUnitConverter();
Map<String, TranslationStatus> shortUnitToTranslationStatus40 = new TreeMap<>();
for (String longUnit :
Validity.getInstance().getStatusToCodes(LstrType.unit).get(Status.regular)) {
String shortUnit = converter.getShortId(longUnit);
shortUnitToTranslationStatus40.put(shortUnit, TranslationStatus.skip_trans);
}
for (String path :
With.in(
config.getRoot()
.iterator("//ldml/units/unitLength[@type=\"short\"]/unit"))) {
XPathParts parts = XPathParts.getFrozenInstance(path);
String longUnit = parts.getAttributeValue(3, "type");
// Add simple units
String shortUnit = converter.getShortId(longUnit);
Set<UnitSystem> systems = converter.getSystemsEnum(shortUnit);
boolean unitsToAddGrammar = GrammarInfo.getUnitsToAddGrammar().contains(shortUnit);
TranslationStatus status =
toTranslate.contains(longUnit)
? (unitsToAddGrammar
? TranslationStatus.has_grammar_M
: TranslationStatus.has_grammar_X)
: unitsToAddGrammar
? TranslationStatus.add_grammar
: TranslationStatus.skip_grammar;
shortUnitToTranslationStatus40.put(shortUnit, status);
}
for (Entry<String, TranslationStatus> entry : shortUnitToTranslationStatus40.entrySet()) {
String shortUnit = entry.getKey();
TranslationStatus status40 = entry.getValue();
if (isVerbose())
System.out.println(
shortUnit
+ "\t"
+ converter.getQuantityFromUnit(shortUnit, false)
+ "\t"
+ converter.getSystemsEnum(shortUnit)
+ "\t"
+ (converter.isSimple(shortUnit) ? "simple" : "complex")
+ "\t"
+ status40);
}
}
static final String marker = "➗";
public void TestValidUnitIdComponents() {
for (String longUnit : VALID_REGULAR_UNITS) {
String shortUnit = SDI.getUnitConverter().getShortId(longUnit);
checkShortUnit(shortUnit);
}
}
public void TestDeprecatedUnitIdComponents() {
for (String longUnit : DEPRECATED_REGULAR_UNITS) {
String shortUnit = SDI.getUnitConverter().getShortId(longUnit);
checkShortUnit(shortUnit);
}
}
public void TestSelectedUnitIdComponents() {
checkShortUnit("curr-chf");
}
public void checkShortUnit(String shortUnit) {
List<String> parts = SPLIT_DASH.splitToList(shortUnit);
List<String> simpleUnit = new ArrayList<>();
UnitIdComponentType lastType = null;
// structure is (prefix* base* suffix*) per ((prefix* base* suffix*)
for (String part : parts) {
UnitIdComponentType type = getUnitIdComponentType(part);
switch (type) {
case prefix:
if (lastType != UnitIdComponentType.prefix && !simpleUnit.isEmpty()) {
simpleUnit.add(marker);
}
break;
case base:
if (lastType != UnitIdComponentType.prefix && !simpleUnit.isEmpty()) {
simpleUnit.add(marker);
}
break;
case suffix:
if (!(lastType == UnitIdComponentType.base
|| lastType == UnitIdComponentType.suffix)) {
if ("metric".equals(part)) { // backward compatibility for metric ton; only
// needed if deprecated ids are allowed
lastType = UnitIdComponentType.prefix;
} else {
errln(
simpleUnit
+ "/"
+ part
+ "; suffix only after base or suffix: "
+ false);
}
}
break;
// could add more conditions on these
case and:
assertNotNull(simpleUnit + "/" + part + "; not at start", lastType);
// fall through
case power:
case per:
assertNotEquals(
simpleUnit + "/" + part + "; illegal after prefix",
UnitIdComponentType.prefix,
lastType);
if (!simpleUnit.isEmpty()) {
simpleUnit.add(marker);
}
break;
}
simpleUnit.add(part + "*" + type.toShortId());
lastType = type;
}
assertTrue(
simpleUnit + ": last item must be base or suffix",
lastType == UnitIdComponentType.base || lastType == UnitIdComponentType.suffix);
logln("\t" + shortUnit + "\t" + simpleUnit.toString());
}
public UnitIdComponentType getUnitIdComponentType(String part) {
return SDI.getUnitIdComponentType(part);
}
public void TestMetricTon() {
assertTrue(
"metric-ton is deprecated", DEPRECATED_REGULAR_UNITS.contains("mass-metric-ton"));
assertEquals(
"metric-ton is deprecated",
"tonne",
SDI.getUnitConverter().fixDenormalized("metric-ton"));
assertEquals(
"to short", "metric-ton", SDI.getUnitConverter().getShortId("mass-metric-ton"));
// assertEquals("to long", "mass-metric-ton",
// SDI.getUnitConverter().getLongId("metric-ton"));
}
public void TestUnitParser() {
UnitParser up = new UnitParser();
for (String longUnit : VALID_REGULAR_UNITS) {
String shortUnit = SDI.getUnitConverter().getShortId(longUnit);
checkParse(up, shortUnit);
}
}
private List<Pair<String, UnitIdComponentType>> checkParse(UnitParser up, String shortUnit) {
up.set(shortUnit);
List<Pair<String, UnitIdComponentType>> results = new ArrayList<>();
Output<UnitIdComponentType> type = new Output<>();
while (true) {
String result = up.nextParse(type);
if (result == null) {
break;
}
results.add(new Pair<>(result, type.value));
}
logln(shortUnit + "\t" + results);
return results;
}
public void TestUnitParserSelected() {
UnitParser up = new UnitParser();
String[][] tests = {
// unit, exception, resultList
{"british-force", "Unit suffix must follow base: british ❌ force"}, // prefix-suffix
{"force", "Unit suffix must follow base: null ❌ force"}, // suffix
{
"british-and-french", "Unit prefix must be followed with base: british ❌ and"
}, // prefix-and
{"british", "Unit prefix must be followed with base: british ❌ null"}, // prefix
{"g-force-light-year", null, "[(g-force,base), (light-year,base)]"}, // suffix
};
for (String[] test : tests) {
String shortUnit = test[0];
String expectedError = test[1];
String expectedResult = test.length <= 2 ? null : test[2];
String actualError = null;
List<Pair<String, UnitIdComponentType>> actualResult = null;
try {
actualResult = checkParse(up, shortUnit);
} catch (Exception e) {
actualError = e.getMessage();
}
assertEquals(shortUnit + " exception", expectedError, actualError);
assertEquals(
shortUnit + " result",
expectedResult,
actualResult == null ? null : actualResult.toString());
}
}
public void TestUnitParserAgainstContinuations() {
UnitParser up = new UnitParser();
UnitConverter uc = SDI.getUnitConverter();
Multimap<String, Continuation> continuations = uc.getContinuations();
Output<UnitIdComponentType> type = new Output<>();
for (String shortUnit : VALID_SHORT_UNITS) {
if (shortUnit.contains("100")) {
logKnownIssue("CLDR-15929", "Code doesn't handle 100");
continue;
}
up.set(shortUnit);
UnitIterator x = UnitConverter.Continuation.split(shortUnit, continuations);
int count = 0;
while (true) {
String upSegment = up.nextParse(type);
String continuationSegment = x.hasNext() ? x.next() : null;
if (upSegment == null || continuationSegment == null) {
assertEquals(
count + ") " + shortUnit + " Same number of segments ",
continuationSegment == null,
upSegment == null);
break;
}
assertTrue(
"type is never suffix or prefix",
UnitIdComponentType.suffix != type.value
&& UnitIdComponentType.prefix != type.value);
++count;
if (!assertEquals(
count + ") " + shortUnit + " Continuation segment vs UnitParser ",
continuationSegment,
upSegment)) {
break; // stop at first difference
}
}
}
}
public static final Set<String> TRUNCATION_EXCEPTIONS =
ImmutableSet.of(
"sievert",
"gray",
"henry",
"lux",
"candela",
"candela-per-square-meter",
"candela-square-meter-per-square-meter");
/** Every subtag must be unique to 8 letters. We also check combinations with prefixes */
public void testTruncation() {
UnitConverter uc = SDI.getUnitConverter();
Multimap<String, String> truncatedToFull = TreeMultimap.create();
Set<String> unitsToTest = Sets.union(uc.baseUnits(), uc.getSimpleUnits());
for (String unit : unitsToTest) {
addTruncation(unit, truncatedToFull);
// also check for adding prefixes
Collection<UnitSystem> systems = uc.getSystemsEnum(unit);
if (systems.contains(UnitSystem.si)
|| UnitConverter.METRIC_TAKING_PREFIXES.contains(unit)) {
if (TRUNCATION_EXCEPTIONS.contains(unit)) {
continue;
}
// get without prefix
String baseUnit = removePrefixIfAny(unit);
for (String prefixPower : UnitConverter.PREFIXES.keySet()) {
addTruncation(prefixPower + baseUnit, truncatedToFull);
}
} else if (systems.contains(UnitSystem.metric)) {
logln("Skipping application of prefixes to: " + unit);
}
}
checkTruncationStatus(truncatedToFull);
}
public String removePrefixIfAny(String unit) {
for (String prefixPower : UnitConverter.PREFIXES.keySet()) {
if (unit.startsWith(prefixPower)) {
return unit.substring(prefixPower.length());
}
}
return unit;
}
static Splitter HYPHEN_SPLITTER = Splitter.on('-');
private void addTruncation(String unit, Multimap<String, String> truncatedToFull) {
for (String subcode : HYPHEN_SPLITTER.split(unit)) {
truncatedToFull.put(subcode.length() <= 8 ? subcode : subcode.substring(0, 8), subcode);
}
}
public void checkTruncationStatus(Multimap<String, String> truncatedToFull) {
for (Entry<String, Collection<String>> entry : truncatedToFull.asMap().entrySet()) {
final String truncated = entry.getKey();
final Collection<String> longForms = entry.getValue();
if (longForms.size() > 1) {
errln("Ambiguous bcp47 format: " + entry);
} else if (isVerbose()) {
if (!longForms.contains(truncated)) {
logln(entry.toString());
}
}
}
}
public void testGetRelated() {
Map<Rational, String> related2 =
converter.getRelatedExamples(
"meter", Sets.difference(UnitSystem.ALL, Set.of(UnitSystem.jpsystem)));
logln(showUnitExamples("meter", related2));
Set<String> generated = new LinkedHashSet<>();
for (String unit : converter.getSimpleUnits()) {
Map<Rational, String> related =
converter.getRelatedExamples(
unit, Sets.difference(UnitSystem.ALL, Set.of(UnitSystem.jpsystem)));
generated.addAll(related.values());
logln(showUnitExamples(unit, related));
}
logln(generated.toString());
}
public String showUnitExamples(String unit, Map<Rational, String> related) {
return "\n"
+ unit
+ "\t#"
+ converter.getSystemsEnum(unit)
+ "\n= "
+ related.entrySet().stream()
.map(
x ->
x.getKey().toString(FormatStyle.approx)
+ " "
+ x.getValue()
+ "\t#"
+ converter.getSystemsEnum(x.getValue()))
.collect(Collectors.joining("\n= "));
}
static class UnitEquivalence implements Comparable<UnitEquivalence> {
final String standard1;
final char operation;
final String standard2;
final UnitId id1;
final UnitId id2;
public UnitEquivalence(
String standard1, char operation, String standard2, UnitId id1, UnitId id2) {
this.standard1 = standard1;
this.operation = operation;
this.standard2 = standard2;
this.id1 = id1;
this.id2 = id2;
}
@Override
public int compareTo(UnitEquivalence other) {
return ComparisonChain.start()
.compare(standard1, other.standard1)
.compare(operation, other.operation)
.compare(standard2, other.standard2)
.compare(id1, other.id1)
.compare(id2, other.id2)
.result();
}
@Override
public int hashCode() {
return Objects.hash(standard1, operation, standard2, id1, id2);
}
@Override
public boolean equals(Object obj) {
return compareTo((UnitEquivalence) obj) == 0;
}
@Override
public String toString() {
return standard1 + " " + operation + " " + standard2 + "\t🟰\t" + id1 + " " + operation
+ " " + id2;
}
public String getStandards() {
return standard1 + " " + operation + " " + standard2;
}
}
static final Set<String> extras =
Set.of("square-meter", "cubic-meter", "square-second", "cubic-second");
public void testRelations() {
Multimap<String, UnitEquivalence> decomps = TreeMultimap.create();
Set<UnitId> unitIds =
converter.getBaseUnitToQuantity().entrySet().stream()
.map(x -> converter.createUnitId(x.getKey()).freeze())
.collect(Collectors.toSet());
extras.forEach(x -> unitIds.add(converter.createUnitId(x).freeze()));
for (UnitId id1 : unitIds) {
String standard1 = converter.getStandardUnit(id1.toString());
if (skipUnit(standard1)) {
continue;
}
for (UnitId id2 : unitIds) {
String standard2 = converter.getStandardUnit(id2.toString());
if (skipUnit(standard2)) {
continue;
}
UnitId mul = id1.times(id2);
String standardMul = converter.getStandardUnit(mul.toString());
if (!skipUnit(standardMul)) {
if (standard1.compareTo(standard2) < 0) { // suppress because commutes
decomps.put(
standardMul,
new UnitEquivalence(standard1, '×', standard2, id1, id2));
// decomps.put(standardMul, standard1 + " × " + standard2 + "\t🟰\t" + id1 +
// " × " + id2);
}
}
UnitId id2Recip = id2.getReciprocal();
UnitId div = id1.times(id2Recip);
String standardDiv = converter.getStandardUnit(div.toString());
if (!skipUnit(standardDiv)) {
decomps.put(
standardDiv, new UnitEquivalence(standard1, '∕', standard2, id1, id2));
// decomps.put(standardDiv, standard1 + " ∕ " + standard2 + "\t🟰\t" + id1 + " ∕
// " + id2);
}
}
}
Multimap<String, String> testCases =
ImmutableMultimap.<String, String>builder()
.put("joule", "second × watt")
.put("joule", "meter × newton")
.put("volt", "ampere × ohm")
.put("watt", "ampere × volt")
.build();
Multimap<String, String> missing = TreeMultimap.create(testCases);
for (Entry<String, Collection<UnitEquivalence>> entry : decomps.asMap().entrySet()) {
String unitId = entry.getKey();
logln(unitId + " 🟰 ");
for (UnitEquivalence item : entry.getValue()) {
logln("\t" + item);
missing.remove(unitId, item.getStandards());
Collection<String> others = missing.get(unitId);
}
}
if (!assertEquals("All cases covered", 0, missing.size())) {
for (Entry<String, String> item : missing.entries()) {
System.out.println(item);
}
}
}
private boolean skipUnit(String unit) {
return !extras.contains(unit)
&& (unit == null || unit.contains("-") || unit.equals("becquerel"));
}
public void testEquivalents() {
List<List<String>> tests =
List.of(List.of("gallon-gasoline-energy-density", "33.705", "kilowatt-hour"));
for (List<String> test : tests) {
final String unit1 = test.get(0);
final Rational expectedFactor = Rational.of(test.get(1));
final String unit2 = test.get(2);
Output<String> baseUnit1String = new Output<>();
ConversionInfo base = converter.parseUnitId(unit1, baseUnit1String, false);
UnitId baseUnit1 = converter.createUnitId(baseUnit1String.value).resolve();
Output<String> baseUnit2String = new Output<>();
ConversionInfo other = converter.parseUnitId(unit2, baseUnit2String, false);
UnitId baseUnit2 = converter.createUnitId(baseUnit2String.value).resolve();
Rational actual = base.factor.divide(other.factor);
assertEquals(test.toString() + ", baseUnits", baseUnit1, baseUnit2);
assertEquals(
test.toString()
+ ", factors, e="
+ expectedFactor.toString(FormatStyle.approx)
+ ", a="
+ actual.toString(FormatStyle.approx),
expectedFactor,
actual);
}
}
public void testUnitSystems() {
Set<String> fails = new LinkedHashSet<>();
if (SHOW_SYSTEMS) {
System.out.println("\n# Show Unit Systems\n#Unit\tCLDR\tNIST*");
}
for (String unit : converter.getSimpleUnits()) {
final Set<UnitSystem> cldrSystems = converter.getSystemsEnum(unit);
ExternalUnitConversionData nistInfo = NistUnits.unitToData.get(unit);
final Set<UnitSystem> nistSystems = nistInfo == null ? Set.of() : nistInfo.systems;
if (SHOW_SYSTEMS) {
System.out.println(
unit //
+ "\t"
+ JOIN_COMMA.join(cldrSystems) //
+ "\t"
+ (nistInfo == null ? "" : JOIN_COMMA.join(nistInfo.systems)));
}
UnitSystemInvariant.test(unit, cldrSystems, fails);
if (!nistSystems.isEmpty() && !cldrSystems.containsAll(nistSystems)
|| cldrSystems.contains(UnitSystem.si) && !nistSystems.contains(UnitSystem.si)
|| cldrSystems.contains(UnitSystem.si_acceptable)
&& !nistSystems.contains(UnitSystem.si_acceptable)) {
if (unit.equals("100-kilometer")) {
continue;
}
fails.add(
"**\t"
+ unit
+ " nistSystems="
+ nistSystems
+ " cldrSystems="
+ cldrSystems);
}
}
if (!fails.isEmpty()) {
errln("Mismatch between NIST and CLDR UnitSystems");
for (String fail : fails) {
System.out.println(fail);
}
}
if (!SHOW_SYSTEMS) {
warnln("Use -DTestUnits:SHOW_SYSTEMS to see the unit systems for units in units.xml");
}
}
static class UnitSystemInvariant {
UnitSystem source;
Set<String> exceptUnits;
UnitSystem contains;
boolean invert;
static final Set<UnitSystemInvariant> invariants =
Set.of(
new UnitSystemInvariant(UnitSystem.si, null, UnitSystem.metric, true),
new UnitSystemInvariant(
UnitSystem.si_acceptable,
Set.of(
"knot",
"astronomical-unit",
"nautical-mile",
"minute",
"hour",
"day",
"arc-second",
"arc-minute",
"degree",
"electronvolt"),
UnitSystem.metric,
true), //
new UnitSystemInvariant(
UnitSystem.si,
Set.of("kilogram", "celsius", "radian", "katal", "steradian"),
UnitSystem.prefixable,
true),
new UnitSystemInvariant(
UnitSystem.metric,
Set.of(
"hectare",
"100-kilometer",
"kilogram",
"celsius",
"radian",
"katal",
"steradian"),
UnitSystem.prefixable,
true));
/**
* If a set of systems contains source, then it must contain contained (if invert == true)
* or must not (if invert = false).
*/
public UnitSystemInvariant(
UnitSystem source, Set<String> exceptUnits, UnitSystem contained, boolean invert) {
this.source = source;
this.exceptUnits = exceptUnits == null ? Set.of() : exceptUnits;
this.contains = contained;
this.invert = invert;
}
public boolean ok(String unit, Set<UnitSystem> trial) {
if (!trial.contains(source) || exceptUnits.contains(unit)) {
return true;
}
if (trial.contains(contains) == invert) {
return true;
}
return false;
}
static void test(String unit, Set<UnitSystem> systems, Set<String> fails) {
for (UnitSystemInvariant invariant : invariants) {
if (!invariant.ok(unit, systems)) {
if (unit.equals("100-kilometer")) {
continue;
}
fails.add("*\t" + unit + "\tfails\t" + invariant);
}
}
}
@Override
public String toString() {
return source + (invert ? " doesn't contain " : " contains ") + contains;
}
}
public void TestRationalFormatting() {
Rational.RationalParser rationalParser = new RationalParser();
List<List<String>> tests =
List.of(
List.of("plain", "PI", "411557987/131002976"),
//
List.of("approx", "125/7", "125/7"),
List.of("approx", "0.0000007˙716049382", "~771.6×10ˆ-9"),
List.of("approx", "PI", "~3.1416"),
//
List.of("repeating", "125/7", "17.˙857142"),
List.of("repeating", "0.0000007˙716049382", "0.0000007˙716049382"),
List.of("repeating", "PI", "12,861,187.09375/4093843"),
//
List.of("repeatingAll", "123456/7919", "123,456/7919"),
List.of("repeatingAll", "PI", "12,861,187.09375/4093843"),
//
List.of("formatted", "PI", "12,861,187.09375/4093843"),
//
List.of("html", "PI", "<sup>12,861,187.09375</sup>/<sub>4093843<sub>"));
int i = 0;
for (List<String> test : tests) {
FormatStyle formatStyle = FormatStyle.valueOf(test.get(0));
String rawSource = test.get(1);
Rational source = converter.getConstants().get(rawSource);
if (source == null) {
source = rationalParser.parse(rawSource);
}
String expected = test.get(2);
assertEquals(
++i + ") " + formatStyle + "(" + rawSource + ")",
expected,
source.toString(formatStyle));
}
}
public void TestSystems2() {
Multimap<String, UnitSystem> unitToSystems = converter.getSourceToSystems();
final Comparator<Iterable<UnitSystem>> systemComparator =
Comparators.lexicographical(Comparator.<UnitSystem>naturalOrder());
Multimap<UnitSystem, String> systemToUnits =
Multimaps.invertFrom(unitToSystems, TreeMultimap.create());
assertEquals("other doesn't occur", Set.of(), systemToUnits.get(UnitSystem.other));
Multimap<Set<UnitSystem>, String> systemSetToUnits =
TreeMultimap.create(systemComparator, Comparator.<String>naturalOrder());
// skip prefixable, since it isn't relevant
for (Entry<String, Collection<UnitSystem>> entry : unitToSystems.asMap().entrySet()) {
Set<UnitSystem> systemSet =
ImmutableSortedSet.copyOf(
Sets.difference(
new TreeSet<>(entry.getValue()),
Set.of(UnitSystem.prefixable)));
systemSetToUnits.put(systemSet, entry.getKey());
}
if (SHOW_SYSTEMS) {
System.out.println();
System.out.println("Set of UnitSystems\tUnits they apply to");
}
Set<String> ONLY_METRIC_AND_OTHERS = Set.of("second", "byte", "bit");
// Test some current invariants
for (Entry<Set<UnitSystem>, Collection<String>> entry :
systemSetToUnits.asMap().entrySet()) {
final Set<UnitSystem> systemSet = entry.getKey();
final Collection<String> unitSet = entry.getValue();
if (SHOW_SYSTEMS) {
System.out.println(systemSet + "\t" + unitSet);
}
if (systemSet.contains(UnitSystem.si)) {
assertNotContains(systemSet, UnitSystem.si_acceptable, unitSet);
assertContains(systemSet, UnitSystem.metric, unitSet);
}
if (systemSet.contains(UnitSystem.metric)) {
assertNotContains(systemSet, UnitSystem.metric_adjacent, unitSet);
if (!ONLY_METRIC_AND_OTHERS.containsAll(unitSet)) {
assertNotContains(systemSet, UnitSystem.ussystem, unitSet);
assertNotContains(systemSet, UnitSystem.uksystem, unitSet);
assertNotContains(systemSet, UnitSystem.jpsystem, unitSet);
}
}
}
if (SHOW_SYSTEMS) {
System.out.print("Unit\tQuantity");
for (UnitSystem sys : UnitSystem.ALL) {
System.out.print("\t" + sys);
}
System.out.println();
for (Entry<String, Collection<UnitSystem>> entry : unitToSystems.asMap().entrySet()) {
final TreeSet<UnitSystem> systemSet = new TreeSet<>(entry.getValue());
final String unit = entry.getKey();
systemSetToUnits.put(systemSet, unit);
System.out.print(unit);
System.out.print("\t");
System.out.print(converter.getQuantityFromUnit(unit, false));
for (UnitSystem sys : UnitSystem.ALL) {
System.out.print("\t" + (systemSet.contains(sys) ? "Y" : ""));
}
System.out.println();
}
}
warnln("Use -DTestUnits:SHOW_SYSTEMS to see details");
}
public <T> boolean assertContains(
final Set<T> systemSet, T unitSystem, Collection<String> units) {
return assertTrue(
units + ": " + systemSet + " contains " + unitSystem,
systemSet.contains(unitSystem));
}
public <T> boolean assertNotContains(
final Set<T> systemSet, T unitSystem, Collection<String> units) {
return assertFalse(
units + ": " + systemSet + " does not contain " + unitSystem,
systemSet.contains(unitSystem));
}
public void testQuantitiesMissingFromPreferences() {
UnitPreferences prefs = SDI.getUnitPreferences();
Set<String> preferenceQuantities = prefs.getQuantities();
Set<String> unitQuantities = converter.getQuantities();
assertEquals(
"pref - unit quantities",
Collections.emptySet(),
Sets.difference(preferenceQuantities, unitQuantities));
final SetView<String> quantitiesNotInPreferences =
Sets.difference(unitQuantities, preferenceQuantities);
if (!quantitiesNotInPreferences.isEmpty()) {
warnln("unit - pref quantities = " + quantitiesNotInPreferences);
}
for (String unit : converter.getSimpleUnits()) {
String quantity = converter.getQuantityFromUnit(unit, false);
if (!quantitiesNotInPreferences.contains(quantity)) {
continue;
}
// we have a unit whose quantity is not in preferences
// get its unit preferences
UnitPreference pref =
prefs.getUnitPreference(Rational.ONE, unit, "default", ULocale.US);
if (pref == null) {
errln(
String.format(
"Default preference is null: input unit=%s, quantity=%s",
unit, quantity));
continue;
}
// ensure that it is metric
Set<UnitSystem> inputSystems = converter.getSystemsEnum(unit);
if (Collections.disjoint(inputSystems, UnitSystem.SiOrMetric)) {
warnln(
String.format(
"There are no explicit preferences for %s, but %s is not metric",
quantity, unit));
}
Set<UnitSystem> prefSystems = converter.getSystemsEnum(pref.unit);
String errorOrWarningString =
String.format(
"Test default preference is metric: input unit=%s, quantity=%s, pref-unit=%s, systems: %s",
unit, quantity, pref.unit, prefSystems);
if (Collections.disjoint(prefSystems, UnitSystem.SiOrMetric)) {
errln(errorOrWarningString);
} else {
logln("OK " + errorOrWarningString);
}
}
}
public void testUnitPreferencesTest() {
try {
final Set<String> warnings = new LinkedHashSet<>();
Files.lines(Path.of(CLDRPaths.TEST_DATA + "units/unitPreferencesTest.txt"))
.forEach(line -> checkUnitPreferencesTest(line, warnings));
if (!warnings.isEmpty()) {
warnln("Mixed unit identifiers not yet checked, count=" + warnings.size());
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public void checkUnitPreferencesTest(String line, Set<String> warnings) {
if (line.startsWith("#") || line.isBlank()) {
return;
}
// # Quantity; Usage; Region; Input (r); Input (d); Input Unit; Output (r);
// Output (d); Output Unit
// Example:
// area; default; 001; 1100000; 1100000.0; square-meter;
// 11/10; 1.1; square-kilometer
// duration; media; 001; 66; 66.0; second; 1; minute; 6;
// 6.0; second
try {
UnitPreferences prefs = SDI.getUnitPreferences();
List<String> parts = SPLIT_SEMI.splitToList(line);
Map<String, Long> highMixed_unit_identifiers = new LinkedHashMap<>();
String quantity = parts.get(0);
String usage = parts.get(1);
String region = parts.get(2);
Rational inputRational = Rational.of(parts.get(3));
double inputDouble = Double.parseDouble(parts.get(4));
String inputUnit = parts.get(5);
// account for multi-part output
int size = parts.size();
// This section has larger elements with integer values
for (int i = 6; i < size - 3; i += 2) {
highMixed_unit_identifiers.put(parts.get(i + 1), Long.parseLong(parts.get(i)));
}
Rational expectedValue = Rational.of(parts.get(size - 3));
Double expectedValueDouble = Double.parseDouble(parts.get(size - 2));
String expectedOutputUnit = parts.get(size - 1);
// Check that the double values are approximately the same as
// the Rational ones
assertTrue(
String.format(
"input rational ~ input double, %s %s", inputRational, inputDouble),
inputRational.approximatelyEquals(inputDouble));
assertTrue(
String.format(
"output rational ~ output double, %s %s",
expectedValue, expectedValueDouble),
expectedValue.approximatelyEquals(expectedValueDouble));
// check that the quantity is consistent
String expectedQuantity = converter.getQuantityFromUnit(inputUnit, false);
assertEquals("Input: Quantity consistency check", expectedQuantity, quantity);
// TODO handle mixed_unit_identifiers
if (!highMixed_unit_identifiers.isEmpty()) {
warnings.add("mixed_unit_identifiers not yet checked: " + line);
return;
}
// check output unit, then value
UnitPreference unitPreference =
prefs.getUnitPreference(inputRational, inputUnit, usage, region);
String actualUnit = unitPreference.unit;
assertEquals("Output unit", expectedOutputUnit, actualUnit);
Rational actualValue = converter.convert(inputRational, inputUnit, actualUnit, false);
assertEquals("Output numeric value", expectedValue, actualValue);
} catch (Exception e) {
errln(e.getMessage() + "\n\t" + line);
}
}
public void testUnitsTest() {
try {
Files.lines(Path.of(CLDRPaths.TEST_DATA + "units/unitsTest.txt"))
.forEach(line -> checkUnitsTest(line));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private void checkUnitsTest(String line) {
if (line.startsWith("#") || line.isBlank()) {
return;
}
// Quantity ; x ; y ; conversion to y (rational) ; test: 1000 x ⟹ y
//
// Use: convert 1000 x units to the y unit; the result should match the final column,
// at the given precision. For example, when the last column is 159.1549,
// round to 4 decimal digits before comparing.
// Example:
// acceleration ; g-force ; meter-per-square-second ; 9.80665 * x ; 9806.65
try {
UnitPreferences prefs = SDI.getUnitPreferences();
List<String> parts = SPLIT_SEMI.splitToList(line);
String quantity = parts.get(0);
String sourceUnit = parts.get(1);
String targetUnit = parts.get(2);
String conversion = parts.get(3);
double expectedNumericValueFor1000 = Rational.of(parts.get(4)).doubleValue();
String expectedQuantity = converter.getQuantityFromUnit(sourceUnit, false);
assertEquals("Input: Quantity consistency check", expectedQuantity, quantity);
// TODO check conversion equation (not particularly important
Rational actualValue =
converter.convert(Rational.of(1000), sourceUnit, targetUnit, false);
assertTrue(
String.format(
"output rational ~ expected double, %s %s",
expectedNumericValueFor1000, actualValue.doubleValue()),
actualValue.approximatelyEquals(expectedNumericValueFor1000));
} catch (Exception e) {
errln(e.getMessage() + "\n\t" + line);
}
}
public void testUnitLocalePreferencesTest() {
try {
Files.lines(Path.of(CLDRPaths.TEST_DATA + "units/unitLocalePreferencesTest.txt"))
.forEach(line -> checkUnitLocalePreferencesTest(line));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private void checkUnitLocalePreferencesTest(String rawLine) {
int hashPos = rawLine.indexOf('#');
String line = hashPos < 0 ? rawLine : rawLine.substring(0, hashPos);
String comment = hashPos < 0 ? "" : "\t# " + rawLine.substring(hashPos + 1);
if (line.isBlank()) {
return;
}
// # input-unit; amount; usage; languageTag; expected-unit; expected-amount # comment
// Example:
// fahrenheit; 1; default; en-u-rg-uszzzz-ms-ussystem-mu-celsius; celsius; -155/9 #
// mu > ms > rg > (likely) region
try {
UnitPreferences prefs = SDI.getUnitPreferences();
List<String> parts = SPLIT_SEMI.splitToList(line);
String sourceUnit = parts.get(0);
Rational sourceAmount = Rational.of(parts.get(1));
String usage = parts.get(2);
String languageTag = parts.get(3);
String expectedUnit = parts.get(4);
Rational expectedAmount = Rational.of(parts.get(5));
String actualUnit;
Rational actualValue;
try {
if (DEBUG)
System.out.println(
String.format(
"%s;\t%s;\t%s;\t%s;\t%s;\t%s%s",
sourceUnit,
sourceAmount.toString(FormatStyle.formatted),
usage,
languageTag,
expectedUnit,
expectedAmount.toString(FormatStyle.formatted),
comment));
final ULocale uLocale = ULocale.forLanguageTag(languageTag);
UnitPreference unitPreference =
prefs.getUnitPreference(sourceAmount, sourceUnit, usage, uLocale);
if (unitPreference == null) { // if the quantity isn't found
throw new IllegalArgumentException(
String.format(
"No unit preferences found for unit: %s, usage: %s, locale:%s",
sourceUnit, usage, languageTag));
}
actualUnit = unitPreference.unit;
actualValue =
converter.convert(sourceAmount, sourceUnit, unitPreference.unit, false);
} catch (Exception e1) {
actualUnit = e1.getMessage();
actualValue = Rational.NaN;
}
if (assertEquals(
String.format(
"ICU unit pref, %s %s %s %s",
sourceUnit,
sourceAmount.toString(FormatStyle.formatted),
usage,
languageTag),
expectedUnit,
actualUnit)) {
assertEquals("CLDR value", expectedAmount, actualValue);
} else if (!comment.isBlank()) {
warnln(comment);
}
} catch (Exception e) {
errln(e.getStackTrace()[0] + ", " + e.getMessage() + "\n\t" + rawLine);
}
}
public void testUnitLocalePreferencesTestIcu() {
if (TEST_ICU) {
try {
Files.lines(Path.of(CLDRPaths.TEST_DATA + "units/unitLocalePreferencesTest.txt"))
.forEach(line -> checkUnitLocalePreferencesTestIcu(line));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
} else {
warnln("Skipping ICU test. To enable, set -DTestUnits:TEST_ICU");
}
}
private void checkUnitLocalePreferencesTestIcu(String rawLine) {
int hashPos = rawLine.indexOf('#');
String line = hashPos < 0 ? rawLine : rawLine.substring(0, hashPos);
String comment = hashPos < 0 ? "" : "\t# " + rawLine.substring(hashPos + 1);
if (line.isBlank()) {
return;
}
// # input-unit; amount; usage; languageTag; expected-unit; expected-amount # comment
// Example:
// fahrenheit; 1; default; en-u-rg-uszzzz-ms-ussystem-mu-celsius; celsius; -155/9 #
// mu > ms > rg > (likely) region
try {
List<String> parts = SPLIT_SEMI.splitToList(line);
String sourceUnit = parts.get(0);
double sourceAmount = icuRational(parts.get(1));
String usage = parts.get(2);
String languageTag = parts.get(3);
String expectedUnit = parts.get(4);
double expectedAmount = icuRational(parts.get(5));
String actualUnit;
float actualValueFloat;
try {
UnlocalizedNumberFormatter nf =
NumberFormatter.with()
.unitWidth(UnitWidth.FULL_NAME)
.precision(Precision.maxSignificantDigits(20));
LocalizedNumberFormatter localized =
nf.usage(usage).locale(Locale.forLanguageTag(languageTag));
final FormattedNumber formatted =
localized.format(
new Measure(sourceAmount, MeasureUnit.forIdentifier(sourceUnit)));
MeasureUnit icuOutputUnit = formatted.getOutputUnit();
actualUnit = icuOutputUnit.getSubtype();
actualValueFloat = formatted.toBigDecimal().floatValue();
} catch (Exception e) {
actualUnit = e.getMessage();
actualValueFloat = Float.NaN;
}
if (assertEquals(
String.format(
"ICU unit pref, %s %s %s %s",
sourceUnit, sourceAmount, usage, languageTag),
expectedUnit,
actualUnit)) {
assertEquals("ICU value", (float) expectedAmount, actualValueFloat);
} else if (!comment.isBlank()) {
warnln(comment);
}
} catch (Exception e) {
errln(e.getStackTrace()[0] + ", " + e.getMessage() + "\n\t" + rawLine);
}
}
private double icuRational(String string) {
string = string.replace(",", "");
int slashPos = string.indexOf('/');
if (slashPos < 0) {
return Double.parseDouble(string);
} else {
return Double.parseDouble(string.substring(0, slashPos))
/ Double.parseDouble(string.substring(slashPos + 1));
}
}
}