/// mathtool2 风格表达式 → Symja 语法的兼容转换。 /// /// 为什么需要:工具页那两个绘图工具原来用 `math_expressions`,输入是 /// `sin(x)`、`ln(x)`、`pi` 这种小写括号写法。统一到 Symja 之后,直接扔掉这套 /// 语法对习惯了它的人是退步,所以这里保留兼容。 /// /// **界面上会显示转换后的表达式** —— 转换这种事最怕悄悄改错,让用户看得见。 /// /// 只做**保守**的转换,不确定的一律原样保留: /// `sin(x)` → `Sin[x]` 函数名(大小写不敏感)+ 紧跟的括号 /// `pi` → `Pi` 常量 /// `e` → `E` 仅当它独立成词、且不与数字相邻(避免毁掉 `1e-3`) /// `2x` → `2*x` 数字紧跟字母时补乘号 /// `(x+1)*2` → 原样 普通括号不是函数调用,不动 library; /// 小写函数名 → Symja 名称。 const _functions = { 'sin': 'Sin', 'cos': 'Cos', 'tan': 'Tan', 'cot': 'Cot', 'sec': 'Sec', 'csc': 'Csc', 'asin': 'ArcSin', 'acos': 'ArcCos', 'atan': 'ArcTan', 'arcsin': 'ArcSin', 'arccos': 'ArcCos', 'arctan': 'ArcTan', 'sinh': 'Sinh', 'cosh': 'Cosh', 'tanh': 'Tanh', 'ln': 'Log', 'log': 'Log', 'lg': 'Log10', 'sqrt': 'Sqrt', 'abs': 'Abs', 'exp': 'Exp', 'floor': 'Floor', 'ceil': 'Ceiling', 'round': 'Round', 'sign': 'Sign', 'max': 'Max', 'min': 'Min', 'mod': 'Mod', }; const _constants = { 'pi': 'Pi', 'inf': 'Infinity', 'infinity': 'Infinity', }; bool _isLetter(String c) { final u = c.codeUnitAt(0); return (u >= 0x41 && u <= 0x5A) || (u >= 0x61 && u <= 0x7A) || c == '_'; } bool _isDigit(String c) { if (c.isEmpty) return false; // 空串不是数字 —— 边界处会传进来,别让它崩 final u = c.codeUnitAt(0); return u >= 0x30 && u <= 0x39; } bool _isIdentChar(String c) => _isLetter(c) || _isDigit(c); /// 从 [from] 起看是不是"指数部分":可选正负号后紧跟数字。 bool _looksLikeExponent(String s, int from) { var k = from; if (k < s.length && (s[k] == '+' || s[k] == '-')) k++; return k < s.length && _isDigit(s[k]); } /// 把 [input] 转换成 Symja 语法。无法识别的地方原样保留。 String toSymjaSyntax(String input) { final out = StringBuffer(); // 括号栈:记住每个左括号应该被闭合成什么 final closers = []; var i = 0; while (i < input.length) { final c = input[i]; if (_isLetter(c)) { var j = i; while (j < input.length && _isIdentChar(input[j])) { j++; } final word = input.substring(i, j); // 跳过空白看后面是不是紧跟 '(' var k = j; while (k < input.length && input[k] == ' ') { k++; } final isCall = k < input.length && input[k] == '('; final fn = _functions[word.toLowerCase()]; if (isCall && fn != null) { out.write(fn); out.write('['); closers.add(']'); i = k + 1; // 吃掉 '(' continue; } // 常量:'e' 要额外小心——别把 1e-3 这种科学计数法拆了 final lower = word.toLowerCase(); if (lower == 'e') { final prev = out.isEmpty ? '' : out.toString()[out.length - 1]; final next = j < input.length ? input[j] : ''; if (!_isDigit(prev) && !_isDigit(next)) { // 空串会被 _isDigit 判成非数字,正是想要的 out.write('E'); i = j; continue; } } else if (_constants[lower] != null) { out.write(_constants[lower]); i = j; continue; } out.write(word); i = j; continue; } if (c == '(') { out.write('('); closers.add(')'); i++; continue; } if (c == ')') { out.write(closers.isNotEmpty ? closers.removeLast() : ')'); i++; continue; } // `2x` -> `2*x`:数字后面紧跟字母时补乘号 if (_isDigit(c)) { var j = i; while (j < input.length && (_isDigit(input[j]) || input[j] == '.')) { j++; } out.write(input.substring(i, j)); if (j < input.length && _isLetter(input[j])) { // 例外:`1e-3`、`2.5E10` 是科学计数法,补了乘号就毁了 final isExponent = (input[j] == 'e' || input[j] == 'E') && _looksLikeExponent(input, j + 1); if (!isExponent) out.write('*'); } i = j; continue; } out.write(c); i++; } return out.toString(); } /// 转换过(即与原串不同)才返回新串,否则返回 null —— 界面据此决定要不要提示。 String? symjaRewriteOf(String input) { final converted = toSymjaSyntax(input); return converted == input ? null : converted; }