diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/ConversionInfoToBase.java b/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/ConversionInfoToBase.java new file mode 100644 index 000000000000..55c32a1713f9 --- /dev/null +++ b/icu4j/main/core/src/main/java/com/ibm/icu/impl/units/ConversionInfoToBase.java @@ -0,0 +1,40 @@ +// © 2025 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html + +package com.ibm.icu.impl.units; + +/** + * Holds information for converting a unit to its base unit. + */ +public class ConversionInfoToBase { + private final double conversionRateToBaseUnit; + private final double offsetToBaseUnit; + private final String baseUnit; + private final String reciprocalBaseUnit; + + public ConversionInfoToBase(double conversionRateToBaseUnit, double offsetToBaseUnit, String baseUnit, + String reciprocalBaseUnit) { + + this.conversionRateToBaseUnit = conversionRateToBaseUnit; + this.offsetToBaseUnit = offsetToBaseUnit; + this.baseUnit = baseUnit; + this.reciprocalBaseUnit = reciprocalBaseUnit; + } + + public double getConversionRateToBaseUnit() { + return conversionRateToBaseUnit; + } + + public double getOffsetToBaseUnit() { + return offsetToBaseUnit; + } + + public String getBaseUnit() { + return baseUnit; + } + + public String getReciprocalBaseUnit() { + return reciprocalBaseUnit; + } + +} 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 c3361db3d759..c37db334e7cf 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 @@ -50,6 +50,21 @@ private UnitsConverter.Factor getFactorToBase(SingleUnitImpl singleUnit) { return result.applyPrefix(unitPrefix).power(power); } + public ConversionInfoToBase getConversionInfoToBase(MeasureUnitImpl measureUnit) { + MeasureUnitImpl BaseUnit = new MeasureUnitImpl(); + for (SingleUnitImpl singleUnit : measureUnit.getSingleUnits()) { + int power = singleUnit.getDimensionality(); + MeasureUnit.MeasurePrefix unitPrefix = singleUnit.getPrefix(); + ConversionRateInfo conversionRateInfo = mapToConversionRate.get(singleUnit.getSimpleUnitID()); + String baseUnit = conversionRateInfo.getTarget(); + + /// TODO: append all the single units to the BaseUnit. + + } + + return getConversionInfoToBase(BaseUnit); + } + public UnitsConverter.Factor getFactorToBase(MeasureUnitImpl measureUnit) { UnitsConverter.Factor result = new UnitsConverter.Factor(); for (SingleUnitImpl singleUnit : 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 16e78dcaf645..1ff12ba719d2 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 @@ -31,6 +31,14 @@ public class MeasureUnitImpl { * NOTE: when it is 0, it means there is no constant denominator. */ private long constantDenominator = 0; + + // Conversion info for the unit to the base unit. + // + // NOTE + // If the conversionInfo is null, then the conversion info is not set for this + // unit. + private ConversionInfoToBase conversionInfo = null; + /** * The list of single units. These may be summed or multiplied, based on the * value of the complexity field. @@ -57,8 +65,10 @@ public MeasureUnitImpl(SingleUnitImpl singleUnit) { * @return A newly parsed object. * @throws IllegalArgumentException in case of incorrect/non-parsed identifier. */ - public static MeasureUnitImpl forIdentifier(String identifier) { - return UnitsParser.parseForIdentifier(identifier); + public static MeasureUnitImpl forIdentifier(String identifier, ConversionRates conversionRates) { + MeasureUnitImpl result = UnitsParser.parseForIdentifier(identifier); + result.conversionInfo = new ConversionInfoToBase(conversionRates); + return result; } /** diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/util/MeasureUnit.java b/icu4j/main/core/src/main/java/com/ibm/icu/util/MeasureUnit.java index a7089583a483..8739e23c9673 100644 --- a/icu4j/main/core/src/main/java/com/ibm/icu/util/MeasureUnit.java +++ b/icu4j/main/core/src/main/java/com/ibm/icu/util/MeasureUnit.java @@ -14,6 +14,7 @@ import java.io.ObjectOutput; import java.io.ObjectStreamException; import java.io.Serializable; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -21,13 +22,17 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.math.MathContext; import com.ibm.icu.impl.CollectionSet; import com.ibm.icu.impl.ICUData; import com.ibm.icu.impl.ICUResourceBundle; import com.ibm.icu.impl.UResource; +import com.ibm.icu.impl.units.ConversionInfoToBase; +import com.ibm.icu.impl.units.ConversionRates; import com.ibm.icu.impl.units.MeasureUnitImpl; import com.ibm.icu.impl.units.SingleUnitImpl; +import com.ibm.icu.impl.units.UnitsConverter.Convertibility; import com.ibm.icu.text.UnicodeSet; @@ -43,6 +48,14 @@ public class MeasureUnit implements Serializable { private static final long serialVersionUID = -1839973855554750484L; + + // Conversion info for the unit to the base unit. + // + // NOTE + // If the conversionInfo is null, then the conversion info is not set for this + // unit. + private final ConversionInfoToBase conversionInfo; + // Cache of MeasureUnits. // All access to the cache or cacheIsPopulated flag must be synchronized on // class MeasureUnit, @@ -408,6 +421,151 @@ public int getPower() { protected MeasureUnit(String type, String subType) { this.type = type; this.subType = subType; + this.conversionInfo = null; + } + + /** + * @internal + * @deprecated This API is ICU internal only. + */ + @Deprecated + protected MeasureUnit(String type, String subType, ConversionInfoToBase conversionInfo) { + this.type = type; + this.subType = subType; + this.conversionInfo = conversionInfo; + } + + /** + * Converts a numeric value from another unit to this unit. + * + *

