/** * @file Utf8UtilTests.cpp * @brief `Utilities/Utf8Util` 的数值解析 / 文本格式化语义回归。 * * 重点用例: * * `parse.bit_exact_vs_strtod` —— **逐位**对比 `TryParseDouble` 与 libc `strtod`。 * 这是"自写 strtod 替身"类体积优化唯一可信的验收标准(见 Code Review 报告 §10.4): * 常见写法(≤15 位有效数字、|十进制指数| ≤ 22)必须逐位相同; * 17 位以上的长数字允许 ≤1 ULP 偏差(实现里已注释该取舍)。 * * `format.*` —— 只断言文档里给出的实例与"绝不出现科学计数法"这类硬约束, * 避免把测试绑死在内部格式细节上。 */ #include #include #include #include #include #include #include #include "UnitTestFramework.hpp" #include "Utf8Util.hpp" namespace { /** @brief 取 double 的位模式。 */ std::uint64_t BitsOf(const double value) { std::uint64_t bits = 0; std::memcpy(&bits, &value, sizeof(bits)); return bits; } /** @brief 两个 double 之间相差多少个 ULP(异号时返回一个很大的数)。 */ long long UlpDifference(const double left, const double right) { if (left == right) { return 0; } const std::uint64_t a = BitsOf(left); const std::uint64_t b = BitsOf(right); if ((a >> 63) != (b >> 63)) { return 1000000; } return (a > b) ? static_cast(a - b) : -static_cast(b - a); } /** @brief 用 `TryParseDouble` 解析整段文本。 */ bool Parse(const std::string& text, double& value) { return jdi::TryParseDouble(text.c_str(), text.c_str() + text.size(), value); } /** @brief 统计十进制有效数字位数(忽略符号/小数点/前导零/指数部分)。 */ int SignificantDigits(const std::string& text) { int digits = 0; bool sawNonZero = false; for (const char character : text) { if (character == 'e' || character == 'E') { break; } if (character < '0' || character > '9') { continue; } if (character != '0') { sawNonZero = true; } if (sawNonZero) { ++digits; } } return digits; } /** @brief 生成 `数字.数字e指数` 形式的随机十进制文本。 */ std::string MakeRandomNumber(std::mt19937& rng, const int maxDigits, const bool allowFraction, const bool allowExponent, const int maxExponent) { std::string text; if ((rng() & 1u) != 0u) { text.push_back('-'); } const int integerDigits = 1 + static_cast(rng() % static_cast(maxDigits)); for (int index = 0; index < integerDigits; ++index) { text.push_back(static_cast('0' + (rng() % 10u))); } if (allowFraction && (rng() % 3u) != 0u) { text.push_back('.'); const int fractionDigits = 1 + static_cast(rng() % static_cast(maxDigits)); for (int index = 0; index < fractionDigits; ++index) { text.push_back(static_cast('0' + (rng() % 10u))); } } if (allowExponent && (rng() % 2u) == 0u) { text.push_back((rng() & 1u) != 0u ? 'e' : 'E'); const bool negativeExponent = (rng() & 1u) != 0u; text.push_back(negativeExponent ? '-' : '+'); const int exponent = static_cast(rng() % static_cast(maxExponent + 1)); const std::string digits = std::to_string(exponent); text += digits; } return text; } } // namespace JDI_TEST(parse_fixed_corpus_matches_strtod) { static const char* const corpus[] = { "0", "1", "-1", "+1", "0.1", "-0.1", "1.5", "123.456", "-123.456", "1e10", "1e-10", "1E+5", "1e22", "1e-22", "3.141592653589793", "2.718281828459045", "6.02214076e23", "1.602176634e-19", "0.000001", "1000000", "-0", "0.0", "123456789012345", }; for (const char* const text : corpus) { double ours = 0.0; const bool accepted = Parse(text, ours); char* stop = nullptr; const double reference = std::strtod(text, &stop); const bool referenceAccepted = (stop != text); JDI_REQUIRE(text, accepted == referenceAccepted); if (accepted && referenceAccepted) { JDI_CHECK_BIT_EQUAL(ours, reference); } } } JDI_TEST(parse_acceptance_semantics) { double value = 0.0; JDI_CHECK(!Parse("", value)); JDI_CHECK(!Parse(" ", value)); JDI_CHECK(!Parse("abc", value)); JDI_CHECK(!Parse("e5", value)); // 尾部杂字符:与 `strtod` 一致,接受前缀数值(历史语义,不要改) JDI_CHECK(Parse("12abc", value)); JDI_CHECK_NEAR(value, 12.0, 0.0); JDI_CHECK(Parse("1,5", value)); JDI_CHECK_NEAR(value, 1.0, 0.0); // 首尾空白应被忽略 JDI_CHECK(Parse(" -2.5\t\r\n", value)); JDI_CHECK_NEAR(value, -2.5, 0.0); // 十六进制浮点 / inf / nan 走标准库回退路径,语义必须与 strtod 相同 static const char* const exotic[] = {"0x1p3", "inf", "-inf", "nan", "infinity", "1e9999", "1e-9999"}; for (const char* const text : exotic) { double ours = 0.0; char* stop = nullptr; const double reference = std::strtod(text, &stop); const bool referenceAccepted = (stop != text); JDI_REQUIRE(text, Parse(text, ours) == referenceAccepted); if (referenceAccepted) { JDI_CHECK_BIT_EQUAL(ours, reference); } } } JDI_TEST(parse_random_corpus_bit_exact) { std::mt19937 rng(20261104u); int mismatches = 0; int beyondOneUlp = 0; int cases = 0; // 常见写法:≤15 位有效数字、|指数| ≤ 22 ⇒ 必须逐位相同。 for (int iteration = 0; iteration < 40000; ++iteration) { const std::string text = MakeRandomNumber(rng, 6, true, true, 22); double ours = 0.0; char* stop = nullptr; const double reference = std::strtod(text.c_str(), &stop); if (stop == text.c_str()) { continue; } ++cases; if (!Parse(text, ours)) { ++mismatches; continue; } if (BitsOf(ours) != BitsOf(reference)) { ++mismatches; if (SignificantDigits(text) <= 15 && std::fabs(reference) > 1e-300 && std::fabs(reference) < 1e300) { std::printf(" [INFO] bit mismatch on short literal: %s ours=%.17g ref=%.17g\n", text.c_str(), ours, reference); } } } JDI_CHECK_EQ(cases, 40000); JDI_CHECK_EQ(mismatches, 0); // 长数字(最多 19 位):允许 ≤1 ULP 偏差(实现里有明确注释与容差约定)。 for (int iteration = 0; iteration < 20000; ++iteration) { const std::string text = MakeRandomNumber(rng, 19, true, true, 22); double ours = 0.0; char* stop = nullptr; const double reference = std::strtod(text.c_str(), &stop); if (stop == text.c_str() || !Parse(text, ours)) { continue; } if (UlpDifference(ours, reference) > 1) { ++beyondOneUlp; } } JDI_CHECK_EQ(beyondOneUlp, 0); } JDI_TEST(format_decimal_never_scientific) { JDI_CHECK_WIDE_EQ(jdi::FormatDecimal(1.0e-5), std::wstring(L"0.00001")); JDI_CHECK_WIDE_EQ(jdi::FormatDecimal(0.0), std::wstring(L"0")); JDI_CHECK_WIDE_EQ(jdi::FormatDecimalFixed(700.0, 3), std::wstring(L"700.000")); static const double values[] = {1.0e-9, 1.0e-6, 1.0e-5, 0.000123456, 1.0, 1234.5678, 1.0e9}; for (const double value : values) { const std::wstring text = jdi::FormatDecimal(value); JDI_REQUIRE("FormatDecimal must not use exponent", text.find(L'e') == std::wstring::npos && text.find(L'E') == std::wstring::npos); } // 文档实例:`FormatValue` = `%.6g` JDI_CHECK_WIDE_EQ(jdi::FormatValue(123.456789), std::wstring(L"123.457")); JDI_CHECK_WIDE_EQ(jdi::FormatValue(1.0), std::wstring(L"1")); } JDI_TEST(format_byte_size_and_frequency) { const std::wstring bytes = jdi::FormatByteSize(1024ull * 1024ull); JDI_REQUIRE("FormatByteSize(1MB) contains MB", bytes.find(L"MB") != std::wstring::npos); const std::wstring hertz = jdi::FormatFrequency(270.5166); JDI_REQUIRE("FormatFrequency contains unit", hertz.find(L"Hz") != std::wstring::npos); JDI_REQUIRE("FormatFrequency has no exponent", hertz.find(L'e') == std::wstring::npos); const std::wstring duration = jdi::FormatDurationCompound(5.06); JDI_REQUIRE("FormatDurationCompound has no exponent", duration.find(L'e') == std::wstring::npos); JDI_REQUIRE("FormatDurationCompound ends with unit", duration.find(L's') != std::wstring::npos); } JDI_TEST(text_trim_and_utf8_conversion) { JDI_CHECK_WIDE_EQ(jdi::Trim(L" abc \t"), std::wstring(L"abc")); JDI_CHECK_WIDE_EQ(jdi::Trim(L" \t\r\n "), std::wstring()); JDI_CHECK_WIDE_EQ(jdi::ToLowerAscii(L"AbC-129"), std::wstring(L"abc-129")); const std::wstring wide = jdi::Utf8ToWide("abc"); JDI_CHECK_WIDE_EQ(wide, std::wstring(L"abc")); JDI_CHECK(jdi::ContainsInsensitive(L"HelloWorld", L"world")); JDI_CHECK(!jdi::ContainsInsensitive(L"HelloWorld", L"xyz")); } JDI_TEST(ticks_nice_step_and_build_ticks) { // `NiceStep` 必须落在 1/2/2.5/5 × 10^n 这类"好读"的格点上 static const double ranges[] = {1.0, 10.0, 123.0, 0.0003, 98765.0}; for (const double range : ranges) { const double step = jdi::NiceStep(range, 10); JDI_REQUIRE("NiceStep > 0", step > 0.0); const double ratio = step / std::pow(10.0, std::floor(std::log10(step))); const bool nice = std::fabs(ratio - 1.0) < 1e-9 || std::fabs(ratio - 2.0) < 1e-9 || std::fabs(ratio - 2.5) < 1e-9 || std::fabs(ratio - 5.0) < 1e-9; JDI_REQUIRE("NiceStep is a round number", nice); } std::vector ticks; jdi::BuildTicks(0.0, 10.0, 1.0, ticks); JDI_CHECK_EQ(ticks.size(), 11); if (ticks.size() == 11) { JDI_CHECK_NEAR(ticks.front(), 0.0, 0.0); JDI_CHECK_NEAR(ticks.back(), 10.0, 0.0); } }