Skip to content

Commit

Permalink
ICU-22655 Implement "special" conversion for speed-beaufort, part 2 i…
Browse files Browse the repository at this point in the history
…cu4c
  • Loading branch information
pedberg-icu committed Mar 10, 2024
1 parent 4c664b2 commit ceee4f0
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 41 deletions.
261 changes: 228 additions & 33 deletions icu4c/source/i18n/units_converter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,49 +270,96 @@ UBool checkSimpleUnit(const MeasureUnitImpl &unit, UErrorCode &status) {
return true;
}

// Map the MeasureUnitImpl for a simpleUnit to a SingleUnitImpl, then use that
// SingleUnitImpl's simpleUnitID to get the corresponding ConversionRateInfo;
// from that we get the specialMappingName (which may be empty if the simple unit
// converts to base using factor + offset instelad of a special mapping).
CharString getSpecialMappingName(const MeasureUnitImpl &simpleUnit, const ConversionRates &ratesInfo,
UErrorCode &status) {
if (!checkSimpleUnit(simpleUnit, status)) {
return CharString();
}
SingleUnitImpl singleUnit = *simpleUnit.singleUnits[0];
const auto conversionUnit = ratesInfo.extractConversionInfo(singleUnit.getSimpleUnitID(), status);
if (U_FAILURE(status)) {
return CharString();
}
if (conversionUnit == nullptr) {
status = U_INTERNAL_PROGRAM_ERROR;
return CharString();
}
CharString result;
result.copyFrom(conversionUnit->specialMappingName, status);
return result;
}

/**
* Extract conversion rate from `source` to `target`
*/
// In ICU4J, this function is partially inlined in the UnitsConverter constructor.
// TODO ICU-22683: Consider splitting handling of special mappings into separate class
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.specialSource = getSpecialMappingName(source, ratesInfo, status);
conversionRate.specialTarget = getSpecialMappingName(target, ratesInfo, status);

if (conversionRate.specialSource.isEmpty() && conversionRate.specialTarget.isEmpty()) {
// 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;
}

conversionRate.factorNum = finalFactor.factorNum;
conversionRate.factorDen = finalFactor.factorDen;
finalFactor.substituteConstants();

// 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.factorNum = finalFactor.factorNum;
conversionRate.factorDen = finalFactor.factorDen;

conversionRate.reciprocal = unitsState == Convertibility::RECIPROCAL;
// 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;
} else if (conversionRate.specialSource.isEmpty() || conversionRate.specialTarget.isEmpty()) {
// Still need to set factorNum/factorDen for either source to base or base to target
if (unitsState != Convertibility::CONVERTIBLE) {
status = UErrorCode::U_ARGUMENT_TYPE_MISMATCH;
return;
}
Factor finalFactor;
if (conversionRate.specialSource.isEmpty()) {
// factorNum/factorDen is for source to base only
finalFactor = loadCompoundFactor(source, ratesInfo, status);
} else {
// factorNum/factorDen is for base to target only
finalFactor = loadCompoundFactor(target, ratesInfo, status);
}
finalFactor.substituteConstants();
conversionRate.factorNum = finalFactor.factorNum;
conversionRate.factorDen = finalFactor.factorDen;
}
}

struct UnitIndexAndDimension : UMemory {
Expand Down Expand Up @@ -569,6 +616,23 @@ int32_t UnitsConverter::compareTwoUnits(const MeasureUnitImpl &firstUnit,
return 0;
}

CharString firstSpecial = getSpecialMappingName(firstUnit, ratesInfo, status);
CharString secondSpecial = getSpecialMappingName(secondUnit, ratesInfo, status);
if (!firstSpecial.isEmpty() || !secondSpecial.isEmpty()) {
if (firstSpecial.isEmpty()) {
// non-specials come first
return -1;
}
if (secondSpecial.isEmpty()) {
// non-specials come first
return 1;
}
// both are specials, compare lexicographically
StringPiece firstSpecialPiece = firstSpecial.toStringPiece();
StringPiece secondSpecialPiece = secondSpecial.toStringPiece();
return firstSpecialPiece.compare(secondSpecialPiece);
}

// 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.
Expand All @@ -593,8 +657,115 @@ int32_t UnitsConverter::compareTwoUnits(const MeasureUnitImpl &firstUnit,
return 0;
}

// TODO per CLDR-17421 and ICU-22683: consider getting the data below from CLDR
static double 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.
0.0, // 0 Bft
0.3, // 1
1.6, // 2
3.4, // 3
5.5, // 4
8.0, // 5
10.8, // 6
13.9, // 7
17.2, // 8
20.8, // 9
24.5, // 10
28.5, // 11
32.7, // 12
36.9, // 13
41.4, // 14
46.1, // 15
51.1, // 16
55.8, // 17
61.4, // artificial end of range 17 to give reasonable midpoint
};