Example usage:

+ *
{@code
+     * MeasureUnit meter = MeasureUnit.METER;
+     * MeasureUnit centimeter = MeasureUnit.CENTIMETER;
+     * BigDecimal valueInCentimeters = new BigDecimal("100");
+     * BigDecimal valueInMeters = meter.convertFrom(valueInCentimeters, centimeter);
+     * }
+ * + *

Note: This method supports conversion only between SINGLE and COMPOUND units. + * Conversion involving MIXED units is not supported.

+ * + *

See {@link Complexity} for details on unit types.

+ * + * @param value The numeric value in the other unit to be converted. + * @param source The unit from which the value is being converted. + * @return The value converted to this unit. + * @throws IllegalArgumentException if one of the units is MIXED. + * @throws UnitsIncompatibleException if the units are not convertible. + * @throws UnsupportedOperationException if conversion info is not set for this unit or the other unit. + * + * @draft ICU 78 + */ + public double convertFrom(double value, MeasureUnit source ) { + if (this.getComplexity() == Complexity.MIXED || source.getComplexity() == Complexity.MIXED) { + throw new IllegalArgumentException("Conversion between mixed units is not supported"); + } + + if (conversionInfo == null || source.conversionInfo == null) { + throw new UnsupportedOperationException("Conversion info not set for this unit"); + } + + String targetBaseUnit = this.conversionInfo.getBaseUnit(); + String sourceBaseUnit = source.conversionInfo.getBaseUnit(); + String sourceReciprocalBaseUnit = source.conversionInfo.getReciprocalBaseUnit(); + + // Determine convertibility + Convertibility convertibility; + if (targetBaseUnit.equals(sourceBaseUnit)) { + convertibility = Convertibility.CONVERTIBLE; + } else if (targetBaseUnit.equals(sourceReciprocalBaseUnit)) { + convertibility = Convertibility.RECIPROCAL; + } else { + throw new UnitsIncompatibleException("Units are not convertible"); + } + + double sourceRate = source.conversionInfo.getConversionRateToBaseUnit(); + double sourceOffset = source.conversionInfo.getOffsetToBaseUnit(); + + double targetRate = this.conversionInfo.getConversionRateToBaseUnit(); + double targetOffset = this.conversionInfo.getOffsetToBaseUnit(); + + + // In order to calculate the final result, there are two cases: + // 1. the units are convertible (linearly convertible) + // in this case, we have the following formulas: + // source * sourceRate + sourceOffset = Base + // target * targetRate + targetOffset = Base + // by solving these equations, we get the final conversion rate and offset. + // target = source * (sourceRate / targetRate) + (targetOffset - sourceOffset)/ targetRate. + // + // 2. the units are reciprocal (reciprocally convertible) + // Note: when the units are reciprocal, the offset is 0. + // in this case, we have the following formulas: + // source * sourceRate = Base + // target * targetRate = 1 / Base + // by solving these equations, we get the final conversion rate and offset. + // target = 1 / (source * sourceRate * targetRate) + if (convertibility == Convertibility.CONVERTIBLE) { + return value * sourceRate / targetRate + (targetOffset - sourceOffset)/ targetRate; + } else { + return 1 / (value * sourceRate * targetRate); + } + } + + /** + * Converts a Measure from another unit to this unit. + * + *

+ * Example usage: + *

+ * + *
{@code
+     * MeasureUnit meter = MeasureUnit.METER;
+     * MeasureUnit centimeter = MeasureUnit.CENTIMETER;
+     * Measure measure = new Measure(new BigDecimal("100"), centimeter);
+     * Measure convertedMeasure = meter.convertFrom(measure);
+     * }
+ * + *

+ * Note: This method supports conversion only between SINGLE and COMPOUND units. + * Conversion involving MIXED units is not supported. + *

+ * + *

+ * See {@link Complexity} for details on unit types. + *

+ * + *

+ * Note: This method is a wrapper around {@link #convertFrom(MeasureUnit, BigDecimal)}. + *

+ * + * @param measure The measure to be converted. + * @return The measure converted to this unit. + * @throws IllegalArgumentException if one of the measure's units is MIXED or conversion info is not set. + * @throws UnitsIncompatibleException if the measure's units are not convertible. + * + * @draft ICU 78 + */ + public Measure convertFrom(Measure measure) { + if (measure == null) { + throw new UnsupportedOperationException("`measure` must not be null"); + } + + MeasureUnit sourceUnit = measure.getUnit(); + + // If the measure's unit is the same as the current unit, no conversion is needed. + if (this.equals(sourceUnit)) { + return measure; + } + + if (this.getComplexity() == Complexity.MIXED || sourceUnit.getComplexity() == Complexity.MIXED) { + throw new UnsupportedOperationException("Conversion involving MIXED units is not supported"); + } + + double sourceValue = measure.getNumber().doubleValue(); + double convertedValue = this.convertFrom( sourceValue, sourceUnit); + + return new Measure(convertedValue, this); } /** @@ -435,7 +593,9 @@ public static MeasureUnit forIdentifier(String identifier) { return NoUnit.BASE; } - return MeasureUnitImpl.forIdentifier(identifier).build(); + ConversionRates conversionRates = new ConversionRates(); + + return MeasureUnitImpl.forIdentifier(identifier, conversionRates).build(); } /** @@ -457,6 +617,14 @@ public static MeasureUnit fromMeasureUnitImpl(MeasureUnitImpl measureUnitImpl) { private MeasureUnit(MeasureUnitImpl measureUnitImpl) { type = null; subType = null; + conversionInfo = null; + this.measureUnitImpl = measureUnitImpl.copy(); + } + + private MeasureUnit(MeasureUnitImpl measureUnitImpl, ConversionInfoToBase conversionInfo) { + type = null; + subType = null; + this.conversionInfo = conversionInfo; this.measureUnitImpl = measureUnitImpl.copy(); } diff --git a/icu4j/main/core/src/main/java/com/ibm/icu/util/UnitsIncompatibleException.java b/icu4j/main/core/src/main/java/com/ibm/icu/util/UnitsIncompatibleException.java new file mode 100644 index 000000000000..23528f931baf --- /dev/null +++ b/icu4j/main/core/src/main/java/com/ibm/icu/util/UnitsIncompatibleException.java @@ -0,0 +1,63 @@ +package com.ibm.icu.util; + +/** + * Exception thrown when an attempt is made to convert between units + * that are not compatible with each other. + * + * Examples of unit compatibility: + * - Converting "meter" to "mile" is linearly convertible (compatible). + * - Converting "liter-per-100km" to "mile-per-gallon" is reciprocally + * convertible (compatible). + * - Converting "meter" to "kilogram" is not convertible (incompatible). + * + * @draft ICU 78 + */ +public class UnitsIncompatibleException extends IllegalArgumentException { + + // TODO: is the number correct? + private static final long serialVersionUID = 1286569061095434541L; + + /** + * Constructs a new UnitsIncompatibleException with no detail message. + * + * @draft ICU 78 + */ + public UnitsIncompatibleException() { + super(); + } + + /** + * Constructs a new UnitsIncompatibleException with the specified detail + * message. + * + * @param message the detail message + * @draft ICU 78 + */ + public UnitsIncompatibleException(String message) { + super(message); + } + + /** + * Constructs a new UnitsIncompatibleException with the specified cause. + * + * @param cause original exception (normally a {@link IllegalArgumentException}) + * @draft ICU 78 + */ + + public UnitsIncompatibleException(Throwable cause) { + super(cause); + } + + /** + * Constructs a new UnitsIncompatibleException with the specified detail + * message and cause. + * + * @param message the detail message + * @param cause the cause (normally an {@link IllegalArgumentException}) + * @draft ICU 78 + */ + public UnitsIncompatibleException(String message, Throwable cause) { + super(message, cause); + } + +}