Files
datatrace/source/icd_mapper.cpp
2026-07-13 15:12:55 +08:00

289 lines
10 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.

#include "pq_app.h"
#include "tinyxml2.h"
#include <windows.h>
#include <algorithm>
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
namespace fs = std::filesystem;
namespace {
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();
}
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;
}
std::string lowerAscii(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return s;
}
bool declaresGbEncoding(const std::string& text) {
std::string head = lowerAscii(text.substr(0, std::min<size_t>(text.size(), 256)));
return head.find("gb2312") != std::string::npos ||
head.find("gbk") != std::string::npos ||
head.find("gb18030") != std::string::npos;
}
bool isLikelyUtf8(const std::string& text) {
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;
}
std::string ansiToUtf8(const std::string& text) {
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;
}
std::string loadXmlText(const fs::path& path) {
std::string raw = readAllBinary(path);
if (declaresGbEncoding(raw) || !isLikelyUtf8(raw)) return ansiToUtf8(raw);
return raw;
}
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);
}
std::string attr(const tinyxml2::XMLElement* e, const char* name) {
const char* v = e ? e->Attribute(name) : nullptr;
return v ? v : "";
}
void collectIcdDescriptions(const tinyxml2::XMLElement* element,
std::unordered_map<std::string, std::string>& 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;
std::string key = lnClass + inst + "$MX$" + doiName;
descByDo[key] = desc;
}
}
}
for (const tinyxml2::XMLElement* child = element->FirstChildElement();
child;
child = child->NextSiblingElement()) {
collectIcdDescriptions(child, descByDo);
}
}
std::unordered_map<std::string, std::string> 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<std::string, std::string> descByDo;
collectIcdDescriptions(doc.RootElement(), descByDo);
return descByDo;
}
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 : "";
}
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;
}
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;
}
tinyxml2::XMLElement* ensureCell(tinyxml2::XMLDocument& doc, tinyxml2::XMLElement* row, int col) {
if (tinyxml2::XMLElement* existing = cellAt(row, col)) return existing;
tinyxml2::XMLElement* cell = doc.NewElement("Cell");
cell->SetAttribute("ss:Index", col);
row->InsertEndChild(cell);
return cell;
}
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());
}
std::vector<std::string> sortedKeysByLength(const std::unordered_map<std::string, std::string>& values) {
std::vector<std::string> keys;
keys.reserve(values.size());
for (const auto& kv : values) keys.push_back(kv.first);
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;
}
bool pathMatchesDo(const std::string& rawPath, const std::string& doKey) {
return rawPath == doKey ||
(rawPath.size() > doKey.size() &&
rawPath.compare(0, doKey.size(), doKey) == 0 &&
rawPath[doKey.size()] == '$');
}
std::string descForPath(const std::string& rawPath,
const std::unordered_map<std::string, std::string>& descByDo,
const std::vector<std::string>& orderedKeys) {
for (const auto& key : orderedKeys) {
if (pathMatchesDo(rawPath, key)) {
auto it = descByDo.find(key);
return it == descByDo.end() ? "" : it->second;
}
}
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;
}
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
bool pqApplyIcdDescriptionsToWorkbook(const std::string& icdPath,
const std::string& workbookPath,
std::string* message) {
try {
auto descByDo = parseIcdDescriptions(fs::u8path(icdPath));
if (descByDo.empty()) {
if (message) *message = "ICD 中没有解析到 LN/DOI desc";
return false;
}
std::vector<std::string> orderedKeys = sortedKeysByLength(descByDo);
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;
}
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;
}
}