#include "CsvLoader.hpp" #include "Utf8Util.hpp" #include #include #include #include #include #include namespace jdi { namespace { /** @brief 单个字段在原始缓冲中的位置区间。 */ struct FieldSpan { const char* begin = nullptr; /**< 字段起始指针。 */ const char* end = nullptr; /**< 字段结束指针(不包含)。 */ }; const double kMissingValue = std::numeric_limits::quiet_NaN(); /**< 缺失值占位。 */ const std::size_t kReadChunkBytes = 4u * 1024u * 1024u; /**< 流式读取块大小。 */ /** * @brief 按比例预留的初始行容量。 * * 先按文件体积估算一个行数,既能避免边解析边扩容(百万行时复制代价可观), * 又不会因为估得过大而浪费内存(超估时 vector 只是容量偏大,数据本身不变)。 */ std::size_t EstimateRowCapacity(unsigned long long fileBytes) { // 典型 CSV 每行约 40-200 字节;取下界 64 字节估算行数,宁可小估也不要多占。 const unsigned long long estimate = fileBytes / 64ull; if (estimate > 4096ull * 1024ull) { return static_cast(4096ull * 1024ull); } return static_cast(estimate); } /** * @brief 打开文件并查询其体积。 * @param [IN] filePath 文件路径。 * @param [OUT] file 文件句柄输出(需调用方关闭)。 * @param [OUT] fileSize 文件字节数输出。 * @param [OUT] error 失败原因描述。 * @return 打开成功返回 true。 */ bool OpenFile(const std::wstring& filePath, FILE*& file, unsigned long long& fileSize, std::wstring& error) { file = _wfopen(filePath.c_str(), L"rb"); if (file == nullptr) { error = L"无法打开文件,请确认路径与访问权限。"; return false; } // 使用 64 位定位接口,避免 long 在部分平台下只有 32 位而无法处理大文件。 if (_fseeki64(file, 0, SEEK_END) != 0) { std::fclose(file); file = nullptr; error = L"无法定位文件末尾。"; return false; } const long long size = _ftelli64(file); if (size < 0) { std::fclose(file); file = nullptr; error = L"无法获取文件长度。"; return false; } if (size == 0) { std::fclose(file); file = nullptr; error = L"文件内容为空。"; return false; } if (_fseeki64(file, 0, SEEK_SET) != 0) { std::fclose(file); file = nullptr; error = L"无法回到文件开头。"; return false; } fileSize = static_cast(size); return true; } /** * @brief 统计一行中某个候选分隔符出现的次数。 * @param [IN] begin 行起始指针。 * @param [IN] end 行结束指针(不包含)。 * @param [IN] candidate 候选分隔符。 * @return 出现次数。 */ int CountDelimiter(const char* begin, const char* end, char candidate) { int count = 0; bool inQuote = false; for (const char* p = begin; p < end; ++p) { if (*p == '"') { inQuote = !inQuote; } else if (*p == candidate && !inQuote) { ++count; } } return count; } /** * @brief 依据表头行推断分隔符。 * @param [IN] begin 表头行起始指针。 * @param [IN] end 表头行结束指针(不包含)。 * @return 推断出的分隔符。 */ char DetectDelimiter(const char* begin, const char* end) { static const char kCandidates[] = {',', ';', '\t', '|'}; char best = ','; int bestCount = 0; for (const char candidate : kCandidates) { const int count = CountDelimiter(begin, end, candidate); if (count > bestCount) { bestCount = count; best = candidate; } } return best; } /** * @brief 将一行文本按分隔符拆分为多个字段(支持双引号包裹与转义)。 * @param [IN] begin 行起始指针。 * @param [IN] end 行结束指针(不包含)。 * @param [IN] delimiter 分隔符。 * @param [OUT] fields 字段区间输出。 */ void SplitFields(const char* begin, const char* end, char delimiter, std::vector& fields) { fields.clear(); const char* fieldBegin = begin; bool inQuote = false; for (const char* p = begin; p < end; ++p) { const char c = *p; if (c == '"') { if (inQuote && (p + 1) < end && p[1] == '"') { ++p; continue; } inQuote = !inQuote; } else if (c == delimiter && !inQuote) { FieldSpan span; span.begin = fieldBegin; span.end = p; fields.push_back(span); fieldBegin = p + 1; } } FieldSpan span; span.begin = fieldBegin; span.end = end; fields.push_back(span); } /** * @brief 将字段区间转换为字符串并去除引号与空白。 * @param [IN] span 字段区间。 * @return 规范化后的字段文本。 */ std::wstring FieldToText(const FieldSpan& span) { std::wstring text = Utf8ToWide(span.begin, static_cast(span.end - span.begin)); return StripQuotes(text); } /** * @brief 判断字段是否应当被整行跳过(空行或注释行)。 * @param [IN] span 首字段区间。 * @param [IN] lineIsEmpty 该行是否为纯空白行。 * @return 需要跳过返回 true。 */ bool ShouldSkipLine(const FieldSpan& span, bool lineIsEmpty) { if (lineIsEmpty) { return true; } if (span.begin == nullptr || span.begin >= span.end) { return false; } const char first = span.begin[0]; return first == '#' || first == '%'; } /** * @brief 解析状态:跨读取块保存进度。 */ struct ParseState { std::string pending; /**< 尚未处理完的尾部字节(不完整行)。 */ std::vector fields; /**< 字段区间复用缓冲。 */ std::vector rowValues; /**< 行数值复用缓冲(列优先存储时的中转)。 */ bool headerReady = false; /**< 是否已解析表头。 */ unsigned long long bytesConsumed = 0; /**< 已处理字节数(用于进度)。 */ }; /** * @brief 处理一块完整行文本(不含换行符)。 * @param [IN,OUT] table 解析结果。 * @param [IN,OUT] state 解析状态。 * @param [IN] begin 行起始指针。 * @param [IN] end 行结束指针(不包含)。 * @return 解析可以继续返回 true;表头非法需要中止时返回 false。 */ bool HandleLine(DataTable& table, ParseState& state, const char* begin, const char* end) { const char* lineEnd = end; while (lineEnd > begin && lineEnd[-1] == '\r') { --lineEnd; } // 跳过纯空白行,但不产生数据行。 const char* scan = begin; while (scan < lineEnd && (*scan == ' ' || *scan == '\t')) { ++scan; } const bool lineIsEmpty = (scan >= lineEnd); if (!state.headerReady) { if (lineIsEmpty) { return true; } // ⚠️ 表头分支也必须跳过注释行:否则以 `#` / `%` 开头的注释行会被当成表头 //(列数变成 1),后面所有真实的表头与数据行都按 1 列解析并被丢弃。 if (scan < lineEnd && (*scan == '#' || *scan == '%')) { return true; } table.delimiter = DetectDelimiter(begin, lineEnd); SplitFields(begin, lineEnd, static_cast(table.delimiter), state.fields); table.headers.clear(); table.headers.reserve(state.fields.size()); for (const FieldSpan& span : state.fields) { std::wstring text = FieldToText(span); if (text.empty()) { text = L"Column" + std::to_wstring(table.headers.size() + 1); } table.headers.push_back(text); } if (table.headers.empty()) { return false; } // 列优先存储:一次性为每列预留容量,避免逐行扩容(百万行时复制代价可观)。 const std::size_t capacity = EstimateRowCapacity(table.sourceBytes); table.columns.assign(table.headers.size(), std::vector()); for (std::vector& column : table.columns) { if (capacity > 0) { column.reserve(capacity); } } state.rowValues.assign(table.headers.size(), kMissingValue); state.headerReady = true; return true; } SplitFields(begin, lineEnd, static_cast(table.delimiter), state.fields); // 空行与注释行(以 # 或 % 开头)不参与数据统计。 if (ShouldSkipLine(state.fields.empty() ? FieldSpan() : state.fields.front(), lineIsEmpty)) { return true; } bool rowHasValue = false; for (std::size_t column = 0; column < state.rowValues.size(); ++column) { double value = kMissingValue; if (column < state.fields.size() && TryParseDouble(state.fields[column].begin, state.fields[column].end, value)) { rowHasValue = true; } state.rowValues[column] = value; } // 整行都没有有效数值时该行直接丢弃(与旧行为一致): // 列优先存储要求每列等长,因此必须整行一起跳过,不能只补某一列。 if (!rowHasValue) { return true; } for (std::size_t column = 0; column < state.rowValues.size(); ++column) { table.columns[column].push_back(state.rowValues[column]); } ++table.rowCount; return true; } /** * @brief 判断列名是否为一个时间轴的列名。 * * 应用的约定:**只有名为 `time`(以及少量等价写法)的列是特殊的时间轴列**, * 其余列一律视为数据信号(不做单位/量纲推断等元数据解析)。 * * @param [IN] header 原始列名。 * @return 该列是时间轴列返回 true。 */ bool LooksLikeTimeHeader(const std::wstring& header) { const std::wstring text = ToLowerAscii(Trim(header)); if (text.empty()) { return false; } return text == L"t" || text == L"time" || text == L"timestamp" || text == L"simulation time" || text == L"时间" || text == L"时间轴"; } /** * @brief 把整表按「时间列」切分为若干时间轴块。 * * 每遇到一个时间列就开启新块;它之后直到下一个时间列之前的非时间列都属于该块。 * 首个时间列之前若存在数据列(例如表中根本没有任何名为 `time` 的列、或数据列出现在 * 第一个时间列之前),这些列并入**第一个块**,并把第一列当作该块的时间轴 *(保持旧版「回退到第一列」的行为)。 * * ⚠️ 每个块会按**自己**的时间列截断:较短的时间轴在其数据区提前结束(其余位置为空), * 因此各块的行数可以不同,列向量会收缩到本块真正有时间的那些行。 * * @param [IN,OUT] table 整表(列会被移动到各个块中)。 * @param [OUT] blocks 切分结果。 */ void SplitIntoBlocks(DataTable& table, std::vector& blocks) { const std::size_t columnCount = table.headers.size(); // 找出所有时间列下标(按出现顺序)。 std::vector timeColumns; for (std::size_t column = 0; column < columnCount; ++column) { if (LooksLikeTimeHeader(table.headers[column])) { timeColumns.push_back(column); } } // 一个时间列都没有:整表当作单块,并把第一列当作时间轴(旧版回退行为)。 if (timeColumns.empty()) { if (columnCount < 2) { return; // 只有一列又没有时间列:没有可用的数据 } timeColumns.push_back(0); } // 每个时间列开启一个块;块的成员 = [时间列, 下一个时间列)。 for (std::size_t index = 0; index < timeColumns.size(); ++index) { const std::size_t timeColumn = timeColumns[index]; const std::size_t blockEnd = (index + 1 < timeColumns.size()) ? timeColumns[index + 1] : columnCount; if (timeColumn >= columnCount || blockEnd <= timeColumn + 1) { continue; // 该时间列之后没有任何数据列 } TableBlock block; block.timeHeader = table.headers[timeColumn]; block.table.delimiter = table.delimiter; block.table.sourceBytes = table.sourceBytes; block.table.headers.reserve(blockEnd - timeColumn); for (std::size_t column = timeColumn; column < blockEnd; ++column) { block.table.headers.push_back(table.headers[column]); block.table.columns.push_back(std::move(table.columns[column])); } block.table.rowCount = table.rowCount; blocks.push_back(std::move(block)); } // 首个时间列之前若还有数据列(非时间列),并入第一个块,并让第一列充当时间轴。 if (!blocks.empty() && timeColumns.front() > 0 && blocks.size() >= 1) { TableBlock& first = blocks.front(); std::vector prepended; for (std::size_t column = 0; column < timeColumns.front(); ++column) { first.table.headers.push_back(table.headers[column]); first.table.columns.push_back(std::move(table.columns[column])); prepended.push_back(column); } if (!prepended.empty()) { // 把原时间列移到 block.table 的最前面(保持 `columns[0]` = 时间列 的约定)。 std::rotate(first.table.headers.begin(), first.table.headers.begin() + static_cast(prepended.size()), first.table.headers.end()); std::rotate(first.table.columns.begin(), first.table.columns.begin() + static_cast(prepended.size()), first.table.columns.end()); first.timeHeader = first.table.headers.front(); } } } /** * @brief 按本块自己的时间列截断(丢掉尾部没有时间的行),并计算有效时间点数量。 * * 多时间轴文件里各块长度不同:较短的时间轴在数据区提前结束,其余行的时间是空的。 * 这里把列向量收缩到最后一行「时间列有效」的位置,既省内存,也让后续的 * `lower_bound`(依赖时间单调)建立在干净的数据上。 * * @param [IN,OUT] block 目标块。 * @return 有效时间点数量。 */ std::size_t TrimBlockRows(TableBlock& block) { if (block.table.columns.empty()) { return 0; } const std::vector& time = block.table.columns.front(); std::size_t lastValid = 0; std::size_t finiteCount = 0; for (std::size_t row = 0; row < time.size(); ++row) { if (std::isfinite(time[row])) { lastValid = row + 1; ++finiteCount; } } if (lastValid == 0) { return 0; } if (lastValid < block.table.rowCount) { for (std::vector& column : block.table.columns) { if (column.size() > lastValid) { column.resize(lastValid); } } block.table.rowCount = lastValid; } return finiteCount; } } // namespace bool LoadCsvSource(const std::wstring& filePath, LoadedSource& source, std::wstring& error, const LoadProgress& progress) { source = LoadedSource(); error.clear(); FILE* file = nullptr; unsigned long long fileSize = 0; if (!OpenFile(filePath, file, fileSize, error)) { return false; } const std::size_t slashPos = filePath.find_last_of(L"\\/"); source.sourceName = (slashPos == std::wstring::npos) ? filePath : filePath.substr(slashPos + 1); DataTable table; table.sourceBytes = fileSize; ParseState state; std::string staging; staging.resize(kReadChunkBytes); bool cancelled = false; bool fatal = false; while (!cancelled && !fatal) { const size_t readCount = std::fread(&staging[0], 1, staging.size(), file); if (readCount == 0) { break; } state.bytesConsumed += readCount; state.pending.append(staging.data(), readCount); // 首个块处理前剥离 UTF-8 BOM,避免首列名被污染(BOM 必然落在第一块内)。 if (!state.headerReady && state.pending.size() >= 3 && static_cast(state.pending[0]) == 0xEF && static_cast(state.pending[1]) == 0xBB && static_cast(state.pending[2]) == 0xBF) { state.pending.erase(0, 3); } // 处理 pending 中所有以换行结束的完整行。 std::size_t lineStart = 0; while (true) { const std::size_t newline = state.pending.find('\n', lineStart); if (newline == std::string::npos) { break; } if (!HandleLine(table, state, state.pending.data() + lineStart, state.pending.data() + newline)) { fatal = true; break; } lineStart = newline + 1; } if (fatal) { break; } state.pending.erase(0, lineStart); if (progress) { LoadProgressInfo info; info.bytesRead = state.bytesConsumed; info.totalBytes = fileSize; info.hasBytes = true; info.ratio = (fileSize > 0) ? (static_cast(state.bytesConsumed) / static_cast(fileSize)) : 0.0; if (info.ratio > 1.0) { info.ratio = 1.0; } if (!progress(info)) { cancelled = true; } } } // 处理文件末尾没有换行的最后一行。 if (!cancelled && !fatal && !state.pending.empty()) { if (!HandleLine(table, state, state.pending.data(), state.pending.data() + state.pending.size())) { fatal = true; } state.pending.clear(); } std::fclose(file); if (cancelled) { error = L"已取消加载。"; return false; } if (fatal || !table.HasData()) { error = L"未能从文件中解析出有效数据表。"; return false; } // 按时间列切分为多个时间轴块,并各自按自己的时间列截断。 SplitIntoBlocks(table, source.blocks); if (source.blocks.empty()) { error = L"未发现任何时间轴列与数据列的组合。"; return false; } // 丢弃没有任何有效时间点的块;若全部无效则报错。 std::vector kept; kept.reserve(source.blocks.size()); for (TableBlock& block : source.blocks) { if (TrimBlockRows(block) >= 2) { kept.push_back(std::move(block)); } } if (kept.empty()) { error = L"时间轴列的有效数值不足(至少需要 2 个)。"; return false; } source.blocks = std::move(kept); if (progress) { LoadProgressInfo info; info.ratio = 1.0; progress(info); } return true; } } // namespace jdi