diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/ConversionRates.java b/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/ConversionRates.java index 4a96014779c..3aa643d7156 100644 --- a/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/ConversionRates.java +++ b/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/ConversionRates.java @@ -11,6 +11,7 @@ import java.util.HashMap; import com.ibm.icu.impl.ICUData; import com.ibm.icu.impl.ICUResourceBundle; +import com.ibm.icu.impl.IllegalIcuArgumentException; import com.ibm.icu.impl.UResource; import com.ibm.icu.util.MeasureUnit; import com.ibm.icu.util.UResourceBundle; @@ -77,6 +78,15 @@ public class ConversionRates { } + // Map the MeasureUnitImpl for a simple unit to its corresponding SimpleUnitID, + // then get the specialMappingName for that SimpleUnitID (which may be null if + // the simple unit converts to base using factor + offset instelad of a special mapping). + protected String getSpecialMappingName(MeasureUnitImpl simpleUnit) { + if (!checkSimpleUnit(simpleUnit)) return null; + String simpleIdentifier = simpleUnit.getSingleUnits().get(0).getSimpleUnitID(); + return this.mapToConversionRate.get(simpleIdentifier).getSpecialMappingName(); + } + public MeasureUnitImpl extractCompoundBaseUnit(MeasureUnitImpl measureUnit) { ArrayList baseUnits = this.extractBaseUnits(measureUnit); @@ -177,11 +187,11 @@ public class ConversionRates { } else if ("offset".equals(keyString)) { offset = valueString; } else if ("special".equals(keyString)) { - special = valueString; + special = valueString; // the name of a special mapping used instead of factor + optional offset. } else if ("systems".equals(keyString)) { systems = value.toString(); // still want the spaces here } else { - assert false : "The key must be target, factor, offset, special. or systems"; + assert false : "The key must be target, factor, offset, special, or systems"; } } @@ -207,7 +217,7 @@ public class ConversionRates { private final String target; private final String conversionRate; private final BigDecimal offset; - private final String special; + private final String specialMappingName; // the name of a special mapping used instead of factor + optional offset. private final String systems; public ConversionRateInfo(String simpleUnit, String target, String conversionRate, String offset, String special, String systems) { @@ -215,7 +225,7 @@ public class ConversionRates { this.target = target; this.conversionRate = conversionRate; this.offset = forNumberWithDivision(offset); - this.special = special; + this.specialMappingName = special; this.systems = systems; } @@ -251,17 +261,24 @@ public class ConversionRates { * @return The conversion rate from this unit to the base unit. */ public String getConversionRate() { + if (conversionRate==null) { + throw new IllegalIcuArgumentException("trying to use a null conversion rate (for special?)"); + } return conversionRate; } /** - * @return The special conversion system for this unit. + * @return The name of the special conversion system for this unit (used instead of factor + optional offset). */ - public String getSpecial() { return special; } + public String getSpecialMappingName() { + return specialMappingName; + } /** * @return The measurement systems this unit belongs to. */ - public String getSystems() { return systems; } + public String getSystems() { + return systems; + } } } diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/MeasureUnitImpl.java b/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/MeasureUnitImpl.java index ed8856075f1..2363dee92be 100644 --- a/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/MeasureUnitImpl.java +++ b/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/MeasureUnitImpl.java @@ -780,6 +780,20 @@ public class MeasureUnitImpl { @Override public int compare(MeasureUnitImpl o1, MeasureUnitImpl o2) { + String special1 = this.conversionRates.getSpecialMappingName(o1); + String special2 = this.conversionRates.getSpecialMappingName(o2); + if (special1 != null || special2 != null) { + if (special1 == null) { + // non-specials come first + return -1; + } + if (special2 == null) { + // non-specials come first + return 1; + } + // both are specials, compare lexicographically + return special1.compareTo(special2); + } BigDecimal factor1 = this.conversionRates.getFactorToBase(o1).getConversionRate(); BigDecimal factor2 = this.conversionRates.getFactorToBase(o2).getConversionRate(); diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/UnitsConverter.java b/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/UnitsConverter.java index da4a0b2ae62..9985e882477 100644 --- a/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/UnitsConverter.java +++ b/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/UnitsConverter.java @@ -6,6 +6,7 @@ import static java.math.MathContext.DECIMAL128; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.regex.Pattern; @@ -16,6 +17,8 @@ public class UnitsConverter { private BigDecimal conversionRate; private boolean reciprocal; private BigDecimal offset; + private String specialSource; + private String specialTarget; /** * Constructor of UnitsConverter. @@ -42,6 +45,7 @@ public class UnitsConverter { * NOTE: * - source and target must be under the same category * - e.g. meter to mile --> both of them are length units. + * This converts from source to base to target (one of those may be a no-op). * * @param source represents the source unit. * @param target represents the target unit. @@ -53,21 +57,38 @@ public class UnitsConverter { throw new IllegalIcuArgumentException("input units must be convertible or reciprocal"); } - Factor sourceToBase = conversionRates.getFactorToBase(source); - Factor targetToBase = conversionRates.getFactorToBase(target); + this.specialSource = conversionRates.getSpecialMappingName(source); + this.specialTarget = conversionRates.getSpecialMappingName(target); - if (convertibility == Convertibility.CONVERTIBLE) { - this.conversionRate = sourceToBase.divide(targetToBase).getConversionRate(); + if (this.specialSource == null && this.specialTarget == null) { + Factor sourceToBase = conversionRates.getFactorToBase(source); + Factor targetToBase = conversionRates.getFactorToBase(target); + + if (convertibility == Convertibility.CONVERTIBLE) { + this.conversionRate = sourceToBase.divide(targetToBase).getConversionRate(); + } else { + assert convertibility == Convertibility.RECIPROCAL; + this.conversionRate = sourceToBase.multiply(targetToBase).getConversionRate(); + } + this.reciprocal = convertibility == Convertibility.RECIPROCAL; + + // calculate the offset + this.offset = conversionRates.getOffset(source, target, sourceToBase, targetToBase, convertibility); + // We should see no offsets for reciprocal conversions - they don't make sense: + assert convertibility != Convertibility.RECIPROCAL || this.offset == BigDecimal.ZERO; } else { - assert convertibility == Convertibility.RECIPROCAL; - this.conversionRate = sourceToBase.multiply(targetToBase).getConversionRate(); + this.reciprocal = false; + this.offset = BigDecimal.ZERO; + if (this.specialSource == null) { + // conversionRate is for source to base only + this.conversionRate = conversionRates.getFactorToBase(source).getConversionRate(); + } else if (this.specialTarget == null) { + // conversionRate is for base to target only + this.conversionRate = conversionRates.getFactorToBase(target).getConversionRate(); + } else { + this.conversionRate = BigDecimal.ONE; + } } - this.reciprocal = convertibility == Convertibility.RECIPROCAL; - - // calculate the offset - this.offset = conversionRates.getOffset(source, target, sourceToBase, targetToBase, convertibility); - // We should see no offsets for reciprocal conversions - they don't make sense: - assert convertibility != Convertibility.RECIPROCAL || this.offset == BigDecimal.ZERO; } static public Convertibility extractConvertibility(MeasureUnitImpl source, MeasureUnitImpl target, ConversionRates conversionRates) { @@ -110,8 +131,34 @@ public class UnitsConverter { return true; } + // Convert inputValue (source) to base then to target public BigDecimal convert(BigDecimal inputValue) { - BigDecimal result = inputValue.multiply(this.conversionRate).add(offset); + BigDecimal result = inputValue; + if (this.specialSource != null || this.specialTarget != null) { + BigDecimal base = inputValue; + // convert input (=source) to base + if (this.specialSource != null) { + // We have a special mapping from source to base (not using factor, offset). + // Currently the only supported mapping is a scale-based mapping for beaufort. + base = (this.specialSource.equals("beaufort"))? + scaleToBase(inputValue, minMetersPerSecForBeaufort): inputValue; + } else { + // Standard mapping (using factor, offset) from source to base. + base = inputValue.multiply(this.conversionRate); + } + // convert base to result (=target) + if (this.specialTarget != null) { + // We have a special mapping from base to target (not using factor, offset). + // Currently the only supported mapping is a scale-based mapping for beaufort. + result = (this.specialTarget.equals("beaufort"))? + baseToScale(base, minMetersPerSecForBeaufort): base; + } else { + // Standard mapping (using factor, offset) from base to target. + result = base.divide(this.conversionRate, DECIMAL128); + } + return result; + } + result = inputValue.multiply(this.conversionRate).add(offset); if (this.reciprocal) { // We should see no offsets for reciprocal conversions - they don't make sense: assert offset == BigDecimal.ZERO; @@ -124,8 +171,33 @@ public class UnitsConverter { return result; } + // Convert inputValue (target) to base then to source public BigDecimal convertInverse(BigDecimal inputValue) { BigDecimal result = inputValue; + if (this.specialSource != null || this.specialTarget != null) { + BigDecimal base = inputValue; + // convert input (=target) to base + if (this.specialTarget != null) { + // We have a special mapping from target to base (not using factor, offset). + // Currently the only supported mapping is a scale-based mapping for beaufort. + base = (this.specialTarget.equals("beaufort"))? + scaleToBase(inputValue, minMetersPerSecForBeaufort): inputValue; + } else { + // Standard mapping (using factor, offset) from target to base. + base = inputValue.multiply(this.conversionRate); + } + // convert base to result (=source) + if (this.specialSource != null) { + // We have a special mapping from base to source (not using factor, offset). + // Currently the only supported mapping is a scale-based mapping for beaufort. + result = (this.specialSource.equals("beaufort"))? + baseToScale(base, minMetersPerSecForBeaufort): base; + } else { + // Standard mapping (using factor, offset) from base to source. + result = base.divide(this.conversionRate, DECIMAL128); + } + return result; + } if (this.reciprocal) { // We should see no offsets for reciprocal conversions - they don't make sense: assert offset == BigDecimal.ZERO; @@ -139,6 +211,63 @@ public class UnitsConverter { return result; } + private static final BigDecimal[] minMetersPerSecForBeaufort = { + // Minimum m/s (base) values for each Bft value, plus an extra artificial value; + // when converting from Bft to m/s, the middle of the range will be used + // (Values from table in Wikipedia, except for artificial value). + // Since this is 0 based, max Beaufort value is thus array dimension minus 2. + BigDecimal.valueOf(0.0), // 0 Bft + BigDecimal.valueOf(0.3), // 1 + BigDecimal.valueOf(1.6), // 2 + BigDecimal.valueOf(3.4), // 3 + BigDecimal.valueOf(5.5), // 4 + BigDecimal.valueOf(8.0), // 5 + BigDecimal.valueOf(10.8), // 6 + BigDecimal.valueOf(13.9), // 7 + BigDecimal.valueOf(17.2), // 8 + BigDecimal.valueOf(20.8), // 9 + BigDecimal.valueOf(24.5), // 10 + BigDecimal.valueOf(28.5), // 11 + BigDecimal.valueOf(32.7), // 12 + BigDecimal.valueOf(36.9), // 13 + BigDecimal.valueOf(41.4), // 14 + BigDecimal.valueOf(46.1), // 15 + BigDecimal.valueOf(51.1), // 16 + BigDecimal.valueOf(55.8), // 17 + BigDecimal.valueOf(61.4), // artificial end of range 17 to give reasonable midpoint + }; + + // Convert from what should be discrete scale values for a particular unit like beaufort + // to a corresponding value in the base unit (which can have any decimal value, like meters/sec). + // First we round the scale value to the nearest integer (in case it is specified with a fractional value), + // then we map that to a value in middle of the range of corresponding base values. + // This can handle different scales, specified by minBaseForScaleValues[]. + private BigDecimal scaleToBase(BigDecimal scaleValue, BigDecimal[] minBaseForScaleValues) { + BigDecimal pointFive = BigDecimal.valueOf(0.5); + BigDecimal scaleAdjust = scaleValue.abs().add(pointFive); // adjust up for later truncation + BigDecimal scaleAdjustCapped = scaleAdjust.min(BigDecimal.valueOf(minBaseForScaleValues.length - 2)); + int scaleIndex = scaleAdjustCapped.intValue(); + // Return midpont of range (the final range uses an articial end to produce reasonable midpoint) + return minBaseForScaleValues[scaleIndex].add(minBaseForScaleValues[scaleIndex + 1]).multiply(pointFive); + } + + // Convert from a value in the base unit (which can have any decimal value, like meters/sec) to a corresponding + // discrete value in a scale (like beaufort), where each scale value represents a range of base values. + // We binary-search the ranges to find the one that contains the specified base value, and return its index. + // This can handle different scales, specified by minBaseForScaleValues[]. + private BigDecimal baseToScale(BigDecimal baseValue, BigDecimal[] minBaseForScaleValues) { + int scaleIndex = Arrays.binarySearch(minBaseForScaleValues, baseValue.abs()); + if (scaleIndex < 0) { + // since our first array entry is 0, this value will always be -2 or less + scaleIndex = -scaleIndex - 2; + } + int scaleMax = minBaseForScaleValues.length - 2; + if (scaleIndex > scaleMax) { + scaleIndex = scaleMax; + } + return BigDecimal.valueOf(scaleIndex); + } + public enum Convertibility { CONVERTIBLE, RECIPROCAL, diff --git a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/impl/UnitsTest.java b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/impl/UnitsTest.java index cd18ce73a38..09d8eeafb3b 100644 --- a/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/impl/UnitsTest.java +++ b/icu4j/main/core/src/test/java/com/ibm/icu/dev/test/impl/UnitsTest.java @@ -279,6 +279,9 @@ public class UnitsTest { new TestData("percent", "portion", UnitsConverter.Convertibility.CONVERTIBLE), new TestData("ofhg", "kilogram-per-square-meter-square-second", UnitsConverter.Convertibility.CONVERTIBLE), new TestData("second-per-meter", "meter-per-second", UnitsConverter.Convertibility.RECIPROCAL), + new TestData("mile-per-hour", "meter-per-second", UnitsConverter.Convertibility.CONVERTIBLE), + new TestData("knot", "meter-per-second", UnitsConverter.Convertibility.CONVERTIBLE), + new TestData("beaufort", "meter-per-second", UnitsConverter.Convertibility.CONVERTIBLE), }; ConversionRates conversionRates = new ConversionRates(); @@ -392,6 +395,8 @@ public class UnitsTest { new TestCase("cubic-meter-per-kilogram", "specific-volume"), new TestCase("meter-per-second", "speed"), new TestCase("second-per-meter", "speed"), + new TestCase("knot", "speed"), + new TestCase("beaufort", "speed"), new TestCase("mile-per-gallon", "consumption"), new TestCase("liter-per-100-kilometer", "consumption"), new TestCase("cubic-meter-per-meter", "consumption"), @@ -447,6 +452,19 @@ public class UnitsTest { new TestData("ton", "pound", 1.0, 2000), new TestData("stone", "pound", 1.0, 14), new TestData("stone", "kilogram", 1.0, 6.35029), + // Speed + new TestData("mile-per-hour", "meter-per-second", 1.0, 0.44704), + new TestData("knot", "meter-per-second", 1.0, 0.514444), + new TestData("beaufort", "meter-per-second", 1.0, 0.95), + new TestData("beaufort", "meter-per-second", 4.0, 6.75), + new TestData("beaufort", "meter-per-second", 7.0, 15.55), + new TestData("beaufort", "meter-per-second", 10.0, 26.5), + new TestData("beaufort", "meter-per-second", 13.0, 39.15), + new TestData("beaufort", "mile-per-hour", 1.0, 2.12509), + new TestData("beaufort", "mile-per-hour", 4.0, 15.099319971367215), + new TestData("beaufort", "mile-per-hour", 7.0, 34.784359341445956), + new TestData("beaufort", "mile-per-hour", 10.0, 59.2788), + new TestData("beaufort", "mile-per-hour", 13.0, 87.5761), // Temperature new TestData("celsius", "fahrenheit", 0.0, 32.0), new TestData("celsius", "fahrenheit", 10.0, 50.0),