mirror of
https://github.com/unicode-org/icu.git
synced 2025-04-08 06:53:45 +00:00
commit
9bcc4b698f
8 changed files with 649 additions and 132 deletions
|
@ -5,7 +5,10 @@
|
|||
|
||||
#if !UCONFIG_NO_FORMATTING
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "charstr.h"
|
||||
#include "double-conversion.h"
|
||||
#include "measunit_impl.h"
|
||||
#include "unicode/errorcode.h"
|
||||
#include "unicode/measunit.h"
|
||||
|
@ -15,6 +18,164 @@
|
|||
U_NAMESPACE_BEGIN
|
||||
|
||||
namespace {
|
||||
|
||||
/* Internal Structure */
|
||||
|
||||
enum Constants {
|
||||
CONSTANT_FT2M, // ft2m stands for foot to meter.
|
||||
CONSTANT_PI, // PI
|
||||
CONSTANT_GRAVITY, // Gravity
|
||||
CONSTANT_G,
|
||||
CONSTANT_GAL_IMP2M3, // Gallon imp to m3
|
||||
CONSTANT_LB2KG, // Pound to Kilogram
|
||||
|
||||
// Must be the last element.
|
||||
CONSTANTS_COUNT
|
||||
};
|
||||
|
||||
typedef enum SigNum {
|
||||
NEGATIVE = -1,
|
||||
POSITIVE = 1,
|
||||
} SigNum;
|
||||
|
||||
/* Represents a conversion factor */
|
||||
struct Factor {
|
||||
double factorNum = 1;
|
||||
double factorDen = 1;
|
||||
double offset = 0;
|
||||
bool reciprocal = false;
|
||||
int32_t constants[CONSTANTS_COUNT] = {};
|
||||
|
||||
void multiplyBy(const Factor &rhs) {
|
||||
factorNum *= rhs.factorNum;
|
||||
factorDen *= rhs.factorDen;
|
||||
for (int i = 0; i < CONSTANTS_COUNT; i++) {
|
||||
constants[i] += rhs.constants[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 divideBy(const Factor &rhs) {
|
||||
factorNum *= rhs.factorDen;
|
||||
factorDen *= rhs.factorNum;
|
||||
for (int i = 0; i < CONSTANTS_COUNT; i++) {
|
||||
constants[i] -= rhs.constants[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);
|
||||
}
|
||||
|
||||
// Apply the power to the factor.
|
||||
void power(int32_t power) {
|
||||
// multiply all the constant by the power.
|
||||
for (int i = 0; i < CONSTANTS_COUNT; i++) {
|
||||
constants[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);
|
||||
}
|
||||
}
|
||||
|
||||
// Flip the `Factor`, for example, factor= 2/3, flippedFactor = 3/2
|
||||
void flip() {
|
||||
std::swap(factorNum, factorDen);
|
||||
|
||||
for (int i = 0; i < CONSTANTS_COUNT; i++) {
|
||||
constants[i] *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply SI prefix to the `Factor`
|
||||
void applySiPrefix(UMeasureSIPrefix siPrefix) {
|
||||
if (siPrefix == UMeasureSIPrefix::UMEASURE_SI_PREFIX_ONE) return; // No need to do anything
|
||||
|
||||
double siApplied = std::pow(10.0, std::abs(siPrefix));
|
||||
|
||||
if (siPrefix < 0) {
|
||||
factorDen *= siApplied;
|
||||
return;
|
||||
}
|
||||
|
||||
factorNum *= siApplied;
|
||||
}
|
||||
|
||||
void substituteConstants() {
|
||||
double constantsValues[CONSTANTS_COUNT];
|
||||
|
||||
// TODO: Load those constant values from units data.
|
||||
constantsValues[CONSTANT_FT2M] = 0.3048;
|
||||
constantsValues[CONSTANT_PI] = 411557987.0 / 131002976.0;
|
||||
constantsValues[CONSTANT_GRAVITY] = 9.80665;
|
||||
constantsValues[CONSTANT_G] = 6.67408E-11;
|
||||
constantsValues[CONSTANT_LB2KG] = 0.45359237;
|
||||
constantsValues[CONSTANT_GAL_IMP2M3] = 0.00454609;
|
||||
|
||||
for (int i = 0; i < CONSTANTS_COUNT; i++) {
|
||||
if (this->constants[i] == 0) { continue;}
|
||||
|
||||
auto absPower = std::abs(this->constants[i]);
|
||||
SigNum powerSig = this->constants[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->constants[i] = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`
|
||||
|
@ -32,7 +193,7 @@ MeasureUnit extractCompoundBaseUnit(const MeasureUnit &source, const ConversionR
|
|||
// we will use `meter`
|
||||
const auto singleUnitImpl = SingleUnitImpl::forMeasureUnit(singleUnit, status);
|
||||
const auto rateInfo = conversionRates.extractConversionInfo(singleUnitImpl.identifier, status);
|
||||
if (U_FAILURE(status)) return result;
|
||||
if (U_FAILURE(status)) { return result; }
|
||||
if (rateInfo == nullptr) {
|
||||
status = U_INTERNAL_PROGRAM_ERROR;
|
||||
return result;
|
||||
|
@ -56,6 +217,206 @@ MeasureUnit extractCompoundBaseUnit(const MeasureUnit &source, const ConversionR
|
|||
return result;
|
||||
}
|
||||
|
||||
// TODO: Load those constant from units data.
|
||||
/*
|
||||
* Adds a single factor element to the `Factor`. e.g "ft3m", "2.333" or "cup2m3". But not "cup2m3^3".
|
||||
*/
|
||||
void addSingleFactorConstant(StringPiece baseStr, int32_t power, SigNum sigNum, Factor &factor,
|
||||
UErrorCode &status) {
|
||||
|
||||
if (baseStr == "ft_to_m") {
|
||||
factor.constants[CONSTANT_FT2M] += power * sigNum;
|
||||
} else if (baseStr == "ft2_to_m2") {
|
||||
factor.constants[CONSTANT_FT2M] += 2 * power * sigNum;
|
||||
} else if (baseStr == "ft3_to_m3") {
|
||||
factor.constants[CONSTANT_FT2M] += 3 * power * sigNum;
|
||||
} else if (baseStr == "in3_to_m3") {
|
||||
factor.constants[CONSTANT_FT2M] += 3 * power * sigNum;
|
||||
factor.factorDen *= 12 * 12 * 12;
|
||||
} else if (baseStr == "gal_to_m3") {
|
||||
factor.factorNum *= 231;
|
||||
factor.constants[CONSTANT_FT2M] += 3 * power * sigNum;
|
||||
factor.factorDen *= 12 * 12 * 12;
|
||||
} else if (baseStr == "gal_imp_to_m3") {
|
||||
factor.constants[CONSTANT_GAL_IMP2M3] += power * sigNum;
|
||||
} else if (baseStr == "G") {
|
||||
factor.constants[CONSTANT_G] += power * sigNum;
|
||||
} else if (baseStr == "gravity") {
|
||||
factor.constants[CONSTANT_GRAVITY] += power * sigNum;
|
||||
} else if (baseStr == "lb_to_kg") {
|
||||
factor.constants[CONSTANT_LB2KG] += power * sigNum;
|
||||
} else if (baseStr == "PI") {
|
||||
factor.constants[CONSTANT_PI] += power * sigNum;
|
||||
} else {
|
||||
if (sigNum == SigNum::NEGATIVE) {
|
||||
factor.factorDen *= std::pow(strToDouble(baseStr, status), power);
|
||||
} else {
|
||||
factor.factorNum *= std::pow(strToDouble(baseStr, status), power);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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.
|
||||
Factor loadCompoundFactor(const MeasureUnit &source, const ConversionRates &ratesInfo,
|
||||
UErrorCode &status) {
|
||||
|
||||
Factor result;
|
||||
MeasureUnitImpl memory;
|
||||
const auto &compoundSourceUnit = MeasureUnitImpl::forMeasureUnit(source, memory, status);
|
||||
if (U_FAILURE(status)) return result;
|
||||
|
||||
for (int32_t i = 0, n = compoundSourceUnit.units.length(); i < n; i++) {
|
||||
auto singleUnit = *compoundSourceUnit.units[i]; // a SingleUnitImpl
|
||||
|
||||
Factor singleFactor = loadSingleFactor(singleUnit.identifier, ratesInfo, status);
|
||||
if (U_FAILURE(status)) return result;
|
||||
|
||||
// Apply SiPrefix before the power, because the power may be will flip the factor.
|
||||
singleFactor.applySiPrefix(singleUnit.siPrefix);
|
||||
|
||||
// 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.
|
||||
*/
|
||||
UBool checkSimpleUnit(const MeasureUnit &unit, UErrorCode &status) {
|
||||
MeasureUnitImpl memory;
|
||||
const auto &compoundSourceUnit = MeasureUnitImpl::forMeasureUnit(unit, memory, status);
|
||||
if (U_FAILURE(status)) return false;
|
||||
|
||||
if (compoundSourceUnit.complexity != UMEASURE_UNIT_SINGLE) { return false; }
|
||||
|
||||
U_ASSERT(compoundSourceUnit.units.length() == 1);
|
||||
auto singleUnit = *(compoundSourceUnit.units[0]);
|
||||
|
||||
if (singleUnit.dimensionality != 1 || singleUnit.siPrefix != UMEASURE_SI_PREFIX_ONE) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract conversion rate from `source` to `target`
|
||||
*/
|
||||
void loadConversionRate(ConversionRate &conversionRate, const MeasureUnit &source,
|
||||
const MeasureUnit &target, UnitsConvertibilityState 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 == UnitsConvertibilityState::CONVERTIBLE) {
|
||||
finalFactor.divideBy(targetToBase);
|
||||
} else if (unitsState == UnitsConvertibilityState::RECIPROCAL) {
|
||||
finalFactor.multiplyBy(targetToBase);
|
||||
} else {
|
||||
status = UErrorCode::U_ARGUMENT_TYPE_MISMATCH;
|
||||
return;
|
||||
}
|
||||
|
||||
finalFactor.substituteConstants();
|
||||
|
||||
conversionRate.factorNum = finalFactor.factorNum;
|
||||
conversionRate.factorDen = finalFactor.factorDen;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
conversionRate.reciprocal = unitsState == UnitsConvertibilityState::RECIPROCAL;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UnitsConvertibilityState U_I18N_API checkConvertibility(const MeasureUnit &source,
|
||||
|
@ -73,6 +434,35 @@ UnitsConvertibilityState U_I18N_API checkConvertibility(const MeasureUnit &sourc
|
|||
return UNCONVERTIBLE;
|
||||
}
|
||||
|
||||
UnitConverter::UnitConverter(MeasureUnit source, MeasureUnit target, const ConversionRates &ratesInfo,
|
||||
UErrorCode &status) {
|
||||
UnitsConvertibilityState unitsState = checkConvertibility(source, target, ratesInfo, status);
|
||||
if (U_FAILURE(status)) return;
|
||||
if (unitsState == UnitsConvertibilityState::UNCONVERTIBLE) {
|
||||
status = U_INTERNAL_PROGRAM_ERROR;
|
||||
return;
|
||||
}
|
||||
|
||||
conversionRate_.source = source;
|
||||
conversionRate_.target = target;
|
||||
|
||||
loadConversionRate(conversionRate_, source, target, unitsState, ratesInfo, status);
|
||||
}
|
||||
|
||||
double UnitConverter::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 (result == 0)
|
||||
return 0.0; // If the result is zero, it does not matter if the conversion are reciprocal or not.
|
||||
if (conversionRate_.reciprocal) { result = 1.0 / result; }
|
||||
return result;
|
||||
}
|
||||
|
||||
U_NAMESPACE_END
|
||||
|
||||
#endif /* #if !UCONFIG_NO_FORMATTING */
|
||||
#endif /* #if !UCONFIG_NO_FORMATTING */
|
||||
|
|
|
@ -15,6 +15,19 @@
|
|||
|
||||
U_NAMESPACE_BEGIN
|
||||
|
||||
/**
|
||||
* Represents the conversion rate between `source` and `target`.
|
||||
*/
|
||||
struct ConversionRate {
|
||||
MeasureUnit source;
|
||||
MeasureUnit target;
|
||||
double factorNum = 1;
|
||||
double factorDen = 1;
|
||||
double sourceOffset = 0;
|
||||
double targetOffset = 0;
|
||||
bool reciprocal = false;
|
||||
};
|
||||
|
||||
enum U_I18N_API UnitsConvertibilityState {
|
||||
RECIPROCAL,
|
||||
CONVERTIBLE,
|
||||
|
@ -26,6 +39,37 @@ UnitsConvertibilityState U_I18N_API checkConvertibility(const MeasureUnit &sourc
|
|||
const ConversionRates &conversionRates,
|
||||
UErrorCode &status);
|
||||
|
||||
/**
|
||||
* Converts from a source `MeasureUnit` to a target `MeasureUnit`.
|
||||
*/
|
||||
class U_I18N_API UnitConverter {
|
||||
public:
|
||||
/**
|
||||
* Constructor of `UnitConverter`.
|
||||
* NOTE:
|
||||
* - source and target must be under the same category
|
||||
* - e.g. meter to mile --> both of them are length units.
|
||||
*
|
||||
* @param source represents the source unit.
|
||||
* @param target represents the target unit.
|
||||
* @param status
|
||||
*/
|
||||
UnitConverter(MeasureUnit source, MeasureUnit target,
|
||||
const ConversionRates &ratesInfo, UErrorCode &status);
|
||||
|
||||
/**
|
||||
* Convert a value in the source unit to another value in the target unit.
|
||||
*
|
||||
* @param input_value the value that needs to be converted.
|
||||
* @param output_value the value that holds the result of the conversion.
|
||||
* @param status
|
||||
*/
|
||||
double convert(double inputValue) const;
|
||||
|
||||
private:
|
||||
ConversionRate conversionRate_;
|
||||
};
|
||||
|
||||
U_NAMESPACE_END
|
||||
|
||||
#endif //__UNITCONVERTER_H__
|
||||
|
|
|
@ -15,6 +15,17 @@ U_NAMESPACE_BEGIN
|
|||
|
||||
namespace {
|
||||
|
||||
void trimSpaces(CharString& factor, UErrorCode& status){
|
||||
CharString trimmed;
|
||||
for (int i = 0 ; i < factor.length(); i++) {
|
||||
if (factor[i] == ' ') continue;
|
||||
|
||||
trimmed.append(factor[i], status);
|
||||
}
|
||||
|
||||
factor = std::move(trimmed);
|
||||
}
|
||||
|
||||
/**
|
||||
* A ResourceSink that collects conversion rate information.
|
||||
*
|
||||
|
@ -84,6 +95,7 @@ class ConversionRateDataSink : public ResourceSink {
|
|||
cr->sourceUnit.append(srcUnit, status);
|
||||
cr->baseUnit.appendInvariantChars(baseUnit, status);
|
||||
cr->factor.appendInvariantChars(factor, status);
|
||||
trimSpaces(cr->factor, status);
|
||||
if (!offset.isBogus()) cr->offset.appendInvariantChars(offset, status);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,6 +68,10 @@ class U_I18N_API ConversionRates {
|
|||
*/
|
||||
const ConversionRateInfo *extractConversionInfo(StringPiece source, UErrorCode &status) const;
|
||||
|
||||
// TODO(younies): hugovdm added this to resolve "git merge" issues. The API
|
||||
// should be improved to make this unnecessary.
|
||||
const MaybeStackVector<ConversionRateInfo> *getInternalList() const { return &conversionInfo_; };
|
||||
|
||||
private:
|
||||
MaybeStackVector<ConversionRateInfo> conversionInfo_;
|
||||
};
|
||||
|
|
|
@ -1075,7 +1075,7 @@ group: units
|
|||
group: unitsformatter
|
||||
unitsdata.o unitconverter.o
|
||||
deps
|
||||
resourcebundle units_extra
|
||||
resourcebundle units_extra double_conversion
|
||||
|
||||
group: decnumber
|
||||
decContext.o decNumber.o
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <cmath>
|
||||
#include <math.h>
|
||||
|
||||
#include "unicode/ctest.h" // for str_timeDelta
|
||||
#include "unicode/curramt.h"
|
||||
|
@ -2172,6 +2173,23 @@ UBool IntlTest::assertNotEquals(const char* message,
|
|||
return TRUE;
|
||||
}
|
||||
|
||||
// http://junit.sourceforge.net/javadoc/org/junit/Assert.html#assertEquals(java.lang.String,%20double,%20double,%20double)
|
||||
UBool IntlTest::assertEqualsNear(const char *message, double expected, double actual, double precision) {
|
||||
double diff = std::abs(expected - actual);
|
||||
double diffPercent = expected != 0? diff / expected : diff; // If the expected is equals zero, we
|
||||
|
||||
if (diffPercent > precision) {
|
||||
errln((UnicodeString) "FAIL: " + message + "; got " + actual + "; expected " + expected);
|
||||
return FALSE;
|
||||
}
|
||||
#ifdef VERBOSE_ASSERTIONS
|
||||
else {
|
||||
logln((UnicodeString) "Ok: " + message + "; got " + expected);
|
||||
}
|
||||
#endif
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static char ASSERT_BUF[256];
|
||||
|
||||
static const char* extractToAssertBuf(const UnicodeString& message) {
|
||||
|
|
|
@ -303,6 +303,7 @@ public:
|
|||
UBool assertEquals(const char* message, const UnicodeSet& expected, const UnicodeSet& actual);
|
||||
UBool assertEquals(const char* message,
|
||||
const std::vector<std::string>& expected, const std::vector<std::string>& actual);
|
||||
UBool assertEqualsNear(const char* message, double expected, double actual, double precision);
|
||||
#if !UCONFIG_NO_FORMATTING
|
||||
UBool assertEquals(const char* message, const Formattable& expected,
|
||||
const Formattable& actual, UBool possibleDataError=FALSE);
|
||||
|
|
|
@ -29,11 +29,11 @@ class UnitsTest : public IntlTest {
|
|||
void testConversionCapability();
|
||||
void testConversions();
|
||||
void testPreferences();
|
||||
// void testBasic();
|
||||
// void testSiPrefixes();
|
||||
// void testMass();
|
||||
// void testTemperature();
|
||||
// void testArea();
|
||||
void testBasic();
|
||||
void testSiPrefixes();
|
||||
void testMass();
|
||||
void testTemperature();
|
||||
void testArea();
|
||||
};
|
||||
|
||||
extern IntlTest *createUnitsTest() { return new UnitsTest(); }
|
||||
|
@ -44,11 +44,11 @@ void UnitsTest::runIndexedTest(int32_t index, UBool exec, const char *&name, cha
|
|||
TESTCASE_AUTO(testConversionCapability);
|
||||
TESTCASE_AUTO(testConversions);
|
||||
TESTCASE_AUTO(testPreferences);
|
||||
// TESTCASE_AUTO(testBasic);
|
||||
// TESTCASE_AUTO(testSiPrefixes);
|
||||
// TESTCASE_AUTO(testMass);
|
||||
// TESTCASE_AUTO(testTemperature);
|
||||
// TESTCASE_AUTO(testArea);
|
||||
TESTCASE_AUTO(testBasic);
|
||||
TESTCASE_AUTO(testSiPrefixes);
|
||||
TESTCASE_AUTO(testMass);
|
||||
TESTCASE_AUTO(testTemperature);
|
||||
TESTCASE_AUTO(testArea);
|
||||
TESTCASE_AUTO_END;
|
||||
}
|
||||
|
||||
|
@ -90,122 +90,163 @@ void UnitsTest::testConversionCapability() {
|
|||
}
|
||||
}
|
||||
|
||||
// void UnitsTest::testBasic() {
|
||||
// IcuTestErrorCode status(*this, "Units testBasic");
|
||||
void UnitsTest::testBasic() {
|
||||
IcuTestErrorCode status(*this, "Units testBasic");
|
||||
|
||||
// // Test Cases
|
||||
// struct TestCase {
|
||||
// const char16_t *source;
|
||||
// const char16_t *target;
|
||||
// const double inputValue;
|
||||
// const double expectedValue;
|
||||
// } testCases[]{{u"meter", u"foot", 1.0, 3.28084}, {u"kilometer", u"foot", 1.0, 328.084}};
|
||||
// Test Cases
|
||||
struct TestCase {
|
||||
StringPiece source;
|
||||
StringPiece target;
|
||||
const double inputValue;
|
||||
const double expectedValue;
|
||||
} testCases[]{
|
||||
{"meter", "foot", 1.0, 3.28084}, //
|
||||
{"kilometer", "foot", 1.0, 3280.84}, //
|
||||
};
|
||||
|
||||
// for (const auto &testCase : testCases) {
|
||||
// assertEquals("test convert", testConvert(testCase.source, testCase.target,
|
||||
// testCase.inputValue),
|
||||
// testCase.expectedValue);
|
||||
// }
|
||||
// }
|
||||
for (const auto &testCase : testCases) {
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
|
||||
// void UnitsTest::testSiPrefixes() {
|
||||
// IcuTestErrorCode status(*this, "Units testSiPrefixes");
|
||||
// // Test Cases
|
||||
// struct TestCase {
|
||||
// const char16_t *source;
|
||||
// const char16_t *target;
|
||||
// const double inputValue;
|
||||
// const double expectedValue;
|
||||
// } testCases[]{
|
||||
// {u"gram", u"kilogram", 1.0, 0.001}, //
|
||||
// {u"milligram", u"kilogram", 1.0, 0.000001}, //
|
||||
// {u"microgram", u"kilogram", 1.0, 0.000000001}, //
|
||||
// {u"megawatt", u"watt", 1, 1000000}, //
|
||||
// {u"megawatt", u"kilowatt", 1.0, 1000}, //
|
||||
// {u"gigabyte", u"byte", 1, 1000000000} //
|
||||
// };
|
||||
MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status);
|
||||
MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status);
|
||||
|
||||
// for (const auto &testCase : testCases) {
|
||||
// assertEquals("test convert", testConvert(testCase.source, testCase.target,
|
||||
// testCase.inputValue),
|
||||
// testCase.expectedValue);
|
||||
// }
|
||||
// }
|
||||
ConversionRates conversionRates(status);
|
||||
UnitConverter converter(source, target, conversionRates, status);
|
||||
|
||||
// void UnitsTest::testMass() {
|
||||
// IcuTestErrorCode status(*this, "Units testMass");
|
||||
assertEqualsNear("test conversion", testCase.expectedValue,
|
||||
converter.convert(testCase.inputValue), 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
// // Test Cases
|
||||
// struct TestCase {
|
||||
// const char16_t *source;
|
||||
// const char16_t *target;
|
||||
// const double inputValue;
|
||||
// const double expectedValue;
|
||||
// } testCases[]{
|
||||
// {u"gram", u"kilogram", 1.0, 0.001}, //
|
||||
// {u"pound", u"kilogram", 1.0, 0.453592}, //
|
||||
// {u"pound", u"kilogram", 2.0, 0.907185}, //
|
||||
// {u"ounce", u"pound", 16.0, 1.0}, //
|
||||
// {u"ounce", u"kilogram", 16.0, 0.453592}, //
|
||||
// {u"ton", u"pound", 1.0, 2000}, //
|
||||
// {u"stone", u"pound", 1.0, 14}, //
|
||||
// {u"stone", u"kilogram", 1.0, 6.35029} //
|
||||
// };
|
||||
void UnitsTest::testSiPrefixes() {
|
||||
IcuTestErrorCode status(*this, "Units testSiPrefixes");
|
||||
// Test Cases
|
||||
struct TestCase {
|
||||
StringPiece source;
|
||||
StringPiece target;
|
||||
const double inputValue;
|
||||
const double expectedValue;
|
||||
} testCases[]{
|
||||
{"gram", "kilogram", 1.0, 0.001}, //
|
||||
{"milligram", "kilogram", 1.0, 0.000001}, //
|
||||
{"microgram", "kilogram", 1.0, 0.000000001}, //
|
||||
{"megagram", "gram", 1.0, 1000000}, //
|
||||
{"megagram", "kilogram", 1.0, 1000}, //
|
||||
{"gigabyte", "byte", 1.0, 1000000000}, //
|
||||
// TODO: Fix `watt` probelms.
|
||||
// {"megawatt", "watt", 1.0, 1000000}, //
|
||||
// {"megawatt", "kilowatt", 1.0, 1000}, //
|
||||
};
|
||||
|
||||
// for (const auto &testCase : testCases) {
|
||||
// assertEquals("test convert", testConvert(testCase.source, testCase.target,
|
||||
// testCase.inputValue),
|
||||
// testCase.expectedValue);
|
||||
// }
|
||||
// }
|
||||
for (const auto &testCase : testCases) {
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
|
||||
// void UnitsTest::testTemperature() {
|
||||
// IcuTestErrorCode status(*this, "Units testTemperature");
|
||||
// // Test Cases
|
||||
// struct TestCase {
|
||||
// const char16_t *source;
|
||||
// const char16_t *target;
|
||||
// const double inputValue;
|
||||
// const double expectedValue;
|
||||
// } testCases[]{
|
||||
// {u"celsius", u"fahrenheit", 0.0, 32.0}, //
|
||||
// {u"celsius", u"fahrenheit", 10.0, 50.0}, //
|
||||
// {u"fahrenheit", u"celsius", 32.0, 0.0}, //
|
||||
// {u"fahrenheit", u"celsius", 89.6, 32}, //
|
||||
// {u"kelvin", u"fahrenheit", 0.0, -459.67}, //
|
||||
// {u"kelvin", u"fahrenheit", 300, 80.33}, //
|
||||
// {u"kelvin", u"celsius", 0.0, -273.15}, //
|
||||
// {u"kelvin", u"celsius", 300.0, 26.85} //
|
||||
// };
|
||||
MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status);
|
||||
MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status);
|
||||
|
||||
// for (const auto &testCase : testCases) {
|
||||
// assertEquals("test convert", testConvert(testCase.source, testCase.target,
|
||||
// testCase.inputValue),
|
||||
// testCase.expectedValue);
|
||||
// }
|
||||
// }
|
||||
ConversionRates conversionRates(status);
|
||||
UnitConverter converter(source, target, conversionRates, status);
|
||||
|
||||
// void UnitsTest::testArea() {
|
||||
// IcuTestErrorCode status(*this, "Units Area");
|
||||
assertEqualsNear("test conversion", testCase.expectedValue,
|
||||
converter.convert(testCase.inputValue), 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
// // Test Cases
|
||||
// struct TestCase {
|
||||
// const char16_t *source;
|
||||
// const char16_t *target;
|
||||
// const double inputValue;
|
||||
// const double expectedValue;
|
||||
// } testCases[]{
|
||||
// {u"square-meter", u"square-yard", 10.0, 11.9599}, //
|
||||
// {u"hectare", u"square-yard", 1.0, 11959.9}, //
|
||||
// {u"square-mile", u"square-foot", 0.0001, 2787.84} //
|
||||
// };
|
||||
void UnitsTest::testMass() {
|
||||
IcuTestErrorCode status(*this, "Units testMass");
|
||||
|
||||
// for (const auto &testCase : testCases) {
|
||||
// assertEquals("test convert", testConvert(testCase.source, testCase.target,
|
||||
// testCase.inputValue),
|
||||
// testCase.expectedValue);
|
||||
// }
|
||||
// }
|
||||
// Test Cases
|
||||
struct TestCase {
|
||||
StringPiece source;
|
||||
StringPiece target;
|
||||
const double inputValue;
|
||||
const double expectedValue;
|
||||
} testCases[]{
|
||||
{"gram", "kilogram", 1.0, 0.001}, //
|
||||
{"pound", "kilogram", 1.0, 0.453592}, //
|
||||
{"pound", "kilogram", 2.0, 0.907185}, //
|
||||
{"ounce", "pound", 16.0, 1.0}, //
|
||||
{"ounce", "kilogram", 16.0, 0.453592}, //
|
||||
{"ton", "pound", 1.0, 2000}, //
|
||||
{"stone", "pound", 1.0, 14}, //
|
||||
{"stone", "kilogram", 1.0, 6.35029} //
|
||||
};
|
||||
|
||||
for (const auto &testCase : testCases) {
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
|
||||
MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status);
|
||||
MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status);
|
||||
|
||||
ConversionRates conversionRates(status);
|
||||
UnitConverter converter(source, target, conversionRates, status);
|
||||
|
||||
assertEqualsNear("test conversion", testCase.expectedValue,
|
||||
converter.convert(testCase.inputValue), 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
void UnitsTest::testTemperature() {
|
||||
IcuTestErrorCode status(*this, "Units testTemperature");
|
||||
// Test Cases
|
||||
struct TestCase {
|
||||
StringPiece source;
|
||||
StringPiece target;
|
||||
const double inputValue;
|
||||
const double expectedValue;
|
||||
} testCases[]{
|
||||
{"celsius", "fahrenheit", 0.0, 32.0}, //
|
||||
{"celsius", "fahrenheit", 10.0, 50.0}, //
|
||||
{"fahrenheit", "celsius", 32.0, 0.0}, //
|
||||
{"fahrenheit", "celsius", 89.6, 32}, //
|
||||
{"kelvin", "fahrenheit", 0.0, -459.67}, //
|
||||
{"kelvin", "fahrenheit", 300, 80.33}, //
|
||||
{"kelvin", "celsius", 0.0, -273.15}, //
|
||||
{"kelvin", "celsius", 300.0, 26.85} //
|
||||
};
|
||||
|
||||
for (const auto &testCase : testCases) {
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
|
||||
MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status);
|
||||
MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status);
|
||||
|
||||
ConversionRates conversionRates(status);
|
||||
UnitConverter converter(source, target, conversionRates, status);
|
||||
|
||||
assertEqualsNear("test conversion", testCase.expectedValue,
|
||||
converter.convert(testCase.inputValue), 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
void UnitsTest::testArea() {
|
||||
IcuTestErrorCode status(*this, "Units Area");
|
||||
|
||||
// Test Cases
|
||||
struct TestCase {
|
||||
StringPiece source;
|
||||
StringPiece target;
|
||||
const double inputValue;
|
||||
const double expectedValue;
|
||||
} testCases[]{
|
||||
{"square-meter", "square-yard", 10.0, 11.9599}, //
|
||||
{"hectare", "square-yard", 1.0, 11959.9}, //
|
||||
{"square-mile", "square-foot", 0.0001, 2787.84} //
|
||||
};
|
||||
|
||||
for (const auto &testCase : testCases) {
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
|
||||
MeasureUnit source = MeasureUnit::forIdentifier(testCase.source, status);
|
||||
MeasureUnit target = MeasureUnit::forIdentifier(testCase.target, status);
|
||||
|
||||
ConversionRates conversionRates(status);
|
||||
UnitConverter converter(source, target, conversionRates, status);
|
||||
|
||||
assertEqualsNear("test conversion", testCase.expectedValue,
|
||||
converter.convert(testCase.inputValue), 0.001);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims whitespace (spaces only) off of the specified string.
|
||||
|
@ -233,10 +274,19 @@ struct UnitsTestContext {
|
|||
};
|
||||
|
||||
/**
|
||||
* WIP(hugovdm): deals with a single data-driven unit test for unit conversions.
|
||||
* This is a UParseLineFn as required by u_parseDelimitedFile.
|
||||
* Deals with a single data-driven unit test for unit conversions.
|
||||
*
|
||||
* context must point at a UnitsTestContext struct.
|
||||
* This is a UParseLineFn as required by u_parseDelimitedFile, intended for
|
||||
* parsing unitsTest.txt.
|
||||
*
|
||||
* @param context Must point at a UnitsTestContext struct.
|
||||
* @param fields A list of pointer-pairs, each pair pointing at the start and
|
||||
* end of each field. End pointers are important because these are *not*
|
||||
* null-terminated strings. (Interpreted as a null-terminated string,
|
||||
* fields[0][0] points at the whole line.)
|
||||
* @param fieldCount The number of fields (pointer pairs) passed to the fields
|
||||
* parameter.
|
||||
* @param pErrorCode Receives status.
|
||||
*/
|
||||
void unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, UErrorCode *pErrorCode) {
|
||||
if (U_FAILURE(*pErrorCode)) { return; }
|
||||
|
@ -282,23 +332,21 @@ void unitsTestDataLineFn(void *context, char *fields[][2], int32_t fieldCount, U
|
|||
if (status.errIfFailureAndReset("msg construction")) { return; }
|
||||
unitsTest->assertNotEquals(msg.data(), UNCONVERTIBLE, convertibility);
|
||||
|
||||
// TODO(hugovdm,younies): the following code can be uncommented (and
|
||||
// fixed) once merged with a UnitConverter branch:
|
||||
// UnitConverter converter(sourceUnit, targetUnit, unitsTest->conversionRates_, status);
|
||||
// if (status.errIfFailureAndReset("constructor: UnitConverter(<%s>, <%s>, status)",
|
||||
// sourceUnit.getIdentifier(), targetUnit.getIdentifier())) {
|
||||
// return;
|
||||
// }
|
||||
// double got = converter.convert(1000);
|
||||
// unitsTest->assertEqualsNear(fields[0][0], expected, got, 0.0001);
|
||||
// Conversion:
|
||||
UnitConverter converter(sourceUnit, targetUnit, *ctx->conversionRates, status);
|
||||
if (status.errIfFailureAndReset("constructor: UnitConverter(<%s>, <%s>, status)",
|
||||
sourceUnit.getIdentifier(), targetUnit.getIdentifier())) {
|
||||
return;
|
||||
}
|
||||
double got = converter.convert(1000);
|
||||
msg.clear();
|
||||
msg.append("Converting 1000 ", status).append(x, status).append(" to ", status).append(y, status);
|
||||
unitsTest->assertEqualsNear(msg.data(), expected, got, 0.0001);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs data-driven unit tests for unit conversion. It looks for the test cases
|
||||
* in source/test/testdata/units/unitsTest.txt, which originates in CLDR.
|
||||
*
|
||||
* TODO(hugovdm,younies): add conversion testing in unitsTestDataLineFn (it only
|
||||
* tests convertability at the moment).
|
||||
*/
|
||||
void UnitsTest::testConversions() {
|
||||
const char *filename = "unitsTest.txt";
|
||||
|
|
Loading…
Add table
Reference in a new issue