Files
Just-DataInspector/Utilities/Inc/Utf8Util.hpp
T
DeepSeek V4.1 Flash 34365b3c1c perf(size): 用自写整数格式化替换全部 std::to_wstring / std::to_string
背景: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**。
2026-11-04 14:31:02 +08:00

236 lines
9.0 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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__