diff --git a/common/testData/units/unitLocalePreferencesTest.txt b/common/testData/units/unitLocalePreferencesTest.txt index bb7446bbf7e..2092844645c 100644 --- a/common/testData/units/unitLocalePreferencesTest.txt +++ b/common/testData/units/unitLocalePreferencesTest.txt @@ -27,17 +27,17 @@ fahrenheit; 1; default; en; fahrenheit; 1 # likely region = US gallon-imperial; 2.5; fluid; en-u-rg-uszzzz-ms-metric; liter; 11.365225 gallon-imperial; 2.5; fluid; en-u-rg-dezzzz; liter; 11.365225 gallon-imperial; 2.5; fluid; en-DE; liter; 11.365225 -gallon-imperial; 2.5; fluid; en-u-rg-uszzzz-ms-uksystem; gallon-imperial; 2.5 +gallon-imperial; 2.5; fluid; en-US-u-rg-uszzzz-ms-uksystem; gallon-imperial; 2.5 # ms-uksystem should behave like GB gallon-imperial; 2.5; fluid; en-u-rg-gbzzzz; gallon-imperial; 2.5 gallon-imperial; 2.5; fluid; en-GB; gallon-imperial; 2.5 gallon-imperial; 2.5; fluid; en-u-rg-uszzzz-ms-ussystem; gallon; 1,420,653,125/473176473 gallon-imperial; 2.5; fluid; en-u-rg-uszzzz; gallon; 1,420,653,125/473176473 gallon-imperial; 2.5; fluid; en-US; gallon; 1,420,653,125/473176473 gallon-imperial; 2.5; fluid; en; gallon; 1,420,653,125/473176473 # likely region = US -ampere; 2.5; default; en; ampere; 2.5 +ampere; 2.5; default; en; ampere; 2.5 # an input unit whose quantity has no preference data should get base units pound-force-foot; 12,345; default; en; kilowatt-hour; 0.004649325714486427205 -kilocandela; 1; default; en; candela; 1,000 # an input unit whose quantity has no preference data gets base units -candela-per-byte; 1; default; en; candela-per-bit; 0.125 # an input unit that has no quantity gets base units -candela-per-cubic-foot; 1; default; en; candela-per-cubic-meter; 1,953,125,000/55306341 # an input unit that has no quantity gets base units -foot; 1; default; de-u-mu-celsius; centimeter; 30.48 # a -mu unit that is not convertable from the input unit is ignored +kilocandela; 1; default; en; candela; 1,000 # an input unit whose quantity has no preference data should get base units +candela-per-byte; 1; default; en; candela-per-bit; 0.125 # an input unit that has no quantity should get base units +candela-per-cubic-foot; 1; default; en; candela-per-cubic-meter; 1,953,125,000/55306341 # an input unit that has no quantity should get base units +foot; 1; default; de-u-mu-celsius; centimeter; 30.48 # a -mu unit that is not convertible from the input unit should get ignored #pound; 28; default; en-u-mu-stone; stone; 2 # only temperature units are supported diff --git a/tools/cldr-code/src/main/java/org/unicode/cldr/tool/GenerateUnitTestData.java b/tools/cldr-code/src/main/java/org/unicode/cldr/tool/GenerateUnitTestData.java index 6fbe61294d7..8c2c8db36f7 100644 --- a/tools/cldr-code/src/main/java/org/unicode/cldr/tool/GenerateUnitTestData.java +++ b/tools/cldr-code/src/main/java/org/unicode/cldr/tool/GenerateUnitTestData.java @@ -5,6 +5,12 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.google.common.collect.TreeMultimap; +import com.ibm.icu.number.LocalizedNumberFormatter; +import com.ibm.icu.number.NumberFormatter; +import com.ibm.icu.number.NumberFormatter.UnitWidth; +import com.ibm.icu.number.UnlocalizedNumberFormatter; +import com.ibm.icu.util.Measure; +import com.ibm.icu.util.MeasureUnit; import com.ibm.icu.util.Output; import java.math.BigInteger; import java.math.MathContext; @@ -12,11 +18,13 @@ import java.util.Comparator; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; +import org.unicode.cldr.util.CLDRLocale; import org.unicode.cldr.util.CLDRPaths; import org.unicode.cldr.util.CldrUtility; import org.unicode.cldr.util.Pair; @@ -47,6 +55,7 @@ public static void main(String[] args) { GenerateUnitTestData item = new GenerateUnitTestData(); item.TestParseUnit(); item.TestUnitPreferences(); + item.testIcu(); } static { @@ -117,7 +126,7 @@ public void TestParseUnit() { + "# 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"); + + "# Generation: Use GenerateUnitTestData.java to regenerate unitsTest.txt.\n"); for (Entry, String> entry : testPrintout.entries()) { pw.println(entry.getValue()); } @@ -129,32 +138,15 @@ public void TestUnitPreferences() { UnitPreferences prefs = SDI.getUnitPreferences(); if (true) { try (TempPrintWriter pw = - TempPrintWriter.openUTF8Writer( - CLDRPaths.TEST_DATA + "units", "unitPreferencesTest.txt")) { + TempPrintWriter.openUTF8Writer( + CLDRPaths.TEST_DATA + "units", "unitPreferencesTest.txt"); + TempPrintWriter pwLocale = + TempPrintWriter.openUTF8Writer( + CLDRPaths.TEST_DATA + "units", + "unitLocalePreferencesTest.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"); + pw.println(getHeader("Region")); + pwLocale.println(getHeader("Locale")); Rational ONE_TENTH = Rational.of(1, 10); // Note that for production usage, precomputed data like the @@ -209,8 +201,19 @@ public void TestUnitPreferences() { baseUnit, uprefs, pw); + for (String sampleLocale : getSampleLocales(regions)) { + showSample( + quantity, + usage, + sampleLocale, + sample, + baseUnit, + uprefs, + pwLocale); + } } pw.println(); + pwLocale.println(); } } } @@ -218,10 +221,56 @@ public void TestUnitPreferences() { } } + static LikelySubtags likely = new LikelySubtags(); + + private Set getSampleLocales(Set regions) { + Set result = new TreeSet<>(); + int count = 2; + for (String region : regions) { + if (--count < 0) { + break; + } + String max = likely.maximize("und_" + region); + String lang = CLDRLocale.getInstance(max).getLanguage(); + result.add(lang); + result.add("zu_" + region); + } + return result; + } + + public String getHeader(String regionOrLocale) { + return "\n# Test data for unit preferences\n" + + CldrUtility.getCopyrightString("# ") + + "\n" + + "#\n" + + "# Format:\n" + + "#\tQuantity;\tUsage;\t" + + regionOrLocale + + ";\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 " + + regionOrLocale + + ".\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: Use GenerateUnitTestData.java to regenerate unitPreferencesTest.txt.\n"; + } + private void showSample( String quantity, String usage, - String sampleRegion, + String sampleRegionOrLocale, Rational sampleBaseValue, String baseUnit, Collection prefs, @@ -233,21 +282,28 @@ private void showSample( Rational baseGeq = converter.convert(pref.geq, topUnit, baseUnit, false); if (sampleBaseValue.compareTo(baseGeq) >= 0) { showSample2( - quantity, usage, sampleRegion, sampleBaseValue, baseUnit, pref.unit, pw); + quantity, + usage, + sampleRegionOrLocale, + sampleBaseValue, + baseUnit, + pref.unit, + pw); gotOne = true; break; } lastUnit = pref.unit; } if (!gotOne) { - showSample2(quantity, usage, sampleRegion, sampleBaseValue, baseUnit, lastUnit, pw); + showSample2( + quantity, usage, sampleRegionOrLocale, sampleBaseValue, baseUnit, lastUnit, pw); } } private void showSample2( String quantity, String usage, - String sampleRegion, + String sampleRegionOrLocale, Rational sampleBaseValue, String baseUnit, String lastUnit, @@ -279,7 +335,7 @@ private void showSample2( + TEST_SEP + usage + TEST_SEP - + sampleRegion + + sampleRegionOrLocale + TEST_SEP + originalSampleBaseValue + TEST_SEP @@ -342,4 +398,19 @@ private void checkUnitConvertability( } } } + + private void testIcu() { + UnlocalizedNumberFormatter nf = + NumberFormatter.with().unitWidth(UnitWidth.FULL_NAME).usage("road"); + + Object tests[][] = {{1d, MeasureUnit.MILE, "en", "result"}}; + for (Object test[] : tests) { + Double value = (Double) test[0]; + MeasureUnit unit = (MeasureUnit) test[1]; + final LocalizedNumberFormatter localized = + nf.locale(Locale.forLanguageTag((String) test[2])); + String actual = (String) test[3]; + actual = localized.format(new Measure(value, unit)).toString(); + } + } } diff --git a/tools/cldr-code/src/main/java/org/unicode/cldr/util/Rational.java b/tools/cldr-code/src/main/java/org/unicode/cldr/util/Rational.java index 925f441f219..c5e74bfa6cc 100644 --- a/tools/cldr-code/src/main/java/org/unicode/cldr/util/Rational.java +++ b/tools/cldr-code/src/main/java/org/unicode/cldr/util/Rational.java @@ -38,6 +38,14 @@ public final class Rational extends Number implements Comparable { public final BigInteger numerator; public final BigInteger denominator; + static final BigInteger BI_TWO = BigInteger.valueOf(2); + static final BigInteger BI_FIVE = BigInteger.valueOf(5); + static final BigInteger BI_MINUS_ONE = BigInteger.valueOf(-1); + static final BigInteger BI_TEN = BigInteger.valueOf(10); + + static final BigDecimal BD_TWO = BigDecimal.valueOf(2); + static final BigDecimal BD_FIVE = BigDecimal.valueOf(5); + // Constraints: // always stored in normalized form. // no common factor > 1 (reduced) @@ -45,16 +53,18 @@ public final class Rational extends Number implements Comparable { // if numerator is zero, denominator is 1 or 0 // if denominator is zero, numerator is 1, -1, or 0 - public static final Rational ZERO = Rational.of(0); - public static final Rational ONE = Rational.of(1); - public static final Rational TWO = Rational.of(2); - public static final Rational NEGATIVE_ONE = ONE.negate(); + // NOTE, the constructor doesn't do any checking, so everything other than these goes + // through Rational.of(...) + public static final Rational ZERO = new Rational(BigInteger.ZERO, BigInteger.ONE); + public static final Rational ONE = new Rational(BigInteger.ONE, BigInteger.ONE); + public static final Rational NaN = new Rational(BigInteger.ZERO, BigInteger.ZERO); + public static final Rational INFINITY = new Rational(BigInteger.ONE, BigInteger.ZERO); - public static final Rational INFINITY = Rational.of(1, 0); + public static final Rational NEGATIVE_ONE = ONE.negate(); public static final Rational NEGATIVE_INFINITY = INFINITY.negate(); - public static final Rational NaN = Rational.of(0, 0); - public static final Rational TEN = Rational.of(10, 1); + public static final Rational TWO = new Rational(BI_TWO, BigInteger.ONE); + public static final Rational TEN = new Rational(BI_TEN, BigInteger.ONE); public static final Rational TENTH = TEN.reciprocal(); public static final char REPTEND_MARKER = '˙'; @@ -203,18 +213,41 @@ public Map getConstants() { } public static Rational of(long numerator, long denominator) { - return new Rational(BigInteger.valueOf(numerator), BigInteger.valueOf(denominator)); + return Rational.of(BigInteger.valueOf(numerator), BigInteger.valueOf(denominator)); } public static Rational of(long numerator) { - return new Rational(BigInteger.valueOf(numerator), BigInteger.ONE); + return Rational.of(BigInteger.valueOf(numerator), BigInteger.ONE); } public static Rational of(BigInteger numerator, BigInteger denominator) { - return new Rational(numerator, denominator); + int dComparison = denominator.compareTo(BigInteger.ZERO); + if (dComparison == 0) { + // catch equivalents to NaN, -INF, +INF + // 0/0 => NaN + // +/0 => INF + // -/0 => -INF + int nComparison = numerator.compareTo(BigInteger.ZERO); + return nComparison < 0 ? NEGATIVE_INFINITY : nComparison > 0 ? INFINITY : NaN; + } else { + // reduce to lowest form + BigInteger gcd = numerator.gcd(denominator); + if (gcd.compareTo(BigInteger.ONE) > 0) { + numerator = numerator.divide(gcd); + denominator = denominator.divide(gcd); + } + if (dComparison < 0) { + // ** NOTE: is already reduced, so safe to use constructor + return new Rational(numerator, denominator); + } else { + // ** NOTE: is already reduced, so safe to use constructor + return new Rational(numerator.negate(), denominator.negate()); + } + } } public static Rational of(BigInteger numerator) { + // ** NOTE: is already reduced, so safe to use constructor return new Rational(numerator, BigInteger.ONE); } @@ -237,56 +270,47 @@ private Rational(BigInteger numerator, BigInteger denominator) { } public Rational add(Rational other) { - BigInteger gcd_den = denominator.gcd(other.denominator); - return new Rational( - numerator - .multiply(other.denominator) - .divide(gcd_den) - .add(other.numerator.multiply(denominator).divide(gcd_den)), - denominator.multiply(other.denominator).divide(gcd_den)); + BigInteger newNumerator = + numerator.multiply(other.denominator).add(other.numerator.multiply(denominator)); + BigInteger newDenominator = denominator.multiply(other.denominator); + return Rational.of(newNumerator, newDenominator); } public Rational subtract(Rational other) { - BigInteger gcd_den = denominator.gcd(other.denominator); - return new Rational( + BigInteger newNumerator = numerator .multiply(other.denominator) - .divide(gcd_den) - .subtract(other.numerator.multiply(denominator).divide(gcd_den)), - denominator.multiply(other.denominator).divide(gcd_den)); + .subtract(other.numerator.multiply(denominator)); + BigInteger newDenominator = denominator.multiply(other.denominator); + return Rational.of(newNumerator, newDenominator); } public Rational multiply(Rational other) { - BigInteger gcd_num_oden = numerator.gcd(other.denominator); - boolean isZero = gcd_num_oden.equals(BigInteger.ZERO); - BigInteger smallNum = isZero ? numerator : numerator.divide(gcd_num_oden); - BigInteger smallODen = isZero ? other.denominator : other.denominator.divide(gcd_num_oden); - - BigInteger gcd_den_onum = denominator.gcd(other.numerator); - isZero = gcd_den_onum.equals(BigInteger.ZERO); - BigInteger smallONum = isZero ? other.numerator : other.numerator.divide(gcd_den_onum); - BigInteger smallDen = isZero ? denominator : denominator.divide(gcd_den_onum); + BigInteger newNumerator = numerator.multiply(other.numerator); + BigInteger newDenominator = denominator.multiply(other.denominator); + return Rational.of(newNumerator, newDenominator); + } - return new Rational(smallNum.multiply(smallONum), smallDen.multiply(smallODen)); + public Rational divide(Rational other) { + BigInteger newNumerator = numerator.multiply(other.denominator); + BigInteger newDenominator = denominator.multiply(other.numerator); + return Rational.of(newNumerator, newDenominator); } public Rational pow(int i) { - return new Rational(numerator.pow(i), denominator.pow(i)); + return Rational.of(numerator.pow(i), denominator.pow(i)); } public static Rational pow10(int i) { return i > 0 ? TEN.pow(i) : TENTH.pow(-i); } - public Rational divide(Rational other) { - return multiply(other.reciprocal()); - } - public Rational reciprocal() { - return new Rational(denominator, numerator); + return Rational.of(denominator, numerator); } public Rational negate() { + // ** NOTE: is already reduced, so safe to use constructor return new Rational(numerator.negate(), denominator); } @@ -299,6 +323,10 @@ public BigDecimal toBigDecimal(MathContext mathContext) { } } + public BigDecimal toBigDecimal() { + return toBigDecimal(MathContext.DECIMAL128); // prevent failures due to repeating fractions + } + @Override public double doubleValue() { if (denominator.equals(BigInteger.ZERO) && numerator.equals(BigInteger.ZERO)) { @@ -315,10 +343,6 @@ public float floatValue() { return toBigDecimal(MathContext.DECIMAL32).floatValue(); } - public BigDecimal toBigDecimal() { - return toBigDecimal(MathContext.DECIMAL128); // prevent failures due to repeating fractions - } - public static Rational of(BigDecimal bigDecimal) { // scale() // If zero or positive, the scale is the number of digits to the right of the decimal point. @@ -328,10 +352,12 @@ public static Rational of(BigDecimal bigDecimal) { final int scale = bigDecimal.scale(); final BigInteger unscaled = bigDecimal.unscaledValue(); if (scale == 0) { + // ** NOTE: is already reduced, so safe to use constructor return new Rational(unscaled, BigInteger.ONE); } else if (scale >= 0) { - return new Rational(unscaled, BigDecimal.ONE.movePointRight(scale).toBigInteger()); + return Rational.of(unscaled, BigDecimal.ONE.movePointRight(scale).toBigInteger()); } else { + // ** NOTE: is already reduced, so safe to use constructor return new Rational( unscaled.multiply(BigDecimal.ONE.movePointLeft(scale).toBigInteger()), BigInteger.ONE); @@ -508,13 +534,6 @@ public Rational abs() { return numerator.signum() >= 0 ? this : this.negate(); } - static final BigInteger BI_TWO = BigInteger.valueOf(2); - static final BigInteger BI_FIVE = BigInteger.valueOf(5); - static final BigInteger BI_MINUS_ONE = BigInteger.valueOf(-1); - - static final BigDecimal BD_TWO = BigDecimal.valueOf(2); - static final BigDecimal BD_FIVE = BigDecimal.valueOf(5); - /** * Goal is to be able to display rationals in a short but exact form, like 1,234,567/3 or * 1.234567E21/3. To do this, find the smallest denominator (excluding powers of 2 and 5), and diff --git a/tools/cldr-code/src/test/java/org/unicode/cldr/unittest/TestUnits.java b/tools/cldr-code/src/test/java/org/unicode/cldr/unittest/TestUnits.java index 961ea4d6bbe..9c0b80dd57e 100644 --- a/tools/cldr-code/src/test/java/org/unicode/cldr/unittest/TestUnits.java +++ b/tools/cldr-code/src/test/java/org/unicode/cldr/unittest/TestUnits.java @@ -748,12 +748,8 @@ public void TestRational() { assertEquals("", Rational.ONE, a3_5.multiply(a3_5.reciprocal())); assertEquals("", Rational.ZERO, a3_5.add(a3_5.negate())); - assertEquals("", Rational.INFINITY, Rational.ZERO.reciprocal()); - assertEquals("", Rational.NEGATIVE_INFINITY, Rational.INFINITY.negate()); assertEquals("", Rational.NEGATIVE_ONE, Rational.ONE.negate()); - assertEquals("", Rational.NaN, Rational.ZERO.divide(Rational.ZERO)); - assertEquals("", BigDecimal.valueOf(2), Rational.of(2, 1).toBigDecimal()); assertEquals("", BigDecimal.valueOf(0.5), Rational.of(1, 2).toBigDecimal()); @@ -767,6 +763,38 @@ public void TestRational() { 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 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() { @@ -1002,6 +1030,7 @@ public void TestConversionLineOrder() { // Test that sorted is in same order as the file. MapComparator conversionOrder = new MapComparator<>(data.keySet()); String lastUnit = null; + Set warnings = new LinkedHashSet<>(); for (Entry entry : sorted.entries()) { final TargetInfo tInfo = entry.getKey(); final String unit = entry.getValue(); @@ -1013,7 +1042,7 @@ public void TestConversionLineOrder() { ConversionInfo info = converter.parseUnitId(unit, metricUnit, false); String metric = metricUnit.value; if (metric.equals(lastMetric)) { - warnln( + warnings.add( "Expected " + lastUnit + " < " @@ -1040,6 +1069,9 @@ public void TestConversionLineOrder() { 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() { @@ -4239,280 +4271,6 @@ public boolean assertNotContains( systemSet.contains(unitSystem)); } - public void testPreferencesWithLocales() { - List> tests = - List.of( - List.of( - 1, - MeasureUnit.FOOT, - "default", - "de-u-mu-celsius", - Rational.of(762, 25), - "centimeter", - "a -mu unit that is not convertable from the input unit is ignored"), - List.of( - 1d, - MeasureUnit.FAHRENHEIT, - "default", - "en-u-rg-uszzzz-ms-ussystem-mu-celsius", - Rational.of(-155, 9), - "celsius"), - List.of( - 1d, - MeasureUnit.FAHRENHEIT, - "default", - "en-u-rg-uszzzz-ms-ussystem-mu-celsius", - Rational.of(-155, 9), - "celsius"), - List.of( - 1d, - MeasureUnit.FAHRENHEIT, - "default", - "en-u-rg-uszzzz-ms-metric", - Rational.of(-155, 9), - "celsius"), - List.of( - 1d, - MeasureUnit.FAHRENHEIT, - "default", - "en-u-rg-dezzzz", - Rational.of(-155, 9), - "celsius"), - List.of( - 1d, - MeasureUnit.FAHRENHEIT, - "default", - "en-DE", - Rational.of(-155, 9), - "celsius"), - List.of( - 1d, - MeasureUnit.FAHRENHEIT, - "default", - "en-US", - Rational.of(1), - "fahrenheit"), - List.of( - 1d, - MeasureUnit.FAHRENHEIT, - "default", - "en", - Rational.of(1), - "fahrenheit"), - List.of( - 2.5d, - MeasureUnit.GALLON_IMPERIAL, - "fluid", - "en-u-rg-uszzzz-ms-metric", - Rational.of(454609, 40000), - "liter"), - List.of( - 2.5d, - MeasureUnit.GALLON_IMPERIAL, - "fluid", - "en-u-rg-dezzzz", - Rational.of(454609, 40000), - "liter"), - List.of( - 2.5d, - MeasureUnit.GALLON_IMPERIAL, - "fluid", - "en-DE", - Rational.of(454609, 40000), - "liter"), - List.of( - 2.5d, - MeasureUnit.GALLON_IMPERIAL, - "fluid", - "en-u-rg-uszzzz-ms-uksystem", - Rational.of(5, 2), - "gallon-imperial"), - List.of( - 2.5d, - MeasureUnit.GALLON_IMPERIAL, - "fluid", - "en-u-rg-gbzzzz", - Rational.of(5, 2), - "gallon-imperial"), - List.of( - 2.5d, - MeasureUnit.GALLON_IMPERIAL, - "fluid", - "en-GB", - Rational.of(5, 2), - "gallon-imperial"), - List.of( - 2.5d, - MeasureUnit.GALLON_IMPERIAL, - "fluid", - "en-u-rg-uszzzz-ms-ussystem", - Rational.of(1420653125, 473176473), - "gallon"), - List.of( - 2.5d, - MeasureUnit.GALLON_IMPERIAL, - "fluid", - "en-u-rg-uszzzz", - Rational.of(1420653125, 473176473), - "gallon"), - List.of( - 2.5d, - MeasureUnit.GALLON_IMPERIAL, - "fluid", - "en-US", - Rational.of(1420653125, 473176473), - "gallon"), - List.of( - 2.5d, - MeasureUnit.GALLON_IMPERIAL, - "fluid", - "en", - Rational.of(1420653125, 473176473), - "gallon"), - List.of( - 2.5d, - MeasureUnit.AMPERE, - "default", - "en", - Rational.of(5, 2), - "ampere"), - List.of( - 12345d, - MeasureUnit.forIdentifier("foot-pound-force"), - "default", - "en", - Rational.of("929865142897285441/200000000000000000000"), - "kilowatt-hour"), - List.of( - 28d, - MeasureUnit.forIdentifier("pound"), - "default", - "en-u-mu-stone", - Rational.TWO, - "stone"), - List.of( - 1, - MeasureUnit.forIdentifier("kilocandela"), - "default", - "en", - Rational.of(1000), - "candela", - "an input unit whose quantity has no preference data gets base units"), - List.of( - 1, - MeasureUnit.forIdentifier("candela-per-byte"), - "default", - "en", - Rational.of(1, 8), - "candela-per-bit", - "an input unit that has no quantity gets base units"), - List.of( - 1, - MeasureUnit.forIdentifier("candela-per-cubic-foot"), - "default", - "en", - Rational.of(1953125000, 55306341), - "candela-per-cubic-meter", - "an input unit that has no quantity gets base units"), - List.of( - 1, - MeasureUnit.FOOT, - "default", - "de-u-mu-celsius", - Rational.of(762, 25), - "centimeter", - "a -mu unit that is not convertable from the input unit is ignored")); - for (boolean isICU : List.of(false, true)) { - for (List test : tests) { - String actualUnit; - Rational actualValue; - - Rational sourceAmount = Rational.of(((Number) test.get(0)).doubleValue()); - final MeasureUnit sourceUnit = (MeasureUnit) test.get(1); - final String sourceUnitString = Units.getShort(sourceUnit.toString()); - String usage = (String) test.get(2); - final String languageTag = (String) test.get(3); - Rational expectedValue = (Rational) test.get(4); - String expectedUnit = (String) test.get(5); - String comment = ""; - if (test.size() > 6) { - comment = "\t# " + (String) test.get(6); - } - if (!isICU) { - try { - if (DEBUG) - System.out.println( - String.format( - "%s;\t%s;\t%s;\t%s;\t%s;\t%s%s", - sourceUnitString, - sourceAmount.toString(FormatStyle.formatted), - usage, - languageTag, - expectedUnit, - expectedValue.toString(FormatStyle.formatted), - comment)); - - UnitPreferences prefs = SDI.getUnitPreferences(); - final ULocale uLocale = ULocale.forLanguageTag(languageTag); - UnitPreference unitPreference = - prefs.getUnitPreference( - sourceAmount, sourceUnitString, 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", - sourceUnitString, usage, languageTag)); - } - actualUnit = unitPreference.unit; - actualValue = - converter.convert( - sourceAmount, sourceUnitString, unitPreference.unit, false); - } catch (Exception e1) { - actualUnit = e1.getMessage(); - actualValue = Rational.NaN; - } - if (assertEquals( - "CLDR unit pref" + test.subList(0, test.size() - 1).toString(), - expectedUnit, - actualUnit)) { - assertEquals( - "CLDR value" + test.subList(0, test.size() - 1).toString(), - expectedValue, - actualValue); - } - } else if (TEST_ICU) { - 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.doubleValue(), sourceUnit)); - MeasureUnit icuOutputUnit = formatted.getOutputUnit(); - actualUnit = icuOutputUnit.getSubtype(); - actualValueFloat = formatted.toBigDecimal().floatValue(); - } catch (Exception e) { - actualUnit = e.getMessage(); - actualValueFloat = Float.NaN; - } - if (assertEquals( - "ICU unit pref" + test.subList(0, test.size() - 1).toString(), - expectedUnit, - actualUnit)) { - assertEquals( - "ICU value" + test.subList(0, test.size() - 1).toString(), - (float) expectedValue.doubleValue(), - actualValueFloat); - } - } - } - } - } - public void testQuantitiesMissingFromPreferences() { UnitPreferences prefs = SDI.getUnitPreferences(); Set preferenceQuantities = prefs.getQuantities(); @@ -4566,14 +4324,18 @@ public void testQuantitiesMissingFromPreferences() { public void testUnitPreferencesTest() { try { + final Set warnings = new LinkedHashSet<>(); Files.lines(Path.of(CLDRPaths.TEST_DATA + "units/unitPreferencesTest.txt")) - .forEach(line -> checkUnitPreferencesTest(line)); + .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) { + public void checkUnitPreferencesTest(String line, Set warnings) { if (line.startsWith("#") || line.isBlank()) { return; } @@ -4622,7 +4384,7 @@ public void checkUnitPreferencesTest(String line) { // TODO handle mixed_unit_identifiers if (!highMixed_unit_identifiers.isEmpty()) { - warnln("mixed_unit_identifiers not yet checked: " + line); + warnings.add("mixed_unit_identifiers not yet checked: " + line); return; } // check output unit, then value @@ -4744,38 +4506,101 @@ private void checkUnitLocalePreferencesTest(String rawLine) { actualUnit = e1.getMessage(); actualValue = Rational.NaN; } - if (assertEquals("CLDR unit pref", expectedUnit, actualUnit)) { + 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); } - if (TEST_ICU) { - 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.doubleValue(), - 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("ICU unit pref", expectedUnit, actualUnit)) { - assertEquals( - "ICU value", (float) expectedAmount.doubleValue(), actualValueFloat); - } + } 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 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); + 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)); } } }