Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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 :
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}

/**
Expand Down
170 changes: 169 additions & 1 deletion icu4j/main/core/src/main/java/com/ibm/icu/util/MeasureUnit.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,25 @@
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;
import java.util.HashSet;
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;


Expand All @@ -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,
Expand Down Expand Up @@ -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.
*
* <p>Example usage:</p>
* <pre>{@code
* MeasureUnit meter = MeasureUnit.METER;
* MeasureUnit centimeter = MeasureUnit.CENTIMETER;
* BigDecimal valueInCentimeters = new BigDecimal("100");
* BigDecimal valueInMeters = meter.convertFrom(valueInCentimeters, centimeter);
* }</pre>
*
* <p>Note: This method supports conversion only between SINGLE and COMPOUND units.
* Conversion involving MIXED units is not supported.</p>
*
* <p>See {@link Complexity} for details on unit types.</p>
*
* @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.
*
* <p>
* Example usage:
* </p>
*
* <pre>{@code
* MeasureUnit meter = MeasureUnit.METER;
* MeasureUnit centimeter = MeasureUnit.CENTIMETER;
* Measure measure = new Measure(new BigDecimal("100"), centimeter);
* Measure convertedMeasure = meter.convertFrom(measure);
* }</pre>
*
* <p>
* Note: This method supports conversion only between SINGLE and COMPOUND units.
* Conversion involving MIXED units is not supported.
* </p>
*
* <p>
* See {@link Complexity} for details on unit types.
* </p>
*
* <p>
* Note: This method is a wrapper around {@link #convertFrom(MeasureUnit, BigDecimal)}.
* </p>
*
* @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);
}

/**
Expand Down Expand Up @@ -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();
}

/**
Expand All @@ -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();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}
Loading