diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /intl/icu/source/i18n/units_converter.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'intl/icu/source/i18n/units_converter.cpp')
-rw-r--r-- | intl/icu/source/i18n/units_converter.cpp | 634 |
1 files changed, 634 insertions, 0 deletions
diff --git a/intl/icu/source/i18n/units_converter.cpp b/intl/icu/source/i18n/units_converter.cpp new file mode 100644 index 0000000000..b89f495121 --- /dev/null +++ b/intl/icu/source/i18n/units_converter.cpp @@ -0,0 +1,634 @@ +// © 2020 and later: Unicode, Inc. and others. +// License & terms of use: http://www.unicode.org/copyright.html + +#include "unicode/utypes.h" + +#if !UCONFIG_NO_FORMATTING + +#include "charstr.h" +#include "cmemory.h" +#include "double-conversion-string-to-double.h" +#include "measunit_impl.h" +#include "putilimp.h" +#include "uassert.h" +#include "unicode/errorcode.h" +#include "unicode/localpointer.h" +#include "unicode/stringpiece.h" +#include "units_converter.h" +#include <algorithm> +#include <cmath> +#include <stdlib.h> +#include <utility> + +U_NAMESPACE_BEGIN +namespace units { + +void U_I18N_API Factor::multiplyBy(const Factor &rhs) { + factorNum *= rhs.factorNum; + factorDen *= rhs.factorDen; + for (int i = 0; i < CONSTANTS_COUNT; i++) { + constantExponents[i] += rhs.constantExponents[i]; + } + + // NOTE + // We need the offset when the source and the target are simple units. e.g. the source is + // celsius and the target is Fahrenheit. Therefore, we just keep the value using `std::max`. + offset = std::max(rhs.offset, offset); +} + +void U_I18N_API Factor::divideBy(const Factor &rhs) { + factorNum *= rhs.factorDen; + factorDen *= rhs.factorNum; + for (int i = 0; i < CONSTANTS_COUNT; i++) { + constantExponents[i] -= rhs.constantExponents[i]; + } + + // NOTE + // We need the offset when the source and the target are simple units. e.g. the source is + // celsius and the target is Fahrenheit. Therefore, we just keep the value using `std::max`. + offset = std::max(rhs.offset, offset); +} + +void U_I18N_API Factor::power(int32_t power) { + // multiply all the constant by the power. + for (int i = 0; i < CONSTANTS_COUNT; i++) { + constantExponents[i] *= power; + } + + bool shouldFlip = power < 0; // This means that after applying the absolute power, we should flip + // the Numerator and Denominator. + + factorNum = std::pow(factorNum, std::abs(power)); + factorDen = std::pow(factorDen, std::abs(power)); + + if (shouldFlip) { + // Flip Numerator and Denominator. + std::swap(factorNum, factorDen); + } +} + +void U_I18N_API Factor::applyPrefix(UMeasurePrefix unitPrefix) { + if (unitPrefix == UMeasurePrefix::UMEASURE_PREFIX_ONE) { + // No need to do anything + return; + } + + int32_t prefixPower = umeas_getPrefixPower(unitPrefix); + double prefixFactor = std::pow((double)umeas_getPrefixBase(unitPrefix), (double)std::abs(prefixPower)); + if (prefixPower >= 0) { + factorNum *= prefixFactor; + } else { + factorDen *= prefixFactor; + } +} + +void U_I18N_API Factor::substituteConstants() { + for (int i = 0; i < CONSTANTS_COUNT; i++) { + if (this->constantExponents[i] == 0) { + continue; + } + + auto absPower = std::abs(this->constantExponents[i]); + Signum powerSig = this->constantExponents[i] < 0 ? Signum::NEGATIVE : Signum::POSITIVE; + double absConstantValue = std::pow(constantsValues[i], absPower); + + if (powerSig == Signum::NEGATIVE) { + this->factorDen *= absConstantValue; + } else { + this->factorNum *= absConstantValue; + } + + this->constantExponents[i] = 0; + } +} + +namespace { + +/* Helpers */ + +using icu::double_conversion::StringToDoubleConverter; + +// TODO: Make this a shared-utility function. +// Returns `double` from a scientific number(i.e. "1", "2.01" or "3.09E+4") +double strToDouble(StringPiece strNum, UErrorCode &status) { + // We are processing well-formed input, so we don't need any special options to + // StringToDoubleConverter. + StringToDoubleConverter converter(0, 0, 0, "", ""); + int32_t count; + double result = converter.StringToDouble(strNum.data(), strNum.length(), &count); + if (count != strNum.length()) { + status = U_INVALID_FORMAT_ERROR; + } + + return result; +} + +// Returns `double` from a scientific number that could has a division sign (i.e. "1", "2.01", "3.09E+4" +// or "2E+2/3") +double strHasDivideSignToDouble(StringPiece strWithDivide, UErrorCode &status) { + int divisionSignInd = -1; + for (int i = 0, n = strWithDivide.length(); i < n; ++i) { + if (strWithDivide.data()[i] == '/') { + divisionSignInd = i; + break; + } + } + + if (divisionSignInd >= 0) { + return strToDouble(strWithDivide.substr(0, divisionSignInd), status) / + strToDouble(strWithDivide.substr(divisionSignInd + 1), status); + } + + return strToDouble(strWithDivide, status); +} + +/* + Adds single factor to a `Factor` object. Single factor means "23^2", "23.3333", "ft2m^3" ...etc. + However, complex factor are not included, such as "ft2m^3*200/3" +*/ +void addFactorElement(Factor &factor, StringPiece elementStr, Signum signum, UErrorCode &status) { + StringPiece baseStr; + StringPiece powerStr; + int32_t power = + 1; // In case the power is not written, then, the power is equal 1 ==> `ft2m^1` == `ft2m` + + // Search for the power part + int32_t powerInd = -1; + for (int32_t i = 0, n = elementStr.length(); i < n; ++i) { + if (elementStr.data()[i] == '^') { + powerInd = i; + break; + } + } + + if (powerInd > -1) { + // There is power + baseStr = elementStr.substr(0, powerInd); + powerStr = elementStr.substr(powerInd + 1); + + power = static_cast<int32_t>(strToDouble(powerStr, status)); + } else { + baseStr = elementStr; + } + + addSingleFactorConstant(baseStr, power, signum, factor, status); +} + +/* + * Extracts `Factor` from a complete string factor. e.g. "ft2m^3*1007/cup2m3*3" + */ +Factor extractFactorConversions(StringPiece stringFactor, UErrorCode &status) { + Factor result; + Signum signum = Signum::POSITIVE; + auto factorData = stringFactor.data(); + for (int32_t i = 0, start = 0, n = stringFactor.length(); i < n; i++) { + if (factorData[i] == '*' || factorData[i] == '/') { + StringPiece factorElement = stringFactor.substr(start, i - start); + addFactorElement(result, factorElement, signum, status); + + start = i + 1; // Set `start` to point to the start of the new element. + } else if (i == n - 1) { + // Last element + addFactorElement(result, stringFactor.substr(start, i + 1), signum, status); + } + + if (factorData[i] == '/') { + signum = Signum::NEGATIVE; // Change the signum because we reached the Denominator. + } + } + + return result; +} + +// Load factor for a single source +Factor loadSingleFactor(StringPiece source, const ConversionRates &ratesInfo, UErrorCode &status) { + const auto conversionUnit = ratesInfo.extractConversionInfo(source, status); + if (U_FAILURE(status)) return Factor(); + if (conversionUnit == nullptr) { + status = U_INTERNAL_PROGRAM_ERROR; + return Factor(); + } + + Factor result = extractFactorConversions(conversionUnit->factor.toStringPiece(), status); + result.offset = strHasDivideSignToDouble(conversionUnit->offset.toStringPiece(), status); + + return result; +} + +// Load Factor of a compound source unit. +// In ICU4J, this is a pair of ConversionRates.getFactorToBase() functions. +Factor loadCompoundFactor(const MeasureUnitImpl &source, const ConversionRates &ratesInfo, + UErrorCode &status) { + + Factor result; + for (int32_t i = 0, n = source.singleUnits.length(); i < n; i++) { + SingleUnitImpl singleUnit = *source.singleUnits[i]; + + Factor singleFactor = loadSingleFactor(singleUnit.getSimpleUnitID(), ratesInfo, status); + if (U_FAILURE(status)) return result; + + // Prefix before power, because: + // - square-kilometer to square-meter: (1000)^2 + // - square-kilometer to square-foot (approximate): (3.28*1000)^2 + singleFactor.applyPrefix(singleUnit.unitPrefix); + + // Apply the power of the `dimensionality` + singleFactor.power(singleUnit.dimensionality); + + result.multiplyBy(singleFactor); + } + + return result; +} + +/** + * Checks if the source unit and the target unit are simple. For example celsius or fahrenheit. But not + * square-celsius or square-fahrenheit. + * + * NOTE: + * Empty unit means simple unit. + * + * In ICU4J, this is ConversionRates.checkSimpleUnit(). + */ +UBool checkSimpleUnit(const MeasureUnitImpl &unit, UErrorCode &status) { + if (U_FAILURE(status)) return false; + + if (unit.complexity != UMEASURE_UNIT_SINGLE) { + return false; + } + if (unit.singleUnits.length() == 0) { + // Empty units means simple unit. + return true; + } + + auto singleUnit = *(unit.singleUnits[0]); + + if (singleUnit.dimensionality != 1 || singleUnit.unitPrefix != UMEASURE_PREFIX_ONE) { + return false; + } + + return true; +} + +/** + * Extract conversion rate from `source` to `target` + */ +// In ICU4J, this function is partially inlined in the UnitsConverter constructor. +void loadConversionRate(ConversionRate &conversionRate, const MeasureUnitImpl &source, + const MeasureUnitImpl &target, Convertibility unitsState, + const ConversionRates &ratesInfo, UErrorCode &status) { + // Represents the conversion factor from the source to the target. + Factor finalFactor; + + // Represents the conversion factor from the source to the base unit that specified in the conversion + // data which is considered as the root of the source and the target. + Factor sourceToBase = loadCompoundFactor(source, ratesInfo, status); + Factor targetToBase = loadCompoundFactor(target, ratesInfo, status); + + // Merger Factors + finalFactor.multiplyBy(sourceToBase); + if (unitsState == Convertibility::CONVERTIBLE) { + finalFactor.divideBy(targetToBase); + } else if (unitsState == Convertibility::RECIPROCAL) { + finalFactor.multiplyBy(targetToBase); + } else { + status = UErrorCode::U_ARGUMENT_TYPE_MISMATCH; + return; + } + + finalFactor.substituteConstants(); + + conversionRate.factorNum = finalFactor.factorNum; + conversionRate.factorDen = finalFactor.factorDen; + + // This code corresponds to ICU4J's ConversionRates.getOffset(). + // In case of simple units (such as: celsius or fahrenheit), offsets are considered. + if (checkSimpleUnit(source, status) && checkSimpleUnit(target, status)) { + conversionRate.sourceOffset = + sourceToBase.offset * sourceToBase.factorDen / sourceToBase.factorNum; + conversionRate.targetOffset = + targetToBase.offset * targetToBase.factorDen / targetToBase.factorNum; + } + // TODO(icu-units#127): should we consider failure if there's an offset for + // a not-simple-unit? What about kilokelvin / kilocelsius? + + conversionRate.reciprocal = unitsState == Convertibility::RECIPROCAL; +} + +struct UnitIndexAndDimension : UMemory { + int32_t index = 0; + int32_t dimensionality = 0; + + UnitIndexAndDimension(const SingleUnitImpl &singleUnit, int32_t multiplier) { + index = singleUnit.index; + dimensionality = singleUnit.dimensionality * multiplier; + } +}; + +void mergeSingleUnitWithDimension(MaybeStackVector<UnitIndexAndDimension> &unitIndicesWithDimension, + const SingleUnitImpl &shouldBeMerged, int32_t multiplier) { + for (int32_t i = 0; i < unitIndicesWithDimension.length(); i++) { + auto &unitWithIndex = *unitIndicesWithDimension[i]; + if (unitWithIndex.index == shouldBeMerged.index) { + unitWithIndex.dimensionality += shouldBeMerged.dimensionality * multiplier; + return; + } + } + + unitIndicesWithDimension.emplaceBack(shouldBeMerged, multiplier); +} + +void mergeUnitsAndDimensions(MaybeStackVector<UnitIndexAndDimension> &unitIndicesWithDimension, + const MeasureUnitImpl &shouldBeMerged, int32_t multiplier) { + for (int32_t unit_i = 0; unit_i < shouldBeMerged.singleUnits.length(); unit_i++) { + auto singleUnit = *shouldBeMerged.singleUnits[unit_i]; + mergeSingleUnitWithDimension(unitIndicesWithDimension, singleUnit, multiplier); + } +} + +UBool checkAllDimensionsAreZeros(const MaybeStackVector<UnitIndexAndDimension> &dimensionVector) { + for (int32_t i = 0; i < dimensionVector.length(); i++) { + if (dimensionVector[i]->dimensionality != 0) { + return false; + } + } + + return true; +} + +} // namespace + +// Conceptually, this modifies factor: factor *= baseStr^(signum*power). +// +// baseStr must be a known constant or a value that strToDouble() is able to +// parse. +void U_I18N_API addSingleFactorConstant(StringPiece baseStr, int32_t power, Signum signum, + Factor &factor, UErrorCode &status) { + if (baseStr == "ft_to_m") { + factor.constantExponents[CONSTANT_FT2M] += power * signum; + } else if (baseStr == "ft2_to_m2") { + factor.constantExponents[CONSTANT_FT2M] += 2 * power * signum; + } else if (baseStr == "ft3_to_m3") { + factor.constantExponents[CONSTANT_FT2M] += 3 * power * signum; + } else if (baseStr == "in3_to_m3") { + factor.constantExponents[CONSTANT_FT2M] += 3 * power * signum; + factor.factorDen *= 12 * 12 * 12; + } else if (baseStr == "gal_to_m3") { + factor.factorNum *= 231; + factor.constantExponents[CONSTANT_FT2M] += 3 * power * signum; + factor.factorDen *= 12 * 12 * 12; + } else if (baseStr == "gal_imp_to_m3") { + factor.constantExponents[CONSTANT_GAL_IMP2M3] += power * signum; + } else if (baseStr == "G") { + factor.constantExponents[CONSTANT_G] += power * signum; + } else if (baseStr == "gravity") { + factor.constantExponents[CONSTANT_GRAVITY] += power * signum; + } else if (baseStr == "lb_to_kg") { + factor.constantExponents[CONSTANT_LB2KG] += power * signum; + } else if (baseStr == "glucose_molar_mass") { + factor.constantExponents[CONSTANT_GLUCOSE_MOLAR_MASS] += power * signum; + } else if (baseStr == "item_per_mole") { + factor.constantExponents[CONSTANT_ITEM_PER_MOLE] += power * signum; + } else if (baseStr == "meters_per_AU") { + factor.constantExponents[CONSTANT_METERS_PER_AU] += power * signum; + } else if (baseStr == "PI") { + factor.constantExponents[CONSTANT_PI] += power * signum; + } else if (baseStr == "sec_per_julian_year") { + factor.constantExponents[CONSTANT_SEC_PER_JULIAN_YEAR] += power * signum; + } else if (baseStr == "speed_of_light_meters_per_second") { + factor.constantExponents[CONSTANT_SPEED_OF_LIGHT_METERS_PER_SECOND] += power * signum; + } else { + if (signum == Signum::NEGATIVE) { + factor.factorDen *= std::pow(strToDouble(baseStr, status), power); + } else { + factor.factorNum *= std::pow(strToDouble(baseStr, status), power); + } + } +} + +/** + * Extracts the compound base unit of a compound unit (`source`). For example, if the source unit is + * `square-mile-per-hour`, the compound base unit will be `square-meter-per-second` + */ +MeasureUnitImpl U_I18N_API extractCompoundBaseUnit(const MeasureUnitImpl &source, + const ConversionRates &conversionRates, + UErrorCode &status) { + + MeasureUnitImpl result; + if (U_FAILURE(status)) return result; + + const auto &singleUnits = source.singleUnits; + for (int i = 0, count = singleUnits.length(); i < count; ++i) { + const auto &singleUnit = *singleUnits[i]; + // Extract `ConversionRateInfo` using the absolute unit. For example: in case of `square-meter`, + // we will use `meter` + const auto rateInfo = + conversionRates.extractConversionInfo(singleUnit.getSimpleUnitID(), status); + if (U_FAILURE(status)) { + return result; + } + if (rateInfo == nullptr) { + status = U_INTERNAL_PROGRAM_ERROR; + return result; + } + + // Multiply the power of the singleUnit by the power of the baseUnit. For example, square-hectare + // must be pow4-meter. (NOTE: hectare --> square-meter) + auto baseUnits = + MeasureUnitImpl::forIdentifier(rateInfo->baseUnit.toStringPiece(), status).singleUnits; + for (int32_t i = 0, baseUnitsCount = baseUnits.length(); i < baseUnitsCount; i++) { + baseUnits[i]->dimensionality *= singleUnit.dimensionality; + // TODO: Deal with SI-prefix + result.appendSingleUnit(*baseUnits[i], status); + + if (U_FAILURE(status)) { + return result; + } + } + } + + return result; +} + +/** + * Determine the convertibility between `source` and `target`. + * For example: + * `meter` and `foot` are `CONVERTIBLE`. + * `meter-per-second` and `second-per-meter` are `RECIPROCAL`. + * `meter` and `pound` are `UNCONVERTIBLE`. + * + * NOTE: + * Only works with SINGLE and COMPOUND units. If one of the units is a + * MIXED unit, an error will occur. For more information, see UMeasureUnitComplexity. + */ +Convertibility U_I18N_API extractConvertibility(const MeasureUnitImpl &source, + const MeasureUnitImpl &target, + const ConversionRates &conversionRates, + UErrorCode &status) { + + if (source.complexity == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED || + target.complexity == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED) { + status = U_ARGUMENT_TYPE_MISMATCH; + return UNCONVERTIBLE; + } + + MeasureUnitImpl sourceBaseUnit = extractCompoundBaseUnit(source, conversionRates, status); + MeasureUnitImpl targetBaseUnit = extractCompoundBaseUnit(target, conversionRates, status); + if (U_FAILURE(status)) return UNCONVERTIBLE; + + MaybeStackVector<UnitIndexAndDimension> convertible; + MaybeStackVector<UnitIndexAndDimension> reciprocal; + + mergeUnitsAndDimensions(convertible, sourceBaseUnit, 1); + mergeUnitsAndDimensions(reciprocal, sourceBaseUnit, 1); + + mergeUnitsAndDimensions(convertible, targetBaseUnit, -1); + mergeUnitsAndDimensions(reciprocal, targetBaseUnit, 1); + + if (checkAllDimensionsAreZeros(convertible)) { + return CONVERTIBLE; + } + + if (checkAllDimensionsAreZeros(reciprocal)) { + return RECIPROCAL; + } + + return UNCONVERTIBLE; +} + +UnitsConverter::UnitsConverter(const MeasureUnitImpl &source, const MeasureUnitImpl &target, + const ConversionRates &ratesInfo, UErrorCode &status) + : conversionRate_(source.copy(status), target.copy(status)) { + this->init(ratesInfo, status); +} + +UnitsConverter::UnitsConverter(StringPiece sourceIdentifier, StringPiece targetIdentifier, + UErrorCode &status) + : conversionRate_(MeasureUnitImpl::forIdentifier(sourceIdentifier, status), + MeasureUnitImpl::forIdentifier(targetIdentifier, status)) { + if (U_FAILURE(status)) { + return; + } + + ConversionRates ratesInfo(status); + this->init(ratesInfo, status); +} + +void UnitsConverter::init(const ConversionRates &ratesInfo, UErrorCode &status) { + if (U_FAILURE(status)) { + return; + } + + if (this->conversionRate_.source.complexity == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED || + this->conversionRate_.target.complexity == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED) { + status = U_ARGUMENT_TYPE_MISMATCH; + return; + } + + Convertibility unitsState = extractConvertibility(this->conversionRate_.source, + this->conversionRate_.target, ratesInfo, status); + if (U_FAILURE(status)) return; + if (unitsState == Convertibility::UNCONVERTIBLE) { + status = U_ARGUMENT_TYPE_MISMATCH; + return; + } + + loadConversionRate(conversionRate_, conversionRate_.source, conversionRate_.target, unitsState, + ratesInfo, status); + +} + +int32_t UnitsConverter::compareTwoUnits(const MeasureUnitImpl &firstUnit, + const MeasureUnitImpl &secondUnit, + const ConversionRates &ratesInfo, UErrorCode &status) { + if (U_FAILURE(status)) { + return 0; + } + + if (firstUnit.complexity == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED || + secondUnit.complexity == UMeasureUnitComplexity::UMEASURE_UNIT_MIXED) { + status = U_ARGUMENT_TYPE_MISMATCH; + return 0; + } + + Convertibility unitsState = extractConvertibility(firstUnit, secondUnit, ratesInfo, status); + if (U_FAILURE(status)) { + return 0; + } + + if (unitsState == Convertibility::UNCONVERTIBLE || unitsState == Convertibility::RECIPROCAL) { + status = U_ARGUMENT_TYPE_MISMATCH; + return 0; + } + + // Represents the conversion factor from the firstUnit to the base + // unit that specified in the conversion data which is considered as + // the root of the firstUnit and the secondUnit. + Factor firstUnitToBase = loadCompoundFactor(firstUnit, ratesInfo, status); + Factor secondUnitToBase = loadCompoundFactor(secondUnit, ratesInfo, status); + + firstUnitToBase.substituteConstants(); + secondUnitToBase.substituteConstants(); + + double firstUnitToBaseConversionRate = firstUnitToBase.factorNum / firstUnitToBase.factorDen; + double secondUnitToBaseConversionRate = secondUnitToBase.factorNum / secondUnitToBase.factorDen; + + double diff = firstUnitToBaseConversionRate - secondUnitToBaseConversionRate; + if (diff > 0) { + return 1; + } + + if (diff < 0) { + return -1; + } + + return 0; +} + +double UnitsConverter::convert(double inputValue) const { + double result = + inputValue + conversionRate_.sourceOffset; // Reset the input to the target zero index. + // Convert the quantity to from the source scale to the target scale. + result *= conversionRate_.factorNum / conversionRate_.factorDen; + + result -= conversionRate_.targetOffset; // Set the result to its index. + + if (conversionRate_.reciprocal) { + if (result == 0) { + return uprv_getInfinity(); + } + result = 1.0 / result; + } + + return result; +} + +double UnitsConverter::convertInverse(double inputValue) const { + double result = inputValue; + if (conversionRate_.reciprocal) { + if (result == 0) { + return uprv_getInfinity(); + } + result = 1.0 / result; + } + result += conversionRate_.targetOffset; + result *= conversionRate_.factorDen / conversionRate_.factorNum; + result -= conversionRate_.sourceOffset; + return result; +} + +ConversionInfo UnitsConverter::getConversionInfo() const { + ConversionInfo result; + result.conversionRate = conversionRate_.factorNum / conversionRate_.factorDen; + result.offset = + (conversionRate_.sourceOffset * (conversionRate_.factorNum / conversionRate_.factorDen)) - + conversionRate_.targetOffset; + result.reciprocal = conversionRate_.reciprocal; + + return result; +} + +} // namespace units +U_NAMESPACE_END + +#endif /* #if !UCONFIG_NO_FORMATTING */ |