expr_syntax.dart 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. /// mathtool2 风格表达式 → Symja 语法的兼容转换。
  2. ///
  3. /// 为什么需要:工具页那两个绘图工具原来用 `math_expressions`,输入是
  4. /// `sin(x)`、`ln(x)`、`pi` 这种小写括号写法。统一到 Symja 之后,直接扔掉这套
  5. /// 语法对习惯了它的人是退步,所以这里保留兼容。
  6. ///
  7. /// **界面上会显示转换后的表达式** —— 转换这种事最怕悄悄改错,让用户看得见。
  8. ///
  9. /// 只做**保守**的转换,不确定的一律原样保留:
  10. /// `sin(x)` → `Sin[x]` 函数名(大小写不敏感)+ 紧跟的括号
  11. /// `pi` → `Pi` 常量
  12. /// `e` → `E` 仅当它独立成词、且不与数字相邻(避免毁掉 `1e-3`)
  13. /// `2x` → `2*x` 数字紧跟字母时补乘号
  14. /// `(x+1)*2` → 原样 普通括号不是函数调用,不动
  15. library;
  16. /// 小写函数名 → Symja 名称。
  17. const _functions = <String, String>{
  18. 'sin': 'Sin',
  19. 'cos': 'Cos',
  20. 'tan': 'Tan',
  21. 'cot': 'Cot',
  22. 'sec': 'Sec',
  23. 'csc': 'Csc',
  24. 'asin': 'ArcSin',
  25. 'acos': 'ArcCos',
  26. 'atan': 'ArcTan',
  27. 'arcsin': 'ArcSin',
  28. 'arccos': 'ArcCos',
  29. 'arctan': 'ArcTan',
  30. 'sinh': 'Sinh',
  31. 'cosh': 'Cosh',
  32. 'tanh': 'Tanh',
  33. 'ln': 'Log',
  34. 'log': 'Log',
  35. 'lg': 'Log10',
  36. 'sqrt': 'Sqrt',
  37. 'abs': 'Abs',
  38. 'exp': 'Exp',
  39. 'floor': 'Floor',
  40. 'ceil': 'Ceiling',
  41. 'round': 'Round',
  42. 'sign': 'Sign',
  43. 'max': 'Max',
  44. 'min': 'Min',
  45. 'mod': 'Mod',
  46. };
  47. const _constants = <String, String>{
  48. 'pi': 'Pi',
  49. 'inf': 'Infinity',
  50. 'infinity': 'Infinity',
  51. };
  52. bool _isLetter(String c) {
  53. final u = c.codeUnitAt(0);
  54. return (u >= 0x41 && u <= 0x5A) || (u >= 0x61 && u <= 0x7A) || c == '_';
  55. }
  56. bool _isDigit(String c) {
  57. if (c.isEmpty) return false; // 空串不是数字 —— 边界处会传进来,别让它崩
  58. final u = c.codeUnitAt(0);
  59. return u >= 0x30 && u <= 0x39;
  60. }
  61. bool _isIdentChar(String c) => _isLetter(c) || _isDigit(c);
  62. /// 从 [from] 起看是不是"指数部分":可选正负号后紧跟数字。
  63. bool _looksLikeExponent(String s, int from) {
  64. var k = from;
  65. if (k < s.length && (s[k] == '+' || s[k] == '-')) k++;
  66. return k < s.length && _isDigit(s[k]);
  67. }
  68. /// 把 [input] 转换成 Symja 语法。无法识别的地方原样保留。
  69. String toSymjaSyntax(String input) {
  70. final out = StringBuffer();
  71. // 括号栈:记住每个左括号应该被闭合成什么
  72. final closers = <String>[];
  73. var i = 0;
  74. while (i < input.length) {
  75. final c = input[i];
  76. if (_isLetter(c)) {
  77. var j = i;
  78. while (j < input.length && _isIdentChar(input[j])) {
  79. j++;
  80. }
  81. final word = input.substring(i, j);
  82. // 跳过空白看后面是不是紧跟 '('
  83. var k = j;
  84. while (k < input.length && input[k] == ' ') {
  85. k++;
  86. }
  87. final isCall = k < input.length && input[k] == '(';
  88. final fn = _functions[word.toLowerCase()];
  89. if (isCall && fn != null) {
  90. out.write(fn);
  91. out.write('[');
  92. closers.add(']');
  93. i = k + 1; // 吃掉 '('
  94. continue;
  95. }
  96. // 常量:'e' 要额外小心——别把 1e-3 这种科学计数法拆了
  97. final lower = word.toLowerCase();
  98. if (lower == 'e') {
  99. final prev = out.isEmpty ? '' : out.toString()[out.length - 1];
  100. final next = j < input.length ? input[j] : '';
  101. if (!_isDigit(prev) && !_isDigit(next)) { // 空串会被 _isDigit 判成非数字,正是想要的
  102. out.write('E');
  103. i = j;
  104. continue;
  105. }
  106. } else if (_constants[lower] != null) {
  107. out.write(_constants[lower]);
  108. i = j;
  109. continue;
  110. }
  111. out.write(word);
  112. i = j;
  113. continue;
  114. }
  115. // `√3` -> `Sqrt[3]`,`√(x+1)` -> `Sqrt[(x+1)]`
  116. //
  117. // 根号是**前置一元**运算符,后面跟的操作数要么是一对配平的括号,要么是
  118. // 一串数字/字母。不能让它走普通括号分支 —— 那样 `√(x+1)` 会变成
  119. // `Sqrt[(x+1)`,右方括号永远配不上。
  120. if (c == '√') {
  121. var k = i + 1;
  122. final start = k;
  123. if (k < input.length && input[k] == '(') {
  124. var depth = 0;
  125. while (k < input.length) {
  126. if (input[k] == '(') {
  127. depth++;
  128. } else if (input[k] == ')') {
  129. depth--;
  130. if (depth == 0) {
  131. k++;
  132. break;
  133. }
  134. }
  135. k++;
  136. }
  137. } else {
  138. while (k < input.length && (_isIdentChar(input[k]) || input[k] == '.')) {
  139. k++;
  140. }
  141. }
  142. final operand = input.substring(start, k);
  143. out.write('Sqrt[');
  144. out.write(operand.isEmpty ? 'Null' : toSymjaSyntax(operand));
  145. out.write(']');
  146. i = k;
  147. continue;
  148. }
  149. if (c == '(') {
  150. out.write('(');
  151. closers.add(')');
  152. i++;
  153. continue;
  154. }
  155. if (c == ')') {
  156. out.write(closers.isNotEmpty ? closers.removeLast() : ')');
  157. i++;
  158. continue;
  159. }
  160. // `2x` -> `2*x`:数字后面紧跟字母时补乘号
  161. if (_isDigit(c)) {
  162. var j = i;
  163. while (j < input.length && (_isDigit(input[j]) || input[j] == '.')) {
  164. j++;
  165. }
  166. out.write(input.substring(i, j));
  167. // 根号也当操作数:`2√3` -> `2*Sqrt[3]`
  168. if (j < input.length && (_isLetter(input[j]) || input[j] == '√')) {
  169. // 例外:`1e-3`、`2.5E10` 是科学计数法,补了乘号就毁了
  170. final isExponent = (input[j] == 'e' || input[j] == 'E') &&
  171. _looksLikeExponent(input, j + 1);
  172. if (!isExponent) out.write('*');
  173. }
  174. i = j;
  175. continue;
  176. }
  177. out.write(c);
  178. i++;
  179. }
  180. return out.toString();
  181. }
  182. /// 转换过(即与原串不同)才返回新串,否则返回 null —— 界面据此决定要不要提示。
  183. String? symjaRewriteOf(String input) {
  184. final converted = toSymjaSyntax(input);
  185. return converted == input ? null : converted;
  186. }