// © 2017 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 "number_decimalquantity.h" #include "number_decnum.h" #include "math.h" #include #include "number_utils.h" #include "numbertest.h" void DecimalQuantityTest::runIndexedTest(int32_t index, UBool exec, const char *&name, char *) { if (exec) { logln("TestSuite DecimalQuantityTest: "); } TESTCASE_AUTO_BEGIN; TESTCASE_AUTO(testDecimalQuantityBehaviorStandalone); TESTCASE_AUTO(testSwitchStorage);; TESTCASE_AUTO(testCopyMove); TESTCASE_AUTO(testAppend); TESTCASE_AUTO(testConvertToAccurateDouble); TESTCASE_AUTO(testUseApproximateDoubleWhenAble); TESTCASE_AUTO(testHardDoubleConversion); TESTCASE_AUTO(testToDouble); TESTCASE_AUTO(testMaxDigits); TESTCASE_AUTO_END; } void DecimalQuantityTest::assertDoubleEquals(UnicodeString message, double a, double b) { if (a == b) { return; } double diff = a - b; diff = diff < 0 ? -diff : diff; double bound = a < 0 ? -a * 1e-6 : a * 1e-6; if (diff > bound) { errln(message + u": " + DoubleToUnicodeString(a) + u" vs " + DoubleToUnicodeString(b) + u" differ by " + DoubleToUnicodeString(diff)); } } void DecimalQuantityTest::assertHealth(const DecimalQuantity &fq) { const char16_t* health = fq.checkHealth(); if (health != nullptr) { errln(UnicodeString(u"HEALTH FAILURE: ") + UnicodeString(health) + u": " + fq.toString()); } } void DecimalQuantityTest::assertToStringAndHealth(const DecimalQuantity &fq, const UnicodeString &expected) { UnicodeString actual = fq.toString(); assertEquals("DecimalQuantity toString failed", expected, actual); assertHealth(fq); } void DecimalQuantityTest::checkDoubleBehavior(double d, bool explicitRequired) { DecimalQuantity fq; fq.setToDouble(d); if (explicitRequired) { assertTrue("Should be using approximate double", !fq.isExplicitExactDouble()); } UnicodeString baseStr = fq.toString(); fq.roundToInfinity(); UnicodeString newStr = fq.toString(); if (explicitRequired) { assertTrue("Should not be using approximate double", fq.isExplicitExactDouble()); } assertDoubleEquals( UnicodeString(u"After conversion to exact BCD (double): ") + baseStr + u" vs " + newStr, d, fq.toDouble()); } void DecimalQuantityTest::testDecimalQuantityBehaviorStandalone() { UErrorCode status = U_ZERO_ERROR; DecimalQuantity fq; assertToStringAndHealth(fq, u""); fq.setToInt(51423); assertToStringAndHealth(fq, u""); fq.adjustMagnitude(-3); assertToStringAndHealth(fq, u""); fq.setToLong(999999999999000L); assertToStringAndHealth(fq, u""); fq.setIntegerLength(2, 5); assertToStringAndHealth(fq, u""); fq.setFractionLength(3, 6); assertToStringAndHealth(fq, u""); fq.setToDouble(987.654321); assertToStringAndHealth(fq, u""); fq.roundToInfinity(); assertToStringAndHealth(fq, u""); fq.roundToIncrement(0.005, RoundingMode::UNUM_ROUND_HALFEVEN, 3, status); assertSuccess("Rounding to increment", status); assertToStringAndHealth(fq, u""); fq.roundToMagnitude(-2, RoundingMode::UNUM_ROUND_HALFEVEN, status); assertSuccess("Rounding to magnitude", status); assertToStringAndHealth(fq, u""); } void DecimalQuantityTest::testSwitchStorage() { UErrorCode status = U_ZERO_ERROR; DecimalQuantity fq; fq.setToLong(1234123412341234L); assertFalse("Should not be using byte array", fq.isUsingBytes()); assertEquals("Failed on initialize", u"1.234123412341234E+15", fq.toScientificString()); assertHealth(fq); // Long -> Bytes fq.appendDigit(5, 0, true); assertTrue("Should be using byte array", fq.isUsingBytes()); assertEquals("Failed on multiply", u"1.2341234123412345E+16", fq.toScientificString()); assertHealth(fq); // Bytes -> Long fq.roundToMagnitude(5, RoundingMode::UNUM_ROUND_HALFEVEN, status); assertSuccess("Rounding to magnitude", status); assertFalse("Should not be using byte array", fq.isUsingBytes()); assertEquals("Failed on round", u"1.23412341234E+16", fq.toScientificString()); assertHealth(fq); } void DecimalQuantityTest::testCopyMove() { // Small numbers (fits in BCD long) { DecimalQuantity a; a.setToLong(1234123412341234L); DecimalQuantity b = a; // copy constructor assertToStringAndHealth(a, u""); assertToStringAndHealth(b, u""); DecimalQuantity c(std::move(a)); // move constructor assertToStringAndHealth(c, u""); c.setToLong(54321L); assertToStringAndHealth(c, u""); c = b; // copy assignment assertToStringAndHealth(b, u""); assertToStringAndHealth(c, u""); b.setToLong(45678); c.setToLong(56789); c = std::move(b); // move assignment assertToStringAndHealth(c, u""); a = std::move(c); // move assignment to a defunct object assertToStringAndHealth(a, u""); } // Large numbers (requires byte allocation) { IcuTestErrorCode status(*this, "testCopyMove"); DecimalQuantity a; a.setToDecNumber({"1234567890123456789", -1}, status); DecimalQuantity b = a; // copy constructor assertToStringAndHealth(a, u""); assertToStringAndHealth(b, u""); DecimalQuantity c(std::move(a)); // move constructor assertToStringAndHealth(c, u""); c.setToDecNumber({"9876543210987654321", -1}, status); assertToStringAndHealth(c, u""); c = b; // copy assignment assertToStringAndHealth(b, u""); assertToStringAndHealth(c, u""); b.setToDecNumber({"876543210987654321", -1}, status); c.setToDecNumber({"987654321098765432", -1}, status); c = std::move(b); // move assignment assertToStringAndHealth(c, u""); a = std::move(c); // move assignment to a defunct object assertToStringAndHealth(a, u""); } } void DecimalQuantityTest::testAppend() { DecimalQuantity fq; fq.appendDigit(1, 0, true); assertEquals("Failed on append", u"1E+0", fq.toScientificString()); assertHealth(fq); fq.appendDigit(2, 0, true); assertEquals("Failed on append", u"1.2E+1", fq.toScientificString()); assertHealth(fq); fq.appendDigit(3, 1, true); assertEquals("Failed on append", u"1.203E+3", fq.toScientificString()); assertHealth(fq); fq.appendDigit(0, 1, true); assertEquals("Failed on append", u"1.203E+5", fq.toScientificString()); assertHealth(fq); fq.appendDigit(4, 0, true); assertEquals("Failed on append", u"1.203004E+6", fq.toScientificString()); assertHealth(fq); fq.appendDigit(0, 0, true); assertEquals("Failed on append", u"1.203004E+7", fq.toScientificString()); assertHealth(fq); fq.appendDigit(5, 0, false); assertEquals("Failed on append", u"1.20300405E+7", fq.toScientificString()); assertHealth(fq); fq.appendDigit(6, 0, false); assertEquals("Failed on append", u"1.203004056E+7", fq.toScientificString()); assertHealth(fq); fq.appendDigit(7, 3, false); assertEquals("Failed on append", u"1.2030040560007E+7", fq.toScientificString()); assertHealth(fq); UnicodeString baseExpected(u"1.2030040560007"); for (int i = 0; i < 10; i++) { fq.appendDigit(8, 0, false); baseExpected.append(u'8'); UnicodeString expected(baseExpected); expected.append(u"E+7"); assertEquals("Failed on append", expected, fq.toScientificString()); assertHealth(fq); } fq.appendDigit(9, 2, false); baseExpected.append(u"009"); UnicodeString expected(baseExpected); expected.append(u"E+7"); assertEquals("Failed on append", expected, fq.toScientificString()); assertHealth(fq); } void DecimalQuantityTest::testConvertToAccurateDouble() { // based on https://github.com/google/double-conversion/issues/28 static double hardDoubles[] = { 1651087494906221570.0, -5074790912492772E-327, 83602530019752571E-327, 2.207817077636718750000000000000, 1.818351745605468750000000000000, 3.941719055175781250000000000000, 3.738609313964843750000000000000, 3.967735290527343750000000000000, 1.328025817871093750000000000000, 3.920967102050781250000000000000, 1.015235900878906250000000000000, 1.335227966308593750000000000000, 1.344520568847656250000000000000, 2.879127502441406250000000000000, 3.695838928222656250000000000000, 1.845344543457031250000000000000, 3.793952941894531250000000000000, 3.211402893066406250000000000000, 2.565971374511718750000000000000, 0.965156555175781250000000000000, 2.700004577636718750000000000000, 0.767097473144531250000000000000, 1.780448913574218750000000000000, 2.624839782714843750000000000000, 1.305290222167968750000000000000, 3.834922790527343750000000000000,}; static double integerDoubles[] = { 51423, 51423e10, 4.503599627370496E15, 6.789512076111555E15, 9.007199254740991E15, 9.007199254740992E15}; for (double d : hardDoubles) { checkDoubleBehavior(d, true); } for (double d : integerDoubles) { checkDoubleBehavior(d, false); } assertDoubleEquals(u"NaN check failed", NAN, DecimalQuantity().setToDouble(NAN).toDouble()); assertDoubleEquals( u"Inf check failed", INFINITY, DecimalQuantity().setToDouble(INFINITY).toDouble()); assertDoubleEquals( u"-Inf check failed", -INFINITY, DecimalQuantity().setToDouble(-INFINITY).toDouble()); // Generate random doubles for (int32_t i = 0; i < 10000; i++) { uint8_t bytes[8]; for (int32_t j = 0; j < 8; j++) { bytes[j] = static_cast(rand() % 256); } double d; uprv_memcpy(&d, bytes, 8); if (std::isnan(d) || !std::isfinite(d)) { continue; } checkDoubleBehavior(d, false); } } void DecimalQuantityTest::testUseApproximateDoubleWhenAble() { static const struct TestCase { double d; int32_t maxFrac; RoundingMode roundingMode; bool usesExact; } cases[] = {{1.2345678, 1, RoundingMode::UNUM_ROUND_HALFEVEN, false}, {1.2345678, 7, RoundingMode::UNUM_ROUND_HALFEVEN, false}, {1.2345678, 12, RoundingMode::UNUM_ROUND_HALFEVEN, false}, {1.2345678, 13, RoundingMode::UNUM_ROUND_HALFEVEN, true}, {1.235, 1, RoundingMode::UNUM_ROUND_HALFEVEN, false}, {1.235, 2, RoundingMode::UNUM_ROUND_HALFEVEN, true}, {1.235, 3, RoundingMode::UNUM_ROUND_HALFEVEN, false}, {1.000000000000001, 0, RoundingMode::UNUM_ROUND_HALFEVEN, false}, {1.000000000000001, 0, RoundingMode::UNUM_ROUND_CEILING, true}, {1.235, 1, RoundingMode::UNUM_ROUND_CEILING, false}, {1.235, 2, RoundingMode::UNUM_ROUND_CEILING, false}, {1.235, 3, RoundingMode::UNUM_ROUND_CEILING, true}}; UErrorCode status = U_ZERO_ERROR; for (TestCase cas : cases) { DecimalQuantity fq; fq.setToDouble(cas.d); assertTrue("Should be using approximate double", !fq.isExplicitExactDouble()); fq.roundToMagnitude(-cas.maxFrac, cas.roundingMode, status); assertSuccess("Rounding to magnitude", status); if (cas.usesExact != fq.isExplicitExactDouble()) { errln(UnicodeString(u"Using approximate double after rounding: ") + fq.toString()); } } } void DecimalQuantityTest::testHardDoubleConversion() { static const struct TestCase { double input; const char16_t* expectedOutput; } cases[] = { { 512.0000000000017, u"512.0000000000017" }, { 4095.9999999999977, u"4095.9999999999977" }, { 4095.999999999998, u"4095.999999999998" }, { 4095.9999999999986, u"4095.9999999999986" }, { 4095.999999999999, u"4095.999999999999" }, { 4095.9999999999995, u"4095.9999999999995" }, { 4096.000000000001, u"4096.000000000001" }, { 4096.000000000002, u"4096.000000000002" }, { 4096.000000000003, u"4096.000000000003" }, { 4096.000000000004, u"4096.000000000004" }, { 4096.000000000005, u"4096.000000000005" }, { 4096.0000000000055, u"4096.0000000000055" }, { 4096.000000000006, u"4096.000000000006" }, { 4096.000000000007, u"4096.000000000007" } }; for (auto& cas : cases) { DecimalQuantity q; q.setToDouble(cas.input); q.roundToInfinity(); UnicodeString actualOutput = q.toPlainString(); assertEquals("", cas.expectedOutput, actualOutput); } } void DecimalQuantityTest::testToDouble() { IcuTestErrorCode status(*this, "testToDouble"); static const struct TestCase { const char* input; // char* for the decNumber constructor double expected; } cases[] = { { "0", 0.0 }, { "514.23", 514.23 }, { "-3.142E-271", -3.142e-271 } }; for (auto& cas : cases) { status.setScope(cas.input); DecimalQuantity q; q.setToDecNumber({cas.input, -1}, status); double actual = q.toDouble(); assertEquals("Doubles should exactly equal", cas.expected, actual); } } void DecimalQuantityTest::testMaxDigits() { IcuTestErrorCode status(*this, "testMaxDigits"); DecimalQuantity dq; dq.setToDouble(876.543); dq.roundToInfinity(); dq.setIntegerLength(0, 2); dq.setFractionLength(0, 2); assertEquals("Should trim, toPlainString", "76.54", dq.toPlainString()); assertEquals("Should trim, toScientificString", "7.654E+1", dq.toScientificString()); assertEquals("Should trim, toLong", 76LL, dq.toLong(true)); assertEquals("Should trim, toFractionLong", (int64_t) 54, (int64_t) dq.toFractionLong(false)); assertEquals("Should trim, toDouble", 76.54, dq.toDouble()); // To test DecNum output, check the round-trip. DecNum dn; dq.toDecNum(dn, status); DecimalQuantity copy; copy.setToDecNum(dn, status); if (!logKnownIssue("13701")) { assertEquals("Should trim, toDecNum", "76.54", copy.toPlainString()); } } #endif /* #if !UCONFIG_NO_FORMATTING */