All checks were successful
Build and Deploy / deploy (push) Successful in 1m12s
Co-authored-by: Cursor <cursoragent@cursor.com>
551 lines
16 KiB
C#
551 lines
16 KiB
C#
using System.Globalization;
|
|
using System.Text;
|
|
|
|
namespace Oms.Application.Services.Orders.Issues;
|
|
|
|
public static class ChineseFormulaEvaluator
|
|
{
|
|
private const string ContainsKeyword = "包含";
|
|
private const string HasValueKeyword = "有值";
|
|
private const string IsEmptyKeyword = "为空";
|
|
|
|
public static bool TryEvaluate(string? expression, OrderRuleEvaluationContext context, out bool result)
|
|
{
|
|
result = false;
|
|
var text = expression?.Trim();
|
|
if (string.IsNullOrWhiteSpace(text))
|
|
{
|
|
result = true;
|
|
return true;
|
|
}
|
|
|
|
try
|
|
{
|
|
var parser = new Parser(text, context);
|
|
result = parser.ParseExpression();
|
|
parser.ExpectEnd();
|
|
return true;
|
|
}
|
|
catch (FormulaParseException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private sealed class FormulaParseException(string message) : Exception(message);
|
|
|
|
private enum TokenKind
|
|
{
|
|
Number,
|
|
String,
|
|
Label,
|
|
And,
|
|
Or,
|
|
Not,
|
|
Plus,
|
|
Minus,
|
|
Multiply,
|
|
Divide,
|
|
Greater,
|
|
GreaterEqual,
|
|
Less,
|
|
LessEqual,
|
|
Equal,
|
|
NotEqual,
|
|
LeftParen,
|
|
RightParen,
|
|
Contains,
|
|
HasValue,
|
|
IsEmpty,
|
|
End
|
|
}
|
|
|
|
private readonly record struct Token(TokenKind Kind, string Text, decimal Number = 0);
|
|
|
|
private sealed class Lexer(string source)
|
|
{
|
|
private int _pos;
|
|
|
|
public Token Next()
|
|
{
|
|
SkipWhitespace();
|
|
if (_pos >= source.Length)
|
|
{
|
|
return new Token(TokenKind.End, string.Empty);
|
|
}
|
|
|
|
var ch = source[_pos];
|
|
if (ch is >= '0' and <= '9' || (ch == '.' && _pos + 1 < source.Length && source[_pos + 1] is >= '0' and <= '9'))
|
|
{
|
|
return ReadNumber();
|
|
}
|
|
|
|
if (ch == '\'')
|
|
{
|
|
return ReadString();
|
|
}
|
|
|
|
if (TryReadOperator(out var opToken))
|
|
{
|
|
return opToken;
|
|
}
|
|
|
|
if (TryReadLabel(out var labelToken))
|
|
{
|
|
return labelToken;
|
|
}
|
|
|
|
throw new FormulaParseException($"无法识别的字符:{source[_pos]}");
|
|
}
|
|
|
|
public Token Peek()
|
|
{
|
|
var saved = _pos;
|
|
var token = Next();
|
|
_pos = saved;
|
|
return token;
|
|
}
|
|
|
|
private void SkipWhitespace()
|
|
{
|
|
while (_pos < source.Length && char.IsWhiteSpace(source[_pos]))
|
|
{
|
|
_pos++;
|
|
}
|
|
}
|
|
|
|
private Token ReadNumber()
|
|
{
|
|
var start = _pos;
|
|
while (_pos < source.Length && (char.IsDigit(source[_pos]) || source[_pos] == '.'))
|
|
{
|
|
_pos++;
|
|
}
|
|
|
|
var text = source[start.._pos];
|
|
if (!decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var number))
|
|
{
|
|
throw new FormulaParseException($"无效数字:{text}");
|
|
}
|
|
|
|
return new Token(TokenKind.Number, text, number);
|
|
}
|
|
|
|
private Token ReadString()
|
|
{
|
|
_pos++;
|
|
var sb = new StringBuilder();
|
|
while (_pos < source.Length)
|
|
{
|
|
var ch = source[_pos++];
|
|
if (ch == '\'')
|
|
{
|
|
if (_pos < source.Length && source[_pos] == '\'')
|
|
{
|
|
sb.Append('\'');
|
|
_pos++;
|
|
continue;
|
|
}
|
|
|
|
return new Token(TokenKind.String, sb.ToString());
|
|
}
|
|
|
|
sb.Append(ch);
|
|
}
|
|
|
|
throw new FormulaParseException("字符串未闭合");
|
|
}
|
|
|
|
private bool TryReadOperator(out Token token)
|
|
{
|
|
token = default;
|
|
if (_pos + 1 < source.Length)
|
|
{
|
|
var two = source.Substring(_pos, 2);
|
|
token = two switch
|
|
{
|
|
"&&" => new Token(TokenKind.And, two),
|
|
"||" => new Token(TokenKind.Or, two),
|
|
">=" => new Token(TokenKind.GreaterEqual, two),
|
|
"<=" => new Token(TokenKind.LessEqual, two),
|
|
"==" => new Token(TokenKind.Equal, two),
|
|
"!=" => new Token(TokenKind.NotEqual, two),
|
|
_ => default
|
|
};
|
|
if (token.Kind != default)
|
|
{
|
|
_pos += 2;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
var ch = source[_pos];
|
|
token = ch switch
|
|
{
|
|
'+' => new Token(TokenKind.Plus, "+"),
|
|
'-' => new Token(TokenKind.Minus, "-"),
|
|
'*' => new Token(TokenKind.Multiply, "*"),
|
|
'/' => new Token(TokenKind.Divide, "/"),
|
|
'>' => new Token(TokenKind.Greater, ">"),
|
|
'<' => new Token(TokenKind.Less, "<"),
|
|
'!' => new Token(TokenKind.Not, "!"),
|
|
'(' => new Token(TokenKind.LeftParen, "("),
|
|
')' => new Token(TokenKind.RightParen, ")"),
|
|
_ => default
|
|
};
|
|
if (token.Kind == default)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_pos++;
|
|
return true;
|
|
}
|
|
|
|
private bool TryReadLabel(out Token token)
|
|
{
|
|
token = default;
|
|
foreach (var label in AutoTriggerFormulaMeta.LabelsByLengthDesc)
|
|
{
|
|
if (!source.AsSpan(_pos).StartsWith(label, StringComparison.Ordinal))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var afterLabel = _pos + label.Length;
|
|
_pos = afterLabel;
|
|
SkipWhitespace();
|
|
if (_pos + ContainsKeyword.Length <= source.Length
|
|
&& source.AsSpan(_pos, ContainsKeyword.Length).SequenceEqual(ContainsKeyword.AsSpan()))
|
|
{
|
|
var afterContainsKeyword = _pos + ContainsKeyword.Length;
|
|
_pos = afterContainsKeyword;
|
|
SkipWhitespace();
|
|
if (_pos < source.Length && source[_pos] == '(')
|
|
{
|
|
_pos++;
|
|
token = new Token(TokenKind.Contains, label);
|
|
return true;
|
|
}
|
|
|
|
// 「包含」后无「(」则回退,继续尝试有值()/为空()/()
|
|
_pos = afterLabel;
|
|
SkipWhitespace();
|
|
}
|
|
|
|
if (TryConsumeKeywordWithEmptyParens(HasValueKeyword))
|
|
{
|
|
token = new Token(TokenKind.HasValue, label);
|
|
return true;
|
|
}
|
|
|
|
if (TryConsumeKeywordWithEmptyParens(IsEmptyKeyword))
|
|
{
|
|
token = new Token(TokenKind.IsEmpty, label);
|
|
return true;
|
|
}
|
|
|
|
if (_pos + 2 <= source.Length && source.AsSpan(_pos, 2).SequenceEqual("()".AsSpan()))
|
|
{
|
|
_pos += 2;
|
|
token = new Token(TokenKind.Label, label);
|
|
return true;
|
|
}
|
|
|
|
_pos = afterLabel - label.Length;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>匹配「有值()」「为空()」这类无参关键字。</summary>
|
|
private bool TryConsumeKeywordWithEmptyParens(string keyword)
|
|
{
|
|
var saved = _pos;
|
|
if (_pos + keyword.Length > source.Length
|
|
|| !source.AsSpan(_pos, keyword.Length).SequenceEqual(keyword.AsSpan()))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_pos += keyword.Length;
|
|
SkipWhitespace();
|
|
if (_pos + 2 <= source.Length && source.AsSpan(_pos, 2).SequenceEqual("()".AsSpan()))
|
|
{
|
|
_pos += 2;
|
|
return true;
|
|
}
|
|
|
|
_pos = saved;
|
|
return false;
|
|
}
|
|
|
|
public int SavePosition() => _pos;
|
|
|
|
public void RestorePosition(int pos) => _pos = pos;
|
|
}
|
|
|
|
private sealed class Parser
|
|
{
|
|
private readonly Lexer _lexer;
|
|
private readonly OrderRuleEvaluationContext _context;
|
|
private Token _current;
|
|
|
|
public Parser(string source, OrderRuleEvaluationContext context)
|
|
{
|
|
_lexer = new Lexer(source);
|
|
_context = context;
|
|
_current = _lexer.Next();
|
|
}
|
|
|
|
public bool ParseExpression() => ParseOr();
|
|
|
|
public void ExpectEnd()
|
|
{
|
|
if (_current.Kind != TokenKind.End)
|
|
{
|
|
throw new FormulaParseException($"公式未预期结束:{_current.Text}");
|
|
}
|
|
}
|
|
|
|
private bool ParseOr()
|
|
{
|
|
var value = ParseAnd();
|
|
while (_current.Kind == TokenKind.Or)
|
|
{
|
|
Advance();
|
|
value = value || ParseAnd();
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
private bool ParseAnd()
|
|
{
|
|
var value = ParseNot();
|
|
while (_current.Kind == TokenKind.And)
|
|
{
|
|
Advance();
|
|
value = value && ParseNot();
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
private bool ParseNot()
|
|
{
|
|
if (_current.Kind == TokenKind.Not)
|
|
{
|
|
Advance();
|
|
return !ParseNot();
|
|
}
|
|
|
|
return ParseComparison();
|
|
}
|
|
|
|
private bool ParseComparison()
|
|
{
|
|
if (_current.Kind is TokenKind.Contains)
|
|
{
|
|
return ParseContains();
|
|
}
|
|
|
|
if (_current.Kind is TokenKind.HasValue or TokenKind.IsEmpty)
|
|
{
|
|
return ParsePresence(_current.Kind == TokenKind.HasValue);
|
|
}
|
|
|
|
var left = ParseAdditive();
|
|
if (_current.Kind is TokenKind.Greater or TokenKind.GreaterEqual or TokenKind.Less or TokenKind.LessEqual or TokenKind.Equal or TokenKind.NotEqual)
|
|
{
|
|
var op = _current.Kind;
|
|
Advance();
|
|
var right = ParseAdditive();
|
|
return Compare(left, op, right);
|
|
}
|
|
|
|
return left != 0;
|
|
}
|
|
|
|
private bool ParseContains()
|
|
{
|
|
var label = _current.Text;
|
|
Advance();
|
|
if (_current.Kind != TokenKind.String)
|
|
{
|
|
throw new FormulaParseException("包含函数缺少字符串参数");
|
|
}
|
|
|
|
var needle = _current.Text;
|
|
Advance();
|
|
if (_current.Kind != TokenKind.RightParen)
|
|
{
|
|
throw new FormulaParseException("包含函数缺少右括号");
|
|
}
|
|
|
|
Advance();
|
|
if (!AutoTriggerFormulaMeta.TryResolve(label, out var fieldRef) || fieldRef == null)
|
|
{
|
|
throw new FormulaParseException($"未知字段:{label}");
|
|
}
|
|
|
|
return OrderFormulaFieldResolver.StringContains(fieldRef, _context, needle);
|
|
}
|
|
|
|
private bool ParsePresence(bool expectHasValue)
|
|
{
|
|
var label = _current.Text;
|
|
Advance();
|
|
if (!AutoTriggerFormulaMeta.TryResolve(label, out var fieldRef) || fieldRef == null)
|
|
{
|
|
throw new FormulaParseException($"未知字段:{label}");
|
|
}
|
|
|
|
var hasValue = OrderFormulaFieldResolver.HasValue(fieldRef, _context);
|
|
return expectHasValue ? hasValue : !hasValue;
|
|
}
|
|
|
|
private decimal ParseAdditive()
|
|
{
|
|
var value = ParseMultiplicative();
|
|
while (_current.Kind is TokenKind.Plus or TokenKind.Minus)
|
|
{
|
|
var op = _current.Kind;
|
|
Advance();
|
|
var right = ParseMultiplicative();
|
|
value = op == TokenKind.Plus ? value + right : value - right;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
private decimal ParseMultiplicative()
|
|
{
|
|
var value = ParseUnary();
|
|
while (_current.Kind is TokenKind.Multiply or TokenKind.Divide)
|
|
{
|
|
var op = _current.Kind;
|
|
Advance();
|
|
var right = ParseUnary();
|
|
value = op == TokenKind.Multiply ? value * right : right == 0 ? 0 : value / right;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
private decimal ParseUnary()
|
|
{
|
|
if (_current.Kind == TokenKind.Minus)
|
|
{
|
|
Advance();
|
|
return -ParseUnary();
|
|
}
|
|
|
|
return ParsePrimary();
|
|
}
|
|
|
|
private decimal ParsePrimary()
|
|
{
|
|
switch (_current.Kind)
|
|
{
|
|
case TokenKind.Number:
|
|
{
|
|
var number = _current.Number;
|
|
Advance();
|
|
return number;
|
|
}
|
|
case TokenKind.Label:
|
|
{
|
|
var label = _current.Text;
|
|
Advance();
|
|
if (!AutoTriggerFormulaMeta.TryResolve(label, out var fieldRef) || fieldRef == null)
|
|
{
|
|
throw new FormulaParseException($"未知字段:{label}");
|
|
}
|
|
|
|
return OrderFormulaFieldResolver.GetNumericValue(fieldRef, _context);
|
|
}
|
|
case TokenKind.LeftParen:
|
|
{
|
|
Advance();
|
|
decimal value;
|
|
if (LooksLikeBooleanGroup())
|
|
{
|
|
var inside = ParseOr();
|
|
if (_current.Kind != TokenKind.RightParen)
|
|
{
|
|
throw new FormulaParseException("缺少右括号");
|
|
}
|
|
|
|
Advance();
|
|
value = inside ? 1 : 0;
|
|
}
|
|
else
|
|
{
|
|
value = ParseAdditive();
|
|
if (_current.Kind != TokenKind.RightParen)
|
|
{
|
|
throw new FormulaParseException("缺少右括号");
|
|
}
|
|
|
|
Advance();
|
|
}
|
|
|
|
return value;
|
|
}
|
|
default:
|
|
throw new FormulaParseException($"无法解析:{_current.Text}");
|
|
}
|
|
}
|
|
|
|
private static bool Compare(decimal left, TokenKind op, decimal right) =>
|
|
op switch
|
|
{
|
|
TokenKind.Greater => left > right,
|
|
TokenKind.GreaterEqual => left >= right,
|
|
TokenKind.Less => left < right,
|
|
TokenKind.LessEqual => left <= right,
|
|
TokenKind.Equal => left == right,
|
|
TokenKind.NotEqual => left != right,
|
|
_ => false
|
|
};
|
|
|
|
private bool LooksLikeBooleanGroup()
|
|
{
|
|
var savedPos = _lexer.SavePosition();
|
|
var depth = 1;
|
|
while (depth > 0)
|
|
{
|
|
var token = _lexer.Next();
|
|
if (token.Kind == TokenKind.End)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (token.Kind == TokenKind.LeftParen)
|
|
{
|
|
depth++;
|
|
continue;
|
|
}
|
|
|
|
if (token.Kind == TokenKind.RightParen)
|
|
{
|
|
depth--;
|
|
continue;
|
|
}
|
|
|
|
if (depth == 1 && token.Kind is TokenKind.And or TokenKind.Or)
|
|
{
|
|
_lexer.RestorePosition(savedPos);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
_lexer.RestorePosition(savedPos);
|
|
return false;
|
|
}
|
|
|
|
private void Advance() => _current = _lexer.Next();
|
|
}
|
|
}
|