#include "pq_app.h" #include "tinyxml2.h" #include #include #include #include #include #include #include #include #include #include #include /* * icd_mapper.cpp * * 功能:把外部 ICD/XML 中 LN/DOI 的 desc 描述补写到工作簿 * “未匹配的原始数据”工作表的“icd中代表的字段”列。 * * 输入: * icdPath 外部 ICD/XML 文件,可能是 UTF-8,也可能声明/实际使用 GBK/GB2312。 * workbookPath main.cpp 生成的 Excel 2003 XML 格式 .xls。 * * 输出: * 直接修改 workbookPath。message 返回解析结果、匹配数量或错误原因。 * * 核心匹配规则: * 1. 从 ICD 的每个 LN 节点读取 lnClass + inst,并遍历其 DOI 子节点; * 2. 组成 DO key:lnClass + inst + "$MX$" + DOI/@name; * 3. 工作簿未匹配页的“原始路径”如果等于该 key,或以 key + "$" 开头, * 就认为该原始 MMS 路径属于这个 DOI,把 DOI/@desc 写入目标列。 * * 阅读本文件时可留意的 C++/库语法: * - namespace fs = std::filesystem 是命名空间别名,后文 fs::path 即文件系统路径类型; * - namespace { ... } 是匿名命名空间,使辅助函数只在当前编译单元内可见; * - tinyxml2::XMLDocument 拥有解析后的节点,XMLNode/XMLElement 指针不能脱离文档存活; * - std::unordered_map 用散列表保存 ICD key -> desc,适合大量精确查询; * - std::sort/std::transform 中的 [](...) { ... } 是短生命周期 lambda(匿名函数); * - MultiByteToWideChar/WideCharToMultiByte 负责 Windows 本地代码页与 UTF-8 转换。 */ namespace fs = std::filesystem; namespace { /** * Function: readAllBinary * * @brief 读取或加载 “readAllBinary” 对应的数据,并转换为本模块后续逻辑使用的内存结构。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param path 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string readAllBinary(const fs::path& path) { std::ifstream ifs(path, std::ios::binary); if (!ifs) throw std::runtime_error("Failed to open file: " + path.string()); std::ostringstream ss; ss << ifs.rdbuf(); return ss.str(); } /** * Function: writeAllBinary * * @brief 把 “writeAllBinary” 对应的内存数据序列化并写入目标位置。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param path 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 */ void writeAllBinary(const fs::path& path, const std::string& text) { std::ofstream ofs(path, std::ios::binary); if (!ofs) throw std::runtime_error("Failed to write file: " + path.string()); ofs << text; } /** * Function: lowerAscii * * @brief 规范化 “lowerAscii” 对应的字符串或标识,减少格式差异对匹配造成的影响。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param s 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string lowerAscii(std::string s) { std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return s; } /** * Function: declaresGbEncoding * * @brief 判断 “declaresGbEncoding” 所表达的条件;该函数只读取输入,不负责修改业务状态。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool declaresGbEncoding(const std::string& text) { std::string head = lowerAscii(text.substr(0, std::min(text.size(), 256))); return head.find("gb2312") != std::string::npos || head.find("gbk") != std::string::npos || head.find("gb18030") != std::string::npos; } /** * Function: isLikelyUtf8 * * @brief 判断 “isLikelyUtf8” 所表达的条件;该函数只读取输入,不负责修改业务状态。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool isLikelyUtf8(const std::string& text) { // 只做 UTF-8 字节合法性判断,不判断语义;合法就优先按 UTF-8 给 tinyxml2。 int remaining = 0; for (unsigned char c : text) { if (remaining == 0) { if ((c & 0x80) == 0) continue; if ((c & 0xE0) == 0xC0) remaining = 1; else if ((c & 0xF0) == 0xE0) remaining = 2; else if ((c & 0xF8) == 0xF0) remaining = 3; else return false; } else { if ((c & 0xC0) != 0x80) return false; --remaining; } } return remaining == 0; } /** * Function: ansiToUtf8 * * @brief 实现 “ansiToUtf8” 对应的模块内功能。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string ansiToUtf8(const std::string& text) { // 936 是 Windows 简体中文代码页,兼容常见 GBK/GB2312 ICD 文件。 if (text.empty()) return ""; int wideLen = MultiByteToWideChar(936, 0, text.data(), (int)text.size(), nullptr, 0); if (wideLen <= 0) return text; std::wstring wide(wideLen, L'\0'); MultiByteToWideChar(936, 0, text.data(), (int)text.size(), wide.data(), wideLen); int utf8Len = WideCharToMultiByte(CP_UTF8, 0, wide.data(), wideLen, nullptr, 0, nullptr, nullptr); std::string out(utf8Len, '\0'); WideCharToMultiByte(CP_UTF8, 0, wide.data(), wideLen, out.data(), utf8Len, nullptr, nullptr); return out; } /** * Function: loadXmlText * * @brief 读取或加载 “loadXmlText” 对应的数据,并转换为本模块后续逻辑使用的内存结构。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param path 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string loadXmlText(const fs::path& path) { std::string raw = readAllBinary(path); // ICD 声明 GB 编码或字节本身不像 UTF-8 时,先转成 UTF-8 再交给 tinyxml2。 if (declaresGbEncoding(raw) || !isLikelyUtf8(raw)) return ansiToUtf8(raw); return raw; } /** * Function: localName * * @brief 实现 “localName” 对应的模块内功能。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param name 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string localName(const char* name) { if (!name) return ""; std::string s(name); size_t pos = s.find(':'); return pos == std::string::npos ? s : s.substr(pos + 1); } /** * Function: attr * * @brief 实现 “attr” 对应的模块内功能。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param e 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param name 待解析、比较、显示或发送的 UTF-8 文本。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string attr(const tinyxml2::XMLElement* e, const char* name) { const char* v = e ? e->Attribute(name) : nullptr; return v ? v : ""; } /** * Function: collectIcdDescriptions * * @brief 遍历输入结构并收集 “collectIcdDescriptions” 所需的候选数据。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param element 当前处理的数据行、单元格、XML 节点或模型项。 * @param descByDo 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 */ void collectIcdDescriptions(const tinyxml2::XMLElement* element, std::unordered_map& descByDo) { if (!element) return; if (localName(element->Name()) == "LN") { std::string lnClass = attr(element, "lnClass"); std::string inst = attr(element, "inst"); if (!lnClass.empty()) { for (const tinyxml2::XMLElement* child = element->FirstChildElement(); child; child = child->NextSiblingElement()) { if (localName(child->Name()) != "DOI") continue; std::string doiName = attr(child, "name"); std::string desc = attr(child, "desc"); if (doiName.empty() || desc.empty()) continue; // 这里构造的是 origin 原始路径前缀。后面路径可继续带 $mag$f 等 DA 后缀。 std::string key = lnClass + inst + "$MX$" + doiName; descByDo[key] = desc; } } } for (const tinyxml2::XMLElement* child = element->FirstChildElement(); child; child = child->NextSiblingElement()) { collectIcdDescriptions(child, descByDo); } } /** * Function: parseIcdDescriptions * * @brief 解析 “parseIcdDescriptions” 对应的文本或结构,提取经过校验的业务字段。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param icdPath 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::unordered_map parseIcdDescriptions(const fs::path& icdPath) { tinyxml2::XMLDocument doc; std::string xml = loadXmlText(icdPath); tinyxml2::XMLError rc = doc.Parse(xml.c_str(), xml.size()); if (rc != tinyxml2::XML_SUCCESS) { throw std::runtime_error("Failed to parse ICD XML: " + std::to_string((int)rc)); } std::unordered_map descByDo; collectIcdDescriptions(doc.RootElement(), descByDo); return descByDo; } /** * Function: cellText * * @brief 实现 “cellText” 对应的模块内功能。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param cell 当前处理的数据行、单元格、XML 节点或模型项。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string cellText(const tinyxml2::XMLElement* cell) { const tinyxml2::XMLElement* data = cell ? cell->FirstChildElement("Data") : nullptr; const char* text = data ? data->GetText() : nullptr; return text ? text : ""; } /** * Function: cellIndex * * @brief 实现 “cellIndex” 对应的模块内功能。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param cell 当前处理的数据行、单元格、XML 节点或模型项。 * @param fallback 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 整数状态、索引或计数;入口函数以 0 表示成功。 */ int cellIndex(const tinyxml2::XMLElement* cell, int fallback) { const char* idx = cell ? cell->Attribute("ss:Index") : nullptr; return idx && *idx ? std::max(1, std::atoi(idx)) : fallback; } /** * Function: cellAt * * @brief 实现 “cellAt” 对应的模块内功能。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param row 当前处理的数据行、单元格、XML 节点或模型项。 * @param col 索引、数量、范围或偏移参数;具体边界由函数体校验。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ tinyxml2::XMLElement* cellAt(tinyxml2::XMLElement* row, int col) { int current = 1; for (tinyxml2::XMLElement* cell = row ? row->FirstChildElement("Cell") : nullptr; cell; cell = cell->NextSiblingElement("Cell")) { current = cellIndex(cell, current); if (current == col) return cell; ++current; } return nullptr; } /** * Function: ensureCell * * @brief 向目标结构追加或保证存在 “ensureCell” 所描述的数据。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param doc tinyxml2 文档对象;节点的生命周期由该文档管理。 * @param row 当前处理的数据行、单元格、XML 节点或模型项。 * @param col 索引、数量、范围或偏移参数;具体边界由函数体校验。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ tinyxml2::XMLElement* ensureCell(tinyxml2::XMLDocument& doc, tinyxml2::XMLElement* row, int col) { if (tinyxml2::XMLElement* existing = cellAt(row, col)) return existing; // Excel XML 允许通过 ss:Index 跳列;补列时直接设置 ss:Index,避免改动前面已有单元格。 tinyxml2::XMLElement* cell = doc.NewElement("Cell"); cell->SetAttribute("ss:Index", col); row->InsertEndChild(cell); return cell; } /** * Function: setCellText * * @brief 把 “setCellText” 对应的变更应用到模型、控件或输出文档。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param doc tinyxml2 文档对象;节点的生命周期由该文档管理。 * @param cell 当前处理的数据行、单元格、XML 节点或模型项。 * @param text 待解析、比较、显示或发送的 UTF-8 文本。 */ void setCellText(tinyxml2::XMLDocument& doc, tinyxml2::XMLElement* cell, const std::string& text) { tinyxml2::XMLElement* data = cell->FirstChildElement("Data"); if (!data) { data = doc.NewElement("Data"); data->SetAttribute("ss:Type", "String"); cell->InsertEndChild(data); } data->SetText(text.c_str()); } /** * Function: sortedKeysByLength * * @brief 实现 “sortedKeysByLength” 对应的模块内功能。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param values 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::vector sortedKeysByLength(const std::unordered_map& values) { std::vector keys; keys.reserve(values.size()); for (const auto& kv : values) keys.push_back(kv.first); // 长路径优先,避免短 key 先命中长 key 的前缀。 std::sort(keys.begin(), keys.end(), [](const std::string& a, const std::string& b) { if (a.size() != b.size()) return a.size() > b.size(); return a < b; }); return keys; } /** * Function: pathMatchesDo * * @brief 实现 “pathMatchesDo” 对应的模块内功能。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param rawPath 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 * @param doKey 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool pathMatchesDo(const std::string& rawPath, const std::string& doKey) { // rawPath 可以正好是 DO,也可以是 DO$DA...;必须在 "$" 边界匹配,避免误命中相似前缀。 return rawPath == doKey || (rawPath.size() > doKey.size() && rawPath.compare(0, doKey.size(), doKey) == 0 && rawPath[doKey.size()] == '$'); } /** * Function: descForPath * * @brief 实现 “descForPath” 对应的模块内功能。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param rawPath 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 * @param descByDo 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @param orderedKeys 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ std::string descForPath(const std::string& rawPath, const std::unordered_map& descByDo, const std::vector& orderedKeys) { for (const auto& key : orderedKeys) { if (pathMatchesDo(rawPath, key)) { auto it = descByDo.find(key); return it == descByDo.end() ? "" : it->second; } } return ""; } /** * Function: findWorksheet * * @brief 在现有数据结构中查找 “findWorksheet” 对应的对象或位置。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param doc tinyxml2 文档对象;节点的生命周期由该文档管理。 * @param sheetName 调用方传入的业务参数;其用途与约束由类型、名称及函数体共同定义。 * @return 构造、解析或查找到的结果;所有权与生命周期遵循返回类型。 */ tinyxml2::XMLElement* findWorksheet(tinyxml2::XMLDocument& doc, const char* sheetName) { tinyxml2::XMLElement* workbook = doc.FirstChildElement("Workbook"); for (tinyxml2::XMLElement* ws = workbook ? workbook->FirstChildElement("Worksheet") : nullptr; ws; ws = ws->NextSiblingElement("Worksheet")) { const char* name = ws->Attribute("ss:Name"); if (name && std::string(name) == sheetName) return ws; } return nullptr; } /** * Function: headerColumn * * @brief 实现 “headerColumn” 对应的模块内功能。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param headerRow 当前处理的数据行、单元格、XML 节点或模型项。 * @param title 待解析、比较、显示或发送的 UTF-8 文本。 * @return 整数状态、索引或计数;入口函数以 0 表示成功。 */ int headerColumn(tinyxml2::XMLElement* headerRow, const std::string& title) { int current = 1; for (tinyxml2::XMLElement* cell = headerRow ? headerRow->FirstChildElement("Cell") : nullptr; cell; cell = cell->NextSiblingElement("Cell")) { current = cellIndex(cell, current); if (cellText(cell) == title) return current; ++current; } return 0; } } // namespace /** * Function: pqApplyIcdDescriptionsToWorkbook * * @brief 实现 “pqApplyIcdDescriptionsToWorkbook” 对应的模块内功能。 * @details 本文件使用 tinyxml2 遍历 ICD/Excel XML,并使用 Windows 代码页 API 兼容 GBK/GB2312;文件操作采用 std::filesystem 与二进制流,失败时抛出异常交给公共入口处理。 * @param icdPath 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 * @param workbookPath 文件或目录路径;函数按 UTF-8/Windows 路径规则读取或写入。 * @param message 输出参数;函数在成功或失败时写入结果或诊断信息。 * @return 条件成立或操作成功时为 true,否则为 false。 */ bool pqApplyIcdDescriptionsToWorkbook(const std::string& icdPath, const std::string& workbookPath, std::string* message) { try { // 第一步:解析 ICD,得到 “DO 路径前缀 -> 中文描述”。 auto descByDo = parseIcdDescriptions(fs::u8path(icdPath)); if (descByDo.empty()) { if (message) *message = "ICD 中没有解析到 LN/DOI desc"; return false; } std::vector orderedKeys = sortedKeysByLength(descByDo); // 第二步:读取 main.cpp 输出的工作簿。该 .xls 实际是 XML,可直接用 tinyxml2 修改。 tinyxml2::XMLDocument doc; std::string workbookXml = readAllBinary(fs::u8path(workbookPath)); tinyxml2::XMLError rc = doc.Parse(workbookXml.c_str(), workbookXml.size()); if (rc != tinyxml2::XML_SUCCESS) { if (message) *message = "表格文件解析失败: " + std::to_string((int)rc); return false; } // 第三步:只处理“未匹配的原始数据”页,因为已匹配页已经有明确映射关系。 tinyxml2::XMLElement* sheet = findWorksheet(doc, "未匹配的原始数据"); if (!sheet) { if (message) *message = "没有找到 sheet: 未匹配的原始数据"; return false; } tinyxml2::XMLElement* table = sheet->FirstChildElement("Table"); tinyxml2::XMLElement* header = table ? table->FirstChildElement("Row") : nullptr; int pathCol = headerColumn(header, "原始路径"); int icdCol = headerColumn(header, "icd中代表的字段"); if (pathCol <= 0 || icdCol <= 0) { if (message) *message = "未匹配 sheet 缺少 原始路径 或 icd中代表的字段 列"; return false; } // 第四步:逐行读取原始路径,按 DO 前缀回填 DOI 描述。 int matched = 0; for (tinyxml2::XMLElement* row = header ? header->NextSiblingElement("Row") : nullptr; row; row = row->NextSiblingElement("Row")) { tinyxml2::XMLElement* pathCell = cellAt(row, pathCol); std::string rawPath = cellText(pathCell); if (rawPath.empty()) continue; std::string desc = descForPath(rawPath, descByDo, orderedKeys); if (desc.empty()) continue; tinyxml2::XMLElement* icdCell = ensureCell(doc, row, icdCol); setCellText(doc, icdCell, desc); ++matched; } tinyxml2::XMLPrinter printer; doc.Print(&printer); writeAllBinary(fs::u8path(workbookPath), printer.CStr()); if (message) { *message = "ICD 导入完成,DOI=" + std::to_string(descByDo.size()) + ",填入未匹配原始数据=" + std::to_string(matched); } return true; } catch (const std::exception& e) { if (message) *message = e.what(); return false; } }