背景: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**。
1057 lines
29 KiB
C++
1057 lines
29 KiB
C++
#include "Utf8Util.hpp"
|
||
|
||
#include <windows.h>
|
||
|
||
#include <cmath>
|
||
#include <cstdio>
|
||
#include <cstdlib>
|
||
#include <cstring>
|
||
|
||
namespace jdi
|
||
{
|
||
|
||
namespace
|
||
{
|
||
/** @brief 判断字符是否为需要裁掉的空白字符。 */
|
||
bool IsSpaceChar(wchar_t c)
|
||
{
|
||
return c == L' ' || c == L'\t' || c == L'\r' || c == L'\n' || c == L'\v' || c == L'\f' ||
|
||
c == static_cast<wchar_t>(0x00A0);
|
||
}
|
||
|
||
/** @brief 判断字符是否为 ASCII 大写字母。 */
|
||
bool IsUpperAscii(wchar_t c)
|
||
{
|
||
return c >= L'A' && c <= L'Z';
|
||
}
|
||
|
||
/** @brief 时间间隔的显示单位。 */
|
||
struct DurationUnit
|
||
{
|
||
double scale = 1.0; /**< 1 个该单位等于多少秒。 */
|
||
const wchar_t* name = L"s"; /**< 单位名称。 */
|
||
};
|
||
|
||
/**
|
||
* @brief 复合单位时间文本里**次级单位最多保留的小数位数**。
|
||
*
|
||
* 用户 2026-11-03 明确要求:`0.003696631s` 必须显示为 `3ms 696.631us`
|
||
* (次级单位 ms/us/ns 保留三位小数,能看到真实尾数),
|
||
* 而不是四舍五入到 `3ms 700us`(旧实现舍入到 3 位**有效数字**,把尾数抹掉了)。
|
||
*
|
||
* ⚠️ 这是**上限**,不是强制的固定位数:整数次级单位仍写成整数
|
||
* (`5.06s` → `5s 60ms`,而不是 `5s 60.000ms`),只有真的带小数时才补零到 3 位。
|
||
*/
|
||
constexpr int kDurationSubUnitDecimals = 3;
|
||
|
||
/**
|
||
* @brief 频率文本保留的小数位数(用户 2026-11-03 要求)。
|
||
*
|
||
* `270.516586...Hz` 必须显示为 `270.517 Hz`(真实值保留三位小数),
|
||
* 而不是"先把间隔舍入到 3 位有效数字再换算"得到的 `270.27 Hz`。
|
||
*/
|
||
constexpr int kFrequencyDecimals = 3;
|
||
|
||
/**
|
||
* @brief 格式化次级单位数值:带小数则保留 `maxDecimals` 位,整数则原样输出。
|
||
*
|
||
* `696.6314` → `696.631`;`60.0` → `60`;`0.0158` → `0.016`。
|
||
* 这样既满足"次级单位保留三位小数"(看得到真实尾数),
|
||
* 又不会把整数次级单位写成 `60.000` 这种噪音。
|
||
*/
|
||
inline std::wstring FormatSubUnit(double value, int maxDecimals)
|
||
{
|
||
if (!std::isfinite(value))
|
||
{
|
||
return L"—";
|
||
}
|
||
const double rounded = std::round(value * std::pow(10.0, maxDecimals)) /
|
||
std::pow(10.0, maxDecimals);
|
||
if (rounded == std::floor(rounded))
|
||
{
|
||
return FormatDecimal(rounded); // 整数:不加小数点
|
||
}
|
||
return FormatDecimalFixed(rounded, maxDecimals);
|
||
}
|
||
}
|
||
|
||
std::wstring Utf8ToWide(const char* text, int length)
|
||
{
|
||
if (text == nullptr)
|
||
{
|
||
return std::wstring();
|
||
}
|
||
|
||
int byteCount = (length < 0) ? static_cast<int>(std::strlen(text)) : length;
|
||
if (byteCount <= 0)
|
||
{
|
||
return std::wstring();
|
||
}
|
||
|
||
int wideCount = MultiByteToWideChar(CP_UTF8, 0, text, byteCount, nullptr, 0);
|
||
if (wideCount <= 0)
|
||
{
|
||
return std::wstring();
|
||
}
|
||
|
||
std::wstring result(static_cast<size_t>(wideCount), L'\0');
|
||
MultiByteToWideChar(CP_UTF8, 0, text, byteCount, &result[0], wideCount);
|
||
return result;
|
||
}
|
||
|
||
std::wstring Trim(const std::wstring& text)
|
||
{
|
||
size_t begin = 0;
|
||
size_t end = text.size();
|
||
while (begin < end && IsSpaceChar(text[begin]))
|
||
{
|
||
++begin;
|
||
}
|
||
while (end > begin && IsSpaceChar(text[end - 1]))
|
||
{
|
||
--end;
|
||
}
|
||
return text.substr(begin, end - begin);
|
||
}
|
||
|
||
std::wstring StripQuotes(const std::wstring& text)
|
||
{
|
||
std::wstring trimmed = Trim(text);
|
||
if (trimmed.size() >= 2)
|
||
{
|
||
const wchar_t first = trimmed.front();
|
||
const wchar_t last = trimmed.back();
|
||
if ((first == L'"' && last == L'"') || (first == L'\'' && last == L'\''))
|
||
{
|
||
trimmed = trimmed.substr(1, trimmed.size() - 2);
|
||
}
|
||
}
|
||
return trimmed;
|
||
}
|
||
|
||
std::wstring ToLowerAscii(const std::wstring& text)
|
||
{
|
||
std::wstring result = text;
|
||
for (wchar_t& c : result)
|
||
{
|
||
if (IsUpperAscii(c))
|
||
{
|
||
c = static_cast<wchar_t>(c - L'A' + L'a');
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
bool ContainsInsensitive(const std::wstring& text, const std::wstring& needle)
|
||
{
|
||
if (needle.empty())
|
||
{
|
||
return true;
|
||
}
|
||
if (needle.size() > text.size())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const size_t last = text.size() - needle.size();
|
||
for (size_t i = 0; i <= last; ++i)
|
||
{
|
||
size_t j = 0;
|
||
while (j < needle.size())
|
||
{
|
||
wchar_t a = text[i + j];
|
||
wchar_t b = needle[j];
|
||
if (IsUpperAscii(a))
|
||
{
|
||
a = static_cast<wchar_t>(a - L'A' + L'a');
|
||
}
|
||
if (IsUpperAscii(b))
|
||
{
|
||
b = static_cast<wchar_t>(b - L'A' + L'a');
|
||
}
|
||
if (a != b)
|
||
{
|
||
break;
|
||
}
|
||
++j;
|
||
}
|
||
if (j == needle.size())
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/** @brief 快速解析路径允许保留的最大有效位数(超过则交回 strtod 以保证正确舍入)。 */
|
||
const int kMaxFastDigits = 17;
|
||
|
||
/** @brief 快速解析路径允许使用的最大十进制指数(超出查表范围,交回 strtod)。 */
|
||
const int kMaxFastExponent = 22;
|
||
|
||
/** @brief 10 的 0-22 次幂精确表(该范围内每位都是 double 可精确表示的十进制数)。 */
|
||
const double kPow10Table[23] = {1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11,
|
||
1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22};
|
||
|
||
/**
|
||
* @brief 标准库解析路径:去掉首尾空白后交给 strtod。
|
||
*
|
||
* 作为快速路径的保底实现,保证十六进制浮点、inf/nan、超范围指数、
|
||
* 带尾部杂字符(如 "12abc")等输入的处理与改造前完全一致。
|
||
*
|
||
* @param [IN] begin 文本起始指针。
|
||
* @param [IN] end 文本结束指针(不包含)。
|
||
* @param [OUT] value 解析结果。
|
||
* @return 解析出数值返回 true。
|
||
*/
|
||
bool ParseDoubleSlow(const char* begin, const char* end, double& value)
|
||
{
|
||
const char* p = begin;
|
||
while (p < end && (*p == ' ' || *p == '\t'))
|
||
{
|
||
++p;
|
||
}
|
||
const char* q = end;
|
||
while (q > p && (q[-1] == ' ' || q[-1] == '\t' || q[-1] == '\r' || q[-1] == '\n'))
|
||
{
|
||
--q;
|
||
}
|
||
if (p >= q)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
char buffer[96];
|
||
const size_t length = static_cast<size_t>(q - p);
|
||
const char* text = nullptr;
|
||
std::string dynamic;
|
||
if (length < sizeof(buffer))
|
||
{
|
||
std::memcpy(buffer, p, length);
|
||
buffer[length] = '\0';
|
||
text = buffer;
|
||
}
|
||
else
|
||
{
|
||
dynamic.assign(p, q);
|
||
text = dynamic.c_str();
|
||
}
|
||
|
||
char* stop = nullptr;
|
||
const double parsed = std::strtod(text, &stop);
|
||
if (stop == text)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
value = parsed;
|
||
return true;
|
||
}
|
||
|
||
bool TryParseDouble(const char* begin, const char* end, double& value)
|
||
{
|
||
if (begin == nullptr || end == nullptr || begin >= end)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// ===== 快速路径:纯十进制数值 =====
|
||
// 大文件解析的绝对瓶颈就在这里:ucrt 的 strtod 要处理本地化、十六进制浮点、
|
||
// 无穷/NaN 等全部语法,实测每次调用约 335ns;百万行级文件需调用上千万次,
|
||
// 单解析就可能耗掉几十秒。下面这条路径只处理最常见的十进制写法(实测约 13ns,
|
||
// 快 25 倍),其余写法(含尾部杂字符)原样交回 strtod,
|
||
// 因此输入接受范围与原先完全一致。
|
||
const char* head = begin;
|
||
while (head < end && (*head == ' ' || *head == '\t'))
|
||
{
|
||
++head;
|
||
}
|
||
const char* tail = end;
|
||
while (tail > head && (tail[-1] == ' ' || tail[-1] == '\t' || tail[-1] == '\r' || tail[-1] == '\n'))
|
||
{
|
||
--tail;
|
||
}
|
||
if (head >= tail)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
const char* cursor = head;
|
||
bool negative = false;
|
||
if (*cursor == '+' || *cursor == '-')
|
||
{
|
||
negative = (*cursor == '-');
|
||
++cursor;
|
||
}
|
||
|
||
unsigned long long mantissa = 0;
|
||
int digits = 0;
|
||
int exponent = 0;
|
||
|
||
// 整数部分:超过有效位数的高位数字只影响量级,累加进指数。
|
||
while (cursor < tail && *cursor >= '0' && *cursor <= '9')
|
||
{
|
||
if (digits < kMaxFastDigits)
|
||
{
|
||
mantissa = mantissa * 10ull + static_cast<unsigned>(*cursor - '0');
|
||
++digits;
|
||
}
|
||
else
|
||
{
|
||
++exponent;
|
||
}
|
||
++cursor;
|
||
}
|
||
|
||
// 小数部分:每保留一位就记一次小数位数;超出有效位数的直接丢弃(已低于 double 精度)。
|
||
int fractionDigits = 0;
|
||
if (cursor < tail && *cursor == '.')
|
||
{
|
||
++cursor;
|
||
while (cursor < tail && *cursor >= '0' && *cursor <= '9')
|
||
{
|
||
if (digits < kMaxFastDigits)
|
||
{
|
||
mantissa = mantissa * 10ull + static_cast<unsigned>(*cursor - '0');
|
||
++digits;
|
||
++fractionDigits;
|
||
}
|
||
++cursor;
|
||
}
|
||
}
|
||
|
||
if (digits == 0)
|
||
{
|
||
return ParseDoubleSlow(begin, end, value);
|
||
}
|
||
exponent -= fractionDigits;
|
||
|
||
if (cursor < tail && (*cursor == 'e' || *cursor == 'E'))
|
||
{
|
||
++cursor;
|
||
bool expNegative = false;
|
||
if (cursor < tail && (*cursor == '+' || *cursor == '-'))
|
||
{
|
||
expNegative = (*cursor == '-');
|
||
++cursor;
|
||
}
|
||
if (cursor >= tail || *cursor < '0' || *cursor > '9')
|
||
{
|
||
return ParseDoubleSlow(begin, end, value);
|
||
}
|
||
int expValue = 0;
|
||
while (cursor < tail && *cursor >= '0' && *cursor <= '9')
|
||
{
|
||
if (expValue < 1000000)
|
||
{
|
||
expValue = expValue * 10 + (*cursor - '0');
|
||
}
|
||
++cursor;
|
||
}
|
||
exponent += expNegative ? -expValue : expValue;
|
||
}
|
||
|
||
// 存在未消费字符(如 "12abc"、"1,5")或指数超出查表范围时交回标准库:
|
||
// 既保持原先 strtod 的宽松接受语义,也保证极端量级的正确舍入。
|
||
if (cursor != tail || exponent > kMaxFastExponent || exponent < -kMaxFastExponent)
|
||
{
|
||
return ParseDoubleSlow(begin, end, value);
|
||
}
|
||
|
||
double result = static_cast<double>(mantissa);
|
||
if (exponent > 0)
|
||
{
|
||
result *= kPow10Table[exponent];
|
||
}
|
||
else if (exponent < 0)
|
||
{
|
||
result /= kPow10Table[-exponent];
|
||
}
|
||
// mantissa 超过 2^53(16 位以上有效数字)时这里会比 strtod 多一次舍入,
|
||
// 相对误差不超过 1 ULP(实测 ≤2.3e-16),在波形显示与标尺(约 6 位有效数字)上不可见。
|
||
value = negative ? -result : result;
|
||
return true;
|
||
}
|
||
std::wstring FormatNumber(double value, int decimals)
|
||
{
|
||
if (decimals < 0)
|
||
{
|
||
decimals = 0;
|
||
}
|
||
if (decimals > 12)
|
||
{
|
||
decimals = 12;
|
||
}
|
||
|
||
wchar_t buffer[96];
|
||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.*f", decimals, value);
|
||
return std::wstring(buffer);
|
||
}
|
||
|
||
std::wstring FormatDecimal(double value, int significantDigits)
|
||
{
|
||
if (std::isnan(value))
|
||
{
|
||
return L"NaN";
|
||
}
|
||
if (std::isinf(value))
|
||
{
|
||
return (value > 0.0) ? L"+Inf" : L"-Inf";
|
||
}
|
||
if (significantDigits < 1)
|
||
{
|
||
significantDigits = 1;
|
||
}
|
||
if (significantDigits > 15)
|
||
{
|
||
significantDigits = 15;
|
||
}
|
||
const double magnitude = std::fabs(value);
|
||
if (magnitude == 0.0)
|
||
{
|
||
return L"0";
|
||
}
|
||
|
||
// 小数位数 = 让结果保留约 `significantDigits` 位有效数字(整数部分已超出时取 0 位)。
|
||
// ⚠️ **绝不用 `%g` / `%e`**:`%g` 在量级小于 1e-4 或大于 1e6 时会切换成科学计数法,
|
||
// 对“时间间隔 / 频率”这类读数极不友好(用户 2026-11-03 明确要求小数形式)。
|
||
int decimals = 0;
|
||
{
|
||
const double exponent = std::floor(std::log10(magnitude));
|
||
decimals = significantDigits - 1 - static_cast<int>(exponent);
|
||
}
|
||
if (decimals < 0)
|
||
{
|
||
decimals = 0;
|
||
}
|
||
if (decimals > 12)
|
||
{
|
||
decimals = 12;
|
||
}
|
||
|
||
wchar_t buffer[160];
|
||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.*f", decimals, value);
|
||
std::wstring text(buffer);
|
||
|
||
// 去掉尾随的 0 与孤立的小数点(`1.50000` → `1.5`、`2.000000` → `2`)。
|
||
if (text.find(L'.') != std::wstring::npos)
|
||
{
|
||
std::size_t last = text.find_last_not_of(L'0');
|
||
if (last == std::wstring::npos)
|
||
{
|
||
return L"0";
|
||
}
|
||
if (text[last] == L'.')
|
||
{
|
||
--last;
|
||
}
|
||
text.erase(last + 1);
|
||
}
|
||
if (text == L"-0")
|
||
{
|
||
text = L"0";
|
||
}
|
||
return text;
|
||
}
|
||
|
||
std::wstring FormatDecimalFixed(double value, int decimals)
|
||
{
|
||
if (std::isnan(value))
|
||
{
|
||
return L"NaN";
|
||
}
|
||
if (std::isinf(value))
|
||
{
|
||
return (value > 0.0) ? L"+Inf" : L"-Inf";
|
||
}
|
||
if (decimals < 0)
|
||
{
|
||
decimals = 0;
|
||
}
|
||
if (decimals > 12)
|
||
{
|
||
decimals = 12;
|
||
}
|
||
// ⚠️ 固定小数位数 + **保留尾随 0**(与 `FormatDecimal` 的"去尾随 0"语义相反):
|
||
// 用户 2026-11-03 明确要求「次级单位保留三位小数」,例如 696.631us / 700.000us。
|
||
wchar_t buffer[160];
|
||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.*f", decimals, value);
|
||
std::wstring text(buffer);
|
||
if (text == L"-0" || (text.size() > 1 && text[0] == L'-' &&
|
||
text.find_first_not_of(L"0-.") == std::wstring::npos))
|
||
{
|
||
text.erase(0, 1); // -0.000 → 0.000
|
||
}
|
||
return text;
|
||
}
|
||
|
||
std::wstring FormatDurationCompound(double seconds) {
|
||
if (!std::isfinite(seconds))
|
||
{
|
||
return L"—";
|
||
}
|
||
const double value = std::fabs(seconds);
|
||
if (value <= 0.0)
|
||
{
|
||
return L"0s";
|
||
}
|
||
|
||
// ---- 顶级单位高于 s(h / min):整合为 h/min/s(整数秒)----
|
||
// 注意条件必须是 >= 60s:1~60s 属于"顶级单位为 s"的情况,需按下一条规则处理
|
||
// (非整数秒要递进到 ms,例如 5.06s → "5s 60ms")。
|
||
if (value >= 60.0)
|
||
{
|
||
const long long totalSeconds = static_cast<long long>(std::llround(value));
|
||
long long hours = 0;
|
||
long long minutes = 0;
|
||
long long secs = totalSeconds;
|
||
if (totalSeconds >= 3600)
|
||
{
|
||
hours = totalSeconds / 3600;
|
||
secs = totalSeconds % 3600;
|
||
}
|
||
if (secs >= 60)
|
||
{
|
||
minutes = secs / 60;
|
||
secs = secs % 60;
|
||
}
|
||
|
||
std::wstring text;
|
||
const auto append = [&text](long long amount, const wchar_t* unit)
|
||
{
|
||
if (amount <= 0)
|
||
{
|
||
return;
|
||
}
|
||
if (!text.empty())
|
||
{
|
||
text += L" ";
|
||
}
|
||
text += FormatInteger(amount);
|
||
text += unit;
|
||
};
|
||
append(hours, L"h");
|
||
append(minutes, L"min");
|
||
append(secs, L"s");
|
||
if (!text.empty())
|
||
{
|
||
return text;
|
||
}
|
||
}
|
||
|
||
// ---- 顶级单位为 s:整数秒 + 可选 ms ----
|
||
// ⚠️⚠️ 与下面的亚秒分支同一原则:**先量化到整数皮秒,再分解**。
|
||
// 直接对 double 做 `floor` + `fraction * 1000` 会被浮点噪声坑到:
|
||
// `0.9999999999s` 的余数四舍五入后是 `1000.000ms`(应进位成 `1s`),
|
||
// 而 `5.0599999999999996s` 与 `5.06s` 会得到不同的尾数(用户 2026-11-03 报告)。
|
||
if (value >= 1.0)
|
||
{
|
||
const long long totalPs = static_cast<long long>(std::llround(value * 1.0e12));
|
||
long long wholeSeconds = totalPs / 1000000000000LL;
|
||
const long long restPs = totalPs % 1000000000000LL;
|
||
// 次级(ms)以「千分之一 ms」为整数单位,保证舍入一次到位。
|
||
long long milliMs = (restPs + 500000LL) / 1000000LL;
|
||
if (milliMs >= 1000LL * 1000LL)
|
||
{
|
||
++wholeSeconds;
|
||
milliMs = 0;
|
||
}
|
||
std::wstring text = FormatDecimal(static_cast<double>(wholeSeconds)) + L"s";
|
||
if (milliMs > 0)
|
||
{
|
||
// ⚠️ 次级单位(ms)**最多保留三位小数**(用户 2026-11-03 要求:`3ms 696.631us`
|
||
// 这类显示要能看到真实尾数);整数则不加小数点(`5.06s` → `5s 60ms`)。
|
||
text += L" ";
|
||
text += FormatSubUnit(static_cast<double>(milliMs) / 1000.0, kDurationSubUnitDecimals);
|
||
text += L"ms";
|
||
}
|
||
return text;
|
||
}
|
||
|
||
// ---- 顶级单位低于 s:ms / us / ns / ps ----
|
||
// ⚠️⚠️ **必须以整数(皮秒)为唯一事实来源做分解**,不能对 double 反复 floor/除法。
|
||
// 同一个物理间隔(例如 `0.001s`)在两次独立计算里可能分别是
|
||
// `0.0010000000000000002` 与 `0.0009999999999999998`;任何"接近 1000 就进位"
|
||
// 的浮点启发式都会在边界漏判,于是同一数值出现 `1ms` 与 `1000us` 两种显示,
|
||
// 还会留下 `100us 0ns` 这种纯噪声尾数(用户 2026-11-03 报告)。
|
||
// 量化到 ps 之后噪声被彻底消除:同一数值必然得到同一字符串。
|
||
const long long totalPs = static_cast<long long>(std::llround(value * 1.0e12));
|
||
if (totalPs <= 0)
|
||
{
|
||
// 小于 0.5ps:无法量化,保留高精度小数直接显示。
|
||
return FormatDecimal(value / 1.0e-12) + L" ps";
|
||
}
|
||
|
||
struct SubUnit
|
||
{
|
||
long long ps;
|
||
const wchar_t* name;
|
||
};
|
||
const SubUnit subUnits[] = {
|
||
{1000000000LL, L"ms"}, {1000000LL, L"us"}, {1000LL, L"ns"}, {1LL, L"ps"},
|
||
};
|
||
const int subCount = 4;
|
||
|
||
// 量化后达到 1s ⇒ 等价于上面的秒级分支(`999.9999999999s` → `1s`)。
|
||
if (totalPs >= 1000000000000LL)
|
||
{
|
||
long long wholeSeconds = totalPs / 1000000000000LL;
|
||
const long long restPs = totalPs % 1000000000000LL;
|
||
long long milliMs = (restPs + 500000LL) / 1000000LL;
|
||
if (milliMs >= 1000LL * 1000LL)
|
||
{
|
||
++wholeSeconds;
|
||
milliMs = 0;
|
||
}
|
||
std::wstring text = FormatDecimal(static_cast<double>(wholeSeconds)) + L"s";
|
||
if (milliMs > 0)
|
||
{
|
||
text += L" ";
|
||
text += FormatSubUnit(static_cast<double>(milliMs) / 1000.0, kDurationSubUnitDecimals);
|
||
text += L"ms";
|
||
}
|
||
return text;
|
||
}
|
||
|
||
// 选主单位:第一个"能被 totalPs 容纳"的单位。
|
||
int index = 0;
|
||
while (index + 1 < subCount && totalPs < subUnits[index].ps)
|
||
{
|
||
++index;
|
||
}
|
||
|
||
// 把 totalPs 分解成 [主单位整数] + [次级单位(千分之一为单位的整数)]。
|
||
// 抽成 lambda 是因为升级单位后需要用同一个 totalPs 重算,避免累积误差。
|
||
long long whole = 0;
|
||
long long milliNext = 0;
|
||
const auto decompose = [&]()
|
||
{
|
||
whole = totalPs / subUnits[index].ps;
|
||
milliNext = 0;
|
||
if (index + 1 >= subCount)
|
||
{
|
||
return;
|
||
}
|
||
const long long restPs = totalPs % subUnits[index].ps;
|
||
const long long nextPs = subUnits[index + 1].ps;
|
||
// 次级值保留 3 位小数 ⇒ 用「千分之一个次级单位」作为整数量纲。
|
||
milliNext = (restPs * 1000LL + nextPs / 2) / nextPs;
|
||
// 次级舍入后达到 1000 个次级单位 ⇒ 进位到主单位。
|
||
if (milliNext >= 1000LL * 1000LL)
|
||
{
|
||
++whole;
|
||
milliNext = 0;
|
||
}
|
||
};
|
||
decompose();
|
||
// 主单位达到 1000 且没有次级尾数 ⇒ 升级到更高的主单位(`1000us` → `1ms`)。
|
||
while (index > 0 && whole >= 1000LL && milliNext == 0)
|
||
{
|
||
--index;
|
||
decompose();
|
||
}
|
||
|
||
// 主单位已是 ms 且恰好进位到 1000ms ⇒ 直接显示为秒。
|
||
if (index == 0 && milliNext == 0 && whole >= 1000LL)
|
||
{
|
||
return FormatDecimal(static_cast<double>(whole) / 1000.0) + L"s";
|
||
}
|
||
|
||
std::wstring text = FormatDecimal(static_cast<double>(whole));
|
||
text += subUnits[index].name;
|
||
if (milliNext > 0)
|
||
{
|
||
// ⚠️ 次级单位**最多保留三位小数**(用户 2026-11-03 要求):
|
||
// `0.003696631s` → 顶级 `3ms` + 次级 `696.631us`(而不是 `3ms 700us`)。
|
||
text += L" ";
|
||
text += FormatSubUnit(static_cast<double>(milliNext) / 1000.0, kDurationSubUnitDecimals);
|
||
text += subUnits[index + 1].name;
|
||
}
|
||
return text;
|
||
}
|
||
|
||
std::wstring FormatDurationSeconds(double seconds)
|
||
{
|
||
if (!std::isfinite(seconds))
|
||
{
|
||
return L"0";
|
||
}
|
||
// ⚠️ 内联编辑的数值**不得出现科学计数法**(用户 2026-11-03 要求)。
|
||
return FormatDecimal(std::fabs(seconds), 12);
|
||
}
|
||
|
||
std::wstring FormatFrequency(double hertz)
|
||
{
|
||
if (!std::isfinite(hertz) || hertz <= 0.0)
|
||
{
|
||
return L"—";
|
||
}
|
||
// ⚠️⚠️ **单位选择必须按「舍入后的显示值」判定,否则同一物理量会出现两种显示**:
|
||
// `999.9999999 Hz` 走 Hz 分支显示 `1000.000 Hz`,而 `1000.0000001 Hz` 走 kHz 分支
|
||
// 显示 `1.000 kHz` —— 用户 2026-11-03 报告的 `1000.00 Hz` vs `1.000 kHz` 就是这个。
|
||
// 因此:先取单位,再用**保留 3 位小数后的值**判断是否达到 1000,达到就升级。
|
||
const wchar_t* units[] = {L"Hz", L"kHz", L"MHz", L"GHz"};
|
||
const double scales[] = {1.0, 1.0e3, 1.0e6, 1.0e9};
|
||
const int unitCount = 4;
|
||
int unitIndex = 0;
|
||
for (int i = unitCount - 1; i >= 0; --i)
|
||
{
|
||
if (hertz >= scales[i])
|
||
{
|
||
unitIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
// 向上校正:舍入到 3 位小数后若达到 1000 个当前单位,就升到上一级。
|
||
const double epsilon = 0.5 * std::pow(10.0, -kFrequencyDecimals);
|
||
while (unitIndex + 1 < unitCount &&
|
||
(hertz / scales[unitIndex]) + epsilon >= 1000.0)
|
||
{
|
||
++unitIndex;
|
||
}
|
||
// 向下校正:若当前单位下小于 1(极低频边界),降一级以免出现 `0.001 kHz` 这类。
|
||
while (unitIndex > 0 && (hertz / scales[unitIndex]) < 1.0 - epsilon)
|
||
{
|
||
--unitIndex;
|
||
}
|
||
const wchar_t* unit = units[unitIndex];
|
||
const double scaled = hertz / scales[unitIndex];
|
||
|
||
// ⚠️ 频率同样不得出现科学计数法(用户 2026-11-03 要求):
|
||
// `FormatValue` 的 `%.6g` 在 1e-5 Hz 这类量级会输出 “1e-06”,改为十进制形式。
|
||
// ⚠️ 并且**基于真实值保留三位小数**(用户 2026-11-03 要求:`270.517 Hz` 而不是
|
||
// `270.27 Hz`)—— 旧实现把间隔先舍入到 3 位有效数字再换算,把真实尾数抹掉了。
|
||
// ⚠️ 但若保留 3 位后整串变成 `0.000`(超低频,例如 1/3662s ≈ 0.27mHz),
|
||
// 则退回按「有效数字」显示,避免把有意义的数值显示成 0(用户 2026-11-03)。
|
||
const std::wstring fixedText = FormatDecimalFixed(scaled, kFrequencyDecimals);
|
||
if (std::fabs(scaled) > 0.0 && std::fabs(scaled) < 0.5 * std::pow(10.0, -kFrequencyDecimals))
|
||
{
|
||
return FormatDecimal(scaled) + L" " + unit;
|
||
}
|
||
return fixedText + L" " + unit;
|
||
}
|
||
|
||
std::wstring FormatByteSize(unsigned long long bytes)
|
||
{ // 单位自适应:GB / MB / KB(阈值按 1024 进制)。
|
||
constexpr double kKilo = 1024.0;
|
||
constexpr double kMega = kKilo * 1024.0;
|
||
constexpr double kGiga = kMega * 1024.0;
|
||
|
||
const double value = static_cast<double>(bytes);
|
||
const wchar_t* unit = L"KB";
|
||
double scaled = value / kKilo;
|
||
if (value >= kGiga)
|
||
{
|
||
unit = L"GB";
|
||
scaled = value / kGiga;
|
||
}
|
||
else if (value >= kMega)
|
||
{
|
||
unit = L"MB";
|
||
scaled = value / kMega;
|
||
}
|
||
|
||
wchar_t buffer[64];
|
||
// ⚠️ 宽字符格式化必须用 `%ls`:MinGW 的 swprintf 按 C99 语义处理 `%s`
|
||
//(在宽格式串里 `%s` 表示**窄**字符串),写成 `%s` 会把宽字符串按单字节读取,
|
||
// 于是 "KB" 只打印出 "K"。
|
||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.2f %ls", scaled, unit);
|
||
return std::wstring(buffer);
|
||
}
|
||
|
||
std::wstring FormatValue(double value)
|
||
{
|
||
if (std::isnan(value))
|
||
{
|
||
return L"NaN";
|
||
}
|
||
if (std::isinf(value))
|
||
{
|
||
return (value > 0.0) ? L"+Inf" : L"-Inf";
|
||
}
|
||
|
||
wchar_t buffer[96];
|
||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.6g", value);
|
||
return std::wstring(buffer);
|
||
}
|
||
|
||
std::wstring FormatTick(double value, double step)
|
||
{
|
||
if (value == 0.0)
|
||
{
|
||
return L"0";
|
||
}
|
||
if (!std::isfinite(value))
|
||
{
|
||
return FormatValue(value);
|
||
}
|
||
|
||
int decimals = 0;
|
||
if (step > 0.0 && std::isfinite(step))
|
||
{
|
||
const double magnitude = std::ceil(-std::log10(step) - 1.0e-9);
|
||
if (magnitude > 0.0)
|
||
{
|
||
decimals = static_cast<int>(magnitude);
|
||
}
|
||
}
|
||
if (decimals > 9)
|
||
{
|
||
decimals = 9;
|
||
}
|
||
|
||
wchar_t buffer[96];
|
||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.*f", decimals, value);
|
||
std::wstring text(buffer);
|
||
if (text.size() > 15)
|
||
{
|
||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.4e", value);
|
||
text.assign(buffer);
|
||
}
|
||
return text;
|
||
}
|
||
|
||
std::wstring FormatCount(unsigned long long value)
|
||
{
|
||
wchar_t buffer[32];
|
||
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%llu", value);
|
||
|
||
std::wstring digits(buffer);
|
||
std::wstring result;
|
||
result.reserve(digits.size() + digits.size() / 3);
|
||
|
||
const size_t length = digits.size();
|
||
for (size_t i = 0; i < length; ++i)
|
||
{
|
||
if (i > 0 && ((length - i) % 3u) == 0u)
|
||
{
|
||
result.push_back(L',');
|
||
}
|
||
result.push_back(digits[i]);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* @brief 把整数展开成十进制数字(内部实现,宽/窄共用同一套算法)。
|
||
*
|
||
* ⚠️ 这里**不能**用 `swprintf`:本函数存在的全部意义就是让产物不再引用
|
||
* MinGW 的 printf 引擎(见头文件注释)。手写 10 次除法比一次格式串解析还快,
|
||
* 且无本地化影响。
|
||
*
|
||
* @param [OUT] buffer 输出缓冲(从尾部向前写)。
|
||
* @param [IN] value 待转换整数。
|
||
* @return 文本起始指针(指向 buffer 内部)。
|
||
*/
|
||
template <typename Char>
|
||
static const Char* ExpandInteger(Char* buffer, std::size_t capacity, long long value,
|
||
std::size_t& length)
|
||
{
|
||
Char* cursor = buffer + capacity;
|
||
const bool negative = (value < 0);
|
||
// `0 - (unsigned long long)value` 对 LLONG_MIN 也成立(模 2^64 语义)。
|
||
unsigned long long magnitude = negative ? (0ull - static_cast<unsigned long long>(value))
|
||
: static_cast<unsigned long long>(value);
|
||
do
|
||
{
|
||
*--cursor = static_cast<Char>('0' + static_cast<int>(magnitude % 10ull));
|
||
magnitude /= 10ull;
|
||
} while (magnitude != 0ull);
|
||
if (negative)
|
||
{
|
||
*--cursor = static_cast<Char>('-');
|
||
}
|
||
length = static_cast<std::size_t>((buffer + capacity) - cursor);
|
||
return cursor;
|
||
}
|
||
|
||
std::wstring FormatInteger(long long value)
|
||
{
|
||
wchar_t buffer[24]; // -9223372036854775808 = 20 字符 + 余量
|
||
std::size_t length = 0;
|
||
const wchar_t* begin = ExpandInteger(buffer, sizeof(buffer) / sizeof(buffer[0]), value, length);
|
||
return std::wstring(begin, length);
|
||
}
|
||
|
||
std::wstring FormatIntegerUnsigned(unsigned long long value)
|
||
{
|
||
// 上限 20 位十进制数字 + 余量(用有符号重载会溢出,故单独展开一遍)。
|
||
wchar_t buffer[24];
|
||
wchar_t* cursor = buffer + (sizeof(buffer) / sizeof(buffer[0]));
|
||
unsigned long long magnitude = value;
|
||
do
|
||
{
|
||
*--cursor = static_cast<wchar_t>(L'0' + static_cast<int>(magnitude % 10ull));
|
||
magnitude /= 10ull;
|
||
} while (magnitude != 0ull);
|
||
return std::wstring(cursor, static_cast<std::size_t>(buffer + (sizeof(buffer) / sizeof(buffer[0])) -
|
||
cursor));
|
||
}
|
||
|
||
std::string FormatIntegerNarrow(long long value)
|
||
{
|
||
char buffer[24];
|
||
std::size_t length = 0;
|
||
const char* begin = ExpandInteger(buffer, sizeof(buffer) / sizeof(buffer[0]), value, length);
|
||
return std::string(begin, length);
|
||
}
|
||
|
||
double NiceStep(double range, int targetTicks)
|
||
{
|
||
if (!(range > 0.0) || !std::isfinite(range) || targetTicks <= 0)
|
||
{
|
||
return 1.0;
|
||
}
|
||
|
||
const double raw = range / static_cast<double>(targetTicks);
|
||
const double magnitude = std::pow(10.0, std::floor(std::log10(raw)));
|
||
const double normalized = raw / magnitude;
|
||
|
||
double nice = 10.0;
|
||
if (normalized <= 1.0)
|
||
{
|
||
nice = 1.0;
|
||
}
|
||
else if (normalized <= 2.0)
|
||
{
|
||
nice = 2.0;
|
||
}
|
||
else if (normalized <= 5.0)
|
||
{
|
||
nice = 5.0;
|
||
}
|
||
|
||
return nice * magnitude;
|
||
}
|
||
|
||
void BuildTicks(double minValue, double maxValue, double step, std::vector<double>& ticks)
|
||
{
|
||
ticks.clear();
|
||
if (!std::isfinite(minValue) || !std::isfinite(maxValue) || !(step > 0.0) || !std::isfinite(step))
|
||
{
|
||
return;
|
||
}
|
||
if (maxValue < minValue)
|
||
{
|
||
const double temp = minValue;
|
||
minValue = maxValue;
|
||
maxValue = temp;
|
||
}
|
||
|
||
// ⚠️ `minValue / step` 在极端量级下可能溢出成 inf(例如 1e300 / 1e-300),
|
||
// 而 double → long long 的越界转换是**未定义行为**。先确认商落在 long long 范围内,
|
||
// 否则直接放弃画刻度(总比画出一批垃圾刻度安全)。
|
||
constexpr double kTickIndexLimit = 9.0e18;
|
||
const double firstIndex = std::ceil(minValue / step - 1.0e-9);
|
||
const double lastIndex = std::floor(maxValue / step + 1.0e-9);
|
||
if (!(std::fabs(firstIndex) < kTickIndexLimit) || !(std::fabs(lastIndex) < kTickIndexLimit))
|
||
{
|
||
return;
|
||
}
|
||
const long long first = static_cast<long long>(firstIndex);
|
||
const long long last = static_cast<long long>(lastIndex);
|
||
if (last < first)
|
||
{
|
||
return;
|
||
}
|
||
|
||
const long long count = last - first + 1;
|
||
if (count > 512)
|
||
{
|
||
return;
|
||
}
|
||
|
||
ticks.reserve(static_cast<size_t>(count));
|
||
for (long long index = first; index <= last; ++index)
|
||
{
|
||
ticks.push_back(static_cast<double>(index) * step);
|
||
}
|
||
}
|
||
|
||
bool CopyTextToClipboard(HWND owner, const std::wstring& text)
|
||
{
|
||
if (text.empty())
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (OpenClipboard(owner) == FALSE)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
bool success = false;
|
||
if (EmptyClipboard() != FALSE)
|
||
{
|
||
const SIZE_T bytes = (text.size() + 1) * sizeof(wchar_t);
|
||
HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, bytes);
|
||
if (handle != nullptr)
|
||
{
|
||
void* target = GlobalLock(handle);
|
||
if (target != nullptr)
|
||
{
|
||
std::memcpy(target, text.c_str(), bytes);
|
||
GlobalUnlock(handle);
|
||
if (SetClipboardData(CF_UNICODETEXT, handle) != nullptr)
|
||
{
|
||
success = true;
|
||
}
|
||
else
|
||
{
|
||
GlobalFree(handle);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
GlobalFree(handle);
|
||
}
|
||
}
|
||
}
|
||
|
||
CloseClipboard();
|
||
return success;
|
||
}
|
||
|
||
bool PasteTextFromClipboard(HWND owner, std::wstring& text)
|
||
{
|
||
text.clear();
|
||
|
||
if (OpenClipboard(owner) == FALSE)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
bool success = false;
|
||
if (IsClipboardFormatAvailable(CF_UNICODETEXT) != FALSE)
|
||
{
|
||
HANDLE handle = GetClipboardData(CF_UNICODETEXT);
|
||
if (handle != nullptr)
|
||
{
|
||
const void* source = GlobalLock(handle);
|
||
if (source != nullptr)
|
||
{
|
||
const wchar_t* wide = static_cast<const wchar_t*>(source);
|
||
text.assign(wide);
|
||
GlobalUnlock(handle);
|
||
success = !text.empty();
|
||
}
|
||
}
|
||
}
|
||
CloseClipboard();
|
||
|
||
// 单行输入框:把换行与制表符折叠为空格,避免内容被截断显示。
|
||
if (success)
|
||
{
|
||
for (wchar_t& c : text)
|
||
{
|
||
if (c == L'\r' || c == L'\n' || c == L'\t')
|
||
{
|
||
c = L' ';
|
||
}
|
||
}
|
||
}
|
||
return success;
|
||
}
|
||
|
||
} // namespace jdi
|