mirror of
https://github.com/unicode-org/icu.git
synced 2025-04-06 22:15:31 +00:00
ICU-22655 Implement "special" conversion for speed-beaufort, part 1 icu4j
This commit is contained in:
parent
09ccfb9956
commit
7d636aecf7
4 changed files with 198 additions and 20 deletions
|
@ -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<SingleUnitImpl> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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 <code>UnitsConverter</code>.
|
||||
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Add table
Reference in a new issue