static int maxBeaufort = UPRV_LENGTHOF(minMetersPerSecForBeaufort) - 2;

// 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[].
double UnitsConverter::scaleToBase(double scaleValue, double minBaseForScaleValues[], int scaleMax) const {
if (scaleValue < 0) {
scaleValue = -scaleValue;
}
scaleValue += 0.5; // adjust up for later truncation
if (scaleValue > (double)scaleMax) {
scaleValue = (double)scaleMax;
}
int scaleInt = (int)scaleValue;
return (minBaseForScaleValues[scaleInt] + minBaseForScaleValues[scaleInt+1])/2.0;
}

// Binary search to find the range that includes key;
// if key (non-negative) is in the range rangeStarts[i] to just under rangeStarts[i+1],
// then we return i; if key is >= rangeStarts[max] then we return max.
// Note that max is the maximum scale value, not the number of elements in the array
// (which should be larger than max).
// The ranges for index 0 start at 0.0.
static int bsearchRanges(double rangeStarts[], int max, double key) {
if (key >= rangeStarts[max]) {
return max;
}
int beg = 0, mid = 0, end = max + 1;
while (beg < end) {
mid = (beg + end) / 2;
if (key < rangeStarts[mid]) {
end = mid;
} else if (key > rangeStarts[mid+1]) {
beg = mid+1;
} else {
break;
}
}
return mid;
}

// 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[].
double UnitsConverter::baseToScale(double baseValue, double minBaseForScaleValues[], int scaleMax) const {
if (baseValue < 0) {
baseValue = -baseValue;
}
int scaleIndex = bsearchRanges(minBaseForScaleValues, scaleMax, baseValue);
return (double)scaleIndex;
}

