feat(resample): 实验性功能新增「重采样」(位于「自由标记」上方)
- Lane::ResampleItem + resample/resampleItems/resamplePanel/resampleScroll, 并在 LaneCarry / SplitLane / PruneLaneSignalItems / RemapAfterSignalInsert|Removal 同步 - 面板:可滚动分组列表(信号色条目栏 + 复选框 + 展开三角),展开后 4 行: 最大/最小数据间隔(只读,勾选时一次性缓存,绘制不扫描采样)+ 重采样间隔/频率(输入) - 输入框联动:改间隔同步频率,改频率同步间隔(输入单位分别为秒 / Hz) - 「构建为新信号」:DataSet::AppendGroupAxis 追加新时间轴 + InsertDerivedSignal, 采样值先按公式/系数/绝对值/镜像/Y偏移变换再等间隔线性插值,时间轴偏移不烘值 - 新信号名 <原信号> (重采样 <自适应单位频率>);Utf8Util::FormatFrequency 为唯一实现 - 非单调轴 / 点太少 / 间隔过大 / 点数超 2000 万 ⇒ 拒绝构建并给出自绘提示 - 顺带加入 FormulaEval(公式编译/求值,供 ApplyDrawTransform 与后续 UI 使用)
This commit is contained in:
1 parent
30e9d65c30
commit
1e1c2432ae
19 files changed
+1979
-56
No files matched your search
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "DataLoader.hpp"
|
||||
#include "DataTable.hpp"
|
||||
#include "FormulaEval.hpp"
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
@@ -230,6 +231,18 @@ namespace jdi
|
||||
float offsetY = 0.0f; /**< Y 轴偏移(叠加到绘制值上),默认 0。 */
|
||||
double offsetTime = 0.0; /**< 时间轴偏移(秒,绘制在 t + offsetTime 处),默认 0。 */
|
||||
|
||||
/**
|
||||
* @brief 「公式映射」表达式(`Y = <formula>`,自变量写作 `f(t)`;空 = 不映射)。
|
||||
*
|
||||
* 结算顺序为 **公式映射 → 系数 → 绝对值 → 垂直镜像 → Y 偏移 → 时间轴偏移**,
|
||||
* 见 `ApplyDrawTransform`。表达式由 `Utilities/FormulaEval` 编译(带缓存),
|
||||
* 非法表达式的取值原样返回采样值(不会让波形消失)。
|
||||
*/
|
||||
std::wstring formula;
|
||||
|
||||
/** @brief 是否启用了「公式映射」。 */
|
||||
bool HasFormula() const { return !formula.empty(); }
|
||||
|
||||
/** @brief 镜像功能的任一分量是否启用(决定「镜像」按钮的激活态)。 */
|
||||
bool Mirrored() const { return mirrorVertical || mirrorHorizontal; }
|
||||
|
||||
@@ -275,6 +288,12 @@ namespace jdi
|
||||
*/
|
||||
inline double ApplyDrawTransform(const SignalStyle& style, double value)
|
||||
{
|
||||
// 「公式映射」最先结算(用户 2026-11-02 明确要求:公式 → 系数 → 偏移)。
|
||||
// ⚠️ 表达式编译结果按样式对象缓存,**不会**逐采样点重新解析。
|
||||
if (!style.formula.empty())
|
||||
{
|
||||
value = EvaluateFormula(style.formula, value);
|
||||
}
|
||||
if (style.gain != 1.0f)
|
||||
{
|
||||
value *= static_cast<double>(style.gain);
|
||||
@@ -325,7 +344,9 @@ namespace jdi
|
||||
*/
|
||||
inline bool DrawTransformOrderPreserving(const SignalStyle& style)
|
||||
{
|
||||
return (style.gain > 0.0f) && !style.absolute && !style.mirrorVertical;
|
||||
// ⚠️ 「公式映射」不保证保序(`x * x` 在负半轴递减),因此一律按非保序处理,
|
||||
// 让包络绘制回退到逐采样点变换(粗粒度分组层只存 min/max,非保序会算错)。
|
||||
return (style.gain > 0.0f) && !style.absolute && !style.mirrorVertical && !style.HasFormula();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -569,6 +590,19 @@ namespace jdi
|
||||
*/
|
||||
bool AddGroup(SignalGroup&& group, int& groupIndex);
|
||||
|
||||
/**
|
||||
* @brief 在某个分组末尾追加一条**新时间轴**(「重采样」派生的信号使用它)。
|
||||
*
|
||||
* 重采样产生的是**新的时间刻度**,无法复用源信号的时间轴;追加后该分组的
|
||||
* `axes` 多一条,返回它的下标,供随后 `InsertDerivedSignal` 的 `axisIndex` 使用。
|
||||
*
|
||||
* @param [IN] groupIndex 目标分组下标。
|
||||
* @param [IN,OUT] axis 时间轴(内部缓冲会被移走)。
|
||||
* @param [OUT] axisIndex 新时间轴在分组内的下标。
|
||||
* @return 追加成功返回 true(轴为空/分组越界时返回 false)。
|
||||
*/
|
||||
bool AppendGroupAxis(int groupIndex, TimeAxis&& axis, std::size_t& axisIndex);
|
||||
|
||||
/**
|
||||
* @brief 在指定分组的指定信号**下方**插入一条新信号(派生信号)。
|
||||
*
|
||||
@@ -831,6 +865,33 @@ namespace jdi
|
||||
double time = 0.0; /**< 显示时刻(秒)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 「重采样」中某个信号自己的参数。
|
||||
*
|
||||
* 该功能把**非固定频率**采样的信号规整成固定间隔采样(超分 / 降采样),
|
||||
* 再把结果派生为一条新信号。参数按信号保存,同一网格内不同信号可以用不同间隔。
|
||||
*
|
||||
* ⚠️ `interval` 只在**首次勾选**(`initialized == false`)时初始化为该信号的最小
|
||||
* 数据间隔;之后用户手动输入的间隔不会被重新初始化覆盖。
|
||||
*/
|
||||
struct ResampleItem
|
||||
{
|
||||
SignalRef ref; /**< 目标信号。 */
|
||||
double interval = 0.0; /**< 重采样间隔(秒,> 0)。 */
|
||||
/** @brief `interval` 是否已经按「该信号最小数据间隔」初始化过。 */
|
||||
bool initialized = false;
|
||||
/**
|
||||
* @brief 该信号的**最大 / 最小**数据间隔(相邻有效采样点时间差的极值)。
|
||||
*
|
||||
* ⚠️ 在勾选该信号时由 `PlotView` 一次性算好并缓存(千万级信号逐帧重算会卡死),
|
||||
* 面板绘制只读这两个字段。数据加载后不再变化,因此无需失效。
|
||||
*/
|
||||
double maxInterval = 0.0;
|
||||
double minInterval = 0.0;
|
||||
/** @brief 参数行是否被**收起**(只保留信号色叠底的条目栏)。 */
|
||||
bool collapsed = false;
|
||||
};
|
||||
|
||||
std::wstring name; /**< 网格名称。 */
|
||||
std::vector<SignalRef> signals; /**< 该网格包含的信号(按加入顺序)。 */
|
||||
bool autoRange = true; /**< 是否依据信号取值范围自动计算 Y 轴。 */
|
||||
@@ -1102,6 +1163,42 @@ namespace jdi
|
||||
*/
|
||||
float edgeCountScroll = 0.0f;
|
||||
|
||||
/**
|
||||
* @brief 「实验性功能 → 重采样」开关(功能总开关)。
|
||||
*
|
||||
* 开启后本网格出现浮动面板:以网格内每个信号为条目(信号色叠底 + 复选框 +
|
||||
* 展开/收起),展开后显示「最大数据间隔 / 最小数据间隔」两行统计与
|
||||
* 「重采样间隔 / 重采样频率」两行**联动输入框**;底部「构建为新信号」按当前参数
|
||||
* 把勾选的信号重采样成固定频率并插到源信号下方。
|
||||
*
|
||||
* 与其它实验性功能一样属于**网格级**状态,重建网格时按信号继承(只由原网格首个信号持有)。
|
||||
*/
|
||||
bool resample = false;
|
||||
|
||||
/** @brief 参与重采样的信号及其参数(只包含被勾选的信号)。 */
|
||||
std::vector<ResampleItem> resampleItems;
|
||||
|
||||
/** 取某个信号的重采样参数(未勾选时返回空指针)。 */
|
||||
const ResampleItem* FindResampleItem(const SignalRef& ref) const;
|
||||
|
||||
/** 取得或创建某信号的重采样参数(用于勾选)。 */
|
||||
ResampleItem& EnsureResampleItem(const SignalRef& ref);
|
||||
|
||||
/** 取消某信号的重采样勾选。 */
|
||||
void RemoveResampleItem(const SignalRef& ref);
|
||||
|
||||
/** 取某信号的重采样勾选状态。 */
|
||||
bool ResampleEnabled(const SignalRef& ref) const
|
||||
{
|
||||
return FindResampleItem(ref) != nullptr;
|
||||
}
|
||||
|
||||
/** 重采样浮动面板的 UI 状态(展开/折起 + 位置)。 */
|
||||
LanePanelState resamplePanel;
|
||||
|
||||
/** @brief 「重采样」面板内容的滚动偏移(客户区像素,0 = 顶部)。 */
|
||||
float resampleScroll = 0.0f;
|
||||
|
||||
/**
|
||||
* @brief 「自由标记」面板里被**收起**的信号分组(点击分组栏切换)。
|
||||
*
|
||||
|
||||
@@ -87,6 +87,7 @@ namespace jdi
|
||||
prune(lane.edgeCenterItems);
|
||||
prune(lane.weakPeakItems);
|
||||
prune(lane.freeMarkerItems);
|
||||
prune(lane.resampleItems);
|
||||
lane.freeMarkerCollapsedGroups.erase(
|
||||
std::remove_if(lane.freeMarkerCollapsedGroups.begin(), lane.freeMarkerCollapsedGroups.end(),
|
||||
[&inLane](const SignalRef& ref) { return !inLane(ref); }),
|
||||
@@ -140,6 +141,11 @@ namespace jdi
|
||||
bool freeMarkerShowDelta = false;
|
||||
bool freeMarkerShowEnum = false;
|
||||
std::vector<SignalRef> freeMarkerCollapsedGroups;
|
||||
/** @brief 「重采样」开关与逐信号参数(详见 `Lane::resample`)。 */
|
||||
bool resample = false;
|
||||
std::vector<Lane::ResampleItem> resampleItems;
|
||||
LanePanelState resamplePanel;
|
||||
float resampleScroll = 0.0f;
|
||||
/** @brief 「均值滤波」面板内容的滚动偏移(详见 `Lane::meanFilterScroll`)。 */
|
||||
float meanFilterScroll = 0.0f;
|
||||
/** @brief 「中心轴边沿计数」面板内容的滚动偏移(详见 `Lane::edgeCenterScroll`)。 */
|
||||
@@ -199,6 +205,10 @@ namespace jdi
|
||||
carry.freeMarkerShowDelta = lane.freeMarkerShowDelta;
|
||||
carry.freeMarkerShowEnum = lane.freeMarkerShowEnum;
|
||||
carry.freeMarkerCollapsedGroups = lane.freeMarkerCollapsedGroups;
|
||||
carry.resample = lane.resample;
|
||||
carry.resampleItems = lane.resampleItems;
|
||||
carry.resamplePanel = lane.resamplePanel;
|
||||
carry.resampleScroll = lane.resampleScroll;
|
||||
for (std::size_t signalIndex = 0; signalIndex < lane.signals.size(); ++signalIndex)
|
||||
{
|
||||
LaneCarry entry = carry;
|
||||
@@ -271,6 +281,13 @@ namespace jdi
|
||||
lane.freeMarkerShowEnum = carry.freeMarkerShowEnum;
|
||||
lane.freeMarkerCollapsedGroups = carry.freeMarkerCollapsedGroups;
|
||||
lane.meanFilterScroll = carry.meanFilterScroll;
|
||||
lane.resample = carry.resample;
|
||||
lane.resampleItems = carry.resampleItems;
|
||||
lane.resamplePanel.open = carry.resamplePanel.open;
|
||||
lane.resamplePanel.collapsed = carry.resamplePanel.collapsed;
|
||||
lane.resamplePanel.x = carry.resamplePanel.x;
|
||||
lane.resamplePanel.y = carry.resamplePanel.y;
|
||||
lane.resampleScroll = carry.resampleScroll;
|
||||
};
|
||||
|
||||
// 收集全部可见信号引用。
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef __FORMULA_EVAL_HPP__
|
||||
#define __FORMULA_EVAL_HPP__
|
||||
|
||||
// 「公式映射」的表达式编译与求值(信号样式区的 `Y = …` 输入框)。
|
||||
//
|
||||
// 语法(刻意保持极小,避免引入脚本引擎与额外的体积):
|
||||
// expr := term (('+' | '-') term)*
|
||||
// term := unary (('*' | '/') unary)*
|
||||
// unary := ('+' | '-') unary | primary
|
||||
// primary := number | '(' expr ')' | variable
|
||||
// number := 十进制小数(支持 e/E 指数)
|
||||
// variable:= `X` / `x` / `f` / `f(t)` —— 都表示"当前采样点的值"
|
||||
// 另外支持常量 `pi`。
|
||||
//
|
||||
// ⚠️ 求值走 `thread_local` 的单条缓存(按 `std::wstring` 的 data 指针 + 内容判定):
|
||||
// 绘制循环里同一条信号的样式对象是稳定的,因此每个信号只需编译一次;
|
||||
// **绝不能**每个采样点都重新解析(千万级采样会慢到不可用)。
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
/**
|
||||
* @brief 校验公式文本是否可以解析。
|
||||
*
|
||||
* 空文本是合法输入(表示"不做映射")。
|
||||
*
|
||||
* @param [IN] text 公式文本(不含 `Y =` 前缀)。
|
||||
* @param [OUT] error 解析失败时的原因(成功时清空),允许为空。
|
||||
* @return 语法合法返回 true。
|
||||
*/
|
||||
bool ValidateFormula(const std::wstring& text, std::wstring* error);
|
||||
|
||||
/**
|
||||
* @brief 计算公式在给定采样值处的取值。
|
||||
*
|
||||
* 文本为空或无法解析时**原样返回** `x`(保证绘制不会因为非法公式而消失)。
|
||||
* 计算结果非有限(如 1/0)时返回 NaN(保持断线语义)。
|
||||
*
|
||||
* @param [IN] text 公式文本。
|
||||
* @param [IN] x 采样值(公式中的 `f(t)` / `X`)。
|
||||
* @return 映射后的值。
|
||||
*/
|
||||
double EvaluateFormula(const std::wstring& text, double x);
|
||||
|
||||
/**
|
||||
* @brief 把用户输入里的自变量写法统一成 `f(t)`(保存前调用)。
|
||||
*
|
||||
* 规则:把独立的 `X` / `x` / `f` 记号改写为 `f(t)`;`f(t)` 原样保留。
|
||||
* 只做记号级替换(不会动标识符中间或数字里的字符)。
|
||||
*
|
||||
* @param [IN] text 用户输入的公式。
|
||||
* @return 归一化后的文本。
|
||||
*/
|
||||
std::wstring NormalizeFormulaText(const std::wstring& text);
|
||||
} // namespace jdi
|
||||
|
||||
#endif // __FORMULA_EVAL_HPP__
|
||||
@@ -121,6 +121,16 @@ namespace jdi
|
||||
*/
|
||||
std::wstring FormatDurationSeconds(double seconds);
|
||||
|
||||
/**
|
||||
* @brief 把频率格式化为带自适应单位的文本(Hz / kHz / MHz / GHz,保留小数)。
|
||||
*
|
||||
* 用于「重采样」面板与派生信号名称(`<原信号> (重采样 1.2 kHz)`)。
|
||||
*
|
||||
* @param [IN] hertz 频率(Hz)。
|
||||
* @return 形如 "1.2 kHz" 的文本;非法值返回 "—"。
|
||||
*/
|
||||
std::wstring FormatFrequency(double hertz);
|
||||
|
||||
/**
|
||||
* @brief 计算“美观”的坐标轴刻度步长(1/2/5 乘以 10 的整数次幂)。
|
||||
* @param [IN] range 坐标轴数值跨度,必须为正数。
|
||||
|
||||
@@ -421,6 +421,23 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DataSet::AppendGroupAxis(int groupIndex, TimeAxis&& axis, std::size_t& axisIndex)
|
||||
{
|
||||
axisIndex = 0;
|
||||
if (groupIndex < 0 || groupIndex >= static_cast<int>(m_groups.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (axis.time.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
SignalGroup& group = m_groups[static_cast<std::size_t>(groupIndex)];
|
||||
group.axes.push_back(std::move(axis));
|
||||
axisIndex = group.axes.size() - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DataSet::InsertDerivedSignal(int groupIndex, int afterSignalIndex, SignalData&& signal,
|
||||
int& newSignalIndex)
|
||||
{
|
||||
@@ -1396,6 +1413,43 @@ namespace jdi
|
||||
weakPeakItems.end());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 重采样
|
||||
|
||||
const Lane::ResampleItem* Lane::FindResampleItem(const SignalRef& ref) const
|
||||
{
|
||||
for (const ResampleItem& item : resampleItems)
|
||||
{
|
||||
if (item.ref == ref)
|
||||
{
|
||||
return &item;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Lane::ResampleItem& Lane::EnsureResampleItem(const SignalRef& ref)
|
||||
{
|
||||
for (ResampleItem& item : resampleItems)
|
||||
{
|
||||
if (item.ref == ref)
|
||||
{
|
||||
return item;
|
||||
}
|
||||
}
|
||||
ResampleItem item;
|
||||
item.ref = ref;
|
||||
resampleItems.push_back(item);
|
||||
return resampleItems.back();
|
||||
}
|
||||
|
||||
void Lane::RemoveResampleItem(const SignalRef& ref)
|
||||
{
|
||||
resampleItems.erase(
|
||||
std::remove_if(resampleItems.begin(), resampleItems.end(),
|
||||
[&ref](const ResampleItem& item) { return item.ref == ref; }),
|
||||
resampleItems.end());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 自由标记
|
||||
|
||||
namespace
|
||||
|
||||
@@ -145,6 +145,7 @@ namespace jdi
|
||||
purgeRefs(lane.meanFilterItems);
|
||||
purgeRefs(lane.edgeCenterItems);
|
||||
purgeRefs(lane.weakPeakItems);
|
||||
purgeRefs(lane.resampleItems);
|
||||
}
|
||||
// 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。
|
||||
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
|
||||
@@ -449,6 +450,9 @@ namespace jdi
|
||||
lane.freeMarkerShowEnum = source.freeMarkerShowEnum;
|
||||
lane.freeMarkerPanel = source.freeMarkerPanel;
|
||||
lane.freeMarkerOpacity = source.freeMarkerOpacity;
|
||||
lane.resample = source.resample;
|
||||
lane.resamplePanel = source.resamplePanel;
|
||||
lane.resampleScroll = source.resampleScroll;
|
||||
// 拆分后每个子网格只保留自己那个信号的均值滤波 / 中心轴 / 弱峰调整 / 自由标记条目。
|
||||
for (const Lane::MeanFilterItem& item : source.meanFilterItems)
|
||||
{
|
||||
@@ -487,6 +491,13 @@ namespace jdi
|
||||
lane.freeMarkerItems.push_back(item);
|
||||
}
|
||||
}
|
||||
for (const Lane::ResampleItem& item : source.resampleItems)
|
||||
{
|
||||
if (item.ref == ref)
|
||||
{
|
||||
lane.resampleItems.push_back(item);
|
||||
}
|
||||
}
|
||||
for (const SignalRef& collapsed : source.freeMarkerCollapsedGroups)
|
||||
{
|
||||
if (collapsed == ref)
|
||||
@@ -557,6 +568,14 @@ namespace jdi
|
||||
++item.ref.signalIndex;
|
||||
}
|
||||
}
|
||||
// 「重采样」条目同样持有信号引用。
|
||||
for (Lane::ResampleItem& item : lane.resampleItems)
|
||||
{
|
||||
if (item.ref.groupIndex == groupIndex && item.ref.signalIndex >= insertPos)
|
||||
{
|
||||
++item.ref.signalIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,7 +710,36 @@ namespace jdi
|
||||
double signalHigh = signal->maxValue;
|
||||
if (style != nullptr)
|
||||
{
|
||||
TransformValueRange(*style, signalLow, signalHigh);
|
||||
if (style->HasFormula())
|
||||
{
|
||||
// ⚠️ 「公式映射」不保证单调(`x*x` / `1/x`)⇒ 端点映射会算错,
|
||||
// 只能逐点扫描(只在自适应重算时发生,不是每帧路径)。
|
||||
double formulaLow = std::numeric_limits<double>::infinity();
|
||||
double formulaHigh = -std::numeric_limits<double>::infinity();
|
||||
for (const double raw : signal->values)
|
||||
{
|
||||
if (!std::isfinite(raw))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double mapped = ApplyDrawTransform(*style, raw);
|
||||
if (!std::isfinite(mapped))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
formulaLow = std::min(formulaLow, mapped);
|
||||
formulaHigh = std::max(formulaHigh, mapped);
|
||||
}
|
||||
if (std::isfinite(formulaLow) && std::isfinite(formulaHigh))
|
||||
{
|
||||
signalLow = formulaLow;
|
||||
signalHigh = formulaHigh;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TransformValueRange(*style, signalLow, signalHigh);
|
||||
}
|
||||
}
|
||||
low = std::min(low, signalLow);
|
||||
high = std::max(high, signalHigh);
|
||||
|
||||
@@ -0,0 +1,586 @@
|
||||
// 「公式映射」表达式的编译 / 求值实现(详见 `FormulaEval.hpp`)。
|
||||
|
||||
#include "FormulaEval.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <cwctype>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace jdi
|
||||
{
|
||||
namespace
|
||||
{
|
||||
/** 公式允许的最大长度(防止恶意/误粘贴的超长文本导致解析耗时)。 */
|
||||
constexpr std::size_t kMaxFormulaLength = 512;
|
||||
/** 允许的最大记号数(与长度一起兜底)。 */
|
||||
constexpr std::size_t kMaxFormulaTokens = 512;
|
||||
|
||||
/** 逆波兰式里的一个运算单元。 */
|
||||
struct FormulaOp
|
||||
{
|
||||
enum class Kind : int
|
||||
{
|
||||
NUMBER = 0, /**< 常量。 */
|
||||
VARIABLE, /**< 自变量(采样值)。 */
|
||||
ADD,
|
||||
SUBTRACT,
|
||||
MULTIPLY,
|
||||
DIVIDE,
|
||||
NEGATE
|
||||
};
|
||||
|
||||
Kind kind = Kind::NUMBER;
|
||||
double value = 0.0;
|
||||
};
|
||||
|
||||
/** 单个自变量占用的栈槽(求值时用)。 */
|
||||
constexpr std::size_t kMaxStack = 64;
|
||||
|
||||
/** 记号类型。 */
|
||||
enum class TokenKind : int
|
||||
{
|
||||
NUMBER = 0,
|
||||
VARIABLE,
|
||||
PLUS,
|
||||
MINUS,
|
||||
STAR,
|
||||
SLASH,
|
||||
LEFT_PAREN,
|
||||
RIGHT_PAREN,
|
||||
END
|
||||
};
|
||||
|
||||
struct Token
|
||||
{
|
||||
TokenKind kind = TokenKind::END;
|
||||
double value = 0.0;
|
||||
};
|
||||
|
||||
/** 把一个公式文本切成记号序列(失败返回 false 并写 error)。 */
|
||||
bool Tokenize(const std::wstring& text, std::vector<Token>& tokens, std::wstring* error)
|
||||
{
|
||||
tokens.clear();
|
||||
std::size_t index = 0;
|
||||
const std::size_t length = text.size();
|
||||
if (length > kMaxFormulaLength)
|
||||
{
|
||||
if (error != nullptr)
|
||||
{
|
||||
*error = L"公式过长(最多 512 个字符)。";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
while (index < length)
|
||||
{
|
||||
const wchar_t ch = text[index];
|
||||
if (std::iswspace(static_cast<wint_t>(ch)) != 0)
|
||||
{
|
||||
++index;
|
||||
continue;
|
||||
}
|
||||
if (ch == L'+')
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::PLUS, 0.0});
|
||||
++index;
|
||||
}
|
||||
else if (ch == L'-')
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::MINUS, 0.0});
|
||||
++index;
|
||||
}
|
||||
else if (ch == L'*')
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::STAR, 0.0});
|
||||
++index;
|
||||
}
|
||||
else if (ch == L'/')
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::SLASH, 0.0});
|
||||
++index;
|
||||
}
|
||||
else if (ch == L'(')
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::LEFT_PAREN, 0.0});
|
||||
++index;
|
||||
}
|
||||
else if (ch == L')')
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::RIGHT_PAREN, 0.0});
|
||||
++index;
|
||||
}
|
||||
else if ((ch >= L'0' && ch <= L'9') || ch == L'.')
|
||||
{
|
||||
// 数字:手工扫描,交给 `wcstod` 转换(保证与内联编辑的数值语义一致)。
|
||||
const std::size_t start = index;
|
||||
while (index < length &&
|
||||
((text[index] >= L'0' && text[index] <= L'9') || text[index] == L'.'))
|
||||
{
|
||||
++index;
|
||||
}
|
||||
if (index < length && (text[index] == L'e' || text[index] == L'E'))
|
||||
{
|
||||
++index;
|
||||
if (index < length && (text[index] == L'+' || text[index] == L'-'))
|
||||
{
|
||||
++index;
|
||||
}
|
||||
while (index < length && text[index] >= L'0' && text[index] <= L'9')
|
||||
{
|
||||
++index;
|
||||
}
|
||||
}
|
||||
const std::wstring literal = text.substr(start, index - start);
|
||||
wchar_t* stop = nullptr;
|
||||
const double value = std::wcstod(literal.c_str(), &stop);
|
||||
if (stop == literal.c_str() || !std::isfinite(value))
|
||||
{
|
||||
if (error != nullptr)
|
||||
{
|
||||
*error = L"数字格式不正确。";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
tokens.push_back(Token{TokenKind::NUMBER, value});
|
||||
}
|
||||
else if (std::iswalpha(static_cast<wint_t>(ch)) != 0)
|
||||
{
|
||||
// 标识符:只允许 X / x / f / f(t) / pi。
|
||||
const std::size_t start = index;
|
||||
while (index < length && std::iswalpha(static_cast<wint_t>(text[index])) != 0)
|
||||
{
|
||||
++index;
|
||||
}
|
||||
std::wstring name = text.substr(start, index - start);
|
||||
// 跳过空格后可能是 `(t)`(`f(t)` 的写法)。
|
||||
std::size_t look = index;
|
||||
while (look < length && std::iswspace(static_cast<wint_t>(text[look])) != 0)
|
||||
{
|
||||
++look;
|
||||
}
|
||||
if (look < length && text[look] == L'(')
|
||||
{
|
||||
std::size_t inner = look + 1;
|
||||
while (inner < length && std::iswspace(static_cast<wint_t>(text[inner])) != 0)
|
||||
{
|
||||
++inner;
|
||||
}
|
||||
if (inner < length && (text[inner] == L't' || text[inner] == L'T'))
|
||||
{
|
||||
++inner;
|
||||
while (inner < length && std::iswspace(static_cast<wint_t>(text[inner])) != 0)
|
||||
{
|
||||
++inner;
|
||||
}
|
||||
if (inner < length && text[inner] == L')')
|
||||
{
|
||||
index = inner + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (wchar_t& letter : name)
|
||||
{
|
||||
letter = static_cast<wchar_t>(std::towlower(static_cast<wint_t>(letter)));
|
||||
}
|
||||
if (name == L"x" || name == L"f")
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::VARIABLE, 0.0});
|
||||
}
|
||||
else if (name == L"pi")
|
||||
{
|
||||
tokens.push_back(Token{TokenKind::NUMBER, 3.14159265358979323846});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (error != nullptr)
|
||||
{
|
||||
*error = L"无法识别的标识符(自变量请写 f(t) 或 X)。";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (error != nullptr)
|
||||
{
|
||||
*error = L"公式含有无法识别的字符。";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
tokens.push_back(Token{TokenKind::END, 0.0});
|
||||
if (tokens.size() > kMaxFormulaTokens)
|
||||
{
|
||||
if (error != nullptr)
|
||||
{
|
||||
*error = L"公式过于复杂。";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 递归下降解析器(直接产出逆波兰式)。 */
|
||||
class Parser
|
||||
{
|
||||
public:
|
||||
Parser(const std::vector<Token>& tokens, std::vector<FormulaOp>& output, std::wstring* error)
|
||||
: m_tokens(tokens), m_output(output), m_error(error)
|
||||
{
|
||||
}
|
||||
|
||||
bool Parse()
|
||||
{
|
||||
if (!ParseExpression())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Current().kind != TokenKind::END)
|
||||
{
|
||||
return SetError(L"公式存在多余的内容(请检查括号是否配对)。");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
const Token& Current() const { return m_tokens[m_position]; }
|
||||
void Advance()
|
||||
{
|
||||
if (m_position + 1 < m_tokens.size())
|
||||
{
|
||||
++m_position;
|
||||
}
|
||||
}
|
||||
bool SetError(const wchar_t* message)
|
||||
{
|
||||
if (m_error != nullptr)
|
||||
{
|
||||
*m_error = message;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ParseExpression()
|
||||
{
|
||||
if (!ParseTerm())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
while (Current().kind == TokenKind::PLUS || Current().kind == TokenKind::MINUS)
|
||||
{
|
||||
const bool add = (Current().kind == TokenKind::PLUS);
|
||||
Advance();
|
||||
if (!ParseTerm())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_output.push_back(FormulaOp{add ? FormulaOp::Kind::ADD
|
||||
: FormulaOp::Kind::SUBTRACT,
|
||||
0.0});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseTerm()
|
||||
{
|
||||
if (!ParseUnary())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
while (Current().kind == TokenKind::STAR || Current().kind == TokenKind::SLASH)
|
||||
{
|
||||
const bool multiply = (Current().kind == TokenKind::STAR);
|
||||
Advance();
|
||||
if (!ParseUnary())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_output.push_back(FormulaOp{multiply ? FormulaOp::Kind::MULTIPLY
|
||||
: FormulaOp::Kind::DIVIDE,
|
||||
0.0});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseUnary()
|
||||
{
|
||||
if (Current().kind == TokenKind::PLUS)
|
||||
{
|
||||
Advance();
|
||||
return ParseUnary();
|
||||
}
|
||||
if (Current().kind == TokenKind::MINUS)
|
||||
{
|
||||
Advance();
|
||||
if (!ParseUnary())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
m_output.push_back(FormulaOp{FormulaOp::Kind::NEGATE, 0.0});
|
||||
return true;
|
||||
}
|
||||
return ParsePrimary();
|
||||
}
|
||||
|
||||
bool ParsePrimary()
|
||||
{
|
||||
const Token& token = Current();
|
||||
if (token.kind == TokenKind::NUMBER)
|
||||
{
|
||||
m_output.push_back(FormulaOp{FormulaOp::Kind::NUMBER, token.value});
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
if (token.kind == TokenKind::VARIABLE)
|
||||
{
|
||||
m_output.push_back(FormulaOp{FormulaOp::Kind::VARIABLE, 0.0});
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
if (token.kind == TokenKind::LEFT_PAREN)
|
||||
{
|
||||
Advance();
|
||||
if (!ParseExpression())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Current().kind != TokenKind::RIGHT_PAREN)
|
||||
{
|
||||
return SetError(L"缺少右括号。");
|
||||
}
|
||||
Advance();
|
||||
return true;
|
||||
}
|
||||
return SetError(L"公式不完整或存在多余的运算符。");
|
||||
}
|
||||
|
||||
const std::vector<Token>& m_tokens;
|
||||
std::vector<FormulaOp>& m_output;
|
||||
std::wstring* m_error;
|
||||
std::size_t m_position = 0;
|
||||
};
|
||||
|
||||
/** 编译结果(逆波兰式);空表示"不可用"。 */
|
||||
using Program = std::vector<FormulaOp>;
|
||||
|
||||
/** 线程内的单条编译缓存(按 data 指针 + 内容判定)。 */
|
||||
struct FormulaCache
|
||||
{
|
||||
const wchar_t* data = nullptr;
|
||||
std::size_t size = 0;
|
||||
std::wstring text;
|
||||
Program program;
|
||||
bool compiled = false;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 取(必要时编译)公式对应的程序。
|
||||
* @return 合法时返回程序指针;空文本或非法时返回 nullptr。
|
||||
*/
|
||||
const Program* AcquireProgram(const std::wstring& text)
|
||||
{
|
||||
if (text.empty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
static thread_local FormulaCache cache;
|
||||
if (cache.data == text.data() && cache.size == text.size() && cache.compiled)
|
||||
{
|
||||
return cache.valid ? &cache.program : nullptr;
|
||||
}
|
||||
// 指针变化:先比内容,内容相同则直接复用(避免同一公式被多个样式副本反复编译)。
|
||||
if (cache.compiled && cache.text == text)
|
||||
{
|
||||
cache.data = text.data();
|
||||
cache.size = text.size();
|
||||
return cache.valid ? &cache.program : nullptr;
|
||||
}
|
||||
cache.text = text;
|
||||
cache.data = text.data();
|
||||
cache.size = text.size();
|
||||
cache.program.clear();
|
||||
cache.valid = false;
|
||||
cache.compiled = true;
|
||||
|
||||
std::vector<Token> tokens;
|
||||
std::wstring error;
|
||||
if (!Tokenize(text, tokens, &error))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
Parser parser(tokens, cache.program, &error);
|
||||
if (!parser.Parse())
|
||||
{
|
||||
cache.program.clear();
|
||||
return nullptr;
|
||||
}
|
||||
cache.valid = true;
|
||||
return &cache.program;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool ValidateFormula(const std::wstring& text, std::wstring* error)
|
||||
{
|
||||
if (error != nullptr)
|
||||
{
|
||||
error->clear();
|
||||
}
|
||||
if (text.empty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
std::vector<Token> tokens;
|
||||
if (!Tokenize(text, tokens, error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::vector<FormulaOp> program;
|
||||
Parser parser(tokens, program, error);
|
||||
if (!parser.Parse())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
double EvaluateFormula(const std::wstring& text, double x)
|
||||
{
|
||||
const Program* program = AcquireProgram(text);
|
||||
if (program == nullptr)
|
||||
{
|
||||
return x; // 空 / 非法:不做映射(保证波形照常显示)
|
||||
}
|
||||
double stack[kMaxStack];
|
||||
std::size_t depth = 0;
|
||||
for (const FormulaOp& op : *program)
|
||||
{
|
||||
switch (op.kind)
|
||||
{
|
||||
case FormulaOp::Kind::NUMBER:
|
||||
case FormulaOp::Kind::VARIABLE:
|
||||
if (depth >= kMaxStack)
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
stack[depth++] = (op.kind == FormulaOp::Kind::NUMBER) ? op.value : x;
|
||||
break;
|
||||
case FormulaOp::Kind::NEGATE:
|
||||
if (depth < 1)
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
stack[depth - 1] = -stack[depth - 1];
|
||||
break;
|
||||
default:
|
||||
{
|
||||
if (depth < 2)
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
const double b = stack[depth - 1];
|
||||
const double a = stack[depth - 2];
|
||||
--depth;
|
||||
double result = 0.0;
|
||||
switch (op.kind)
|
||||
{
|
||||
case FormulaOp::Kind::ADD:
|
||||
result = a + b;
|
||||
break;
|
||||
case FormulaOp::Kind::SUBTRACT:
|
||||
result = a - b;
|
||||
break;
|
||||
case FormulaOp::Kind::MULTIPLY:
|
||||
result = a * b;
|
||||
break;
|
||||
case FormulaOp::Kind::DIVIDE:
|
||||
result = a / b;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
stack[depth - 1] = result;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (depth != 1)
|
||||
{
|
||||
return std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
return std::isfinite(stack[0]) ? stack[0] : std::numeric_limits<double>::quiet_NaN();
|
||||
}
|
||||
|
||||
std::wstring NormalizeFormulaText(const std::wstring& text)
|
||||
{
|
||||
std::wstring output;
|
||||
output.reserve(text.size() + 8);
|
||||
const std::size_t length = text.size();
|
||||
std::size_t index = 0;
|
||||
while (index < length)
|
||||
{
|
||||
const wchar_t ch = text[index];
|
||||
if (std::iswalpha(static_cast<wint_t>(ch)) == 0)
|
||||
{
|
||||
output.push_back(ch);
|
||||
++index;
|
||||
continue;
|
||||
}
|
||||
const std::size_t start = index;
|
||||
while (index < length && std::iswalpha(static_cast<wint_t>(text[index])) != 0)
|
||||
{
|
||||
++index;
|
||||
}
|
||||
std::wstring name = text.substr(start, index - start);
|
||||
// 已经是 `f(t)`:跳过随后的 `(t)`,整体原样输出(统一写成小写形式)。
|
||||
std::size_t look = index;
|
||||
while (look < length && std::iswspace(static_cast<wint_t>(text[look])) != 0)
|
||||
{
|
||||
++look;
|
||||
}
|
||||
bool hasArgument = false;
|
||||
if (look < length && text[look] == L'(')
|
||||
{
|
||||
std::size_t inner = look + 1;
|
||||
while (inner < length && std::iswspace(static_cast<wint_t>(text[inner])) != 0)
|
||||
{
|
||||
++inner;
|
||||
}
|
||||
if (inner < length && (text[inner] == L't' || text[inner] == L'T'))
|
||||
{
|
||||
++inner;
|
||||
while (inner < length && std::iswspace(static_cast<wint_t>(text[inner])) != 0)
|
||||
{
|
||||
++inner;
|
||||
}
|
||||
if (inner < length && text[inner] == L')')
|
||||
{
|
||||
hasArgument = true;
|
||||
index = inner + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::wstring lowered = name;
|
||||
for (wchar_t& letter : lowered)
|
||||
{
|
||||
letter = static_cast<wchar_t>(std::towlower(static_cast<wint_t>(letter)));
|
||||
}
|
||||
if (hasArgument && (lowered == L"f" || lowered == L"x"))
|
||||
{
|
||||
// 统一写成小写规范形式 `f(t)`(保留用户其余写法)。
|
||||
output += L"f(t)";
|
||||
}
|
||||
else if (!hasArgument && (lowered == L"x" || lowered == L"f"))
|
||||
{
|
||||
output += L"f(t)";
|
||||
}
|
||||
else
|
||||
{
|
||||
output += name;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
} // namespace jdi
|
||||
@@ -471,6 +471,32 @@ namespace jdi
|
||||
return FormatValue(std::fabs(seconds));
|
||||
}
|
||||
|
||||
std::wstring FormatFrequency(double hertz)
|
||||
{
|
||||
if (!std::isfinite(hertz) || hertz <= 0.0)
|
||||
{
|
||||
return L"—";
|
||||
}
|
||||
const wchar_t* unit = L"Hz";
|
||||
double scaled = hertz;
|
||||
if (hertz >= 1.0e9)
|
||||
{
|
||||
unit = L"GHz";
|
||||
scaled = hertz / 1.0e9;
|
||||
}
|
||||
else if (hertz >= 1.0e6)
|
||||
{
|
||||
unit = L"MHz";
|
||||
scaled = hertz / 1.0e6;
|
||||
}
|
||||
else if (hertz >= 1.0e3)
|
||||
{
|
||||
unit = L"kHz";
|
||||
scaled = hertz / 1.0e3;
|
||||
}
|
||||
return FormatValue(scaled) + L" " + unit;
|
||||
}
|
||||
|
||||
std::wstring FormatByteSize(unsigned long long bytes)
|
||||
{ // 单位自适应:GB / MB / KB(阈值按 1024 进制)。
|
||||
constexpr double kKilo = 1024.0;
|
||||
|
||||
Reference in new issue
Block a user