Files
Just-DataInspector/Utilities/Src/MatLoader.cpp
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

714 lines
22 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.
// MATLAB `.mat`(v7.3 / HDF5)加载器。
//
// 支持两种内容(详见 MatLoader.hpp 的说明):
// * Simulink SDI / `SimulationData.Dataset` 导出:`#refs#` 里的元素图 + `#sigstream#` 的样本流;
// * 普通 v7.3 变量:数值矩阵按"列 = 信号、行 = 采样"读取(时间轴 = 采样序号)。
#include "MatLoader.hpp"
#include "MatH5.hpp"
#include "Utf8Util.hpp"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <map>
#include <set>
#include <string>
#include <vector>
namespace jdi
{
namespace
{
/** @brief 一个 SDI 信号(或子数据集)的解析结果。 */
struct SdiElement
{
bool isSignal = false;
bool isDataset = false;
std::wstring name;
std::vector<std::uint64_t> children; /**< 子数据集元素的引用目标。 */
int dataStream = -1; /**< 数据流下标。 */
int timeStream = -1; /**< 时间流下标。 */
};
std::wstring TrimNumber(double value)
{
// ⚠️ 整数分支改走 `FormatInteger`(不再经 `swprintf`):本文件是产物里
// 最后一个引用 MinGW printf 引擎的地方,留着它 gdtoa 就断不掉。
if (value == static_cast<double>(static_cast<long long>(value)))
{
return FormatInteger(static_cast<long long>(value));
}
// 浮点分支必须保留 `%g` 语义(去掉尾随 0 的最短表示),因此仍走 `swprintf`;
// 此时库函数来自系统 UCRT(见 Makefile 的 `UCRT_STDIO` 开关)。
wchar_t buffer[64] = {};
std::swprintf(buffer, 64, L"%g", value);
return buffer;
}
/**
* @brief 把 SDI 里读到的 `DataR2` / `TimeR2` 数值转成流下标。
*
* ⚠️ 不能直接 `static_cast<int>(double)`:文件里那个数可能是 NaN、无穷或超出 `int` 范围,
* 而浮点 → 整型的越界转换在 C++ 里是**未定义行为**(x86-64 上会得到 `INT_MIN` 之类的
* 垃圾下标,随后拿去拼数据集名)。非法值统一返回 -1(= 没有流)。
*/
int StreamIndexFromDouble(double value)
{
if (!std::isfinite(value) || value < 0.0 || value > 2147483647.0)
{
return -1;
}
return static_cast<int>(value);
}
/** @brief 去掉文件名中的扩展名。 */
std::wstring StemOf(const std::wstring& path)
{
const std::size_t slash = path.find_last_of(L"\\/");
std::wstring name = (slash == std::wstring::npos) ? path : path.substr(slash + 1);
const std::size_t dot = name.find_last_of(L'.');
if (dot != std::wstring::npos && dot > 0)
{
name.resize(dot);
}
return name;
}
/** @brief 读取一个「流」的全部 double 样本(`#sigstream#/<n>/#data#`)。 */
bool ReadStream(const MatHdf5& file, std::uint64_t streamObject, std::vector<double>& out)
{
out.clear();
const std::uint64_t data = file.Member(streamObject, "#data#");
if (data == 0)
{
return false;
}
// ⚠️ SDI 的 `#data#` 是**字节流**(`uint8` 数据集,长度 = 采样数 × 8),
// 而不是 double 数据集:必须按 8 字节一组重新解释成 double。
// `ReadDoubleStream` 会逐 chunk 解压并**直接写入** `out`:
// 不生成“整个数据集的未压缩字节”,也不做第二次拷贝(GB 级文件的关键)。
return file.ReadDoubleStream(data, out) && !out.empty();
}
/** @brief 解析 `#refs#` 里的一个元素(信号 / 子数据集)。 */
void ParseElement(const MatHdf5& file, std::uint64_t address, SdiElement& element)
{
element = SdiElement();
std::wstring elementType;
file.ReadText(file.Member(address, "ElementType"), elementType);
file.ReadText(file.Member(address, "Name"), element.name);
const std::uint64_t values = file.Member(address, "Values");
if (elementType == L"dataset")
{
element.isDataset = true;
if (values != 0)
{
file.ReadReferences(values, element.children);
}
return;
}
element.isSignal = true;
if (values == 0)
{
return;
}
// 名字为空时退回 timeseries 自己的 Name。
if (element.name.empty())
{
file.ReadText(file.Member(values, "Name"), element.name);
}
std::vector<double> numbers;
if (file.ReadNumbers(file.Member(values, "DataR2"), numbers) && !numbers.empty())
{
element.dataStream = StreamIndexFromDouble(numbers.front());
}
numbers.clear();
if (file.ReadNumbers(file.Member(values, "TimeR2"), numbers) && !numbers.empty())
{
element.timeStream = StreamIndexFromDouble(numbers.front());
}
}
/** @brief 追加一条信号(时间 + 数值)到某个分组的时间轴块集合。 */
void AppendSignal(LoadedSource& source, const std::wstring& name, std::vector<double>&& time,
std::vector<double>&& values)
{
if (time.empty() || values.empty())
{
return;
}
TableBlock block;
block.timeHeader = L"time";
DataTable& table = block.table;
table.headers.push_back(L"time");
table.headers.push_back(name);
table.rowCount = std::min(time.size(), values.size());
time.resize(table.rowCount);
values.resize(table.rowCount);
table.columns.push_back(std::move(time));
table.columns.push_back(std::move(values));
source.blocks.push_back(std::move(block));
}
/** @brief 加载进度上报器(`.mat` 解析拆成“读文件”与“解码每个样本流”两段)。 */
struct ProgressReporter
{
const LoadProgress* sink = nullptr; /**< 目标回调(可空)。 */
double low = 0.0; /**< 本阶段占的进度区间下界。 */
double high = 1.0; /**< 本阶段占的进度区间上界。 */
std::uint64_t read = 0; /**< 已读字节。 */
std::uint64_t total = 0; /**< 总字节。 */
mutable double lastRatio = -1.0; /**< 已上报的最大比例(保证单调不回退)。 */
/** @brief 上报一个 0-1 的阶段内进度(向宿主传递字节数与百分比)。 */
bool Report(double fraction) const
{
if (sink == nullptr || !(*sink))
{
return true;
}
if (fraction < 0.0)
{
fraction = 0.0;
}
if (fraction > 1.0)
{
fraction = 1.0;
}
double ratio = low + (high - low) * fraction;
// 「已读字节」与「逐流解码」是两条独立进度来源:取较大值,避免进度条回退。
if (ratio < lastRatio)
{
ratio = lastRatio;
}
else
{
lastRatio = ratio;
}
LoadProgressInfo info;
info.ratio = ratio;
// 总字节已知时一并上报(遮罩层据此显示“已加载 / 总大小”)。
info.hasBytes = (total > 0);
info.bytesRead = (total > 0) ? std::min(read, total) : 0;
info.totalBytes = total;
return (*sink)(info);
}
};
/** @brief SDI(`SimulationData.Dataset`)结构的分组读取。 */
bool LoadSdi(const MatHdf5& file, std::uint64_t refs, std::uint64_t sigstream,
const std::wstring& stem, std::vector<LoadedSourceGroup>& groups,
std::wstring& error, const ProgressReporter& reporter)
{
// 1) 枚举 `#refs#` 里的全部元素。
std::map<std::uint64_t, SdiElement> elements;
for (const auto& member : file.Members(refs))
{
if (file.IsDataset(member.second))
{
continue;
}
SdiElement element;
ParseElement(file, member.second, element);
if (element.isSignal || element.isDataset)
{
elements[member.second] = std::move(element);
}
}
if (elements.empty())
{
error = L"文件中没有 Simulink 数据元素。";
return false;
}
// 2) 找出"根"数据集:没有被任何其它数据集当作子元素引用的那些。
std::set<std::uint64_t> childAddresses;
for (const auto& entry : elements)
{
for (const std::uint64_t child : entry.second.children)
{
childAddresses.insert(child);
}
}
std::vector<std::uint64_t> roots;
for (const auto& entry : elements)
{
if (!entry.second.isDataset)
{
continue;
}
if (childAddresses.count(entry.first) == 0)
{
roots.push_back(entry.first);
}
}
if (roots.empty())
{
// 没有任何子数据集关系:所有信号归入同一组。
for (const auto& entry : elements)
{
if (entry.second.isSignal)
{
roots.push_back(entry.first);
}
}
}
// 3) 深度优先遍历:路径上的数据集名字用 `-` 连接成分组名。
struct Pending
{
std::uint64_t address = 0;
std::vector<std::wstring> path;
int depth = 0;
};
std::vector<Pending> stack;
for (const std::uint64_t root : roots)
{
const auto found = elements.find(root);
if (found == elements.end())
{
continue;
}
Pending pending;
pending.address = root;
if (found->second.isDataset && !found->second.name.empty())
{
pending.path.push_back(found->second.name);
}
stack.push_back(std::move(pending));
}
// ⚠️ **先收集信号清单、再解码**(用户 2026-10-19 要求:边解析边填充):
// * 旧实现把**每条**信号的时间列和时间流各拷一份(`streams` 缓存 + `AppendSignal`
// 里的整份复制)⇒ GB 级文件会出现“文件 + 解压字节 + 每条信号两份 double”叠加;
// * 现在按「分组 + 时间流」合并成一个 `TableBlock`:时间列只存一份、信号列直接
// `std::move` 进数据模型,且每条数据流解码完立刻进入最终缓冲。
struct PendingSignal
{
std::wstring groupName;
std::wstring signalName;
int timeStream = -1;
int dataStream = -1;
};
std::vector<PendingSignal> signals;
int unnamedSignals = 0;
while (!stack.empty())
{
Pending pending = std::move(stack.back());
stack.pop_back();
const auto found = elements.find(pending.address);
if (found == elements.end())
{
continue;
}
const SdiElement& element = found->second;
if (element.isDataset)
{
for (const std::uint64_t child : element.children)
{
Pending next;
next.address = child;
next.path = pending.path;
const auto childElement = elements.find(child);
if (childElement != elements.end() && childElement->second.isDataset &&
!childElement->second.name.empty())
{
next.path.push_back(childElement->second.name);
}
stack.push_back(std::move(next));
}
continue;
}
if (!element.isSignal || element.dataStream < 0)
{
continue;
}
PendingSignal signal;
for (const std::wstring& piece : pending.path)
{
if (!signal.groupName.empty())
{
signal.groupName += L"-";
}
signal.groupName += piece;
}
if (signal.groupName.empty())
{
signal.groupName = stem;
}
signal.signalName = element.name;
if (signal.signalName.empty())
{
signal.signalName = L"信号" + FormatInteger(++unnamedSignals);
}
signal.timeStream = element.timeStream;
signal.dataStream = element.dataStream;
signals.push_back(std::move(signal));
}
// 2) 解码:每个(分组,时间流)一个时间轴块;每条数据流只解码一次并直接进列。
std::map<std::wstring, std::size_t> groupIndex;
std::map<std::pair<std::wstring, int>, std::size_t> blockIndex;
std::size_t decoded = 0;
const auto reportStream = [&]()
{
++decoded;
const double fraction = (signals.empty())
? 1.0
: static_cast<double>(decoded) /
static_cast<double>(signals.size());
return reporter.Report(std::min(1.0, fraction));
};
for (PendingSignal& signal : signals)
{
// 分组(每个分组名对应一个 `LoadedSourceGroup`)。
std::size_t groupSlot = 0;
const auto existing = groupIndex.find(signal.groupName);
if (existing == groupIndex.end())
{
LoadedSourceGroup group;
group.name = signal.groupName;
group.source.sourceName = signal.groupName;
groups.push_back(std::move(group));
groupSlot = groups.size() - 1;
groupIndex.emplace(signal.groupName, groupSlot);
}
else
{
groupSlot = existing->second;
}
LoadedSource& source = groups[groupSlot].source;
// 数据列:直接解码到最终 vector(不再经过 `Dataset.data` 的整块字节)。
std::vector<double> values;
const std::uint64_t dataObject = file.Member(sigstream, FormatIntegerNarrow(signal.dataStream));
if (dataObject == 0 || !ReadStream(file, dataObject, values) || values.empty())
{
if (!reportStream())
{
return false;
}
continue;
}
// 找(或建)该信号所属的时间轴块。
const std::pair<std::wstring, int> key{signal.groupName, signal.timeStream};
std::size_t blockSlot = 0;
const auto blockFound = blockIndex.find(key);
if (blockFound == blockIndex.end())
{
TableBlock block;
block.timeHeader = L"time";
std::vector<double> time;
bool haveTime = false;
if (signal.timeStream >= 0)
{
const std::uint64_t timeObject =
file.Member(sigstream, FormatIntegerNarrow(signal.timeStream));
haveTime = timeObject != 0 && ReadStream(file, timeObject, time) &&
!time.empty();
}
if (!haveTime)
{
// 时间流缺失:退化为采样序号(秒),保证信号仍可显示(长度在下面裁剪)。
time.resize(values.size());
for (std::size_t index = 0; index < values.size(); ++index)
{
time[index] = static_cast<double>(index);
}
}
// ⚠️ **时间轴按内容去重**:SDI 里每条 timeseries 都有自己的时间流,
// 但内容通常完全一致。若同一分组里已经有一模一样的时间列,就直接复用它
//(否则每条信号各存一份时间轴:1.29GB / 5 信号的文件会多占 ~500MB)。
std::size_t reused = SIZE_MAX;
for (std::size_t index = 0; index < source.blocks.size(); ++index)
{
const std::vector<double>& existingTime = source.blocks[index].table.columns[0];
if (existingTime.size() != time.size())
{
continue;
}
if (time.empty() ||
std::memcmp(existingTime.data(), time.data(),
time.size() * sizeof(double)) == 0)
{
reused = index;
break;
}
}
if (reused != SIZE_MAX)
{
blockIndex.emplace(key, reused);
blockSlot = reused;
}
else
{
block.table.headers.push_back(block.timeHeader);
block.table.columns.push_back(std::move(time));
source.blocks.push_back(std::move(block));
blockSlot = source.blocks.size() - 1;
blockIndex.emplace(key, blockSlot);
}
}
else
{
blockSlot = blockFound->second;
}
TableBlock& block = source.blocks[blockSlot];
// 时间列长度与信号不一致(损坏 / 异构)⇒ 单独成块,用采样序号当时间,
// 保持“每条信号都能显示”的既有语义,而不是把数据截掉或错位。
if (block.table.columns[0].size() != values.size())
{
TableBlock own;
own.timeHeader = L"time";
std::vector<double> time(values.size());
for (std::size_t index = 0; index < values.size(); ++index)
{
time[index] = static_cast<double>(index);
}
own.table.headers.push_back(own.timeHeader);
own.table.columns.push_back(std::move(time));
own.table.headers.push_back(signal.signalName);
own.table.columns.push_back(std::move(values));
source.blocks.push_back(std::move(own));
if (!reportStream())
{
return false;
}
continue;
}
block.table.headers.push_back(signal.signalName);
block.table.columns.push_back(std::move(values));
if (!reportStream())
{
return false;
}
}
// 3) 统一行数:同一块内取各列最小长度(时间流与数据流长度不一致时按最小裁剪)。
for (LoadedSourceGroup& group : groups)
{
for (TableBlock& block : group.source.blocks)
{
std::size_t rows = SIZE_MAX;
for (const std::vector<double>& column : block.table.columns)
{
rows = std::min(rows, column.size());
}
if (rows == SIZE_MAX)
{
rows = 0;
}
for (std::vector<double>& column : block.table.columns)
{
if (column.size() > rows)
{
column.resize(rows);
}
}
block.table.rowCount = rows;
}
}
if (groups.empty())
{
error = L"文件中没有可用的信号数据。";
return false;
}
return true;
}
/**
* @brief 普通 v7.3 变量:数值矩阵 / 结构体。
*
* ⚠️ 必须带**深度上限**与**已访问集合**:HDF5 允许硬链接成环,损坏文件也可能构造出
* 极深嵌套,无保护的成员递归会直接栈溢出(崩溃,无错误提示)。
*/
void LoadPlainObject(const MatHdf5& file, std::uint64_t address, const std::wstring& name,
LoadedSource& source, int depth = 0,
std::set<std::uint64_t>* visited = nullptr)
{
constexpr int kMaxDepth = 32;
if (depth > kMaxDepth)
{
return;
}
std::set<std::uint64_t> localVisited;
std::set<std::uint64_t>& seen = (visited != nullptr) ? *visited : localVisited;
if (address != 0 && !seen.insert(address).second)
{
return; // 成环:已经展开过这个对象
}
if (file.IsDataset(address))
{
// ⚠️ 只解析元信息(维度),**不要**用 `ReadDataset`:那会把整块数据再解码一遍。
// 旧实现先 `ReadNumbers` 解码一次、再 `ReadDataset` 只为拿 `dims`,
// 大矩阵的时间与内存都翻倍。
MatHdf5::Dataset dataset;
if (!file.ReadDatasetInfo(address, dataset) || dataset.dims.empty())
{
return;
}
std::vector<double> numbers;
if (!file.ReadNumbers(address, numbers) || numbers.empty())
{
return;
}
std::size_t rows = 1;
std::size_t columns = 1;
if (dataset.dims.size() >= 2)
{
rows = dataset.dims[0];
columns = dataset.dims[1];
}
else if (dataset.dims.size() == 1)
{
rows = dataset.dims[0];
}
if (rows == 0 || columns == 0 || rows * columns != numbers.size())
{
return;
}
// 列 = 信号、行 = 采样(与 MATLAB 的列优先存储一致)。
for (std::size_t column = 0; column < columns; ++column)
{
std::vector<double> values(rows);
for (std::size_t row = 0; row < rows; ++row)
{
values[row] = numbers[column * rows + row];
}
std::vector<double> time(rows);
for (std::size_t row = 0; row < rows; ++row)
{
time[row] = static_cast<double>(row);
}
const std::wstring signalName =
(columns == 1) ? name : (name + L"(" + TrimNumber(static_cast<double>(column + 1)) + L")");
AppendSignal(source, signalName, std::move(time), std::move(values));
}
return;
}
// 结构体:字段各自作为信号(子结构体递归)。
for (const auto& member : file.Members(address))
{
if (member.first.empty())
{
continue;
}
std::wstring childName(member.first.begin(), member.first.end());
LoadPlainObject(file, member.second, childName, source, depth + 1, &seen);
}
}
} // namespace
bool LoadMatSourceGroups(const std::wstring& filePath, std::vector<LoadedSourceGroup>& groups,
std::wstring& error, const LoadProgress& progress)
{
error.clear();
groups.clear();
MatHdf5 file;
// ------------------------------------------------------------------
// 进度拆成三段:
// 0.00 – 0.02 打开文件(立刻知道总大小;遮罩层显示「0 / 总大小」)
// 0.02 – 0.98 随机读取 + 逐条解码样本流(进度按**实际已读字节**推进)
// 0.98 – 1.00 收尾
// ⚠️ 旧实现是内存映射:字节瞬间「全部可访问」⇒ 文件读取百分比立刻 100%,
// 而真正的解析还要跑很久(用户 2026-10-20 报告“这部分不太对劲”)。
// 现在每次读盘都累计到 `MatHdf5::BytesRead()`,百分比随真实 I/O 推进。
// ------------------------------------------------------------------
ProgressReporter parseReporter;
parseReporter.sink = &progress;
parseReporter.low = 0.02;
parseReporter.high = 0.98;
const MatHdf5::OpenProgress openProgress =
[&parseReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
{
parseReporter.read = bytesRead;
parseReporter.total = totalBytes;
return parseReporter.Report(totalBytes > 0 ? static_cast<double>(bytesRead) /
static_cast<double>(totalBytes)
: 0.0);
};
if (!file.Open(filePath, error, openProgress))
{
return false; // 用户取消时 `error` 为空
}
// 流式解码期间的字节进度(每读一块回调一次);返回 false 即取消。
file.SetReadProgress(
[&parseReporter](std::uint64_t bytesRead, std::uint64_t totalBytes) -> bool
{
parseReporter.read = bytesRead;
parseReporter.total = totalBytes;
return parseReporter.Report(totalBytes > 0 ? static_cast<double>(bytesRead) /
static_cast<double>(totalBytes)
: 0.0);
});
if (!parseReporter.Report(0.0))
{
error.clear();
return false;
}
const std::wstring stem = StemOf(filePath);
const std::uint64_t refs = file.Member(file.Root(), "#refs#");
const std::uint64_t sigstream = file.Member(file.Root(), "#sigstream#");
if (refs != 0 && sigstream != 0)
{
if (!LoadSdi(file, refs, sigstream, stem, groups, error, parseReporter))
{
if (file.Cancelled())
{
error.clear(); // 用户取消:不显示错误
}
return false;
}
}
else
{
LoadedSourceGroup group;
group.source.sourceName = stem;
for (const auto& member : file.Members(file.Root()))
{
if (member.first.empty() || member.first[0] == '#')
{
continue;
}
std::wstring name(member.first.begin(), member.first.end());
LoadPlainObject(file, member.second, name, group.source);
}
if (!group.source.HasData())
{
error = L".mat 文件中没有可用的数值数据。";
return false;
}
groups.push_back(std::move(group));
}
if (progress)
{
LoadProgressInfo info;
info.ratio = 1.0;
info.hasBytes = (file.FileBytes() > 0);
info.bytesRead = file.FileBytes();
info.totalBytes = file.FileBytes();
progress(info);
}
return true;
}
} // namespace jdi