Files
Just-DataInspector/Utilities/Src/FormulaEval.cpp
T
DeepSeek-V4.1-Flash 74456b9305 fix(formula,resample): 公式映射卡顿根治 + 语法/输入框完善 + 重采样面板三处修正
一、「公式映射」卡顿(用户报告:只是点进输入框就卡,编辑过公式后彻底没法用)
1. 真因:菜单内联编辑提交时无条件回写初值 ⇒ 点进输入框再点别处就把
   formula = "f(t)" 写进样式;而 HasFormula() 只判 !empty(),于是
   DrawTransformOrderPreserving() 变 false、ComputeLaneRange() 每帧逐点扫描
   整条信号(2580 万采样 ≈ 960ms/帧)。
   修法:新增 FormulaIsIdentity()(空串与 f(t) 等价);提交时归一化后恒等即清空;
   BuildLaneVisuals() 为「网格幅值范围」加指纹缓存(数据指针/长度/统计值 +
   影响数值的样式字段),键不变直接复用(实测 0.0005ms)。
2. 真实公式仍然贵:新增 PlotView::AcquireFormulaEnvelope()(变换后的逐分组
   包络,65536 组,按需算一次并缓存),BuildPolyline 的包络带改用它。
   实测:静止 215→21ms、中键拖动 8ms、缩放最坏 843→81ms;极值仍逐点统计
   (不会漏尖峰)。
3. AcquireProgram 只比 data() 指针 + 长度 ⇒ std::wstring 短串走 SSO 时,
   等长改公式会一直复用旧程序(f(t)^2→f(t)^3 波形不更新)。改为按内容判定。

二、公式语法(Utilities/FormulaEval)
- 新增 ^ 幂(右结合,2^3^2=512、2^-1=0.5、-2^2=-4),支持 2^f(t) 与 f(t)^2;
- 常量 e / pi(大小写均接受,保存前统一小写);
- 数据点只允许 X / x / f(t):单独的 f 不再接受(且不归一化,直接报错);
- 数字扫描修正:2e 里的 e 不再被当成指数吞掉。

三、「公式映射」输入框(SignalPanelMenu)
- Y= 画在输入框内部(非编辑态),进入编辑态只显示公式本身;
- label 留空 ⇒ 输入框占满整行,与下方「系数调整」同宽。

四、「重采样」面板
- 修复点进间隔/频率输入框后输入框整块消失(paintOverlays 的 PaintEditField
  名单漏了 RESAMPLE_INTERVAL / RESAMPLE_FREQUENCY);
- 最大/最小数据间隔:3 位有效数字 + Δt 复合单位,末尾追加(频率)尾缀;
- 间隔显示自适应单位、编辑态用秒;频率显示自适应单位、编辑态用 Hz(均不带单位);
- 面板宽度按含(频率)尾缀的文本量宽。

五、十进制格式化(Utf8Util)
- 新增 FormatDecimal(绝不产生科学计数法);FormatDurationCompound /
  FormatDurationSeconds / FormatFrequency 改用它,Δt/间隔/频率读数不再出现 1e-05。

验证:离屏探针 .preflight/perf/probe/formulacheck_main.cpp 75/75;
make release / make debug 零 warning;Release 856,064 B。
2026-11-03 02:55:22 +08:00

639 lines
15 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.
// 「公式映射」表达式的编译 / 求值实现(详见 `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,
POWER, /**< 幂(`a ^ b`)。 */
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,
CARET,
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::CARET, 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;
}
// 指数部分必须**真的带数字**才算数字的一部分:
// `2e` 里的 `e` 是自然常数(不能把 `e` 吞进食指后当成数字 2 再用掉它)。
if (index < length && (text[index] == L'e' || text[index] == L'E'))
{
std::size_t probe = index + 1;
if (probe < length && (text[probe] == L'+' || text[probe] == L'-'))
{
++probe;
}
if (probe < length && text[probe] >= L'0' && text[probe] <= L'9')
{
index = probe;
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(t) 作为数据点,以及常量 e / 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;
}
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;
}
}
}
for (wchar_t& letter : name)
{
letter = static_cast<wchar_t>(std::towlower(static_cast<wint_t>(letter)));
}
if (name == L"x" || (name == L"f" && hasArgument))
{
tokens.push_back(Token{TokenKind::VARIABLE, 0.0});
}
else if (name == L"pi")
{
tokens.push_back(Token{TokenKind::NUMBER, 3.14159265358979323846});
}
else if (name == L"e")
{
tokens.push_back(Token{TokenKind::NUMBER, 2.71828182845904523536});
}
else
{
if (error != nullptr)
{
*error = L"无法识别的标识符(数据点请写 f(t) 或 X;常量只支持 e / pi)。";
}
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 ParsePower();
}
/**
* @brief 幂运算(优先级高于乘除、低于一元正负号;**右结合**)。
*
* `power := primary ('^' unary)?`
* * `2^3^2` = 2^(3^2)(右结合);
* * `2^-1` 允许(指数的符号);
* * `-2^2` = -(2^2)(一元负号作用于整个幂)。
*/
bool ParsePower()
{
if (!ParsePrimary())
{
return false;
}
if (Current().kind != TokenKind::CARET)
{
return true;
}
Advance();
if (!ParseUnary())
{
return false;
}
m_output.push_back(FormulaOp{FormulaOp::Kind::POWER, 0.0});
return true;
}
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>;
/** 线程内的单条编译缓存(按内容判定)。 */
struct FormulaCache
{
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;
// ⚠️⚠️ **必须比较内容,不能只比 `data()` 指针 + 长度**:`std::wstring` 的短串走
// **SSO**(缓冲区就在对象内部),把同一个样式对象的公式改成**等长**的另一条时,
// `data()` 与 `size()` 都不会变 ⇒ 只比指针会一直复用**旧程序**
//(实测:`f(t)^2` 改成 `f(t)^3` 后波形不更新,用户无法察觉公式已失效)。
// 比较顺序:先比长度(最便宜),再比内容(短公式只是十几字节的 memcmp)。
if (cache.compiled && cache.size == text.size() && cache.text == text)
{
return cache.valid ? &cache.program : nullptr;
}
cache.text = text;
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;
case FormulaOp::Kind::POWER:
result = std::pow(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)));
}
// 数据点:`X` / `x` / `f(t)`(含 `F(T)`)一律写成规范形式 `f(t)`。
// ⚠️ **单独的 `f` 不归一化**:它不是合法写法(用户 2026-11-03 明确要求),
// 原样输出交给 `ValidateFormula` 报错,而不是悄悄当成 `f(t)` 接受。
if (lowered == L"x" || (lowered == L"f" && hasArgument))
{
output += L"f(t)";
}
// 常量统一为小写(`E` / `Pi` / `PI` → `e` / `pi`)。
else if (lowered == L"e")
{
output += L"e";
}
else if (lowered == L"pi")
{
output += L"pi";
}
else
{
output += name;
}
}
return output;
}
} // namespace jdi