/**
* A ResourceSink that collects conversion rate information.
*
- * Calls to put() collects ConversionRateInfo instances for into the vector
- * passed to the constructor, but only if an identical instance isn't already
- * present.
- *
* This class is for use by ures_getAllItemsWithFallback. Example code for
* collecting conversion info for "mile" and "foot" into conversionInfoOutput:
*
* UErrorCode status = U_ZERO_ERROR;
+ * ures_getByKey(unitsBundle, "convertUnits", &fillIn, &status);
* MaybeStackVector<ConversionRateInfo> conversionInfoOutput;
* ConversionRateDataSink convertSink(conversionInfoOutput);
- * ures_getByKey(unitsBundle, "convertUnits", &fillIn, &status);
* ures_getAllItemsWithFallback(fillIn, "mile", convertSink, status);
* ures_getAllItemsWithFallback(fillIn, "foot", convertSink, status);
*/
class ConversionRateDataSink : public ResourceSink {
public:
+ /**
+ * Constructor.
+ * @param out The vector to which ConversionRateInfo instances are to be
+ * added.
+ */
explicit ConversionRateDataSink(MaybeStackVector<ConversionRateInfo> &out) : outVector(out) {}
/**
* Adds the conversion rate information found in value to the output vector.
- * @param key The source unit identifier.
+ *
+ * Each call to put() collects a ConversionRateInfo instance for the
+ * specified source unit identifier into the vector passed to the
+ * constructor, but only if an identical instance isn't already present.
+ *
+ * @param source The source unit identifier.
* @param value A resource containing conversion rate info (a target and
* factor, and possibly an offset).
* @param noFallback Ignored.
// category. I should make this code more efficient after
// double-checking we're fine with relying on such a detail from the
// CLDR spec?
- CharString tmpTarget;
- tmpTarget.appendInvariantChars(target, lenTarget, status);
+ fLastTarget.clear();
+ fLastTarget.appendInvariantChars(target, lenTarget, status);
if (U_FAILURE(status)) return;
for (int32_t i = 0, len = outVector.length(); i < len; i++) {
if (strcmp(outVector[i]->source.data(), source) == 0 &&
- strcmp(outVector[i]->target.data(), tmpTarget.data()) == 0) {
+ strcmp(outVector[i]->target.data(), fLastTarget.data()) == 0) {
return;
}
}
return;
} else {
cr->source.append(source, lenSource, status);
- cr->target.append(tmpTarget.data(), tmpTarget.length(), status);
+ cr->target.append(fLastTarget.data(), fLastTarget.length(), status);
cr->factor.appendInvariantChars(factor, lenFactor, status);
if (offset != NULL) cr->offset.appendInvariantChars(offset, lenOffset, status);
}
}
+ /**
+ * Returns the MeasureUnit that was the conversion target of the most recent
+ * call to put() - typically meaning the most recent call to
+ * ures_getAllItemsWithFallback().
+ */
+ MeasureUnit getLastTarget(UErrorCode &status) {
+ return MeasureUnit::forIdentifier(fLastTarget.data(), status);
+ }
+
private:
MaybeStackVector<ConversionRateInfo> &outVector;
+
+ // TODO(review): felt like a hack: provides easy access to the most recent
+ // target. This hack is another point making me wonder if doing this
+ // ResourceSink thing is worthwhile. Functional style is not more verbose,
+ // and IMHO more readable than this object-based approach where the output
+ // seems/feels like a side-effect.
+ CharString fLastTarget;
};
-// WIP/FIXME: this class is currently unused code (dead?) - putUnitPref() has
-// all the features we need whereas this doesn't handle fallback to
-// usage="default" and region="001" yet. If we want to fix that here, this code
-// will get quite a bit more complicated.
+// WIP/FIXME: partially due to my skepticism of using the ResourceSink design
+// for units resources, this class is currently unused code (dead?) -
+// collectUnitPrefs() has all the features we need whereas this doesn't handle
+// fallback to usage="default" and region="001" yet. If we want to fix that
+// here, this code will get quite a bit more complicated.
class UnitPreferencesSink : public ResourceSink {
public:
explicit UnitPreferencesSink(MaybeStackVector<UnitPreference> &out) : outVector(out) {}
* down to a particular usage and region (example:
* "unitPreferenceData/length/road/GB").
*/
-void putUnitPref(UResourceBundle *usageData, MaybeStackVector<UnitPreference> &outVector,
- UErrorCode &status) {
+void collectUnitPrefs(UResourceBundle *usageData, MaybeStackVector<UnitPreference> &outVector,
+ UErrorCode &status) {
if (U_FAILURE(status)) return;
StackUResourceBundle prefBundle;
}
}
+void processSingleUnit(const MeasureUnit &unit, const UResourceBundle *convertUnitsBundle,
+ ConversionRateDataSink &convertSink, MeasureUnit *baseSingleUnit,
+ UErrorCode &status) {
+ int32_t dimensionality = unit.getDimensionality(status);
+
+ MeasureUnit simple = unit;
+ if (dimensionality != 1 || simple.getSIPrefix(status) != UMEASURE_SI_PREFIX_ONE) {
+ simple = unit.withDimensionality(1, status).withSIPrefix(UMEASURE_SI_PREFIX_ONE, status);
+ }
+ ures_getAllItemsWithFallback(convertUnitsBundle, simple.getIdentifier(), convertSink, status);
+
+ if (baseSingleUnit != NULL) {
+ *baseSingleUnit = convertSink.getLastTarget(status).withDimensionality(dimensionality, status);
+ }
+}
+
} // namespace
+MaybeStackVector<ConversionRateInfo> getConversionRatesInfo(const MeasureUnit source, const MeasureUnit target,
+ MeasureUnit *baseCompoundUnit,
+ UErrorCode &status) {
+ MaybeStackVector<ConversionRateInfo> result;
+
+ int32_t sourceUnitsLength, targetUnitsLength;
+ LocalArray<MeasureUnit> sourceUnits = source.splitToSingleUnits(sourceUnitsLength, status);
+ LocalArray<MeasureUnit> targetUnits = target.splitToSingleUnits(targetUnitsLength, status);
+
+ LocalUResourceBundlePointer unitsBundle(ures_openDirect(NULL, "units", &status));
+ StackUResourceBundle convertUnitsBundle;
+ ures_getByKey(unitsBundle.getAlias(), "convertUnits", convertUnitsBundle.getAlias(), &status);
+
+ ConversionRateDataSink convertSink(result);
+ if (baseCompoundUnit != NULL) {
+ *baseCompoundUnit = MeasureUnit();
+ }
+ for (int i = 0; i < sourceUnitsLength; i++) {
+ MeasureUnit baseUnit;
+ processSingleUnit(sourceUnits[i], convertUnitsBundle.getAlias(), convertSink, &baseUnit, status);
+ if (baseCompoundUnit != NULL) {
+ if (source.getComplexity(status) == UMEASURE_UNIT_SEQUENCE) {
+ // TODO(hugovdm): add consistency checks.
+ *baseCompoundUnit = baseUnit;
+ } else {
+ *baseCompoundUnit = baseCompoundUnit->product(baseUnit, status);
+ }
+ }
+ }
+ if (baseCompoundUnit != NULL) {
+ fprintf(stderr, "source base: %s\n", baseCompoundUnit->getIdentifier());
+ *baseCompoundUnit = MeasureUnit();
+ }
+ for (int i = 0; i < targetUnitsLength; i++) {
+ MeasureUnit baseUnit;
+ processSingleUnit(targetUnits[i], convertUnitsBundle.getAlias(), convertSink, &baseUnit, status);
+ if (baseCompoundUnit != NULL) {
+ if (source.getComplexity(status) == UMEASURE_UNIT_SEQUENCE) {
+ // TODO(hugovdm): add consistency checks.
+ *baseCompoundUnit = baseUnit;
+ } else {
+ *baseCompoundUnit = baseCompoundUnit->product(baseUnit, status);
+ }
+ }
+ }
+ return result;
+}
+
/**
* Fetches the units data that would be needed for the given usage.
*
}
// Collect all the preferences into unitPreferences
- putUnitPref(stackBundle.getAlias(), unitPreferences, status);
+ collectUnitPrefs(stackBundle.getAlias(), unitPreferences, status);
// Load ConversionRateInfo for each of the units in unitPreferences
for (int32_t i = 0; i < unitPreferences.length(); i++) {
U_NAMESPACE_END
-#endif /* #if !UCONFIG_NO_FORMATTING */
\ No newline at end of file
+#endif /* #if !UCONFIG_NO_FORMATTING */
U_NAMESPACE_BEGIN
-struct U_I18N_API ConversionRateInfo {
+// Encapsulates "convertUnits" information from units resources, specifying how
+// to convert from one unit to another.
+class U_I18N_API ConversionRateInfo {
+ public:
+ ConversionRateInfo() {};
+ ConversionRateInfo(StringPiece source, StringPiece target, StringPiece factor, StringPiece offset,
+ UErrorCode &status)
+ : source(), target(), factor(), offset() {
+ this->source.append(source, status);
+ this->target.append(target, status);
+ this->factor.append(factor, status);
+ this->offset.append(offset, status);
+ };
CharString source;
- CharString target;
+ CharString target; // FIXME/WIP: baseUnit
CharString factor;
CharString offset;
bool reciprocal = false;
};
+// Encapsulates unitPreferenceData information from units resources, specifying
+// a sequence of output unit preferences.
struct U_I18N_API UnitPreference {
UnitPreference() : geq(1) {}
CharString unit;
CharString skeleton;
};
-// TODO(hugo): Add a comment.
+/**
+ * Collects and returns ConversionRateInfo needed to convert from source to
+ * target.
+ *
+ * @param source The source unit (the unit type converted from).
+ * @param target The target unit (the unit type converted to).
+ * @param baseCompoundUnit Output parameter: if not NULL, it will be set to the
+ * base unit type used as pivot for converting from source to target.
+ * @param status Receives status.
+ */
+MaybeStackVector<ConversionRateInfo> U_I18N_API getConversionRatesInfo(MeasureUnit source,
+ MeasureUnit target,
+ MeasureUnit *baseCompoundUnit,
+ UErrorCode &status);
+
+/**
+ * Collects the data needed for converting the inputUnit type to output units
+ * for the given region and usage.
+ *
+ * WARNING: This function only supports simple units presently.
+ * WIP/TODO(hugovdm): add support for UMEASURE_UNIT_SEQUENCE and
+ * UMEASURE_UNIT_COMPOUND complexities.
+ *
+ * @param outputRegion The region code for which output preferences are desired
+ * (e.g. US or 001).
+ * @param usage The "usage" parameter, such as "person", "road", or "fluid".
+ * Unrecognised usages are treated as "default".
+ * @param inputUnit The input unit type: the type of the units that will be
+ * converted.
+ * @param category Output parameter, this will be set to the category associated
+ * with the inputUnit. TODO(hugovdm): this might get specified instead of
+ * requested. Or it may be unnecessary to return it.
+ * @param baseUnit Output parameter, this will be set to the base unit through
+ * which conversions are made (e.g. "kilogram", "meter", or "year").
+ * TODO(hugovdm): find out if this is needed/useful.
+ * @param conversionInfo Output parameter, a vector of ConversionRateInfo
+ * instances needed to be able to convert from inputUnit to all the output units
+ * found in unitPreferences.
+ * @param unitPreferences Output parameter, a vector of all the output
+ * preferences for the given region, usage, and input unit type (which
+ * determines the category).
+ * @param status Receives status.
+ */
void U_I18N_API getUnitsData(const char *outputRegion, const char *usage, const MeasureUnit &inputUnit,
CharString &category, MeasureUnit &baseUnit,
MaybeStackVector<ConversionRateInfo> &conversionInfo,
MaybeStackVector<UnitPreference> &unitPreferences, UErrorCode &status);
-// TODO(hugo): Implement
-MaybeStackVector<ConversionRateInfo>
- U_I18N_API getConversionRatesInfo(MeasureUnit source, MeasureUnit target, UErrorCode &status);
+// // TODO(hugo): Implement
+// // Compound units as source and target, conversion rates for each piece.
+// MaybeStackVector<ConversionRateInfo>
+// U_I18N_API getConversionRatesInfo(MeasureUnit source, MeasureUnit target, UErrorCode &status);
U_NAMESPACE_END
}
status = U_INTERNAL_PROGRAM_ERROR;
- return ConversionRateInfo();
+ // WIP/TODO(review): cargo-culting or magic-incantation, this fixes the warning:
+ // unitconverter.cpp:197:12: warning: returning reference to local temporary object [-Wreturn-stack-address]
+ // But I'm not confident in what I'm doing, having only done some casual
+ // reading about the possible negative consequencies of returning std::move.
+ return std::move(ConversionRateInfo("pound", "kilogram", "0.453592", "0", status));
}
/*/
void testConversions();
void testPreferences();
+ void testGetConversionRateInfo();
void testGetUnitsData();
void testBasic();
TESTCASE_AUTO_BEGIN;
TESTCASE_AUTO(testConversions);
TESTCASE_AUTO(testPreferences);
+ TESTCASE_AUTO(testGetConversionRateInfo);
TESTCASE_AUTO(testGetUnitsData);
TESTCASE_AUTO(testBasic);
MeasureUnit sourceUnit = MeasureUnit::forIdentifier(testCase.source, status);
MeasureUnit targetUnit = MeasureUnit::forIdentifier(testCase.target, status);
+ // TODO(younies): enable this again
+
// UnitConverter converter(sourceUnit, targetUnit, status);
// double actual = converter.convert(testCase.inputValue);
MeasureUnit sourceUnit = MeasureUnit::forIdentifier(testCase.source, status);
MeasureUnit targetUnit = MeasureUnit::forIdentifier(testCase.target, status);
+ // TODO(younies): enable this again
+
// UnitConverter converter(sourceUnit, targetUnit, status);
// double actual = converter.convert(testCase.inputValue);
quantity.length(), quantity.data(), x.length(), x.data(), y.length(), y.data(), expected,
commentConversionFormula.length(), commentConversionFormula.data());
} else {
+ // TODO(younies): enable this again
+ //
// UnitConverter converter(sourceUnit, targetUnit, status);
// if (status.errIfFailureAndReset("constructor: UnitConverter(<%s>, <%s>, status)",
// sourceUnit.getIdentifier(), targetUnit.getIdentifier())) {
}
}
+void UnitsTest::testGetConversionRateInfo() {
+ struct {
+ const char *sourceUnit;
+ const char *targetUnit;
+ const char *threeExpectedOutputs[3];
+ const char *baseUnit;
+ } testCases[]{
+ {"centimeter-per-square-milligram",
+ "inch-per-square-ounce",
+ {"pound", "stone", "ton"},
+ "kilogram"},
+ {"liter", "gallon", {"liter", "gallon", NULL}, "cubic-meter"},
+ {"stone-and-pound", "ton", {"pound", "stone", "ton"}, "kilogram"},
+ {"mile-per-hour", "dekameter-per-hour", {"mile", "hour", "meter"}, "meter-per-second"},
+ };
+ for (const auto &t : testCases) {
+ logln("---testing: source=\"%s\", target=\"%s\", expectedBaseUnit=\"%s\"", t.sourceUnit,
+ t.targetUnit, t.baseUnit);
+ IcuTestErrorCode status(*this, "testGetConversionRateInfo");
+
+ MeasureUnit baseCompoundUnit;
+ MeasureUnit sourceUnit = MeasureUnit::forIdentifier(t.sourceUnit, status);
+ MeasureUnit targetUnit = MeasureUnit::forIdentifier(t.targetUnit, status);
+ MaybeStackVector<ConversionRateInfo> conversionInfo =
+ getConversionRatesInfo(sourceUnit, targetUnit, &baseCompoundUnit, status);
+
+ logln("---found BaseUnit=\"%s\"", baseCompoundUnit.getIdentifier());
+ for (int i = 0; i < conversionInfo.length(); i++) {
+ ConversionRateInfo *cri;
+ cri = conversionInfo[i];
+ logln("* conversionInfo %d: source=\"%s\", target=\"%s\", factor=\"%s\", offset=\"%s\"", i,
+ cri->source.data(), cri->target.data(), cri->factor.data(), cri->offset.data());
+ assertTrue("ConversionRateInfo has source, target, and factor",
+ cri->source.length() > 0 && cri->target.length() > 0 && cri->factor.length() > 0);
+ }
+ }
+}
+
// We test "successfully loading some data", not specific output values, since
// this would duplicate some of the input data. We leave end-to-end testing to
// take care of that. Running `intltest` with `-v` will print out the loaded