Files
datatrace/source/main.cpp

2417 lines
95 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 <algorithm>
#include <cctype>
#include <cerrno>
#include <cmath>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <functional>
#include <limits>
#include <iostream>
#include <map>
#include <regex>
#include <set>
#include <sstream>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "json.hpp" // nlohmann::json (单头文件版)
#include "pq_app.h"
#include "tinyxml2.h" // tinyxml2
/*
* main.cpp
*
* 本文件是“映射解析 + 报表生成”的核心,也保留了命令行入口。
*
* 主流程:
* 1. loadValueMetas() 读取 ICD/XML 模板,把每个 <Value> 的 name、desc、DO、DA、
* Topic、DataType、Item、Sequence 等上下文整理成 ValueMeta。
* 2. readJsonDataFile() 读取 DATATopic 的 jsondataflattenJson() 压平成
* Value_... -> value 的键值对mergeSequences() 把连续编号合并成数组。
* 3. buildItemsForSheet() 按 SheetSpec 生成每张工作表的 OutItem 行。
* 4. addOriginFileToContext() 读取 TraceTopic 的 origin/MMS 数据matchOrigin()
* 用 DO + DA + 相位 + 序列范围匹配最终值和原始值。
* 5. writeExcelWorkbook() 输出 Excel 2003 XML 格式的 .xls供 Excel/GUI 预览打开。
*
* 标志/转换要点:
* - Sequence/@value == 7 代表星形接线 star。
* - Sequence/@value == 112 代表角形接线 delta。
* - DATA_TYPE "1" 会规范成 "01",用于匹配统计/闪变/实时 Sheet。
* - 指标名前缀 G_/MAX_/MIN_ 分别转换成 95值/最大值/最小值;无前缀默认平均值。
* - DA 中 phs* 是相位占位符,星形转换为 phsA/phsB/phsC角形转换为
* phsAB/phsBC/phsCA。
* - Value 名称中的 %start,end% 是 XML 序列模板;运行时显示成 [start-end]。
*/
using json = nlohmann::json;
namespace fs = std::filesystem;
/*
* XML <Value> 节点的完整业务元信息。
*
* 输入loadValueMetas() 在递归遍历 XML 时填充。
* 输出:后续用于把 jsondata 的 key 映射成 DO/DA、中文描述、接线方式、Topic/DataType。
*/
struct ValueMeta {
std::string name; // Value/@name可能是普通指标 V1也可能是序列模板 MAX_V_%2,50%。
std::string desc; // Value/@desc最终表格“名称”列主要来自这里。
std::string doPath; // Value/@DOIEC 61850 MMS 对象路径前半段。
std::string daPath; // Value/@DAIEC 61850 MMS 数据属性路径后半段。
std::string wiring = "common"; // star/delta/common由 Sequence/@value 推断。
std::string topic; // 父级 Topic/@name区分 HISDATA/RTDATA。
std::string dataType; // 父级 DataType/@value区分 01/02/03。
std::string itemName; // 父级 Item/@name如 V/I/PQ/F_S/F_L。
std::string seqValue; // 父级 Sequence/@value后续判断 T 相或接线方式会用到。
bool hasOffset = false; // Value/@Offset 是否存在。
int offset = 0; // 最终值偏移量,用于把最终值数组范围挪到 origin 对齐范围。
bool isSequence = false; // name 是否匹配 %start,end% 序列模板。
int seqStart = 0; // 例如 %2,50% 里的 2。
int seqEnd = 0; // 例如 %2,50% 里的 50。
std::string seqPrefixName; // 例如 MAX_V_%2,50% 的前缀 MAX_V_。
};
static std::string readAll(const fs::path& p) {
std::ifstream ifs(p, std::ios::binary);
if (!ifs) throw std::runtime_error("Failed to open: " + p.string());
std::ostringstream oss;
oss << ifs.rdbuf();
return oss.str();
}
static std::string ltrimBOM(std::string s) {
const std::string bom = "\xEF\xBB\xBF";
if (s.rfind(bom, 0) == 0) s.erase(0, bom.size());
return s;
}
// DO/DA 中有时会出现 A||A||B 这种重复候选;这里保持顺序并去重,减少表格噪声。
static std::string collapseRepeatedAlternatives(const std::string& s) {
std::vector<std::string> parts;
std::unordered_set<std::string> seen;
size_t start = 0;
while (true) {
size_t pos = s.find("||", start);
std::string part = pos == std::string::npos ? s.substr(start) : s.substr(start, pos - start);
if (!seen.count(part)) {
seen.insert(part);
parts.push_back(part);
}
if (pos == std::string::npos) break;
start = pos + 2;
}
std::ostringstream oss;
for (size_t i = 0; i < parts.size(); ++i) {
if (i) oss << "||";
oss << parts[i];
}
return oss.str();
}
static std::string toStr(const json& v) {
if (v.is_null()) return "null";
if (v.is_boolean()) return v.get<bool>() ? "true" : "false";
if (v.is_number_integer()) return std::to_string(v.get<long long>());
if (v.is_number_unsigned()) return std::to_string(v.get<unsigned long long>());
if (v.is_number_float()) {
std::ostringstream oss;
oss.setf(std::ios::fixed);
oss.precision(6);
oss << v.get<double>();
std::string s = oss.str();
while (s.find('.') != std::string::npos && !s.empty() && s.back() == '0') s.pop_back();
if (!s.empty() && s.back() == '.') s.pop_back();
if (s.empty()) s = "0";
return s;
}
if (v.is_string()) return v.get<std::string>();
return v.dump();
}
/*
* 把嵌套 JSON 压平成下划线连接的 key。
*
* 输入示例:{"Value":{"V":{"A":{"MAX_V1":12}}}}
* 输出示例Value_V_A_MAX_V1 -> 12
*
* 数组会直接转成逗号分隔值,这样后续单元格可以拆成多列展示。
*/
static void flattenJson(const json& j, const std::string& prefix,
std::vector<std::pair<std::string, std::string>>& out) {
if (j.is_object()) {
for (auto it = j.begin(); it != j.end(); ++it) {
std::string key = prefix.empty() ? it.key() : (prefix + "_" + it.key());
flattenJson(it.value(), key, out);
}
return;
}
if (j.is_array()) {
std::ostringstream oss;
for (size_t i = 0; i < j.size(); ++i) {
if (i) oss << ",";
oss << toStr(j[i]);
}
out.push_back({prefix, oss.str()});
return;
}
out.push_back({prefix, toStr(j)});
}
/*
* 读取 XMLValue/@name -> 完整元信息列表。
*
* 输入xmlPath通常是下载的 ICD/XML 映射文件。
* 输出unordered_map<name, vector<ValueMeta>>。同一个 name 可能在不同 Topic、
* DataType 或接线方式下出现,所以值是 vector。
*
* 递归 walk 的上下文参数含义:
* wiring 当前 Sequence 推断出的接线方式,默认 common
* topic 最近一个父级 Topic/@name
* dataType 最近一个父级 DataType/@value
* itemName 最近一个父级 Item/@name
* seqValue 最近一个父级 Sequence/@value。
*/
static std::unordered_map<std::string, std::vector<ValueMeta>> loadValueMetas(const fs::path& xmlPath) {
std::unordered_map<std::string, std::vector<ValueMeta>> mp;
tinyxml2::XMLDocument doc;
std::string xmlContent = readAll(xmlPath);
if (doc.Parse(xmlContent.c_str()) != tinyxml2::XML_SUCCESS) {
throw std::runtime_error("Failed to parse xml: " + xmlPath.string());
}
std::regex seqRe(R"(^(.*)%(\d+),(\d+)%$)");
std::function<void(tinyxml2::XMLElement*, std::string, std::string, std::string, std::string, std::string)> walk =
[&](tinyxml2::XMLElement* e,
std::string wiring,
std::string topic,
std::string dataType,
std::string itemName,
std::string seqValue) {
for (; e; e = e->NextSiblingElement()) {
std::string nodeName = e->Name() ? e->Name() : "";
std::string nextWiring = wiring;
std::string nextTopic = topic;
std::string nextDataType = dataType;
std::string nextItemName = itemName;
std::string nextSeqValue = seqValue;
// Topic/DataType/Item/Sequence 不是最终数据项,而是给子孙 Value 提供上下文。
if (nodeName == "Topic") {
const char* name = e->Attribute("name");
nextTopic = name ? name : "";
} else if (nodeName == "DataType") {
const char* value = e->Attribute("value");
nextDataType = value ? value : "";
} else if (nodeName == "Item") {
const char* name = e->Attribute("name");
nextItemName = name ? name : "";
}
if (nodeName == "Sequence") {
const char* seqValue = e->Attribute("value");
nextSeqValue = seqValue ? seqValue : "";
// 项目约定7=星形接线112=角形接线;其他 Sequence 保持 common。
if (seqValue && std::string(seqValue) == "7") nextWiring = "star";
else if (seqValue && std::string(seqValue) == "112") nextWiring = "delta";
}
if (nodeName == "Value") {
const char* name = e->Attribute("name");
const char* desc = e->Attribute("desc");
const char* doPath = e->Attribute("DO");
const char* daPath = e->Attribute("DA");
const char* offset = e->Attribute("Offset");
if (name) {
ValueMeta vm;
vm.name = name;
vm.desc = desc ? desc : "";
vm.doPath = doPath ? collapseRepeatedAlternatives(doPath) : "";
vm.daPath = daPath ? collapseRepeatedAlternatives(daPath) : "";
vm.wiring = nextWiring;
vm.topic = nextTopic;
vm.dataType = nextDataType;
vm.itemName = nextItemName;
vm.seqValue = nextSeqValue;
if (offset) {
vm.hasOffset = true;
vm.offset = std::stoi(offset);
}
std::smatch m;
// name 中的 %start,end% 表示一个序列模板,后面要和 jsondata 的 [start-end] 对齐。
if (std::regex_match(vm.name, m, seqRe)) {
vm.isSequence = true;
vm.seqPrefixName = m[1].str(); // 例如 MAX_V
vm.seqStart = std::stoi(m[2].str());
vm.seqEnd = std::stoi(m[3].str());
}
mp[vm.name].push_back(vm);
}
}
walk(e->FirstChildElement(), nextWiring, nextTopic, nextDataType, nextItemName, nextSeqValue);
}
};
walk(doc.RootElement(), "common", "", "", "", "");
return mp;
}
// 相位
static const std::vector<std::string> PHASES = {"A", "B", "C", "T"};
static std::unordered_map<std::string, int> phaseRank() {
std::unordered_map<std::string, int> m;
for (int i = 0; i < (int)PHASES.size(); ++i) m[PHASES[i]] = i;
return m;
}
// 从 key 中提取 “相位之后的 metricName”
// 例如Value_V_A_MAX_V1 -> MAX_V1
static std::string metricAfterPhase(const std::string& key) {
std::string base = key;
auto bpos = base.find('[');
if (bpos != std::string::npos) base = base.substr(0, bpos);
std::vector<std::string> parts;
std::stringstream ss(base);
std::string tok;
while (std::getline(ss, tok, '_')) parts.push_back(tok);
auto pr = phaseRank();
int lastPhaseI = -1;
for (int i = 0; i < (int)parts.size(); ++i) {
if (pr.find(parts[i]) != pr.end()) lastPhaseI = i;
}
if (lastPhaseI == -1 || lastPhaseI + 1 >= (int)parts.size()) return "";
std::string m = parts[lastPhaseI + 1];
for (int i = lastPhaseI + 2; i < (int)parts.size(); ++i) m += "_" + parts[i];
return m;
}
// 尝试把 key 识别成序列:
// 1) ..._<number>
// 2) ..._<LETTERS><number> (如 ..._V2, ..._VA50)
static bool parseSequenceKey(const std::string& key, std::string& groupId, int& idx) {
auto posBracket = key.find('[');
std::string base = (posBracket == std::string::npos) ? key : key.substr(0, posBracket);
static const std::regex re_underscore_num(R"(^(.*)_(\d+)$)");
static const std::regex re_letters_num(R"(^([A-Za-z]+)(\d+)$)");
std::smatch m;
if (std::regex_match(base, m, re_underscore_num)) {
groupId = m[1].str();
idx = std::stoi(m[2].str());
return true;
}
auto lastUnd = base.rfind('_');
if (lastUnd != std::string::npos) {
std::string pre = base.substr(0, lastUnd);
std::string last = base.substr(lastUnd + 1);
std::smatch m2;
if (std::regex_match(last, m2, re_letters_num)) {
groupId = pre + "_" + m2[1].str();
idx = std::stoi(m2[2].str());
return true;
}
}
return false;
}
/*
* 合并 jsondata 中的序列项。
*
* 输入flattenJson() 得到的 key/value 列表,例如:
* Value_V_A_MAX_V_2=...
* Value_V_A_MAX_V_3=...
*
* 输出:合并成 Value_V_A_MAX_V[2-3]=v2,v3。
*
* 保护规则:
* 如果相位后的 metricName 已经是 XML 中定义的真实指标名,例如 V1/MAX_V1/G_V1
* 就不能把它误当成“序列编号 1”合并否则会把普通指标压成数组。
*/
static std::vector<std::pair<std::string, std::string>>
mergeSequences(const std::vector<std::pair<std::string, std::string>>& flat,
const std::unordered_set<std::string>& xmlNames) {
struct SeqGroup { size_t firstPos; std::map<int, std::string> idx2val; };
std::unordered_map<std::string, SeqGroup> groups;
std::vector<bool> isSeq(flat.size(), false);
std::vector<std::string> key2group(flat.size());
for (size_t i = 0; i < flat.size(); ++i) {
const auto& k = flat[i].first;
// ✅ 如果 XML 定义了该指标(例如 V1/MAX_V1/...),就不要当作序列
std::string mname = metricAfterPhase(k);
if (!mname.empty() && xmlNames.count(mname)) {
continue;
}
std::string gid; int idx = 0;
if (parseSequenceKey(k, gid, idx)) {
isSeq[i] = true;
key2group[i] = gid;
auto& g = groups[gid];
if (g.idx2val.empty()) g.firstPos = i;
g.idx2val[idx] = flat[i].second;
}
}
std::unordered_map<std::string, bool> mergeable;
for (auto& kv : groups) {
mergeable[kv.first] = (kv.second.idx2val.size() >= 2);
}
std::vector<std::pair<std::string, std::string>> out;
std::unordered_map<std::string, bool> emitted;
for (size_t i = 0; i < flat.size(); ++i) {
if (isSeq[i] && mergeable[key2group[i]]) {
const std::string& gid = key2group[i];
if (emitted[gid]) continue;
emitted[gid] = true;
const auto& g = groups[gid];
int start = g.idx2val.begin()->first;
int end = g.idx2val.rbegin()->first;
std::ostringstream oss;
for (int x = start; x <= end; ++x) {
if (x != start) oss << ",";
auto it = g.idx2val.find(x);
oss << (it == g.idx2val.end() ? "null" : it->second);
}
std::ostringstream key;
key << gid << "[" << start << "-" << end << "]";
out.push_back({key.str(), oss.str()});
} else {
out.push_back(flat[i]);
}
}
return out;
}
struct ParsedKey {
std::string phase; // A/B/C/T/""
std::string stat; // 95值/平均值/最大值/最小值
std::string baseName; // 去掉 G_/MAX_/MIN_ 后的 name
};
/*
* 从 jsondata key 中提取排序和展示维度。
*
* 输入Value_V_A_MAX_V1、Value_I_T_G_THD 等。
* 输出:
* phase A/B/C/T
* stat 由指标名前缀转换G_=95值MAX_=最大值MIN_=最小值,默认平均值;
* baseName 去掉统计前缀后的基础指标名,用于匹配 XML Value/@name。
*/
static ParsedKey parseForSort(const std::string& key) {
std::string base = key;
auto bpos = base.find('[');
if (bpos != std::string::npos) base = base.substr(0, bpos);
std::vector<std::string> parts;
std::stringstream ss(base);
std::string tok;
while (std::getline(ss, tok, '_')) parts.push_back(tok);
auto pr = phaseRank();
int lastPhaseI = -1;
std::string phase;
for (int i = 0; i < (int)parts.size(); ++i) {
if (pr.find(parts[i]) != pr.end()) {
lastPhaseI = i;
phase = parts[i];
}
}
std::string metric;
if (lastPhaseI != -1 && lastPhaseI + 1 < (int)parts.size()) {
metric = parts[lastPhaseI + 1];
for (int i = lastPhaseI + 2; i < (int)parts.size(); ++i) metric += "_" + parts[i];
} else {
metric = parts.empty() ? "" : parts.back();
}
ParsedKey pk;
pk.phase = phase;
pk.stat = "平均值";
pk.baseName = metric;
auto startsWith = [&](const std::string& s, const std::string& pre) {
return s.size() >= pre.size() && s.compare(0, pre.size(), pre) == 0;
};
if (startsWith(metric, "G_")) { pk.stat = "95值"; pk.baseName = metric.substr(2); }
else if (startsWith(metric, "MAX_")) { pk.stat = "最大值"; pk.baseName = metric.substr(4); }
else if (startsWith(metric, "MIN_")) { pk.stat = "最小值"; pk.baseName = metric.substr(4); }
return pk;
}
static std::string cleanDescSuffix(std::string desc) {
static const std::regex re(R"((平均值|最大值|最小值|95值)$)");
return std::regex_replace(desc, re, "");
}
static int getDisplayWidth(const std::string& s) {
int w = 0;
for (size_t i = 0; i < s.size(); ) {
unsigned char c = static_cast<unsigned char>(s[i]);
if (c < 0x80) { // ASCII
w += 1;
++i;
} else if ((c & 0xE0) == 0xC0 && i + 1 < s.size()) { // 2字节UTF-8
w += 2;
i += 2;
} else if ((c & 0xF0) == 0xE0 && i + 2 < s.size()) { // 3字节UTF-8中文常见
w += 2;
i += 3;
} else if ((c & 0xF8) == 0xF0 && i + 3 < s.size()) { // 4字节UTF-8
w += 2;
i += 4;
} else {
w += 1;
++i;
}
}
return w;
}
static void appendPad(std::ostringstream& oss, const std::string& s, int targetWidth) {
oss << s;
int pad = targetWidth - getDisplayWidth(s);
if (pad < 1) pad = 1; // 至少留 1 个空格,避免粘连
oss << std::string(pad, ' ');
}
static bool startsWithStr(const std::string& s, const std::string& pre) {
return s.size() >= pre.size() && s.compare(0, pre.size(), pre) == 0;
}
// 从类似 Value_V_A_MAX_V[2-50] 中取出区间
static bool parseKeyRange(const std::string& key, int& start, int& end) {
std::smatch m;
std::regex re(R"(\[(\d+)-(\d+)\])");
if (std::regex_search(key, m, re)) {
start = std::stoi(m[1].str());
end = std::stoi(m[2].str());
return true;
}
return false;
}
// 给普通项找 meta找不到时再去匹配序列模板项
static std::string trimRightUnderscore(std::string s) {
while (!s.empty() && s.back() == '_') {
s.pop_back();
}
return s;
}
static std::string rawMetricNameFromKey(const std::string& key) {
return metricAfterPhase(key);
}
static std::string preferredMetaName(const std::string& rawMetric, const ParsedKey& pk) {
if (startsWithStr(rawMetric, "G_")) return pk.baseName;
return rawMetric.empty() ? pk.baseName : rawMetric;
}
static std::string secondaryMetaName(const std::string& rawMetric, const ParsedKey& pk) {
std::string preferred = preferredMetaName(rawMetric, pk);
std::string secondary = (preferred == pk.baseName) ? rawMetric : pk.baseName;
return secondary == preferred ? "" : secondary;
}
static bool isNeutralDesc(const std::string& desc) {
return desc.find("95值") == std::string::npos &&
desc.find("平均值") == std::string::npos &&
desc.find("最大值") == std::string::npos &&
desc.find("最小值") == std::string::npos;
}
static bool statMatchesMeta(const ValueMeta& vm, const ParsedKey& pk) {
return !pk.stat.empty() && vm.desc.find(pk.stat) != std::string::npos;
}
static bool daMatchesPhase(const std::string& daPath, const std::string& phase) {
bool phased = daPath.find("phs*") != std::string::npos;
if (phase == "A" || phase == "B" || phase == "C") return phased;
if (phase == "T") return !phased;
return true;
}
static bool wiringAllowed(const ValueMeta& vm, const std::string& wiring) {
if (wiring.empty() || wiring == "any") return true;
return vm.wiring == wiring || vm.wiring == "common";
}
struct MetaFilter {
std::string topic;
std::string dataType;
};
static bool metaFilterAllowed(const ValueMeta& vm, const MetaFilter& filter) {
if (!filter.topic.empty() && vm.topic != filter.topic) return false;
if (!filter.dataType.empty() && vm.dataType != filter.dataType) return false;
return true;
}
// 多个 ValueMeta 都可能同名时用相位、统计类型、Offset、Tot 等特征打分选最像的那一个。
static int scoreMetaForKey(const ValueMeta& vm, const ParsedKey& pk) {
int score = 0;
if (daMatchesPhase(vm.daPath, pk.phase)) score += 20;
if (!pk.stat.empty() && vm.desc.find(pk.stat) != std::string::npos) score += 8;
if (startsWithStr(pk.stat, "95") && isNeutralDesc(vm.desc)) score += 6;
if (vm.hasOffset) score += 5;
if (pk.phase == "T" && vm.doPath.find("Tot") != std::string::npos) score += 4;
if ((pk.phase == "A" || pk.phase == "B" || pk.phase == "C") && vm.doPath.find("Tot") == std::string::npos) score += 4;
return score;
}
static const ValueMeta* bestMeta(const std::vector<const ValueMeta*>& candidates, const ParsedKey& pk) {
const ValueMeta* best = nullptr;
int bestScore = -1;
bool hasStatMatched = false;
for (const ValueMeta* vm : candidates) {
if (statMatchesMeta(*vm, pk)) {
hasStatMatched = true;
break;
}
}
for (const ValueMeta* vm : candidates) {
if (hasStatMatched && !statMatchesMeta(*vm, pk)) continue;
int score = scoreMetaForKey(*vm, pk);
if (!best || score > bestScore) {
best = vm;
bestScore = score;
}
}
return best;
}
static void collectByName(const std::unordered_map<std::string, std::vector<ValueMeta>>& metas,
const std::string& name,
const std::string& wiring,
const MetaFilter& filter,
std::vector<const ValueMeta*>& out) {
if (name.empty()) return;
auto it = metas.find(name);
if (it == metas.end()) return;
for (const auto& vm : it->second) {
if (wiringAllowed(vm, wiring) && metaFilterAllowed(vm, filter)) out.push_back(&vm);
}
}
static std::vector<const ValueMeta*> collectSequenceCandidates(
const std::unordered_map<std::string, std::vector<ValueMeta>>& metas,
const std::string& wantedName,
const ParsedKey& pk,
const std::string& wiring,
const MetaFilter& filter,
int keyStart,
int keyEnd)
{
std::vector<const ValueMeta*> out;
std::string wanted = trimRightUnderscore(wantedName);
for (const auto& kv : metas) {
for (const ValueMeta& vm : kv.second) {
if (!vm.isSequence) continue;
if (!wiringAllowed(vm, wiring)) continue;
if (!metaFilterAllowed(vm, filter)) continue;
if (vm.seqStart != keyStart || vm.seqEnd != keyEnd) continue;
std::string xmlPrefix = trimRightUnderscore(vm.seqPrefixName);
std::string xmlBase = xmlPrefix;
if (startsWithStr(xmlBase, "G_")) xmlBase = xmlBase.substr(2);
else if (startsWithStr(xmlBase, "MAX_")) xmlBase = xmlBase.substr(4);
else if (startsWithStr(xmlBase, "MIN_")) xmlBase = xmlBase.substr(4);
if (xmlPrefix == wanted || xmlBase == wanted) out.push_back(&vm);
}
}
return out;
}
static const ValueMeta* findMetaForKey(
const std::unordered_map<std::string, std::vector<ValueMeta>>& metas,
const std::string& key,
const ParsedKey& pk,
const std::string& wiring = "any",
const MetaFilter& filter = {})
{
std::string rawMetric = rawMetricNameFromKey(key);
std::string preferred = preferredMetaName(rawMetric, pk);
std::string secondary = secondaryMetaName(rawMetric, pk);
// 第一优先级key 已经是 [start-end] 数组时,匹配 XML 的 %start,end% 序列模板。
int keyStart = 0, keyEnd = 0;
if (parseKeyRange(key, keyStart, keyEnd)) {
std::string seqPreferred = rawMetric.empty() ? preferred : rawMetric;
std::string seqSecondary = (seqPreferred == preferred) ? secondary : preferred;
auto seqCandidates = collectSequenceCandidates(metas, seqPreferred, pk, wiring, filter, keyStart, keyEnd);
if (seqCandidates.empty()) seqCandidates = collectSequenceCandidates(metas, seqSecondary, pk, wiring, filter, keyStart, keyEnd);
if (!seqCandidates.empty()) return bestMeta(seqCandidates, pk);
}
// 第二优先级按指标名精确匹配。G_ 前缀会优先转成 baseName因为 95 值常复用同一 XML 描述。
std::vector<const ValueMeta*> exactCandidates;
collectByName(metas, preferred, wiring, filter, exactCandidates);
if (exactCandidates.empty()) collectByName(metas, secondary, wiring, filter, exactCandidates);
return bestMeta(exactCandidates, pk);
}
static bool parseDaValueOffset(const std::string& daPath, int& offset) {
std::smatch m;
std::regex re(R"(%(-?\d+))");
if (!std::regex_search(daPath, m, re)) return false;
offset = std::stoi(m[1].str());
return true;
}
// 把 XML 中序列 DA如 phs*Har[%-2]$mag$f按“数组基准 + 取值偏移量”改显示成 phs*Har[0-48]$mag$f
static std::string rewriteDaRangeForDisplay(const std::string& daPath,
const ValueMeta& vm,
int keyStart,
int keyEnd) {
if (!vm.isSequence) return daPath;
(void)keyStart;
(void)keyEnd;
int valueOffset = 0;
parseDaValueOffset(daPath, valueOffset);
int dispStart = vm.seqStart + valueOffset;
int dispEnd = vm.seqEnd + valueOffset;
std::smatch m;
std::regex re(R"(\[%(-?\d+)\])");
if (std::regex_search(daPath, m, re)) {
std::ostringstream oss;
oss << "[" << dispStart << "-" << dispEnd << "]";
return std::regex_replace(daPath, re, oss.str(), std::regex_constants::format_first_only);
}
return daPath;
}
// 在当前目录找一个后缀文件(找不到就抛)
static fs::path findFirstByExt(const std::string& ext) {
for (auto& e : fs::directory_iterator(fs::current_path())) {
if (!e.is_regular_file()) continue;
if (e.path().extension() == ext) return e.path();
}
throw std::runtime_error("No file found with extension: " + ext + " in current folder");
}
static fs::path findFirstInputTxt() {
for (auto& e : fs::directory_iterator(fs::current_path())) {
if (!e.is_regular_file()) continue;
if (e.path().extension() != ".txt") continue;
std::string name = e.path().filename().string();
if (name == "final_sorted.txt") continue; // 跳过输出文件
return e.path();
}
throw std::runtime_error("No input .txt found in current folder");
}
struct HeaderItem {
std::string key;
std::string val;
};
static bool isHeaderKey(const std::string& key) {
return key == "DATA_TYPE" ||
key == "Monitor" ||
key == "Value_FLAG" ||
key == "Value_TIME" ||
key == "Value_interval";
}
// 【新增】相别排序A相 -> B相 -> C相 -> T相
static int phaseOrderOf(const std::string& phase)
{
if (phase == "A") return 1;
if (phase == "B") return 2;
if (phase == "C") return 3;
if (phase == "T") return 0;
return 99;
}
struct OutItem {
std::string stat;
std::string category;
int phaseOrder; // 【新增】A/B/C/T 排序
std::string attrSort;
std::string phase;
std::string line;
std::string label;
std::string arrayBase;
std::string valueOffset;
std::string finalOffset;
std::string configText;
std::string finalText;
std::string doPath;
std::string daPath;
std::string wiring = "common"; // star/delta/common
bool hasOffset = false;
int offset = 0;
};
/*
* 一张输出工作表的筛选条件。
*
* topic/dataType/wiring 决定从 XML ValueMeta 中取哪些项;
* showStatBlocks 决定统计类 Sheet 是否按 95值/平均值/最大值/最小值分段显示;
* frontType 用于 GUI 选择统计(cfg_stat_data)或实时(cfg_3s_data)时裁剪 Sheet。
*/
struct SheetSpec {
std::string name;
std::string topic;
std::string dataType;
std::string wiring;
bool showStatBlocks = false;
std::string frontType;
};
struct SheetStats {
std::string sheetName;
int normal = 0;
int abnormal = 0;
int missing = 0;
};
struct OriginRecord {
std::string path;
std::string value;
std::string reportName;
int sourceOrder = 0;
};
struct OriginHeader {
std::string title;
std::string fileName;
int sourceOrder = 0;
std::vector<HeaderItem> fields;
};
struct OriginData {
std::vector<OriginRecord> records;
std::vector<OriginHeader> headers;
std::unordered_map<std::string, std::vector<size_t>> pathToRecords;
std::unordered_set<size_t> used; // matchOrigin 命中后标记,最后可输出“已匹配/未匹配”清单。
};
struct OriginMatch {
std::string path;
std::string value;
std::string reportName;
int sourceOrder = -1;
int rangeStart = std::numeric_limits<int>::min();
int rangeEnd = std::numeric_limits<int>::min();
std::vector<int> valueIndexes;
};
struct ComparePlan {
int originPickStart = std::numeric_limits<int>::min();
int originPickEnd = std::numeric_limits<int>::min();
};
struct JsonDataBucket {
std::vector<HeaderItem> headers;
std::unordered_map<std::string, std::string> valueByKey;
bool hasData = false;
};
struct WorkbookContext {
fs::path xmlPath;
std::unordered_map<std::string, std::vector<ValueMeta>> valueMetas;
std::unordered_set<std::string> xmlNames; // XML 中存在的原始 name用于避免序列误合并。
std::unordered_map<std::string, JsonDataBucket> dataByType; // DATA_TYPE -> jsondata 桶。
OriginData originData;
bool hasOrigin = false;
std::string frontType;
};
static std::string jsonScalarForOutput(const json& v) {
if (v.is_string()) return v.get<std::string>();
return v.dump();
}
static std::string reportNameFromRoot(const json& root, const fs::path& originPath, int sourceOrder) {
if (root.is_object() && root.contains("rpt_id")) return jsonScalarForOutput(root["rpt_id"]);
if (root.is_object() && root.contains("rpt_name")) return jsonScalarForOutput(root["rpt_name"]);
if (root.is_object() && root.contains("report_name")) return jsonScalarForOutput(root["report_name"]);
if (root.is_object() && root.contains("rpt_no")) return "rpt_no=" + jsonScalarForOutput(root["rpt_no"]);
return originPath.filename().u8string();
}
static const std::vector<std::string>& originHeaderKeys() {
static const std::vector<std::string> keys = {
"rpt_no",
"data_time",
"mp_id",
"rpt_id",
"voltage_level",
"v_wiring_type"
};
return keys;
}
static const std::vector<std::string>& originHeaderDisplayKeys() {
static const std::vector<std::string> keys = {
"rpt_no",
"data_time",
"mp_id",
"voltage_level",
"v_wiring_type"
};
return keys;
}
static std::string rootHeaderValue(const json& root, const std::string& key) {
if (!root.is_object()) return "";
auto it = root.find(key);
if (it == root.end()) return "";
return jsonScalarForOutput(*it);
}
static void appendOriginHeader(OriginData& data,
const fs::path& originPath,
int sourceOrder,
const json& root,
const std::string& reportName) {
OriginHeader header;
header.title = reportName.empty() ? originPath.filename().u8string() : reportName;
header.fileName = originPath.filename().u8string();
header.sourceOrder = sourceOrder;
for (const auto& key : originHeaderKeys()) {
header.fields.push_back({key, rootHeaderValue(root, key)});
}
data.headers.push_back(std::move(header));
}
static std::vector<json> parseJsonRoots(const std::string& text) {
std::vector<json> roots;
size_t pos = 0;
while (pos < text.size()) {
while (pos < text.size() && std::isspace(static_cast<unsigned char>(text[pos]))) ++pos;
if (pos >= text.size()) break;
char open = text[pos];
if (open != '{' && open != '[') {
throw std::runtime_error("origin file contains unexpected content outside JSON object");
}
// origin 文件可能是多个 JSON 对象直接拼接;这里用括号深度扫描出每个完整根对象。
bool inString = false;
bool escape = false;
int depth = 0;
size_t end = pos;
for (; end < text.size(); ++end) {
char c = text[end];
if (inString) {
if (escape) escape = false;
else if (c == '\\') escape = true;
else if (c == '"') inString = false;
continue;
}
if (c == '"') {
inString = true;
} else if (c == '{' || c == '[') {
++depth;
} else if (c == '}' || c == ']') {
--depth;
if (depth == 0) {
++end;
break;
}
}
}
if (depth != 0) throw std::runtime_error("origin file contains incomplete JSON object");
roots.push_back(json::parse(text.substr(pos, end - pos)));
pos = end;
}
return roots;
}
static void appendOriginRoot(OriginData& data, const fs::path& originPath, int sourceOrder, const json& root) {
std::string reportName = reportNameFromRoot(root, originPath, sourceOrder);
appendOriginHeader(data, originPath, sourceOrder, root, reportName);
// 常见 origin 结构是 { "mms_json": { "path": value } };如果没有 mms_json则直接把根对象当路径表。
const json* mms = &root;
if (root.is_object() && root.contains("mms_json") && root["mms_json"].is_object()) {
mms = &root["mms_json"];
}
if (!mms->is_object()) {
throw std::runtime_error("origin file does not contain an object mms_json: " + originPath.string());
}
for (auto it = mms->begin(); it != mms->end(); ++it) {
OriginRecord rec;
rec.path = it.key();
rec.value = jsonScalarForOutput(it.value());
rec.reportName = reportName;
rec.sourceOrder = sourceOrder;
size_t idx = data.records.size();
data.records.push_back(rec);
data.pathToRecords[rec.path].push_back(idx);
}
}
static void appendOriginData(OriginData& data, const fs::path& originPath, int sourceOrder) {
std::string raw = ltrimBOM(readAll(originPath));
for (const auto& root : parseJsonRoots(raw)) {
appendOriginRoot(data, originPath, sourceOrder, root);
}
}
static OriginData loadOriginData(const std::vector<fs::path>& originPaths) {
OriginData data;
for (size_t i = 0; i < originPaths.size(); ++i) {
appendOriginData(data, originPaths[i], static_cast<int>(i));
}
return data;
}
static OriginData loadOriginData(const fs::path& originPath) {
std::vector<fs::path> paths{originPath};
return loadOriginData(paths);
}
static const OriginRecord* firstUnusedRecord(OriginData* origin, const std::string& path) {
if (!origin) return nullptr;
auto it = origin->pathToRecords.find(path);
if (it == origin->pathToRecords.end()) return nullptr;
for (size_t idx : it->second) {
if (!origin->used.count(idx)) return &origin->records[idx];
}
if (!it->second.empty()) return &origin->records[it->second.front()];
return nullptr;
}
static void markRecordUsed(OriginData* origin, const OriginRecord* rec) {
if (!origin || !rec) return;
size_t idx = static_cast<size_t>(rec - origin->records.data());
if (idx < origin->records.size()) origin->used.insert(idx);
}
static std::string statInstanceSuffix(const std::string& stat) {
if (stat == "平均值") return "1";
if (stat == "最大值") return "2";
if (stat == "最小值") return "3";
if (stat == "95值") return "4";
return "";
}
/*
* 把统计类型转换到 DO 实例号。
*
* 规则来自原始路径约定:平均/最大/最小/95值分别用 1/2/3/4 替换 DO 头部末尾的 0。
* 例如 xxx0$MX$... + 最大值 -> xxx2$MX$...
*/
static std::string applyStatInstance(const std::string& doPath, const std::string& stat) {
std::string suffix = statInstanceSuffix(stat);
if (suffix.empty()) return doPath;
size_t dollar = doPath.find('$');
std::string head = dollar == std::string::npos ? doPath : doPath.substr(0, dollar);
std::string tail = dollar == std::string::npos ? "" : doPath.substr(dollar);
if (!head.empty() && head.back() == '0') {
head.back() = suffix[0];
return head + tail;
}
return doPath;
}
static std::string phaseTokenForDa(const std::string& phase, const std::string& wiring) {
// 角形接线的 A/B/C 在 DA 中对应线电压 AB/BC/CA星形对应普通相 A/B/C。
if (wiring == "delta") {
if (phase == "A") return "phsAB";
if (phase == "B") return "phsBC";
if (phase == "C") return "phsCA";
}
if (phase == "A") return "phsA";
if (phase == "B") return "phsB";
if (phase == "C") return "phsC";
return "phs";
}
static std::string concreteDaPath(std::string daPath, const std::string& phase, const std::string& wiring) {
size_t pos = 0;
const std::string from = "phs*";
const std::string to = phaseTokenForDa(phase, wiring);
while ((pos = daPath.find(from, pos)) != std::string::npos) {
daPath.replace(pos, from.size(), to);
pos += to.size();
}
return daPath;
}
static std::string makeOriginPath(const std::string& doPath,
const std::string& daPath,
const std::string& stat,
const std::string& phase,
const std::string& wiring) {
if (doPath.empty() || daPath.empty()) return "";
// origin 的 MMS 路径由 DO 与具体相位后的 DA 拼接stat 预留给 DO 实例号转换。
return doPath + "$" + concreteDaPath(daPath, phase, wiring);
}
static std::vector<std::string> makeOriginPathCandidates(const std::string& doPath,
const std::string& daPath,
const std::string& stat,
const std::string& phase,
const std::string& wiring) {
std::vector<std::string> paths;
std::string primary = makeOriginPath(doPath, daPath, stat, phase, wiring);
if (!primary.empty()) paths.push_back(primary);
return paths;
}
static bool parseOriginRangePath(const std::string& path,
std::string& before,
int& start,
int& end,
std::string& after) {
std::smatch m;
static const std::regex re(R"(^(.+)\[(\d+)-(\d+)\](.*)$)");
if (!std::regex_match(path, m, re)) return false;
before = m[1].str();
start = std::stoi(m[2].str());
end = std::stoi(m[3].str());
after = m[4].str();
return start <= end;
}
static bool parseOriginIndexedPath(const std::string& path,
const std::string& before,
const std::string& after,
int& index) {
if (path.size() < before.size() + after.size() + 3) return false;
if (path.compare(0, before.size(), before) != 0) return false;
if (!after.empty() &&
path.compare(path.size() - after.size(), after.size(), after) != 0) {
return false;
}
size_t midStart = before.size();
size_t midEnd = after.empty() ? path.size() : path.size() - after.size();
if (midEnd <= midStart + 2 || path[midStart] != '[' || path[midEnd - 1] != ']') return false;
std::string number = path.substr(midStart + 1, midEnd - midStart - 2);
if (number.empty() || !std::all_of(number.begin(), number.end(), [](unsigned char ch) {
return std::isdigit(ch) != 0;
})) {
return false;
}
index = std::stoi(number);
return true;
}
static OriginMatch matchOrigin(OriginData* origin,
const std::string& doPath,
const std::string& daPath,
const std::string& stat,
const std::string& phase,
const std::string& wiring,
int pickStartOverride = std::numeric_limits<int>::min(),
int pickEndOverride = std::numeric_limits<int>::min()) {
OriginMatch match;
if (!origin) return match;
/*
* 匹配步骤:
* 1. 根据 DO/DA/stat/phase/wiring 生成候选 origin 路径;
* 2. 若候选路径是 [start-end] 序列,按 sourceOrder 分组,选覆盖目标区间最多的报告;
* 3. 将选中报告中的连续索引拼成逗号值,并记录 valueIndexes 用于最终值对齐;
* 4. 普通路径则取第一个未使用记录,避免同一路径在多个输出行重复占用。
*/
for (const std::string& path : makeOriginPathCandidates(doPath, daPath, stat, phase, wiring)) {
if (path.empty()) continue;
std::string before, after;
int start = 0, end = 0;
if (parseOriginRangePath(path, before, start, end, after)) {
int pickStart = pickStartOverride == std::numeric_limits<int>::min() ? start : pickStartOverride;
int pickEnd = pickEndOverride == std::numeric_limits<int>::min() ? end : pickEndOverride;
struct IndexedRecord {
int index = 0;
size_t recordIndex = 0;
};
std::map<int, std::vector<IndexedRecord>> bySource;
for (size_t recIndex = 0; recIndex < origin->records.size(); ++recIndex) {
int indexedPath = 0;
if (!parseOriginIndexedPath(origin->records[recIndex].path, before, after, indexedPath)) continue;
bySource[origin->records[recIndex].sourceOrder].push_back({indexedPath, recIndex});
}
int bestSource = -1;
int bestIntersection = 0;
bool bestHasPickStart = false;
for (const auto& kv : bySource) {
int intersection = 0;
bool hasPickStart = false;
for (const auto& rec : kv.second) {
if (rec.index >= pickStart && rec.index <= pickEnd) ++intersection;
if (rec.index == pickStart) hasPickStart = true;
}
if (intersection <= 0) continue;
if (bestSource < 0 ||
(hasPickStart && !bestHasPickStart) ||
(hasPickStart == bestHasPickStart && intersection > bestIntersection)) {
bestSource = kv.first;
bestIntersection = intersection;
bestHasPickStart = hasPickStart;
}
}
if (bestSource >= 0) {
std::map<int, size_t> indexToRecord;
int displayStart = std::numeric_limits<int>::max();
int displayEnd = std::numeric_limits<int>::min();
for (const auto& rec : bySource[bestSource]) {
if (indexToRecord.count(rec.index)) continue;
indexToRecord[rec.index] = rec.recordIndex;
displayStart = std::min(displayStart, rec.index);
displayEnd = std::max(displayEnd, rec.index);
}
std::ostringstream values;
bool any = false;
std::string reportName;
for (int i = displayStart; i <= displayEnd; ++i) {
if (i != displayStart) values << ",";
match.valueIndexes.push_back(i);
auto it = indexToRecord.find(i);
if (it == indexToRecord.end()) continue;
const OriginRecord& rec = origin->records[it->second];
any = true;
values << rec.value;
if (reportName.empty()) reportName = rec.reportName;
markRecordUsed(origin, &rec);
}
if (any) {
match.path = before + "[" + std::to_string(displayStart) + "-" + std::to_string(displayEnd) + "]" + after;
match.value = values.str();
match.reportName = reportName;
match.sourceOrder = bestSource;
match.rangeStart = displayStart;
match.rangeEnd = displayEnd;
return match;
}
}
continue;
}
const OriginRecord* rec = firstUnusedRecord(origin, path);
if (rec) {
match.path = path;
match.value = rec->value;
match.reportName = rec->reportName;
match.sourceOrder = rec->sourceOrder;
markRecordUsed(origin, rec);
return match;
}
}
return match;
}
static size_t utf8CharLen(unsigned char c) {
if (c < 0x80) return 1;
if ((c & 0xE0) == 0xC0) return 2;
if ((c & 0xF0) == 0xE0) return 3;
if ((c & 0xF8) == 0xF0) return 4;
return 1;
}
static std::vector<std::string> wrapDisplay(const std::string& s, int width) {
std::vector<std::string> lines;
if (s.empty()) {
lines.push_back("");
return lines;
}
std::string current;
int currentWidth = 0;
for (size_t i = 0; i < s.size(); ) {
size_t len = utf8CharLen(static_cast<unsigned char>(s[i]));
if (i + len > s.size()) len = 1;
std::string ch = s.substr(i, len);
int chWidth = getDisplayWidth(ch);
if (!current.empty() && currentWidth + chWidth > width) {
lines.push_back(current);
current.clear();
currentWidth = 0;
}
current += ch;
currentWidth += chWidth;
i += len;
}
if (!current.empty()) lines.push_back(current);
return lines;
}
static std::string padCell(const std::string& s, int width) {
int pad = width - getDisplayWidth(s);
return s + std::string(pad > 0 ? pad : 0, ' ');
}
static std::string arrayBaseForMeta(const ValueMeta* vm);
static std::string valueOffsetForDa(const std::string& daPath);
static std::string finalOffsetForMeta(const ValueMeta* vm);
static std::string makeCompareLine(const OutItem& item, OriginData* origin) {
OriginMatch om = matchOrigin(origin, item.doPath, item.daPath, item.stat, item.phase, item.wiring);
const int NAME_W = 30;
const int CONFIG_W = 62;
const int PATH_W = 50;
const int FINAL_W = 70;
const int ORIGIN_W = 70;
std::vector<std::string> nameLines = wrapDisplay(item.label, NAME_W);
std::vector<std::string> configLines = wrapDisplay(item.configText, CONFIG_W);
std::vector<std::string> pathLines = wrapDisplay(om.path.empty() ? "" : ("\"" + om.path + "\""), PATH_W);
std::vector<std::string> finalLines = wrapDisplay(item.finalText, FINAL_W);
std::vector<std::string> originLines = wrapDisplay(om.value, ORIGIN_W);
size_t rows = std::max({nameLines.size(), configLines.size(), pathLines.size(), finalLines.size(), originLines.size()});
std::ostringstream oss;
for (size_t i = 0; i < rows; ++i) {
if (i) oss << "\n";
oss << "| " << padCell(i < nameLines.size() ? nameLines[i] : "", NAME_W)
<< " | " << padCell(i < configLines.size() ? configLines[i] : "", CONFIG_W)
<< " | " << padCell(i < pathLines.size() ? pathLines[i] : "", PATH_W)
<< " | " << padCell(i < finalLines.size() ? finalLines[i] : "", FINAL_W)
<< " | " << padCell(i < originLines.size() ? originLines[i] : "", ORIGIN_W)
<< " |";
}
return oss.str();
}
static bool makeOutItemForWiring(const std::unordered_map<std::string, std::vector<ValueMeta>>& valueMetas,
const std::string& key,
const std::string& val,
const ParsedKey& pk,
const std::string& category,
const std::string& wiring,
const MetaFilter& filter,
bool requireMeta,
OutItem& out) {
const ValueMeta* vm = findMetaForKey(valueMetas, key, pk, wiring, filter);
if (requireMeta && !vm) return false;
// label 用“去掉统计后缀的 desc + stat”组成例如“电压最大值”。
std::string desc = vm ? vm->desc : "";
std::string descClean = cleanDescSuffix(desc);
std::string attrSort = !descClean.empty() ? descClean : pk.baseName;
std::string label = (attrSort.empty() ? pk.baseName : attrSort) + pk.stat;
std::string doDisp = vm ? vm->doPath : "";
std::string daDisp = vm ? vm->daPath : "";
int keyStart = 0, keyEnd = 0;
if (vm && vm->isSequence && parseKeyRange(key, keyStart, keyEnd)) {
// XML 中 DA 写的是 [%offset] 占位符,表格展示时改成最终可对齐的 [start-end] 范围。
daDisp = rewriteDaRangeForDisplay(daDisp, *vm, keyStart, keyEnd);
}
std::ostringstream oss;
const int DO_W = 32;
const int DA_W = 38;
const int DESC_W = 42;
std::string doStr = "DO=\"" + doDisp + "\"";
std::string daStr = "DA=\"" + daDisp + "\"";
std::string showKey = key;
if (showKey.compare(0, 6, "Value_") == 0) showKey.erase(0, 6);
appendPad(oss, doStr, DO_W);
appendPad(oss, daStr, DA_W);
appendPad(oss, label, DESC_W);
std::string finalText = showKey + " = " + val;
oss << finalText;
out = {
pk.stat,
category,
phaseOrderOf(pk.phase),
attrSort,
pk.phase,
oss.str(),
label,
arrayBaseForMeta(vm),
valueOffsetForDa(vm ? vm->daPath : ""),
finalOffsetForMeta(vm),
doStr + " " + daStr,
finalText,
doDisp,
daDisp,
wiring,
vm && vm->hasOffset,
vm ? vm->offset : 0
};
return true;
}
static std::string replaceKeyRange(const std::string& key, int start, int end);
static std::string arrayBaseForMeta(const ValueMeta* vm) {
if (!vm || !vm->isSequence) return "0";
return std::to_string(vm->seqStart) + "-->" + std::to_string(vm->seqEnd);
}
static std::string valueOffsetForDa(const std::string& daPath) {
std::smatch m;
static const std::regex re(R"(%(-?\d+))");
if (!std::regex_search(daPath, m, re)) return "0";
return std::to_string(std::stoi(m[1].str()));
}
static std::string finalOffsetForMeta(const ValueMeta* vm) {
if (!vm || !vm->hasOffset) return "0";
return std::to_string(vm->offset);
}
static std::string metaNameToKeyName(const ValueMeta& vm) {
std::smatch m;
static const std::regex re(R"(^(.*)%(\d+),(\d+)%$)");
if (std::regex_match(vm.name, m, re)) {
std::string prefix = m[1].str();
if (!prefix.empty() && prefix.back() == '_') prefix.pop_back();
return prefix + "[" + m[2].str() + "-" + m[3].str() + "]";
}
return vm.name;
}
static std::string dataKeyForMeta(const ValueMeta& vm, const std::string& phase) {
std::string key = "Value_" + vm.itemName;
if (!phase.empty()) key += "_" + phase;
key += "_" + metaNameToKeyName(vm);
return key;
}
static std::string lookupNameForMeta(const ValueMeta& vm) {
std::string name = metaNameToKeyName(vm);
if (!vm.hasOffset) return name;
int start = 0, end = 0;
if (!parseKeyRange(name, start, end)) return name;
return replaceKeyRange(name, start + vm.offset, end + vm.offset);
}
static std::string lookupDataKeyForMeta(const ValueMeta& vm, const std::string& phase) {
std::string key = "Value_" + vm.itemName;
if (!phase.empty()) key += "_" + phase;
key += "_" + lookupNameForMeta(vm);
return key;
}
static std::string displayKeyForMeta(const ValueMeta& vm, const std::string& phase) {
std::string key = vm.itemName;
if (!phase.empty()) key += "_" + phase;
key += "_" + metaNameToKeyName(vm);
return key;
}
static std::vector<std::string> phasesForMeta(const ValueMeta& vm, const std::string& sheetWiring) {
if (vm.seqValue == "8") return {"T"};
if (vm.wiring == "star" || vm.wiring == "delta") return {"A", "B", "C"};
if (sheetWiring == "star" || sheetWiring == "delta") return {"A", "B", "C"};
return {""};
}
static std::string categoryForKeyAndItem(const std::string& key, const std::string& itemName) {
if (itemName == "V" || itemName == "I" || itemName == "PQ" || itemName == "F_S" || itemName == "F_L") return itemName;
if (key.find("_V_") != std::string::npos) return "V";
if (key.find("_I_") != std::string::npos) return "I";
if (key.find("PQ_") != std::string::npos || key.find("_PQ_") != std::string::npos) return "PQ";
return itemName.empty() ? "Z" : itemName;
}
static OutItem makeOutItemFromMeta(const ValueMeta& vm,
const std::string& phase,
const std::string& sheetWiring,
const std::unordered_map<std::string, std::string>& valueByKey) {
std::string dataKey = dataKeyForMeta(vm, phase);
// lookupDataKeyForMeta 会把 XML Offset 应用到取数 key 上,使最终值和 XML 定义对齐。
auto it = valueByKey.find(lookupDataKeyForMeta(vm, phase));
std::string val = it == valueByKey.end() ? "" : it->second;
ParsedKey pk = parseForSort(dataKey);
if (pk.phase.empty()) pk.phase = phase;
std::string descClean = cleanDescSuffix(vm.desc);
std::string attrSort = !descClean.empty() ? descClean : metaNameToKeyName(vm);
std::string label = vm.desc.empty() ? (attrSort + pk.stat) : vm.desc;
if (vm.topic == "RTDATA" || vm.dataType == "02" || vm.dataType == "03") {
label = vm.desc.empty() ? attrSort : vm.desc;
}
std::string daDisp = vm.daPath;
int keyStart = 0, keyEnd = 0;
if (vm.isSequence && parseKeyRange(dataKey, keyStart, keyEnd)) {
daDisp = rewriteDaRangeForDisplay(daDisp, vm, keyStart, keyEnd);
}
std::string doStr = "DO=\"" + vm.doPath + "\"";
std::string daStr = "DA=\"" + daDisp + "\"";
std::string displayKey = displayKeyForMeta(vm, phase);
std::string finalText = displayKey + " = " + val;
std::string category = categoryForKeyAndItem(dataKey, vm.itemName);
return {
pk.stat,
category,
phaseOrderOf(pk.phase),
attrSort,
pk.phase,
"",
label,
arrayBaseForMeta(&vm),
valueOffsetForDa(vm.daPath),
finalOffsetForMeta(&vm),
doStr + " " + daStr,
finalText,
vm.doPath,
daDisp,
sheetWiring,
vm.hasOffset,
vm.offset
};
}
static bool alreadyAddedMetaRow(std::unordered_set<std::string>& seen,
const ValueMeta& vm,
const std::string& phase,
const std::string& sheetWiring) {
std::string key = vm.topic + "|" + sheetWiring + "|" + vm.itemName + "|" +
vm.seqValue + "|" + phase + "|" + vm.name + "|" + vm.doPath + "|" + vm.daPath;
if (seen.count(key)) return true;
seen.insert(key);
return false;
}
static bool specAllowsDataType(const SheetSpec& spec, const std::string& dataType) {
std::stringstream ss(spec.dataType);
std::string part;
while (std::getline(ss, part, ',')) {
if (part == dataType) return true;
}
return false;
}
static std::vector<OutItem> buildItemsForSheet(const std::unordered_map<std::string, std::vector<ValueMeta>>& valueMetas,
const SheetSpec& spec,
const std::unordered_map<std::string, std::string>& valueByKey) {
std::vector<OutItem> out;
std::unordered_set<std::string> seen;
// 以 XML 定义为主生成行,即使 jsondata 缺值也保留表格行,后续用红色标出缺失。
for (const auto& kv : valueMetas) {
for (const ValueMeta& vm : kv.second) {
if (vm.topic != spec.topic || !specAllowsDataType(spec, vm.dataType)) continue;
if (!wiringAllowed(vm, spec.wiring)) continue;
if (vm.doPath.empty() && vm.daPath.empty()) continue;
for (const std::string& phase : phasesForMeta(vm, spec.wiring)) {
if (alreadyAddedMetaRow(seen, vm, phase, spec.wiring)) continue;
out.push_back(makeOutItemFromMeta(vm, phase, spec.wiring, valueByKey));
}
}
}
return out;
}
static std::string htmlEscape(const std::string& s) {
std::string out;
out.reserve(s.size());
for (char c : s) {
if (c == '&') out += "&amp;";
else if (c == '<') out += "&lt;";
else if (c == '>') out += "&gt;";
else if (c == '"') out += "&quot;";
else out += c;
}
return out;
}
static std::vector<std::string> splitCommaValues(const std::string& s) {
std::vector<std::string> values;
std::stringstream ss(s);
std::string item;
while (std::getline(ss, item, ',')) values.push_back(item);
if (values.empty()) values.push_back("");
return values;
}
static std::string trimValue(std::string value) {
value.erase(value.begin(), std::find_if(value.begin(), value.end(), [](unsigned char ch) {
return std::isspace(ch) == 0;
}));
value.erase(std::find_if(value.rbegin(), value.rend(), [](unsigned char ch) {
return std::isspace(ch) == 0;
}).base(), value.end());
return value;
}
static std::string lowerValue(std::string value) {
std::transform(value.begin(), value.end(), value.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
return value;
}
static bool parseNumberValue(const std::string& text, long double& out) {
std::string s = trimValue(text);
if (s.empty()) return false;
errno = 0;
char* end = nullptr;
const char* begin = s.c_str();
long double value = std::strtold(begin, &end);
if (begin == end) return false;
while (end && *end && std::isspace(static_cast<unsigned char>(*end))) ++end;
if (end && *end) return false;
if (errno == ERANGE) return false;
if (!std::isfinite(static_cast<double>(value))) return false;
out = value;
return true;
}
static std::string fixedSixNumber(long double value) {
std::ostringstream oss;
oss.setf(std::ios::fixed, std::ios::floatfield);
oss << std::setprecision(6) << static_cast<double>(value);
std::string out = oss.str();
if (out == "-0.000000") out = "0.000000";
return out;
}
static std::string roundOriginValueForDisplay(const std::string& value) {
long double number = 0;
if (!parseNumberValue(value, number)) return trimValue(value);
return fixedSixNumber(number);
}
static std::vector<std::string> roundOriginValuesForDisplay(const std::vector<std::string>& values) {
std::vector<std::string> out;
out.reserve(values.size());
for (const auto& value : values) out.push_back(roundOriginValueForDisplay(value));
return out;
}
static std::string compareValueKey(const std::string& value) {
std::string trimmed = trimValue(value);
long double number = 0;
// 数字统一为 6 位小数比较,避免 "1" 与 "1.000000" 被误判不同;非数字则小写比较。
if (parseNumberValue(trimmed, number)) return fixedSixNumber(number);
return lowerValue(trimmed);
}
static bool hasNullValue(const std::vector<std::string>& values) {
for (const auto& value : values) {
if (lowerValue(trimValue(value)) == "null") return true;
}
return false;
}
static bool valuesEqualForCompare(const std::vector<std::string>& finalValues,
const std::vector<std::string>& originValues,
int originStartIndex = 0) {
if (originStartIndex < 0) return false;
if (originValues.size() < static_cast<size_t>(originStartIndex) + finalValues.size()) return false;
for (size_t i = 0; i < finalValues.size(); ++i) {
if (compareValueKey(finalValues[i]) != compareValueKey(originValues[originStartIndex + i])) return false;
}
return true;
}
static int displayIndexForOriginIndex(const std::vector<int>& originIndexes, int originIndex) {
auto it = std::find(originIndexes.begin(), originIndexes.end(), originIndex);
if (it == originIndexes.end()) return -1;
return static_cast<int>(std::distance(originIndexes.begin(), it));
}
static bool valuesEqualForCompareByOriginIndex(const std::vector<std::string>& finalValues,
const std::vector<std::string>& originValues,
const std::vector<int>& originIndexes,
int originPickStart) {
if (originPickStart == std::numeric_limits<int>::min()) {
return valuesEqualForCompare(finalValues, originValues, 0);
}
if (originIndexes.empty()) {
int originStartIndex = originPickStart < 0 ? -1 : originPickStart;
return valuesEqualForCompare(finalValues, originValues, originStartIndex);
}
for (size_t i = 0; i < finalValues.size(); ++i) {
int displayIndex = displayIndexForOriginIndex(originIndexes, originPickStart + static_cast<int>(i));
if (displayIndex < 0 || displayIndex >= static_cast<int>(originValues.size())) return false;
if (compareValueKey(finalValues[i]) != compareValueKey(originValues[displayIndex])) return false;
}
return true;
}
static bool parseArrayBaseRange(const std::string& arrayBase, int& start, int& end) {
std::smatch m;
static const std::regex re(R"(^\s*(-?\d+)\s*-->\s*(-?\d+)\s*$)");
if (!std::regex_match(arrayBase, m, re)) return false;
start = std::stoi(m[1].str());
end = std::stoi(m[2].str());
return start <= end;
}
static int parseOffsetText(const std::string& text, int fallback = 0) {
try {
if (trimValue(text).empty()) return fallback;
return std::stoi(trimValue(text));
} catch (...) {
return fallback;
}
}
static ComparePlan comparePlanForItem(const std::string& finalKey, const OutItem& item) {
ComparePlan plan;
(void)finalKey;
int baseStart = 0;
int baseEnd = 0;
if (!parseArrayBaseRange(item.arrayBase, baseStart, baseEnd)) return plan;
int valueOffset = parseOffsetText(item.valueOffset, 0);
// originPickStart/End 表示原始值中真正要拿来和最终值第 1 列比较的数组下标区间。
plan.originPickStart = baseStart + valueOffset;
plan.originPickEnd = baseEnd + valueOffset;
return plan;
}
static std::string matchStyleForValues(bool hasOriginContext,
bool hasOriginMatch,
const std::vector<std::string>& finalValues,
const std::vector<std::string>& originDisplayValues,
int originStartIndex = 0) {
if (hasNullValue(finalValues)) return "MatchRed";
if (!hasOriginContext) return "";
if (!hasOriginMatch) return "MatchRed";
return valuesEqualForCompare(finalValues, originDisplayValues, originStartIndex) ? "MatchGreen" : "MatchOrange";
}
static void addRowStats(SheetStats* stats, const std::string& rowStyle) {
if (!stats) return;
if (rowStyle == "MatchGreen") ++stats->normal;
else if (rowStyle == "MatchOrange") ++stats->abnormal;
else if (rowStyle == "MatchRed") ++stats->missing;
}
static std::string cellAttrs(const std::string& styleId, const std::string& extra = "") {
std::string attrs;
if (!styleId.empty()) attrs += " ss:StyleID=\"" + styleId + "\"";
attrs += extra;
return attrs;
}
static bool splitFinalText(const std::string& text, std::string& key, std::string& value) {
size_t pos = text.find(" = ");
if (pos == std::string::npos) {
key = text;
value.clear();
return false;
}
key = text.substr(0, pos);
value = text.substr(pos + 3);
return true;
}
static std::string replaceKeyRange(const std::string& key, int start, int end) {
std::regex re(R"(\[(\d+)-(\d+)\])");
std::ostringstream oss;
oss << "[" << start << "-" << end << "]";
return std::regex_replace(key, re, oss.str(), std::regex_constants::format_first_only);
}
static std::string offsetFinalKey(const std::string& key, const OutItem& item) {
if (!item.hasOffset) return key;
int start = 0, end = 0;
if (!parseKeyRange(key, start, end)) return key;
return replaceKeyRange(key, start + item.offset, end + item.offset);
}
static std::string originSeriesKey(const std::string& finalKey, const OutItem& item) {
int start = 0, end = 0;
if (!parseKeyRange(item.daPath, start, end)) return finalKey;
return replaceKeyRange(finalKey, start, end);
}
static std::string originSeriesKeyForMatch(const std::string& finalKey,
const OutItem& item,
const OriginMatch& match) {
if (match.rangeStart != std::numeric_limits<int>::min() &&
match.rangeEnd != std::numeric_limits<int>::min()) {
return replaceKeyRange(finalKey, match.rangeStart, match.rangeEnd);
}
return originSeriesKey(finalKey, item);
}
static void writeSsCell(std::ofstream& ofs, const std::string& value, const std::string& attrs = "") {
ofs << "<Cell" << attrs << "><Data ss:Type=\"String\">" << htmlEscape(value) << "</Data></Cell>";
}
static void writeSsRowStart(std::ofstream& ofs, int height = 0) {
if (height > 0) ofs << "<Row ss:AutoFitHeight=\"0\" ss:Height=\"" << height << "\">";
else ofs << "<Row>";
}
static std::string displayLabelForSheet(const OutItem& item) {
if (item.phase == "A" || item.phase == "B" || item.phase == "C") {
return item.phase + "" + item.label;
}
return item.label;
}
static std::string originHeaderFieldValue(const OriginHeader& header, const std::string& key) {
for (const auto& field : header.fields) {
if (field.key == key) return field.val;
}
return "";
}
static std::vector<OriginHeader> visibleOriginHeadersForSheet(OriginData* origin,
const std::vector<OutItem>& items,
const std::string& wiring) {
std::vector<OriginHeader> visible;
if (!origin) return visible;
OriginData probe = *origin;
std::set<int> usedOrders;
for (const auto& item : items) {
if (item.wiring != wiring) continue;
OriginMatch om = matchOrigin(&probe, item.doPath, item.daPath, item.stat, item.phase, item.wiring);
if (!om.value.empty() && om.sourceOrder >= 0) usedOrders.insert(om.sourceOrder);
}
for (const auto& header : origin->headers) {
if (usedOrders.count(header.sourceOrder)) visible.push_back(header);
}
return visible;
}
static void writeDataWorksheet(std::ofstream& ofs,
const std::string& sheetName,
const std::string& wiring,
const std::vector<HeaderItem>& headers,
const std::vector<std::string>& blocks,
const std::vector<OutItem>& items,
OriginData* origin,
bool showStatBlocks,
SheetStats* stats) {
const int maxValues = 64;
std::vector<OriginHeader> visibleOriginHeaders = visibleOriginHeadersForSheet(origin, items, wiring);
const int totalCols = std::max(9 + maxValues, 3 + static_cast<int>(visibleOriginHeaders.size()));
/*
* 每个数据项写两行:
* 第一行“最终值”:来自 jsondata按逗号拆到 值1..值64
* 第二行“原始值”:来自 origin按同一比较区间对齐展示。
* 行颜色:
* MatchGreen 最终值与原始值一致;
* MatchOrange 找到原始值但内容不一致;
* MatchRed 最终值为 null 或没有匹配到原始值;
* OriginPickBlue 标出序列比较时真正对齐起点。
*/
ofs << "<Worksheet ss:Name=\"" << htmlEscape(sheetName) << "\"><Table>\n";
ofs << "<Column ss:Width=\"210\"/><Column ss:Width=\"75\"/><Column ss:Width=\"85\"/><Column ss:Width=\"85\"/>";
ofs << "<Column ss:Width=\"310\"/><Column ss:Width=\"120\"/><Column ss:Width=\"280\"/>";
ofs << "<Column ss:Width=\"55\"/><Column ss:Width=\"150\"/>";
for (int i = 0; i < maxValues; ++i) ofs << "<Column ss:Width=\"90\"/>";
ofs << "\n";
writeSsRowStart(ofs);
writeSsCell(ofs, "头部信息", " ss:StyleID=\"Section\" ss:MergeAcross=\"" + std::to_string(totalCols - 1) + "\"");
ofs << "</Row>\n";
size_t originHeaderRows = visibleOriginHeaders.empty() ? 0 : originHeaderDisplayKeys().size() + 1;
size_t headerRows = std::max(headers.size(), originHeaderRows);
for (size_t row = 0; row < headerRows; ++row) {
writeSsRowStart(ofs);
if (row < headers.size()) {
writeSsCell(ofs, headers[row].key, " ss:StyleID=\"Default\"");
writeSsCell(ofs, headers[row].val, " ss:StyleID=\"Default\"");
} else {
writeSsCell(ofs, "", " ss:StyleID=\"Default\"");
writeSsCell(ofs, "", " ss:StyleID=\"Default\"");
}
if (!visibleOriginHeaders.empty()) {
if (row == 0) {
writeSsCell(ofs, "原始报告", " ss:StyleID=\"Section\"");
for (const auto& oh : visibleOriginHeaders) {
writeSsCell(ofs, oh.fileName.empty() ? oh.title : oh.fileName, " ss:StyleID=\"Section\"");
}
} else {
size_t fieldIndex = row - 1;
const auto& displayKeys = originHeaderDisplayKeys();
std::string key = fieldIndex < displayKeys.size() ? displayKeys[fieldIndex] : "";
writeSsCell(ofs, key, " ss:StyleID=\"Default\"");
for (const auto& oh : visibleOriginHeaders) {
writeSsCell(ofs, key.empty() ? "" : originHeaderFieldValue(oh, key), " ss:StyleID=\"Default\"");
}
}
}
ofs << "</Row>\n";
}
writeSsRowStart(ofs);
writeSsCell(ofs, "名称", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "数组基准", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "取值偏移量", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "最终值偏移", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "配置项", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "报告名", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "原始路径", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "类型", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "字段", " ss:StyleID=\"Head\"");
for (int i = 1; i <= maxValues; ++i) writeSsCell(ofs, "" + std::to_string(i), " ss:StyleID=\"Head\"");
ofs << "</Row>\n";
auto writeItems = [&](const std::string& statFilter) {
std::string lastCategory;
std::string lastPhase;
for (const auto& item : items) {
if (!statFilter.empty() && item.stat != statFilter) continue;
if (item.wiring != wiring) continue;
if (item.category != lastCategory) {
writeSsRowStart(ofs);
writeSsCell(ofs, item.category, " ss:StyleID=\"Sub\" ss:MergeAcross=\"" + std::to_string(totalCols - 1) + "\"");
ofs << "</Row>\n";
lastCategory.clear();
lastPhase.clear();
}
if (item.phase != lastPhase) {
writeSsRowStart(ofs);
writeSsCell(ofs, item.phase + "", " ss:StyleID=\"Sub\" ss:MergeAcross=\"" + std::to_string(totalCols - 1) + "\"");
ofs << "</Row>\n";
}
std::string finalKey, finalValue;
splitFinalText(item.finalText, finalKey, finalValue);
finalKey = offsetFinalKey(finalKey, item);
ComparePlan comparePlan = comparePlanForItem(finalKey, item);
OriginMatch om = matchOrigin(origin, item.doPath, item.daPath, item.stat, item.phase, item.wiring,
comparePlan.originPickStart, comparePlan.originPickEnd);
std::vector<std::string> finalValues = splitCommaValues(finalValue);
std::vector<std::string> originValuesRaw = splitCommaValues(om.value);
std::vector<std::string> originValues = roundOriginValuesForDisplay(originValuesRaw);
bool hasOriginMatch = origin && !om.value.empty();
int finalRangeStart = 0;
int finalRangeEnd = 0;
bool isSequenceRow = parseKeyRange(finalKey, finalRangeStart, finalRangeEnd);
int originStartIndex = -1;
if (comparePlan.originPickStart != std::numeric_limits<int>::min() &&
om.rangeStart != std::numeric_limits<int>::min()) {
originStartIndex = displayIndexForOriginIndex(om.valueIndexes, comparePlan.originPickStart);
}
std::string rowStyle;
if (hasNullValue(finalValues)) rowStyle = "MatchRed";
else if (!origin) rowStyle = "";
else if (!hasOriginMatch) rowStyle = "MatchRed";
else rowStyle = valuesEqualForCompareByOriginIndex(finalValues, originValues, om.valueIndexes,
comparePlan.originPickStart)
? "MatchGreen"
: "MatchOrange";
addRowStats(stats, rowStyle);
std::string originKey = originSeriesKeyForMatch(finalKey, item, om);
writeSsRowStart(ofs);
writeSsCell(ofs, displayLabelForSheet(item), cellAttrs(rowStyle, " ss:MergeDown=\"1\""));
writeSsCell(ofs, item.arrayBase, cellAttrs(rowStyle, " ss:MergeDown=\"1\""));
writeSsCell(ofs, item.valueOffset, cellAttrs(rowStyle, " ss:MergeDown=\"1\""));
writeSsCell(ofs, item.finalOffset, cellAttrs(rowStyle, " ss:MergeDown=\"1\""));
writeSsCell(ofs, item.configText, cellAttrs(rowStyle, " ss:MergeDown=\"1\""));
writeSsCell(ofs, om.reportName, cellAttrs(rowStyle, " ss:MergeDown=\"1\""));
writeSsCell(ofs, om.path.empty() ? "" : om.path, cellAttrs(rowStyle, " ss:MergeDown=\"1\""));
writeSsCell(ofs, "最终值", cellAttrs(rowStyle));
writeSsCell(ofs, finalKey, cellAttrs(rowStyle));
for (int i = 0; i < maxValues; ++i) writeSsCell(ofs, i < (int)finalValues.size() ? finalValues[i] : "", cellAttrs(rowStyle));
ofs << "</Row>\n";
writeSsRowStart(ofs);
writeSsCell(ofs, "原始值", cellAttrs(rowStyle, " ss:Index=\"8\""));
writeSsCell(ofs, om.value.empty() ? "" : originKey, cellAttrs(rowStyle));
for (int i = 0; i < maxValues; ++i) {
std::string attrs = (isSequenceRow && hasOriginMatch && i == originStartIndex)
? " ss:StyleID=\"OriginPickBlue\""
: cellAttrs(rowStyle);
writeSsCell(ofs, i < (int)originValues.size() ? originValues[i] : "", attrs);
}
ofs << "</Row>\n";
lastCategory = item.category;
lastPhase = item.phase;
}
};
if (showStatBlocks) {
for (const auto& stat : blocks) {
writeSsRowStart(ofs);
writeSsCell(ofs, stat, " ss:StyleID=\"Section\" ss:MergeAcross=\"" + std::to_string(totalCols - 1) + "\"");
ofs << "</Row>\n";
writeItems(stat);
}
} else {
writeItems("");
}
ofs << "</Table></Worksheet>\n";
}
static void writeOriginRecordWorksheet(std::ofstream& ofs,
const OriginData& origin,
const std::string& sheetName,
bool matched,
bool roundValues) {
const int maxValues = 50;
ofs << "<Worksheet ss:Name=\"" << htmlEscape(sheetName) << "\"><Table>\n";
ofs << "<Column ss:Width=\"320\"/><Column ss:Width=\"120\"/><Column ss:Width=\"160\"/><Column ss:Width=\"220\"/>";
for (int i = 0; i < maxValues; ++i) ofs << "<Column ss:Width=\"90\"/>";
ofs << "\n";
writeSsRowStart(ofs);
writeSsCell(ofs, "原始路径", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "报告名", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "icd中代表的字段", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "原始值", " ss:StyleID=\"Head\"");
for (int i = 1; i <= maxValues; ++i) writeSsCell(ofs, "" + std::to_string(i), " ss:StyleID=\"Head\"");
ofs << "</Row>\n";
for (size_t i = 0; i < origin.records.size(); ++i) {
bool isUsed = origin.used.count(i) != 0;
if (isUsed != matched) continue;
const auto& rec = origin.records[i];
std::vector<std::string> originValues = splitCommaValues(rec.value);
if (roundValues) originValues = roundOriginValuesForDisplay(originValues);
writeSsRowStart(ofs);
writeSsCell(ofs, rec.path);
writeSsCell(ofs, rec.reportName);
writeSsCell(ofs, "");
writeSsCell(ofs, rec.value);
for (int j = 0; j < maxValues; ++j) writeSsCell(ofs, j < (int)originValues.size() ? originValues[j] : "");
ofs << "</Row>\n";
}
ofs << "</Table></Worksheet>\n";
}
static void writeMatchedWorksheet(std::ofstream& ofs, const OriginData& origin) {
writeOriginRecordWorksheet(ofs, origin, "已匹配的原始数据", true, true);
}
static void writeUnmatchedWorksheet(std::ofstream& ofs, const OriginData& origin) {
writeOriginRecordWorksheet(ofs, origin, "未匹配的原始数据", false, true);
}
static void writeSummaryWorksheet(std::ofstream& ofs, const std::vector<SheetStats>& stats) {
ofs << "<Worksheet ss:Name=\"统计信息\"><Table>\n";
ofs << "<Column ss:Width=\"180\"/><Column ss:Width=\"110\"/><Column ss:Width=\"110\"/><Column ss:Width=\"110\"/><Column ss:Width=\"90\"/>\n";
writeSsRowStart(ofs);
writeSsCell(ofs, "工作表", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "数据正常个数", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "数据异常个数", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "数据缺失个数", " ss:StyleID=\"Head\"");
writeSsCell(ofs, "总数", " ss:StyleID=\"Head\"");
ofs << "</Row>\n";
int totalNormal = 0;
int totalAbnormal = 0;
int totalMissing = 0;
for (const auto& item : stats) {
int total = item.normal + item.abnormal + item.missing;
totalNormal += item.normal;
totalAbnormal += item.abnormal;
totalMissing += item.missing;
writeSsRowStart(ofs);
writeSsCell(ofs, item.sheetName);
writeSsCell(ofs, std::to_string(item.normal));
writeSsCell(ofs, std::to_string(item.abnormal));
writeSsCell(ofs, std::to_string(item.missing));
writeSsCell(ofs, std::to_string(total));
ofs << "</Row>\n";
}
writeSsRowStart(ofs);
writeSsCell(ofs, "合计", " ss:StyleID=\"Section\"");
writeSsCell(ofs, std::to_string(totalNormal), " ss:StyleID=\"Section\"");
writeSsCell(ofs, std::to_string(totalAbnormal), " ss:StyleID=\"Section\"");
writeSsCell(ofs, std::to_string(totalMissing), " ss:StyleID=\"Section\"");
writeSsCell(ofs, std::to_string(totalNormal + totalAbnormal + totalMissing), " ss:StyleID=\"Section\"");
ofs << "</Row>\n";
ofs << "</Table></Worksheet>\n";
}
static std::vector<HeaderItem> headersForSheet(const std::vector<HeaderItem>& headers, const std::string& dataType) {
std::vector<HeaderItem> out = headers;
std::string displayDataType = dataType;
std::replace(displayDataType.begin(), displayDataType.end(), ',', '/');
bool found = false;
for (auto& h : out) {
if (h.key == "DATA_TYPE") {
h.val = displayDataType;
found = true;
break;
}
}
if (!found) out.insert(out.begin(), {"DATA_TYPE", displayDataType});
return out;
}
static bool specAllowsFrontType(const SheetSpec& spec, const std::string& frontType) {
return frontType.empty() || spec.frontType.empty() || spec.frontType == frontType;
}
static void sortOutItems(std::vector<OutItem>& items) {
std::unordered_map<std::string, int> statRank{
{"95值", 0}, {"平均值", 1}, {"最大值", 2}, {"最小值", 3}
};
std::sort(items.begin(), items.end(), [&](const OutItem& a, const OutItem& b) {
int sa = statRank.count(a.stat) ? statRank[a.stat] : 99;
int sb = statRank.count(b.stat) ? statRank[b.stat] : 99;
if (sa != sb) return sa < sb;
static std::map<std::string,int> catRank = {
{"V", 0},
{"I", 1},
{"PQ", 2},
{"F_S", 3},
{"F_L", 4},
{"Z", 99}
};
int ca = catRank.count(a.category) ? catRank[a.category] : 90;
int cb = catRank.count(b.category) ? catRank[b.category] : 90;
if (ca != cb) return ca < cb;
if (a.phaseOrder != b.phaseOrder) return a.phaseOrder < b.phaseOrder;
if (a.attrSort != b.attrSort) return a.attrSort < b.attrSort;
return a.configText < b.configText;
});
}
static void writeExcelWorkbook(const fs::path& outPath,
const std::vector<std::string>& blocks,
const std::unordered_map<std::string, std::vector<ValueMeta>>& valueMetas,
const std::unordered_map<std::string, JsonDataBucket>& dataByType,
OriginData* origin,
const std::string& frontType) {
std::ofstream ofs(outPath, std::ios::binary);
if (!ofs) throw std::runtime_error("Failed to open output file: " + outPath.string());
ofs << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
ofs << "<?mso-application progid=\"Excel.Sheet\"?>\n";
ofs << "<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\" "
<< "xmlns:o=\"urn:schemas-microsoft-com:office:office\" "
<< "xmlns:x=\"urn:schemas-microsoft-com:office:excel\" "
<< "xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\">\n";
ofs << "<Styles>\n";
ofs << "<Style ss:ID=\"Default\" ss:Name=\"Normal\"><Alignment ss:Horizontal=\"Left\" ss:Vertical=\"Center\"/><Borders><Border ss:Position=\"Bottom\" ss:LineStyle=\"Continuous\" ss:Weight=\"1\"/><Border ss:Position=\"Left\" ss:LineStyle=\"Continuous\" ss:Weight=\"1\"/><Border ss:Position=\"Right\" ss:LineStyle=\"Continuous\" ss:Weight=\"1\"/><Border ss:Position=\"Top\" ss:LineStyle=\"Continuous\" ss:Weight=\"1\"/></Borders><Font ss:FontName=\"SimSun\" ss:Size=\"11\"/></Style>\n";
ofs << "<Style ss:ID=\"Head\" ss:Parent=\"Default\"><Font ss:Bold=\"1\"/><Interior ss:Color=\"#D9EAD3\" ss:Pattern=\"Solid\"/></Style>\n";
ofs << "<Style ss:ID=\"Section\" ss:Parent=\"Default\"><Font ss:Bold=\"1\"/><Interior ss:Color=\"#E8EEF7\" ss:Pattern=\"Solid\"/></Style>\n";
ofs << "<Style ss:ID=\"Sub\" ss:Parent=\"Default\"><Font ss:Bold=\"1\"/><Interior ss:Color=\"#F4F4F4\" ss:Pattern=\"Solid\"/></Style>\n";
ofs << "<Style ss:ID=\"MatchGreen\" ss:Parent=\"Default\"><Interior ss:Color=\"#D9EAD3\" ss:Pattern=\"Solid\"/></Style>\n";
ofs << "<Style ss:ID=\"MatchOrange\" ss:Parent=\"Default\"><Interior ss:Color=\"#FCE4D6\" ss:Pattern=\"Solid\"/></Style>\n";
ofs << "<Style ss:ID=\"MatchRed\" ss:Parent=\"Default\"><Interior ss:Color=\"#F4CCCC\" ss:Pattern=\"Solid\"/></Style>\n";
ofs << "<Style ss:ID=\"OriginPickBlue\" ss:Parent=\"Default\"><Interior ss:Color=\"#CFE2F3\" ss:Pattern=\"Solid\"/></Style>\n";
ofs << "</Styles>\n";
// SheetSpec 是报表目录统计数据和实时数据各自按接线方式、DATA_TYPE 分 Sheet。
std::vector<SheetSpec> sheets = {
{"星形接线", "HISDATA", "01", "star", true, "cfg_stat_data"},
{"角型接线", "HISDATA", "01", "delta", true, "cfg_stat_data"},
{"星型闪变02", "HISDATA", "02", "star", false, "cfg_stat_data"},
{"角形闪变02", "HISDATA", "02", "delta", false, "cfg_stat_data"},
{"星型闪变03", "HISDATA", "03", "star", false, "cfg_stat_data"},
{"角形闪变03", "HISDATA", "03", "delta", false, "cfg_stat_data"},
{"星型实时", "RTDATA", "01", "star", false, "cfg_3s_data"},
{"角形实时", "RTDATA", "01", "delta", false, "cfg_3s_data"},
{"星型实时闪变02", "RTDATA", "02", "star", false, "cfg_3s_data"},
{"角形实时闪变02", "RTDATA", "02", "delta", false, "cfg_3s_data"},
{"星型实时闪变03", "RTDATA", "03", "star", false, "cfg_3s_data"},
{"角形实时闪变03", "RTDATA", "03", "delta", false, "cfg_3s_data"}
};
const std::vector<HeaderItem> emptyHeaders;
const std::unordered_map<std::string, std::string> emptyValues;
std::vector<SheetStats> summaryStats;
for (const auto& spec : sheets) {
if (!specAllowsFrontType(spec, frontType)) continue;
auto bucketIt = dataByType.find(spec.dataType);
const std::vector<HeaderItem>& headers = bucketIt == dataByType.end() ? emptyHeaders : bucketIt->second.headers;
const std::unordered_map<std::string, std::string>& valueByKey =
bucketIt == dataByType.end() ? emptyValues : bucketIt->second.valueByKey;
std::vector<OutItem> sheetItems = buildItemsForSheet(valueMetas, spec, valueByKey);
sortOutItems(sheetItems);
SheetStats stats;
stats.sheetName = spec.name;
writeDataWorksheet(ofs, spec.name, spec.wiring, headersForSheet(headers, spec.dataType), blocks, sheetItems, origin, spec.showStatBlocks, &stats);
summaryStats.push_back(stats);
}
writeSummaryWorksheet(ofs, summaryStats);
OriginData emptyOrigin;
const OriginData& originForRawSheets = origin ? *origin : emptyOrigin;
writeMatchedWorksheet(ofs, originForRawSheets);
writeUnmatchedWorksheet(ofs, originForRawSheets);
ofs << "</Workbook>\n";
}
static const std::vector<std::string>& statBlocks() {
static const std::vector<std::string> blocks = {"95值", "平均值", "最大值", "最小值"};
return blocks;
}
static void writeWorkbookStage(WorkbookContext& ctx, const fs::path& outPath) {
// 每次重写工作簿前清空 used让匹配过程从头计算避免多次刷新后状态累积。
if (ctx.hasOrigin) ctx.originData.used.clear();
OriginData* origin = ctx.hasOrigin ? &ctx.originData : nullptr;
writeExcelWorkbook(outPath, statBlocks(), ctx.valueMetas, ctx.dataByType, origin, ctx.frontType);
}
static WorkbookContext createWorkbookTemplateFromXml(const fs::path& xmlPath, const fs::path& outPath, const std::string& frontType = "") {
WorkbookContext ctx;
ctx.xmlPath = xmlPath;
ctx.frontType = frontType;
ctx.valueMetas = loadValueMetas(xmlPath);
ctx.xmlNames.reserve(ctx.valueMetas.size() * 2);
// xmlNames 只保存 XML 原名,用于 mergeSequences 防止 V1/G_V1 被误合并。
for (const auto& kv : ctx.valueMetas) ctx.xmlNames.insert(kv.first);
writeWorkbookStage(ctx, outPath);
return ctx;
}
static void collectDataTypedJsonRoots(const json& node, std::vector<const json*>& roots) {
if (node.is_object()) {
if (node.contains("DATA_TYPE")) {
roots.push_back(&node);
return;
}
for (auto it = node.begin(); it != node.end(); ++it) collectDataTypedJsonRoots(it.value(), roots);
} else if (node.is_array()) {
for (const auto& item : node) collectDataTypedJsonRoots(item, roots);
}
}
static std::string normalizeDataType(std::string dataType) {
dataType.erase(std::remove_if(dataType.begin(), dataType.end(),
[](unsigned char c) { return std::isspace(c) != 0; }),
dataType.end());
if (dataType.size() == 1 && std::isdigit(static_cast<unsigned char>(dataType[0]))) {
dataType = "0" + dataType;
}
return dataType.empty() ? "01" : dataType;
}
static std::string dataTypeFromMerged(const std::vector<std::pair<std::string, std::string>>& merged) {
for (const auto& kv : merged) {
if (kv.first == "DATA_TYPE") return normalizeDataType(kv.second);
}
return "01";
}
static void applyJsonDataToWorkbook(WorkbookContext& ctx, const json& data, const fs::path& outPath) {
std::vector<const json*> roots;
collectDataTypedJsonRoots(data, roots);
if (roots.empty()) roots.push_back(&data);
for (const json* root : roots) {
std::vector<std::pair<std::string, std::string>> flat;
flattenJson(*root, "", flat);
auto merged = mergeSequences(flat, ctx.xmlNames);
std::string dataType = dataTypeFromMerged(merged);
JsonDataBucket& bucket = ctx.dataByType[dataType];
// 同一个 DATA_TYPE 多次出现时保留第一份,避免 MQ 重复消息覆盖已生成结果。
if (bucket.hasData) continue;
bucket.headers.clear();
bucket.valueByKey.clear();
bucket.valueByKey.reserve(merged.size() * 2);
for (const auto& kv : merged) {
bucket.valueByKey[kv.first] = kv.second;
if (isHeaderKey(kv.first)) bucket.headers.push_back({kv.first, kv.second});
}
bucket.hasData = true;
}
writeWorkbookStage(ctx, outPath);
}
static void applyOriginJsonToWorkbook(WorkbookContext& ctx,
const json& originRoot,
const fs::path& originPath,
int sourceOrder,
const fs::path& outPath) {
appendOriginRoot(ctx.originData, originPath, sourceOrder, originRoot);
ctx.hasOrigin = true;
writeWorkbookStage(ctx, outPath);
}
static void addOriginJsonToContext(WorkbookContext& ctx,
const json& originRoot,
const fs::path& originPath,
int sourceOrder) {
appendOriginRoot(ctx.originData, originPath, sourceOrder, originRoot);
ctx.hasOrigin = true;
}
static json readJsonDataFile(const fs::path& txtPath) {
std::string raw = ltrimBOM(readAll(txtPath));
try {
return json::parse(raw);
} catch (...) {
std::vector<json> roots = parseJsonRoots(raw);
if (roots.size() == 1) return roots.front();
return roots;
}
}
static void applyOriginFileToWorkbook(WorkbookContext& ctx,
const fs::path& originPath,
int sourceOrder,
const fs::path& outPath) {
std::string raw = ltrimBOM(readAll(originPath));
for (const auto& root : parseJsonRoots(raw)) {
applyOriginJsonToWorkbook(ctx, root, originPath, sourceOrder, outPath);
}
}
static void addOriginFileToContext(WorkbookContext& ctx,
const fs::path& originPath,
int sourceOrder) {
std::string raw = ltrimBOM(readAll(originPath));
for (const auto& root : parseJsonRoots(raw)) {
addOriginJsonToContext(ctx, root, originPath, sourceOrder);
}
}
bool pqRunParser(const std::string& xmlPath,
const std::string& jsonPath,
const std::vector<std::string>& originPaths,
const std::string& outPath,
std::string* err,
const std::string& frontType) {
std::vector<std::string> jsonPaths;
if (!jsonPath.empty()) jsonPaths.push_back(jsonPath);
return pqRunParser(xmlPath, jsonPaths, originPaths, outPath, err, frontType);
}
bool pqRunParser(const std::string& xmlPath,
const std::vector<std::string>& jsonPaths,
const std::vector<std::string>& originPaths,
const std::string& outPath,
std::string* err,
const std::string& frontType) {
try {
fs::path xmlFile = fs::u8path(xmlPath);
fs::path output = outPath.empty() ? fs::path("final_sorted.xls") : fs::u8path(outPath);
// 解析顺序固定:先 XML 建模板,再填 jsondata最后追加 origin 并重写带对比结果的工作簿。
WorkbookContext ctx = createWorkbookTemplateFromXml(xmlFile, output, frontType);
for (const auto& jsonPath : jsonPaths) {
if (!jsonPath.empty()) applyJsonDataToWorkbook(ctx, readJsonDataFile(fs::u8path(jsonPath)), output);
}
for (size_t i = 0; i < originPaths.size(); ++i) {
if (!originPaths[i].empty()) addOriginFileToContext(ctx, fs::u8path(originPaths[i]), static_cast<int>(i));
}
if (!originPaths.empty()) writeWorkbookStage(ctx, output);
return true;
} catch (const std::exception& ex) {
if (err) *err = ex.what();
return false;
}
}
int main(int argc, char** argv) {
try {
if (argc == 1) {
// 无参数启动 GUI带参数则走下面的旧命令行解析模式。
return runPqGuiApp();
}
fs::path xmlFile, txtFile;
std::vector<fs::path> originFiles;
bool hasOrigin = false;
const fs::path outPath = "final_sorted.xls";
if (argc >= 2) {
// ✅ Windows 中文路径/文件名:用 u8path 解析
xmlFile = fs::u8path(argv[1]);
if (argc >= 3) txtFile = fs::u8path(argv[2]);
if (argc >= 4) {
hasOrigin = true;
for (int i = 3; i < argc; ++i) {
originFiles.push_back(fs::u8path(argv[i]));
}
}
} else {
xmlFile = findFirstByExt(".xml");
}
std::cout << "[INFO] XML: " << xmlFile << "\n";
WorkbookContext ctx = createWorkbookTemplateFromXml(xmlFile, outPath);
std::cout << "[OK] Template generated: " << outPath << "\n";
if (txtFile.empty()) txtFile = findFirstInputTxt();
std::cout << "[INFO] TXT: " << txtFile << "\n";
applyJsonDataToWorkbook(ctx, readJsonDataFile(txtFile), outPath);
std::cout << "[OK] JSON data applied: " << outPath << "\n";
if (hasOrigin) {
std::cout << "[INFO] ORIGIN:";
for (const auto& originFile : originFiles) std::cout << " " << originFile;
std::cout << "\n";
for (size_t i = 0; i < originFiles.size(); ++i) {
addOriginFileToContext(ctx, originFiles[i], static_cast<int>(i));
std::cout << "[OK] Origin loaded: " << originFiles[i] << "\n";
}
writeWorkbookStage(ctx, outPath);
std::cout << "[OK] Origin data applied: " << outPath << "\n";
}
std::cout << "[OK] Generated: final_sorted.xls\n";
return 0;
} catch (const std::exception& ex) {
std::cerr << "[ERR] " << ex.what() << "\n";
std::cerr << "Usage:\n"
<< " pq_tool <LN_D001.xml> [data.txt] [origin1.txt origin2.txt ...]\n"
<< "Or put one .xml and one .txt in current folder and run without args.\n";
return 1;
}
}