背景:Code Review 报告 §10.3 / §11.5 建议「替换 std::to_wstring / std::to_string,
可去掉 gdtoa 链(≈33 KB)」。本轮把它做完,并给出这条建议的**真实边界**。
一、新增 `Utf8Util::FormatInteger*`(手写十进制展开,不经 printf)
* `FormatInteger(long long)` / `FormatIntegerUnsigned(unsigned long long)` /
`FormatIntegerNarrow(long long)`(后者供 MatLoader 拼 HDF5 成员路径)。
* 语义与 `std::to_wstring` / `std::to_string` 完全一致:`0` → `"0"`、负数带 `-`、
`LLONG_MIN` 用模 2^64 语义取绝对值(不会溢出)。
* 这样做的直接理由:预编译 libstdc++ 的 `to_string` 内部转调 `__mingw_swprintf`,
工程里只要留一处,MinGW 的 printf 引擎(`mingw_wpformat.o`)+ gdtoa 就必然被链入。
二、全仓替换(76 处 `to_wstring` + 2 处 `to_string`,20 个文件)
* `MatLoader.cpp` 的 `TrimNumber` 整数分支也改为 `FormatInteger`(它曾是产物里
**最后一个**引用 MinGW printf 的调用点)。
* 需要的地方补 `#include "Utf8Util.hpp"`(仅 `MatLoader.cpp` 一个新 include,其余由
`PlotViewInternal` / `SignalPanelInternal` / `ApplicationInternal` / `DataModelInternal`
已经包含)。
三、⚠️ 实测结论:gdtoa **去不掉**(把这件事彻底查清并写进 Makefile 注释)
* 只替换 `to_wstring`/`to_string`:**−1,024 B**(787,968 → 786,944)。
* 再叠加 `-D__USE_MINGW_ANSI_STDIO=0`(把 `swprintf` 让给系统 UCRT):**0 字节**。
* 根因(定位到文件与行):libstdc++ 的
`lib/gcc/.../include/c++/x86_64-w64-mingw32/bits/os_defines.h:48-51` **无条件**执行
`#undef __USE_MINGW_ANSI_STDIO` + `#define __USE_MINGW_ANSI_STDIO 1`
(注释:libstdc++ depends on POSIX-definitions of those functions)。
`g++ -E -dM` 实测最终值恒为 1 ⇒ **任何 C++ TU 的 `swprintf` / `strtod` 都是 MinGW 版本**,
命令行无法覆盖。
* 要真省这 33 KB 只能自己实现「浮点 → 十进制(正确舍入)」与「浮点解析的
十六进制/inf/nan 分支」,等价于重写 gdtoa。**判定:不做**(风险 >> 收益)。
* 该开关已回退,Makefile 里保留一段说明,避免后续接手者重复这两次实验。
四、验证
* 新增单元测试 `integer_replacement_matches_std`:12 个有符号样本(含 LLONG_MIN/MAX)
+ 5 个无符号样本(含 ULLONG_MAX),与 `std::to_wstring` **逐字符对照**,
并校验窄/宽版本内容一致。
* `UnitTests`:`checks=260 failed=0 cases=19`(新增 41 条断言)。
* `OffscreenChecks -all`:6 个检查全 PASS,`checks=87 failed=0`。
* `panels -mode compare`:7 个面板像素哈希与几何**逐项不变**
(整数文本从 `to_wstring` 换到手写实现后,渲染结果零差异 —— 这是最强的语义证据)。
* release 全量零 error / 零 warning;产物 787,968 → **786,944 B**。
486 lines
15 KiB
C++
486 lines
15 KiB
C++
/**
|
||
* @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 <cmath>
|
||
#include <cstdint>
|
||
#include <cstdio>
|
||
#include <cstdlib>
|
||
#include <cstring>
|
||
#include <cwchar>
|
||
#include <random>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
#include <stdio.h>
|
||
#include <wchar.h>
|
||
|
||
#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<long long>(a - b) : -static_cast<long long>(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<int>(rng() % static_cast<unsigned>(maxDigits));
|
||
for (int index = 0; index < integerDigits; ++index)
|
||
{
|
||
text.push_back(static_cast<char>('0' + (rng() % 10u)));
|
||
}
|
||
if (allowFraction && (rng() % 3u) != 0u)
|
||
{
|
||
text.push_back('.');
|
||
const int fractionDigits = 1 + static_cast<int>(rng() % static_cast<unsigned>(maxDigits));
|
||
for (int index = 0; index < fractionDigits; ++index)
|
||
{
|
||
text.push_back(static_cast<char>('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<int>(rng() % static_cast<unsigned>(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"));
|
||
}
|
||
|
||
/**
|
||
* @brief `FormatInteger*` 必须与 `std::to_wstring` / `std::to_string` 语义**完全一致**。
|
||
*
|
||
* 这组函数替代了全仓 76 + 2 处标准库转换,目的是让产物不再依赖
|
||
* libstdc++ 的 `to_string`(它内部转调 `__mingw_swprintf`)。任何一位差异都会
|
||
* 直接改变界面文本,因此按「逐字符 + 与标准库对照」双重判定。
|
||
*/
|
||
JDI_TEST(integer_replacement_matches_std)
|
||
{
|
||
static const long long samples[] = {0,
|
||
1,
|
||
-1,
|
||
9,
|
||
10,
|
||
99,
|
||
100,
|
||
-100,
|
||
1234567890123ll,
|
||
-1234567890123ll,
|
||
0x7FFFFFFFFFFFFFFFll, // LLONG_MAX
|
||
(-0x7FFFFFFFFFFFFFFFll) - 1ll}; // LLONG_MIN
|
||
|
||
for (const long long value : samples)
|
||
{
|
||
const std::wstring wide = jdi::FormatInteger(value);
|
||
const std::wstring reference = std::to_wstring(value);
|
||
JDI_CHECK_WIDE_EQ(wide, reference);
|
||
// 窄版本:内容必须与宽版本逐字符相同(只是字符宽度不同)
|
||
const std::string narrow = jdi::FormatIntegerNarrow(value);
|
||
JDI_CHECK_EQ(static_cast<long long>(narrow.size()), static_cast<long long>(wide.size()));
|
||
bool same = (narrow.size() == wide.size());
|
||
for (std::size_t index = 0; same && index < narrow.size(); ++index)
|
||
{
|
||
same = (narrow[index] == static_cast<char>(wide[index]));
|
||
}
|
||
JDI_CHECK(same);
|
||
}
|
||
|
||
// 无符号版本(`size_t` 常走这条):覆盖 ULLONG_MAX
|
||
static const unsigned long long unsignedSamples[] = {0ull, 1ull, 42ull, 4294967295ull,
|
||
18446744073709551615ull};
|
||
for (const unsigned long long value : unsignedSamples)
|
||
{
|
||
const std::wstring ours = jdi::FormatIntegerUnsigned(value);
|
||
const std::wstring reference = std::to_wstring(value);
|
||
JDI_CHECK_WIDE_EQ(ours, reference);
|
||
}
|
||
}
|
||
|
||
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<double> 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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief MinGW ANSI stdio 与 UCRT printf 在**本程序实际使用的格式**上是否逐字符一致。
|
||
*
|
||
* 用途:评估"把 `swprintf` 让给系统 UCRT 实现(`__USE_MINGW_ANSI_STDIO=0`)"
|
||
* 这条体积优化是否可行 —— 若成立可以省掉整个 `mingw_wpformat` + `gdtoa` 链(≈33KB)。
|
||
* 判据极其严格:**逐字符相同**,一个字符的差异都不接受(显示文本是产品语义)。
|
||
*/
|
||
JDI_TEST(stdio_mingw_vs_ucrt_formats)
|
||
{
|
||
std::mt19937 rng(20261107u);
|
||
std::uniform_real_distribution<double> distribution(-1.0e7, 1.0e7);
|
||
std::uniform_real_distribution<double> magnitude(-40.0, 40.0);
|
||
|
||
int mismatches = 0;
|
||
int compared = 0;
|
||
static const int decimalsList[] = {0, 1, 2, 3, 4, 6, 8, 12};
|
||
|
||
for (int iteration = 0; iteration < 8000; ++iteration)
|
||
{
|
||
double value = distribution(rng);
|
||
// 覆盖极小 / 极大 / 整数量级,以及 FormatDecimal 会遇到的量级
|
||
if ((iteration % 7) == 0)
|
||
{
|
||
value = std::pow(10.0, magnitude(rng)) * ((iteration % 2 == 0) ? 1.0 : -1.0);
|
||
}
|
||
if ((iteration % 11) == 0)
|
||
{
|
||
value = static_cast<double>(iteration);
|
||
}
|
||
|
||
wchar_t ours[128] = {};
|
||
wchar_t theirs[128] = {};
|
||
|
||
for (const int decimals : decimalsList)
|
||
{
|
||
swprintf(ours, 128, L"%.*f", decimals, value);
|
||
_snwprintf(theirs, 128, L"%.*f", decimals, value);
|
||
++compared;
|
||
if (wcscmp(ours, theirs) != 0)
|
||
{
|
||
if (mismatches < 5)
|
||
{
|
||
std::printf(" [INFO] %%.%df value=%.17g ours=\"%ls\" ucrt=\"%ls\"\n", decimals, value, ours,
|
||
theirs);
|
||
}
|
||
++mismatches;
|
||
}
|
||
}
|
||
|
||
struct WideFormatProbe
|
||
{
|
||
const wchar_t* format;
|
||
};
|
||
static const WideFormatProbe probes[] = {{L"%.6g"}, {L"%.4e"}, {L"%.2f"}, {L"%g"}};
|
||
for (const WideFormatProbe& probe : probes)
|
||
{
|
||
swprintf(ours, 128, probe.format, value);
|
||
_snwprintf(theirs, 128, probe.format, value);
|
||
++compared;
|
||
if (wcscmp(ours, theirs) != 0)
|
||
{
|
||
if (mismatches < 5)
|
||
{
|
||
std::printf(" [INFO] \"%ls\" value=%.17g ours=\"%ls\" ucrt=\"%ls\"\n", probe.format, value,
|
||
ours, theirs);
|
||
}
|
||
++mismatches;
|
||
}
|
||
}
|
||
|
||
// 非浮点格式(颜色十六进制 / 计数 / 百分比尾缀)
|
||
const unsigned long long counter = static_cast<unsigned long long>(iteration) * 7919ull;
|
||
swprintf(ours, 128, L"%llu", counter);
|
||
_snwprintf(theirs, 128, L"%llu", counter);
|
||
++compared;
|
||
if (wcscmp(ours, theirs) != 0)
|
||
{
|
||
++mismatches;
|
||
}
|
||
|
||
swprintf(ours, 128, L"%02X%02X%02X", iteration & 0xFF, (iteration >> 3) & 0xFF, (iteration >> 6) & 0xFF);
|
||
_snwprintf(theirs, 128, L"%02X%02X%02X", iteration & 0xFF, (iteration >> 3) & 0xFF,
|
||
(iteration >> 6) & 0xFF);
|
||
++compared;
|
||
if (wcscmp(ours, theirs) != 0)
|
||
{
|
||
++mismatches;
|
||
}
|
||
|
||
swprintf(ours, 128, L"%.2f%%", value / 1000.0);
|
||
_snwprintf(theirs, 128, L"%.2f%%", value / 1000.0);
|
||
++compared;
|
||
if (wcscmp(ours, theirs) != 0)
|
||
{
|
||
++mismatches;
|
||
}
|
||
|
||
// `%ls`:`FormatByteSize` 的 `%.2f %ls` 依赖"宽格式串里的 %ls = 宽字符串"。
|
||
// ⚠️ MSVC/UCRT 的宽 printf 里 `%s` 也表示**宽**字符串,与 MinGW ANSI(`%s`=窄)相反,
|
||
// 因此全仓禁止在宽格式串里写 `%s`(已有注释约束),这里顺带把 `%ls` 也钉住。
|
||
swprintf(ours, 128, L"%.2f %ls", value / 1024.0, L"MB");
|
||
_snwprintf(theirs, 128, L"%.2f %ls", value / 1024.0, L"MB");
|
||
++compared;
|
||
if (wcscmp(ours, theirs) != 0)
|
||
{
|
||
if (mismatches < 5)
|
||
{
|
||
std::printf(" [INFO] \"%%.2f %%ls\" ours=\"%ls\" ucrt=\"%ls\"\n", ours, theirs);
|
||
}
|
||
++mismatches;
|
||
}
|
||
|
||
swprintf(ours, 128, L"%lld", static_cast<long long>(iteration) * -7919ll);
|
||
_snwprintf(theirs, 128, L"%lld", static_cast<long long>(iteration) * -7919ll);
|
||
++compared;
|
||
if (wcscmp(ours, theirs) != 0)
|
||
{
|
||
++mismatches;
|
||
}
|
||
}
|
||
|
||
std::printf(" [INFO] compared %d format calls, %d mismatches\n", compared, mismatches);
|
||
JDI_CHECK_EQ(mismatches, 0);
|
||
}
|