blob: b3f38e2d6d6981cb5b4ae57ce56f817c28ddb0c7 [file] [log] [blame]
// © 2020 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
package com.ibm.icu.dev.test.impl;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.BufferedReader;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.ArrayList;
import java.util.List;
import org.junit.Test;
import com.ibm.icu.dev.test.TestUtil;
import com.ibm.icu.impl.Pair;
import com.ibm.icu.impl.units.ComplexUnitsConverter;
import com.ibm.icu.impl.units.ConversionRates;
import com.ibm.icu.impl.units.MeasureUnitImpl;
import com.ibm.icu.impl.units.UnitConverter;
import com.ibm.icu.impl.units.UnitsRouter;
import com.ibm.icu.util.Measure;
import com.ibm.icu.util.MeasureUnit;
public class UnitsTest {
public static boolean compareTwoBigDecimal(BigDecimal expected, BigDecimal actual, BigDecimal delta) {
BigDecimal diff =
expected.abs().compareTo(BigDecimal.ZERO) < 1 ?
expected.subtract(actual).abs() :
(expected.subtract(actual).divide(expected, MathContext.DECIMAL128)).abs();
if (diff.compareTo(delta) == -1) return true;
return false;
}
@Test
public void testComplexUnitsConverter() {
class TestCase {
String input;
String output;
BigDecimal value;
Measure[] expected;
// For mixed units, accuracy of the smallest unit
double accuracy;
TestCase(String input, String output, BigDecimal value, Measure[] expected, double accuracy) {
this.input = input;
this.output = output;
this.value = value;
this.expected = expected;
this.accuracy = accuracy;
}
}
TestCase[] testCases = new TestCase[] {
// Significantly less than 2.0.
new TestCase(
"foot", "foot-and-inch", BigDecimal.valueOf(1.9999),
new Measure[] {new Measure(1, MeasureUnit.FOOT), new Measure(11.9988, MeasureUnit.INCH)},
0),
// A minimal nudge under 2.0, rounding up to 2.0 ft, 0 in.
// TODO(icu-units#108): this matches double precision calculations
// from C++, but BigDecimal is in use: do we want Java to be more
// precise than C++?
new TestCase(
"foot", "foot-and-inch", BigDecimal.valueOf(2.0).subtract(ComplexUnitsConverter.EPSILON),
new Measure[] {new Measure(2, MeasureUnit.FOOT), new Measure(0, MeasureUnit.INCH)}, 0),
// A slightly bigger nudge under 2.0, *not* rounding up to 2.0 ft!
new TestCase("foot", "foot-and-inch",
BigDecimal.valueOf(2.0).subtract(
ComplexUnitsConverter.EPSILON.multiply(BigDecimal.valueOf(3.0))),
new Measure[] {new Measure(1, MeasureUnit.FOOT),
new Measure(BigDecimal.valueOf(12.0).subtract(
ComplexUnitsConverter.EPSILON.multiply(
BigDecimal.valueOf(36.0))),
MeasureUnit.INCH)},
0),
// Testing precision with meter and light-year.
//
// DBL_EPSILON light-years, ~2.22E-16 light-years, is ~2.1 meters
// (maximum precision when exponent is 0).
//
// 1e-16 light years is 0.946073 meters.
// A 2.1 meter nudge under 2.0 light years, rounding up to 2.0 ly, 0 m.
// TODO(icu-units#108): this matches double precision calculations
// from C++, but BigDecimal is in use: do we want Java to be more
// precise than C++?
new TestCase("light-year", "light-year-and-meter",
BigDecimal.valueOf(2.0).subtract(ComplexUnitsConverter.EPSILON),
new Measure[] {new Measure(2, MeasureUnit.LIGHT_YEAR),
new Measure(0, MeasureUnit.METER)},
0),
// // TODO(icu-units#108): figure out precision thresholds for BigDecimal?
// // This test passes in C++ due to double-precision rounding.
// // A 2.1 meter nudge under 1.0 light years, rounding up to 1.0 ly, 0 m.
// new TestCase("light-year", "light-year-and-meter",
// BigDecimal.valueOf(1.0).subtract(ComplexUnitsConverter.EPSILON),
// new Measure[] {new Measure(1, MeasureUnit.LIGHT_YEAR),
// new Measure(0, MeasureUnit.METER)},
// 0),
// 1e-15 light years is 9.46073 meters (calculated using "bc" and
// the CLDR conversion factor). With double-precision maths in C++,
// we get 10.5. In this case, we're off by a bit more than 1 meter.
// With Java BigDecimal, we get accurate results.
new TestCase("light-year", "light-year-and-meter", BigDecimal.valueOf(1.0 + 1e-15),
new Measure[] {new Measure(1, MeasureUnit.LIGHT_YEAR),
new Measure(9.46073, MeasureUnit.METER)},
0 /* meters, precision */),
// TODO(icu-units#108): reconsider whether epsilon rounding is desirable:
//
// 2e-16 light years is 1.892146 meters. For C++ double, we consider
// this in the noise, and thus expect a 0. (This test fails when
// 2e-16 is increased to 4e-16.) For Java, using BigDecimal, we
// actually get a good result.
new TestCase("light-year", "light-year-and-meter", BigDecimal.valueOf(1.0 + 2e-16),
new Measure[] {new Measure(1, MeasureUnit.LIGHT_YEAR),
new Measure(1.892146, MeasureUnit.METER)},
0),
};
ConversionRates rates = new ConversionRates();
MeasureUnit input, output;
List<Measure> measures;
for (TestCase testCase : testCases) {
input = MeasureUnit.forIdentifier(testCase.input);
output = MeasureUnit.forIdentifier(testCase.output);
final MeasureUnitImpl inputImpl = MeasureUnitImpl.forIdentifier(input.getIdentifier());
final MeasureUnitImpl outputImpl = MeasureUnitImpl.forIdentifier(output.getIdentifier());
ComplexUnitsConverter converter = new ComplexUnitsConverter(inputImpl, outputImpl, rates);
measures = converter.convert(testCase.value, null);
assertEquals("measures length", testCase.expected.length, measures.size());
int i = 0;
for (Measure measure : measures) {
double accuracy = 0.0;
if (i == testCase.expected.length - 1) {
accuracy = testCase.accuracy;
}
assertTrue("input " + testCase.value + ", output measure " + i + ": expected " +
testCase.expected[i] + ", expected unit " +
testCase.expected[i].getUnit() + " got unit " + measure.getUnit(),
testCase.expected[i].getUnit().equals(measure.getUnit()));
assertEquals("input " + testCase.value + ", output measure " + i + ": expected " +
testCase.expected[i] + ", expected number " +
testCase.expected[i].getNumber() + " got number " + measure.getNumber(),
testCase.expected[i].getNumber().doubleValue(),
measure.getNumber().doubleValue(), accuracy);
i++;
}
}
// TODO(icu-units#63): test negative numbers!
}
@Test
public void testComplexUnitConverterSorting() {
MeasureUnitImpl source = MeasureUnitImpl.forIdentifier("meter");
MeasureUnitImpl target = MeasureUnitImpl.forIdentifier("inch-and-foot");
ConversionRates conversionRates = new ConversionRates();
ComplexUnitsConverter complexConverter = new ComplexUnitsConverter(source, target, conversionRates);
List<Measure> measures = complexConverter.convert(BigDecimal.valueOf(10.0), null);
assertEquals(measures.size(), 2);
assertEquals("inch-and-foot unit 0", "inch", measures.get(0).getUnit().getIdentifier());
assertEquals("inch-and-foot unit 1", "foot", measures.get(1).getUnit().getIdentifier());
assertEquals("inch-and-foot value 0", 9.7008, measures.get(0).getNumber().doubleValue(), 0.0001);
assertEquals("inch-and-foot value 1", 32, measures.get(1).getNumber().doubleValue(), 0.0001);
}
@Test
public void testExtractConvertibility() {
class TestData {
MeasureUnitImpl source;
MeasureUnitImpl target;
UnitConverter.Convertibility expected;
TestData(String source, String target, UnitConverter.Convertibility convertibility) {
this.source = MeasureUnitImpl.UnitsParser.parseForIdentifier(source);
this.target = MeasureUnitImpl.UnitsParser.parseForIdentifier(target);
this.expected = convertibility;
}
}
TestData[] tests = {
new TestData("meter", "foot", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("kilometer", "foot", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("hectare", "square-foot", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("kilometer-per-second", "second-per-meter", UnitConverter.Convertibility.RECIPROCAL),
new TestData("square-meter", "square-foot", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("kilometer-per-second", "foot-per-second", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("square-hectare", "pow4-foot", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("square-kilometer-per-second", "second-per-square-meter", UnitConverter.Convertibility.RECIPROCAL),
new TestData("cubic-kilometer-per-second-meter", "second-per-square-meter", UnitConverter.Convertibility.RECIPROCAL),
new TestData("square-meter-per-square-hour", "hectare-per-square-second", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("hertz", "revolution-per-second", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("millimeter", "meter", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("yard", "meter", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("ounce-troy", "kilogram", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("percent", "portion", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("ofhg", "kilogram-per-square-meter-square-second", UnitConverter.Convertibility.CONVERTIBLE),
new TestData("second-per-meter", "meter-per-second", UnitConverter.Convertibility.RECIPROCAL),
};
ConversionRates conversionRates = new ConversionRates();
for (TestData test :
tests) {
assertEquals(test.expected, UnitConverter.extractConvertibility(test.source, test.target, conversionRates));
}
}
// TODO(icu-units#92): add UnitsTest::testConverter(), to replace or extend this test.
@Test
public void testConverterForTemperature() {
class TestData {
MeasureUnitImpl source;
MeasureUnitImpl target;
BigDecimal input;
BigDecimal expected;
TestData(String source, String target, double input, double expected) {
this.source = MeasureUnitImpl.UnitsParser.parseForIdentifier(source);
this.target = MeasureUnitImpl.UnitsParser.parseForIdentifier(target);
this.input = BigDecimal.valueOf(input);
this.expected = BigDecimal.valueOf(expected);
}
}
TestData[] tests = {
new TestData("celsius", "fahrenheit", 1000, 1832),
new TestData("fahrenheit", "fahrenheit", 1000, 1000),
};
ConversionRates conversionRates = new ConversionRates();
for (TestData test :
tests) {
UnitConverter converter = new UnitConverter(test.source, test.target, conversionRates);
assertEquals(test.expected.doubleValue(), converter.convert(test.input).doubleValue(), (0.001));
}
}
@Test
public void testConverterWithCLDRTests() throws IOException {
class TestCase {
String category;
String sourceString;
String targetString;
MeasureUnitImpl source;
MeasureUnitImpl target;
BigDecimal input;
BigDecimal expected;
TestCase(String line) {
String[] fields = line
.replaceAll(" ", "") // Remove all the spaces.
.replaceAll(",", "") // Remove all the commas.
.replaceAll("\t", "")
.split(";");
this.category = fields[0].replaceAll(" ", "");
this.sourceString = fields[1];
this.targetString = fields[2];
this.source = MeasureUnitImpl.UnitsParser.parseForIdentifier(fields[1]);
this.target = MeasureUnitImpl.UnitsParser.parseForIdentifier(fields[2]);
this.input = BigDecimal.valueOf(1000);
this.expected = new BigDecimal(fields[4]);
}
}
String codePage = "UTF-8";
ArrayList<TestCase> tests = new ArrayList<>();
try (BufferedReader f = TestUtil.getDataReader("cldr/units/unitsTest.txt", codePage)) {
while (true) {
String line = f.readLine();
if (line == null) break;
if (line.isEmpty() || line.startsWith("#")) continue;
tests.add(new TestCase(line));
}
}
ConversionRates conversionRates = new ConversionRates();
for (TestCase testCase :
tests) {
UnitConverter converter = new UnitConverter(testCase.source, testCase.target, conversionRates);
BigDecimal got = converter.convert(testCase.input);
if (compareTwoBigDecimal(testCase.expected, got, BigDecimal.valueOf(0.000001))) {
continue;
} else {
fail(new StringBuilder()
.append(testCase.category)
.append(": Converting 1000 ")
.append(testCase.sourceString)
.append(" to ")
.append(testCase.targetString)
.append(", got ")
.append(got)
.append(", expected ")
.append(testCase.expected.toString())
.toString());
}
BigDecimal inverted = converter.convertInverse(testCase.input);
if (compareTwoBigDecimal(BigDecimal.valueOf(1000), inverted, BigDecimal.valueOf(0.000001))) {
continue;
} else {
fail(new StringBuilder()
.append("Converting back to ")
.append(testCase.sourceString)
.append(" from ")
.append(testCase.targetString)
.append(": got ")
.append(inverted)
.append(", expected 1000")
.toString());
}
}
}
@Test
public void testUnitPreferencesWithCLDRTests() throws IOException {
class TestCase {
final ArrayList<Pair<String, MeasureUnitImpl>> outputUnitInOrder = new ArrayList<>();
final ArrayList<BigDecimal> expectedInOrder = new ArrayList<>();
/**
* Test Case Data
*/
@SuppressWarnings("unused")
String category;
String usage;
String region;
Pair<String, MeasureUnitImpl> inputUnit;
BigDecimal input;
TestCase(String line) {
String[] fields = line
.replaceAll(" ", "") // Remove all the spaces.
.replaceAll(",", "") // Remove all the commas.
.replaceAll("\t", "")
.split(";");
String category = fields[0];
String usage = fields[1];
String region = fields[2];
String inputValue = fields[4];
String inputUnit = fields[5];
ArrayList<Pair<String, String>> outputs = new ArrayList<>();
for (int i = 6; i < fields.length - 2; i += 2) {
if (i == fields.length - 3) { // last field
outputs.add(Pair.of(fields[i + 2], fields[i + 1]));
} else {
outputs.add(Pair.of(fields[i + 1], fields[i]));
}
}
this.insertData(category, usage, region, inputUnit, inputValue, outputs);
}
private void insertData(String category,
String usage,
String region,
String inputUnitString,
String inputValue,
ArrayList<Pair<String, String>> outputs /* Unit Identifier, expected value */) {
this.category = category;
this.usage = usage;
this.region = region;
this.inputUnit = Pair.of(inputUnitString, MeasureUnitImpl.UnitsParser.parseForIdentifier(inputUnitString));
this.input = new BigDecimal(inputValue);
for (Pair<String, String> output :
outputs) {
outputUnitInOrder.add(Pair.of(output.first, MeasureUnitImpl.UnitsParser.parseForIdentifier(output.first)));
expectedInOrder.add(new BigDecimal(output.second));
}
}
}
// Read Test data from the unitPreferencesTest
String codePage = "UTF-8";
ArrayList<TestCase> tests = new ArrayList<>();
try (BufferedReader f = TestUtil.getDataReader("cldr/units/unitPreferencesTest.txt", codePage)) {
while (true) {
String line = f.readLine();
if (line == null) break;
if (line.isEmpty() || line.startsWith("#")) continue;
tests.add(new TestCase(line));
}
}
for (TestCase testCase :
tests) {
UnitsRouter router = new UnitsRouter(testCase.inputUnit.second, testCase.region, testCase.usage);
List<Measure> measures = router.route(testCase.input, null).measures;
assertEquals("Measures size must be the same as expected units",
measures.size(), testCase.expectedInOrder.size());
assertEquals("Measures size must be the same as output units",
measures.size(), testCase.outputUnitInOrder.size());
for (int i = 0; i < measures.size(); i++) {
if (!UnitsTest
.compareTwoBigDecimal(testCase.expectedInOrder.get(i),
BigDecimal.valueOf(measures.get(i).getNumber().doubleValue()),
BigDecimal.valueOf(0.00001))) {
fail(testCase.toString() + measures.toString());
}
}
}
}
}