double UnitsConverter::convert(double inputValue) const {
double result =
double result = inputValue;
if (!conversionRate_.specialSource.isEmpty() || !conversionRate_.specialTarget.isEmpty()) {
double base = inputValue;
// convert input (=source) to base
if (!conversionRate_.specialSource.isEmpty()) {
// 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 = (conversionRate_.specialSource == StringPiece("beaufort"))?
scaleToBase(inputValue, minMetersPerSecForBeaufort, maxBeaufort): inputValue;
} else {
// Standard mapping (using factor) from source to base.
base = inputValue * conversionRate_.factorNum / conversionRate_.factorDen;
}
// convert base to result (=target)
if (!conversionRate_.specialTarget.isEmpty()) {
// 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 = (conversionRate_.specialTarget == StringPiece("beaufort"))?
baseToScale(base, minMetersPerSecForBeaufort, maxBeaufort): base;
} else {
// Standard mapping (using factor) from base to target.
result = base * conversionRate_.factorDen / conversionRate_.factorNum;
}
return result;
}
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;
Expand All @@ -613,6 +784,30 @@ double UnitsConverter::convert(double inputValue) const {

double UnitsConverter::convertInverse(double inputValue) const {
double result = inputValue;
if (!conversionRate_.specialSource.isEmpty() || !conversionRate_.specialTarget.isEmpty()) {
double base = inputValue;
// convert input (=target) to base
if (!conversionRate_.specialTarget.isEmpty()) {
// We have a special mapping from target to base (not using factor).
// Currently the only supported mapping is a scale-based mapping for beaufort.
base = (conversionRate_.specialTarget == StringPiece("beaufort"))?
scaleToBase(inputValue, minMetersPerSecForBeaufort, maxBeaufort): inputValue;
} else {
// Standard mapping (using factor) from target to base.
base = inputValue * conversionRate_.factorNum / conversionRate_.factorDen;
}
// convert base to result (=source)
if (!conversionRate_.specialSource.isEmpty()) {
// We have a special mapping from base to source (not using factor).
// Currently the only supported mapping is a scale-based mapping for beaufort.
result = (conversionRate_.specialSource == StringPiece("beaufort"))?
baseToScale(base, minMetersPerSecForBeaufort, maxBeaufort): base;
} else {
// Standard mapping (using factor) from base to source.
result = base * conversionRate_.factorDen / conversionRate_.factorNum;
}
return result;
}
if (conversionRate_.reciprocal) {
if (result == 0) {
return uprv_getInfinity();
Expand Down
21 changes: 20 additions & 1 deletion icu4c/source/i18n/units_converter.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,18 +110,22 @@ void U_I18N_API addSingleFactorConstant(StringPiece baseStr, int32_t power, Sign

/**
* Represents the conversion rate between `source` and `target`.
* TODO ICU-22683: COnsider moving the handling of special mappings (e.g. beaufort) to a separate
* struct.
*/
struct U_I18N_API ConversionRate : public UMemory {
const MeasureUnitImpl source;
const MeasureUnitImpl target;
CharString specialSource;
CharString specialTarget;
double factorNum = 1;
double factorDen = 1;
double sourceOffset = 0;
double targetOffset = 0;
bool reciprocal = false;

ConversionRate(MeasureUnitImpl &&source, MeasureUnitImpl &&target)
: source(std::move(source)), target(std::move(target)) {}
: source(std::move(source)), target(std::move(target)), specialSource(), specialTarget() {}
};

enum Convertibility {
Expand Down Expand Up @@ -224,6 +228,21 @@ class U_I18N_API UnitsConverter : public UMemory {
* Initialises the object.
*/
void init(const ConversionRates &ratesInfo, UErrorCode &status);

/**
* 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).
* This can handle different scales, specified by minBaseForScaleValues[].
*/
double scaleToBase(double scaleValue, double minBaseForScaleValues[], int scaleMax) const;

/**
* 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.
* This can handle different scales, specified by minBaseForScaleValues[].
*/
double baseToScale(double baseValue, double minBaseForScaleValues[], int scaleMax) const;

};

} // namespace units
Expand Down
4 changes: 2 additions & 2 deletions icu4c/source/i18n/units_data.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class ConversionRateDataSink : public ResourceSink {
} else if (uprv_strcmp(key, "offset") == 0) {
offset = value.getUnicodeString(status);
} else if (uprv_strcmp(key, "special") == 0) {
special = value.getUnicodeString(status);
special = value.getUnicodeString(status); // the name of a special mapping used instead of factor + optional offset.
} else if (uprv_strcmp(key, "systems") == 0) {
systems = value.getUnicodeString(status);
}
Expand All @@ -116,7 +116,7 @@ class ConversionRateDataSink : public ResourceSink {
trimSpaces(cr->factor, status);
}
if (!offset.isBogus()) cr->offset.appendInvariantChars(offset, status);
if (!special.isBogus()) cr->offset.appendInvariantChars(special, status);
if (!special.isBogus()) cr->specialMappingName.appendInvariantChars(special, status);
cr->systems.appendInvariantChars(systems, status);
}
}
Expand Down
4 changes: 2 additions & 2 deletions icu4c/source/i18n/units_data.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class U_I18N_API ConversionRateInfo : public UMemory {
ConversionRateInfo() {}
ConversionRateInfo(StringPiece sourceUnit, StringPiece baseUnit, StringPiece factor,
StringPiece offset, UErrorCode &status)
: sourceUnit(), baseUnit(), factor(), offset(), special() {
: sourceUnit(), baseUnit(), factor(), offset(), specialMappingName() {
this->sourceUnit.append(sourceUnit, status);
this->baseUnit.append(baseUnit, status);
this->factor.append(factor, status);
Expand All @@ -41,7 +41,7 @@ class U_I18N_API ConversionRateInfo : public UMemory {
CharString baseUnit;
CharString factor;
CharString offset;
CharString special;
CharString specialMappingName; // the name of a special mapping used instead of factor + optional offset.
CharString systems;
};

Expand Down
Loading

0 comments on commit ceee4f0

Please sign in to comment.