背景: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**。
236 lines
9.0 KiB
C++
236 lines
9.0 KiB
C++
#ifndef __UTF8_UTIL_HPP__
|
||
#define __UTF8_UTIL_HPP__
|
||
|
||
#include <windows.h>
|
||
|
||
#include <cstddef>
|
||
#include <string>
|
||
#include <vector>
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
/**
|
||
* @brief 将 UTF-8 编码的字节序列转换为宽字符串(UTF-16LE)。
|
||
* @param [IN] text UTF-8 文本起始地址。
|
||
* @param [IN] length 文本字节数,传入负值表示按 '\0' 结尾自动计算长度。
|
||
* @return 转换后的宽字符串,输入非法时返回空字符串。
|
||
*/
|
||
std::wstring Utf8ToWide(const char* text, int length = -1);
|
||
|
||
/**
|
||
* @brief 去除字符串首尾的空白字符(含制表、换行与不间断空格)。
|
||
* @param [IN] text 待处理文本。
|
||
* @return 去除首尾空白后的新字符串。
|
||
*/
|
||
std::wstring Trim(const std::wstring& text);
|
||
|
||
/**
|
||
* @brief 去除字符串首尾的成对引号(单引号或双引号)。
|
||
* @param [IN] text 待处理文本。
|
||
* @return 去除引号后的新字符串。
|
||
*/
|
||
std::wstring StripQuotes(const std::wstring& text);
|
||
|
||
/**
|
||
* @brief 仅针对 ASCII 字母执行小写转换(其余字符原样保留)。
|
||
* @param [IN] text 待处理文本。
|
||
* @return 转换后的新字符串。
|
||
*/
|
||
std::wstring ToLowerAscii(const std::wstring& text);
|
||
|
||
/**
|
||
* @brief 判断字符串是否包含指定子串(ASCII 大小写不敏感)。
|
||
* @param [IN] text 被检索文本。
|
||
* @param [IN] needle 子串文本。
|
||
* @return 包含返回 true,否则返回 false。
|
||
*/
|
||
bool ContainsInsensitive(const std::wstring& text, const std::wstring& needle);
|
||
|
||
/**
|
||
* @brief 从字符区间中解析一个双精度浮点数(允许尾部存在非数字残留字符)。
|
||
* @param [IN] begin 区间起始指针。
|
||
* @param [IN] end 区间结束指针(不包含)。
|
||
* @param [OUT] value 解析结果输出。
|
||
* @return 解析成功返回 true,区间为空或完全无法识别时返回 false。
|
||
*/
|
||
bool TryParseDouble(const char* begin, const char* end, double& value);
|
||
|
||
/**
|
||
* @brief 以固定小数位格式化数值。
|
||
* @param [IN] value 待格式化数值。
|
||
* @param [IN] decimals 小数位数。
|
||
* @return 格式化后的文本。
|
||
*/
|
||
std::wstring FormatNumber(double value, int decimals);
|
||
|
||
/**
|
||
* @brief 把字节数格式化为自适应单位的文本(GB / MB / KB),保留两位小数。
|
||
*
|
||
* 单位阀值:1 GB = 1024 MB、1 MB = 1024 KB;小于 1 KB 时也按 KB 显示。
|
||
* 只输出"数字 + 单位"(如 "1.69 GB"),便于拼接为"已读取 / 总量"。
|
||
*
|
||
* @param [IN] bytes 字节数。
|
||
* @return 形如 "812.34 MB" 的文本。
|
||
*/
|
||
std::wstring FormatByteSize(unsigned long long bytes);
|
||
|
||
/**
|
||
* @brief 以自适应有效位格式化数值(NaN/Inf 会被转换为可读文本)。
|
||
* @param [IN] value 待格式化数值。
|
||
* @return 格式化后的文本。
|
||
*/
|
||
std::wstring FormatValue(double value);
|
||
|
||
/**
|
||
* @brief 以**十进制小数**形式格式化数值(**绝不出现科学计数法**)。
|
||
*
|
||
* 与 `FormatValue`(`%.6g`)的区别:
|
||
* * `FormatValue(1.0e-5)` → `"1e-05"`(科学计数法,不适合显示时间间隔 / 频率);
|
||
* * `FormatDecimal(1.0e-5)` → `"0.00001"`。
|
||
*
|
||
* 小数位数按“保留 `significantDigits` 位有效数字”自适应(整数部分超过有效位时取 0 位),
|
||
* 末尾多余的 0 会被去掉;`-0` 归一成 `0`。
|
||
*
|
||
* @param [IN] value 待格式化数值。
|
||
* @param [IN] significantDigits 期望保留的有效数字位数(1–15,默认 6)。
|
||
* @return 形如 `0.00001` / `25.641` / `1000000` 的文本。
|
||
*/
|
||
std::wstring FormatDecimal(double value, int significantDigits = 6);
|
||
|
||
/**
|
||
* @brief 以**固定小数位数**格式化数值(绝不出现科学计数法,保留尾随 0)。
|
||
*
|
||
* 与 `FormatDecimal` 的区别:后者按「有效数字位数」自适应小数位并去掉尾随 0;
|
||
* 本函数按**小数位数**精确输出,**保留尾随 0**(用户 2026-11-03 要求:
|
||
* 「次级单位保留三位小数」,例如 `696.631us`、`700.000us`)。
|
||
*
|
||
* 例:`FormatDecimalFixed(0.003696631 * 1e6, 3)` → `"3696.631"`;
|
||
* `FormatDecimalFixed(700.0, 3)` → `"700.000"`。
|
||
*
|
||
* @param [IN] value 待格式化数值。
|
||
* @param [IN] decimals 小数位数(0–12,默认 3)。
|
||
* @return 定点小数文本;NaN/Inf 返回 "NaN"/"+Inf"/"-Inf"。
|
||
*/
|
||
std::wstring FormatDecimalFixed(double value, int decimals = 3);
|
||
|
||
/**
|
||
* @brief 依据坐标轴刻度步长格式化刻度标签。
|
||
* @param [IN] value 刻度值。
|
||
* @param [IN] step 刻度步长,用于推断所需小数位。
|
||
* @return 刻度标签文本。
|
||
*/
|
||
std::wstring FormatTick(double value, double step);
|
||
|
||
/**
|
||
* @brief 将整数格式化为带千位分隔符的文本。
|
||
* @param [IN] value 待格式化整数。
|
||
* @return 格式化后的文本。
|
||
*/
|
||
std::wstring FormatCount(unsigned long long value);
|
||
|
||
/**
|
||
* @brief 把整数转换为**纯十进制**宽字符串(`std::to_wstring` 的替代品)。
|
||
*
|
||
* ⚠️ 为什么不用 `std::to_wstring`:预编译的 libstdc++ 里它转调 `__mingw_swprintf`,
|
||
* 会把 MinGW 的 printf 引擎(`mingw_wpformat.o`)连同 gdtoa 整套
|
||
* (`strtodg` / `gdtoa` / `gethex` / `hexnan`)一起拉进产物,map 实测 `.text`
|
||
* 合计 ≈33 KB。只要工程里还有一处 `std::to_wstring` / `std::to_string`,
|
||
* 这条依赖链就断不掉(本轮实测:只把 `swprintf` 让给 UCRT 是 **0 字节**收益)。
|
||
*
|
||
* 本实现手写十进制展开:无本地化、无格式串解析、无堆分配(除返回的字符串本身),
|
||
* 语义与 `std::to_wstring` 完全一致(`-0` → `"0"`,最小值也能正确取绝对值)。
|
||
*
|
||
* @param [IN] value 待转换整数(含符号)。
|
||
* @return 十进制文本,例如 `-123`。
|
||
*/
|
||
std::wstring FormatInteger(long long value);
|
||
|
||
/**
|
||
* @brief 把无符号整数转换为纯十进制宽字符串(避免 `size_t` 走有符号重载的隐式转换告警)。
|
||
* @param [IN] value 待转换整数。
|
||
* @return 十进制文本。
|
||
*/
|
||
std::wstring FormatIntegerUnsigned(unsigned long long value);
|
||
|
||
/**
|
||
* @brief 把整数转换为纯十进制**窄**字符串(`std::to_string` 的替代品)。
|
||
*
|
||
* 只在构造 HDF5 成员路径时使用(`MatLoader`),语义同 `FormatInteger`。
|
||
*
|
||
* @param [IN] value 待转换整数。
|
||
* @return 十进制文本。
|
||
*/
|
||
std::string FormatIntegerNarrow(long long value);
|
||
|
||
/**
|
||
* @brief 把时间间隔格式化为「Δt 语义」的复合单位文本(不含 "Δt:" 前缀)。
|
||
*
|
||
* 规则(用户明确要求,与双光标差值的显示完全一致):
|
||
* - 顶级单位高于 s(即 h / min)时**一路整合到 s**,例如 3662s → "1h 1min 2s";
|
||
* - 顶级单位为 s 及以下时,若不是该单位的整数,则**向下一级递进**,
|
||
* 例如 5.06s → "5s 60ms";整秒数不加下级单位,例如 5s → "5s";
|
||
* - 顶级单位为 ps 时不再递进次级单位,例如 5.123ps → "5.123 ps"。
|
||
*
|
||
* @param [IN] seconds 时间间隔(秒)。
|
||
* @return 形如 "1h 1min 2s" / "5s 60ms" 的间隔文本。
|
||
*/
|
||
std::wstring FormatDurationCompound(double seconds);
|
||
|
||
/**
|
||
* @brief 把时间间隔格式化为「秒」数值文本(不含单位),用于内联编辑。
|
||
*
|
||
* 编辑时的量级**始终是秒**:即使显示为 "1h 1min 2s",进入编辑后也是 3602。
|
||
*
|
||
* @param [IN] seconds 时间间隔(秒)。
|
||
* @return 形如 "3602" 的文本。
|
||
*/
|
||
std::wstring FormatDurationSeconds(double seconds);
|
||
|
||
/**
|
||
* @brief 把频率格式化为带自适应单位的文本(Hz / kHz / MHz / GHz,保留小数)。
|
||
*
|
||
* 用于「重采样」面板与派生信号名称(`<原信号> (重采样 1.2 kHz)`)。
|
||
*
|
||
* @param [IN] hertz 频率(Hz)。
|
||
* @return 形如 "1.2 kHz" 的文本;非法值返回 "—"。
|
||
*/
|
||
std::wstring FormatFrequency(double hertz);
|
||
|
||
/**
|
||
* @brief 计算“美观”的坐标轴刻度步长(1/2/5 乘以 10 的整数次幂)。
|
||
* @param [IN] range 坐标轴数值跨度,必须为正数。
|
||
* @param [IN] targetTicks 期望的刻度数量。
|
||
* @return 刻度步长,输入非法时返回 1.0。
|
||
*/
|
||
double NiceStep(double range, int targetTicks);
|
||
|
||
/**
|
||
* @brief 依据范围与步长生成刻度值序列。
|
||
* @param [IN] minValue 范围下限。
|
||
* @param [IN] maxValue 范围上限。
|
||
* @param [IN] step 刻度步长。
|
||
* @param [OUT] ticks 输出刻度序列(按升序排列)。
|
||
*/
|
||
void BuildTicks(double minValue, double maxValue, double step, std::vector<double>& ticks);
|
||
|
||
/**
|
||
* @brief 将文本复制到系统剪贴板。
|
||
* @param [IN] owner 剪贴板所有者窗口句柄,允许为空。
|
||
* @param [IN] text 待复制文本。
|
||
* @return 复制成功返回 true。
|
||
*/
|
||
bool CopyTextToClipboard(HWND owner, const std::wstring& text);
|
||
|
||
/**
|
||
* @brief 从系统剪贴板读取文本。
|
||
* @param [IN] owner 剪贴板所有者窗口句柄,允许为空。
|
||
* @param [OUT] text 读出的文本,剪贴板无文本时清空。
|
||
* @return 读到非空文本返回 true。
|
||
*/
|
||
bool PasteTextFromClipboard(HWND owner, std::wstring& text);
|
||
|
||
} // namespace jdi
|
||
|
||
#endif // __UTF8_UTIL_HPP__
|