expr_syntax.dart 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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. if (c == '(') {
  116. out.write('(');
  117. closers.add(')');
  118. i++;
  119. continue;
  120. }
  121. if (c == ')') {
  122. out.write(closers.isNotEmpty ? closers.removeLast() : ')');
  123. i++;
  124. continue;
  125. }
  126. // `2x` -> `2*x`:数字后面紧跟字母时补乘号
  127. if (_isDigit(c)) {
  128. var j = i;
  129. while (j < input.length && (_isDigit(input[j]) || input[j] == '.')) {
  130. j++;
  131. }
  132. out.write(input.substring(i, j));
  133. if (j < input.length && _isLetter(input[j])) {
  134. // 例外:`1e-3`、`2.5E10` 是科学计数法,补了乘号就毁了
  135. final isExponent = (input[j] == 'e' || input[j] == 'E') &&
  136. _looksLikeExponent(input, j + 1);
  137. if (!isExponent) out.write('*');
  138. }
  139. i = j;
  140. continue;
  141. }
  142. out.write(c);
  143. i++;
  144. }
  145. return out.toString();
  146. }
  147. /// 转换过(即与原串不同)才返回新串,否则返回 null —— 界面据此决定要不要提示。
  148. String? symjaRewriteOf(String input) {
  149. final converted = toSymjaSyntax(input);
  150. return converted == input ? null : converted;
  151. }