summaryrefslogtreecommitdiff
path: root/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/UnitsConverter.java
diff options
context:
space:
mode:
Diffstat (limited to 'icu4j/main/core/src/main/java/com/ibm/icu/impl/units/UnitsConverter.java')
-rw-r--r--icu4j/main/core/src/main/java/com/ibm/icu/impl/units/UnitsConverter.java157
1 files changed, 144 insertions, 13 deletions
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 da4a0b2ae..fffb70b32 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,16 +6,20 @@ 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;
import com.ibm.icu.impl.IllegalIcuArgumentException;
import com.ibm.icu.util.MeasureUnit;
+// TODO ICU-22683: Consider splitting handling of special mappings into separate (possibly internal) class
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 +46,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 +58,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 +132,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 +172,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 +212,64 @@ public class UnitsConverter {
return result;
}
+ // TODO per CLDR-17421 and ICU-22683: consider getting the data below from CLDR
+ 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,