- Lane::ResampleItem + resample/resampleItems/resamplePanel/resampleScroll, 并在 LaneCarry / SplitLane / PruneLaneSignalItems / RemapAfterSignalInsert|Removal 同步 - 面板:可滚动分组列表(信号色条目栏 + 复选框 + 展开三角),展开后 4 行: 最大/最小数据间隔(只读,勾选时一次性缓存,绘制不扫描采样)+ 重采样间隔/频率(输入) - 输入框联动:改间隔同步频率,改频率同步间隔(输入单位分别为秒 / Hz) - 「构建为新信号」:DataSet::AppendGroupAxis 追加新时间轴 + InsertDerivedSignal, 采样值先按公式/系数/绝对值/镜像/Y偏移变换再等间隔线性插值,时间轴偏移不烘值 - 新信号名 <原信号> (重采样 <自适应单位频率>);Utf8Util::FormatFrequency 为唯一实现 - 非单调轴 / 点太少 / 间隔过大 / 点数超 2000 万 ⇒ 拒绝构建并给出自绘提示 - 顺带加入 FormulaEval(公式编译/求值,供 ApplyDrawTransform 与后续 UI 使用)
587 lines
14 KiB
C++
587 lines
14 KiB
C++
// 「公式映射」表达式的编译 / 求值实现(详见 `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
|