Compare commits
	
		
			9 Commits
		
	
	
		
			1.0.0+4
			...
			5cf66cd1f2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5cf66cd1f2 | |||
| 6590c33732 | |||
| 587e243ee3 | |||
| 50857f2d2e | |||
| ebe9f89c9b | |||
| e6a52b8b74 | |||
| c9190d05a1 | |||
| 40dc6f8511 | |||
| 2a56a83898 | 
| @@ -1,5 +1,6 @@ | ||||
| // === 在 abstract class Expr 中添加声明 === | ||||
| import 'dart:math' show sqrt, cos, sin, tan; | ||||
| import 'dart:math' show sqrt, cos, sin, tan, pow, log, exp, asin, acos, atan; | ||||
| import 'parser.dart'; | ||||
|  | ||||
| abstract class Expr { | ||||
|   Expr simplify(); | ||||
| @@ -7,6 +8,9 @@ abstract class Expr { | ||||
|   /// 新增:对表达式进行“求值/数值化”——尽可能把可算的部分算出来 | ||||
|   Expr evaluate(); | ||||
|  | ||||
|   /// Substitute variable with value | ||||
|   Expr substitute(String varName, Expr value); | ||||
|  | ||||
|   @override | ||||
|   String toString(); | ||||
|  | ||||
| @@ -27,6 +31,9 @@ class IntExpr extends Expr { | ||||
|   @override | ||||
|   Expr evaluate() => this; | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => this; | ||||
|  | ||||
|   @override | ||||
|   String toString() => value.toString(); | ||||
| } | ||||
| @@ -42,10 +49,31 @@ class DoubleExpr extends Expr { | ||||
|   @override | ||||
|   Expr evaluate() => this; | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => this; | ||||
|  | ||||
|   @override | ||||
|   String toString() => value.toString(); | ||||
| } | ||||
|  | ||||
| // === VarExpr === | ||||
| class VarExpr extends Expr { | ||||
|   final String name; | ||||
|   VarExpr(this.name); | ||||
|  | ||||
|   @override | ||||
|   Expr simplify() => this; | ||||
|  | ||||
|   @override | ||||
|   Expr evaluate() => this; | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => name == varName ? value : this; | ||||
|  | ||||
|   @override | ||||
|   String toString() => name; | ||||
| } | ||||
|  | ||||
| // === FractionExpr.evaluate === | ||||
| class FractionExpr extends Expr { | ||||
|   final int numerator; | ||||
| @@ -74,6 +102,9 @@ class FractionExpr extends Expr { | ||||
|   @override | ||||
|   Expr evaluate() => simplify(); | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => this; | ||||
|  | ||||
|   @override | ||||
|   String toString() => "$numerator/$denominator"; | ||||
| } | ||||
| @@ -120,6 +151,17 @@ class AddExpr extends Expr { | ||||
|       return IntExpr(l.value + r.value); | ||||
|     } | ||||
|  | ||||
|     // 小数相加 | ||||
|     if (l is DoubleExpr && r is DoubleExpr) { | ||||
|       return DoubleExpr(l.value + r.value); | ||||
|     } | ||||
|     if (l is IntExpr && r is DoubleExpr) { | ||||
|       return DoubleExpr(l.value + r.value); | ||||
|     } | ||||
|     if (l is DoubleExpr && r is IntExpr) { | ||||
|       return DoubleExpr(l.value + r.value); | ||||
|     } | ||||
|  | ||||
|     // 分数相加 / 分数与整数相加 | ||||
|     if (l is FractionExpr && r is FractionExpr) { | ||||
|       return FractionExpr( | ||||
| @@ -140,6 +182,14 @@ class AddExpr extends Expr { | ||||
|       ).simplify(); | ||||
|     } | ||||
|  | ||||
|     // 分数与小数相加 | ||||
|     if (l is FractionExpr && r is DoubleExpr) { | ||||
|       return DoubleExpr(l.numerator / l.denominator + r.value); | ||||
|     } | ||||
|     if (l is DoubleExpr && r is FractionExpr) { | ||||
|       return DoubleExpr(l.value + r.numerator / r.denominator); | ||||
|     } | ||||
|  | ||||
|     // 合并同类的 sqrt 项: a*sqrt(X) + b*sqrt(X) = (a+b)*sqrt(X) | ||||
|     var a = _asSqrtTerm(l); | ||||
|     var b = _asSqrtTerm(r); | ||||
| @@ -150,6 +200,12 @@ class AddExpr extends Expr { | ||||
|     return AddExpr(l, r); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => AddExpr( | ||||
|     left.substitute(varName, value), | ||||
|     right.substitute(varName, value), | ||||
|   ); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "($left + $right)"; | ||||
| } | ||||
| @@ -184,6 +240,18 @@ class SubExpr extends Expr { | ||||
|     if (l is IntExpr && r is IntExpr) { | ||||
|       return IntExpr(l.value - r.value); | ||||
|     } | ||||
|  | ||||
|     // 小数相减 | ||||
|     if (l is DoubleExpr && r is DoubleExpr) { | ||||
|       return DoubleExpr(l.value - r.value); | ||||
|     } | ||||
|     if (l is IntExpr && r is DoubleExpr) { | ||||
|       return DoubleExpr(l.value - r.value); | ||||
|     } | ||||
|     if (l is DoubleExpr && r is IntExpr) { | ||||
|       return DoubleExpr(l.value - r.value); | ||||
|     } | ||||
|  | ||||
|     if (l is FractionExpr && r is FractionExpr) { | ||||
|       return FractionExpr( | ||||
|         l.numerator * r.denominator - r.numerator * l.denominator, | ||||
| @@ -203,6 +271,14 @@ class SubExpr extends Expr { | ||||
|       ).simplify(); | ||||
|     } | ||||
|  | ||||
|     // 分数与小数相减 | ||||
|     if (l is FractionExpr && r is DoubleExpr) { | ||||
|       return DoubleExpr(l.numerator / l.denominator - r.value); | ||||
|     } | ||||
|     if (l is DoubleExpr && r is FractionExpr) { | ||||
|       return DoubleExpr(l.value - r.numerator / r.denominator); | ||||
|     } | ||||
|  | ||||
|     // 处理同类 sqrt 项: a*sqrt(X) - b*sqrt(X) = (a-b)*sqrt(X) | ||||
|     var a = _asSqrtTerm(l); | ||||
|     var b = _asSqrtTerm(r); | ||||
| @@ -213,6 +289,12 @@ class SubExpr extends Expr { | ||||
|     return SubExpr(l, r); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => SubExpr( | ||||
|     left.substitute(varName, value), | ||||
|     right.substitute(varName, value), | ||||
|   ); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "($left - $right)"; | ||||
| } | ||||
| @@ -268,6 +350,18 @@ class MulExpr extends Expr { | ||||
|     if (l is IntExpr && r is IntExpr) { | ||||
|       return IntExpr(l.value * r.value); | ||||
|     } | ||||
|  | ||||
|     // 小数相乘 | ||||
|     if (l is DoubleExpr && r is DoubleExpr) { | ||||
|       return DoubleExpr(l.value * r.value); | ||||
|     } | ||||
|     if (l is IntExpr && r is DoubleExpr) { | ||||
|       return DoubleExpr(l.value * r.value); | ||||
|     } | ||||
|     if (l is DoubleExpr && r is IntExpr) { | ||||
|       return DoubleExpr(l.value * r.value); | ||||
|     } | ||||
|  | ||||
|     if (l is FractionExpr && r is IntExpr) { | ||||
|       return FractionExpr(l.numerator * r.value, l.denominator).simplify(); | ||||
|     } | ||||
| @@ -281,6 +375,14 @@ class MulExpr extends Expr { | ||||
|       ).simplify(); | ||||
|     } | ||||
|  | ||||
|     // 分数与小数相乘 | ||||
|     if (l is FractionExpr && r is DoubleExpr) { | ||||
|       return DoubleExpr(l.numerator / l.denominator * r.value); | ||||
|     } | ||||
|     if (l is DoubleExpr && r is FractionExpr) { | ||||
|       return DoubleExpr(l.value * r.numerator / r.denominator); | ||||
|     } | ||||
|  | ||||
|     // sqrt * sqrt: sqrt(a)*sqrt(a) = a | ||||
|     if (l is SqrtExpr && | ||||
|         r is SqrtExpr && | ||||
| @@ -296,6 +398,12 @@ class MulExpr extends Expr { | ||||
|     return MulExpr(l, r); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => MulExpr( | ||||
|     left.substitute(varName, value), | ||||
|     right.substitute(varName, value), | ||||
|   ); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "($left * $right)"; | ||||
| } | ||||
| @@ -378,6 +486,12 @@ class DivExpr extends Expr { | ||||
|     return DivExpr(l, r); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => DivExpr( | ||||
|     left.substitute(varName, value), | ||||
|     right.substitute(varName, value), | ||||
|   ); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "($left / $right)"; | ||||
| } | ||||
| @@ -430,7 +544,11 @@ class SqrtExpr extends Expr { | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   String toString() => "sqrt($inner)"; | ||||
|   Expr substitute(String varName, Expr value) => | ||||
|       SqrtExpr(inner.substitute(varName, value)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "\\sqrt{${inner.toString()}}"; | ||||
| } | ||||
|  | ||||
| // === CosExpr === | ||||
| @@ -456,6 +574,10 @@ class CosExpr extends Expr { | ||||
|     return CosExpr(i); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => | ||||
|       CosExpr(inner.substitute(varName, value)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "cos($inner)"; | ||||
| } | ||||
| @@ -483,6 +605,10 @@ class SinExpr extends Expr { | ||||
|     return SinExpr(i); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => | ||||
|       SinExpr(inner.substitute(varName, value)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "sin($inner)"; | ||||
| } | ||||
| @@ -510,10 +636,316 @@ class TanExpr extends Expr { | ||||
|     return TanExpr(i); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => | ||||
|       TanExpr(inner.substitute(varName, value)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "tan($inner)"; | ||||
| } | ||||
|  | ||||
| // === PowExpr === | ||||
| class PowExpr extends Expr { | ||||
|   final Expr left, right; | ||||
|   PowExpr(this.left, this.right); | ||||
|  | ||||
|   @override | ||||
|   Expr simplify() { | ||||
|     var l = left.simplify(); | ||||
|     var r = right.simplify(); | ||||
|  | ||||
|     // x^0 = 1 | ||||
|     if (r is IntExpr && r.value == 0) return IntExpr(1); | ||||
|     // x^1 = x | ||||
|     if (r is IntExpr && r.value == 1) return l; | ||||
|     // 1^x = 1 | ||||
|     if (l is IntExpr && l.value == 1) return IntExpr(1); | ||||
|     // 0^x = 0 (for x != 0) | ||||
|     if (l is IntExpr && l.value == 0 && !(r is IntExpr && r.value == 0)) { | ||||
|       return IntExpr(0); | ||||
|     } | ||||
|  | ||||
|     return PowExpr(l, r); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr evaluate() { | ||||
|     var l = left.evaluate(); | ||||
|     var r = right.evaluate(); | ||||
|  | ||||
|     // x^0 = 1 | ||||
|     if (r is IntExpr && r.value == 0) return IntExpr(1); | ||||
|     // x^1 = x | ||||
|     if (r is IntExpr && r.value == 1) return l; | ||||
|     // 1^x = 1 | ||||
|     if (l is IntExpr && l.value == 1) return IntExpr(1); | ||||
|     // 0^x = 0 (for x != 0) | ||||
|     if (l is IntExpr && l.value == 0 && !(r is IntExpr && r.value == 0)) { | ||||
|       return IntExpr(0); | ||||
|     } | ||||
|  | ||||
|     // If both are numbers, compute | ||||
|     if (l is IntExpr && r is IntExpr) { | ||||
|       return DoubleExpr(pow(l.value.toDouble(), r.value.toDouble()).toDouble()); | ||||
|     } | ||||
|     if (l is DoubleExpr && r is IntExpr) { | ||||
|       return DoubleExpr(pow(l.value, r.value.toDouble()).toDouble()); | ||||
|     } | ||||
|     if (l is IntExpr && r is DoubleExpr) { | ||||
|       return DoubleExpr(pow(l.value.toDouble(), r.value).toDouble()); | ||||
|     } | ||||
|     if (l is DoubleExpr && r is DoubleExpr) { | ||||
|       return DoubleExpr(pow(l.value, r.value).toDouble()); | ||||
|     } | ||||
|  | ||||
|     return PowExpr(l, r); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => PowExpr( | ||||
|     left.substitute(varName, value), | ||||
|     right.substitute(varName, value), | ||||
|   ); | ||||
|  | ||||
|   @override | ||||
|   String toString() { | ||||
|     String leftStr = left.toString(); | ||||
|     String rightStr = right.toString(); | ||||
|  | ||||
|     // Remove outer parentheses | ||||
|     if (leftStr.startsWith('(') && leftStr.endsWith(')')) { | ||||
|       leftStr = leftStr.substring(1, leftStr.length - 1); | ||||
|     } | ||||
|     if (rightStr.startsWith('(') && rightStr.endsWith(')')) { | ||||
|       rightStr = rightStr.substring(1, rightStr.length - 1); | ||||
|     } | ||||
|  | ||||
|     // Add parentheses around base if it's a complex expression | ||||
|     bool needsParens = | ||||
|         !(left is VarExpr || left is IntExpr || left is DoubleExpr); | ||||
|     String base = needsParens ? '($leftStr)' : leftStr; | ||||
|  | ||||
|     return '$base^{$rightStr}'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| // === LogExpr === | ||||
| class LogExpr extends Expr { | ||||
|   final Expr inner; | ||||
|   LogExpr(this.inner); | ||||
|  | ||||
|   @override | ||||
|   Expr simplify() => LogExpr(inner.simplify()); | ||||
|  | ||||
|   @override | ||||
|   Expr evaluate() { | ||||
|     var i = inner.evaluate(); | ||||
|     if (i is IntExpr) { | ||||
|       return DoubleExpr(log(i.value.toDouble())); | ||||
|     } | ||||
|     if (i is FractionExpr) { | ||||
|       return DoubleExpr(log(i.numerator / i.denominator)); | ||||
|     } | ||||
|     if (i is DoubleExpr) { | ||||
|       return DoubleExpr(log(i.value)); | ||||
|     } | ||||
|     return LogExpr(i); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => | ||||
|       LogExpr(inner.substitute(varName, value)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "log($inner)"; | ||||
| } | ||||
|  | ||||
| // === ExpExpr === | ||||
| class ExpExpr extends Expr { | ||||
|   final Expr inner; | ||||
|   ExpExpr(this.inner); | ||||
|  | ||||
|   @override | ||||
|   Expr simplify() => ExpExpr(inner.simplify()); | ||||
|  | ||||
|   @override | ||||
|   Expr evaluate() { | ||||
|     var i = inner.evaluate(); | ||||
|     if (i is IntExpr) { | ||||
|       return DoubleExpr(exp(i.value.toDouble())); | ||||
|     } | ||||
|     if (i is FractionExpr) { | ||||
|       return DoubleExpr(exp(i.numerator / i.denominator)); | ||||
|     } | ||||
|     if (i is DoubleExpr) { | ||||
|       return DoubleExpr(exp(i.value)); | ||||
|     } | ||||
|     return ExpExpr(i); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => | ||||
|       ExpExpr(inner.substitute(varName, value)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "exp($inner)"; | ||||
| } | ||||
|  | ||||
| // === AsinExpr === | ||||
| class AsinExpr extends Expr { | ||||
|   final Expr inner; | ||||
|   AsinExpr(this.inner); | ||||
|  | ||||
|   @override | ||||
|   Expr simplify() => AsinExpr(inner.simplify()); | ||||
|  | ||||
|   @override | ||||
|   Expr evaluate() { | ||||
|     var i = inner.evaluate(); | ||||
|     if (i is IntExpr) { | ||||
|       return DoubleExpr(asin(i.value.toDouble())); | ||||
|     } | ||||
|     if (i is FractionExpr) { | ||||
|       return DoubleExpr(asin(i.numerator / i.denominator)); | ||||
|     } | ||||
|     if (i is DoubleExpr) { | ||||
|       return DoubleExpr(asin(i.value)); | ||||
|     } | ||||
|     return AsinExpr(i); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => | ||||
|       AsinExpr(inner.substitute(varName, value)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "asin($inner)"; | ||||
| } | ||||
|  | ||||
| // === AcosExpr === | ||||
| class AcosExpr extends Expr { | ||||
|   final Expr inner; | ||||
|   AcosExpr(this.inner); | ||||
|  | ||||
|   @override | ||||
|   Expr simplify() => AcosExpr(inner.simplify()); | ||||
|  | ||||
|   @override | ||||
|   Expr evaluate() { | ||||
|     var i = inner.evaluate(); | ||||
|     if (i is IntExpr) { | ||||
|       return DoubleExpr(acos(i.value.toDouble())); | ||||
|     } | ||||
|     if (i is FractionExpr) { | ||||
|       return DoubleExpr(acos(i.numerator / i.denominator)); | ||||
|     } | ||||
|     if (i is DoubleExpr) { | ||||
|       return DoubleExpr(acos(i.value)); | ||||
|     } | ||||
|     return AcosExpr(i); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => | ||||
|       AcosExpr(inner.substitute(varName, value)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "acos($inner)"; | ||||
| } | ||||
|  | ||||
| // === AtanExpr === | ||||
| class AtanExpr extends Expr { | ||||
|   final Expr inner; | ||||
|   AtanExpr(this.inner); | ||||
|  | ||||
|   @override | ||||
|   Expr simplify() => AtanExpr(inner.simplify()); | ||||
|  | ||||
|   @override | ||||
|   Expr evaluate() { | ||||
|     var i = inner.evaluate(); | ||||
|     if (i is IntExpr) { | ||||
|       return DoubleExpr(atan(i.value.toDouble())); | ||||
|     } | ||||
|     if (i is FractionExpr) { | ||||
|       return DoubleExpr(atan(i.numerator / i.denominator)); | ||||
|     } | ||||
|     if (i is DoubleExpr) { | ||||
|       return DoubleExpr(atan(i.value)); | ||||
|     } | ||||
|     return AtanExpr(i); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => | ||||
|       AtanExpr(inner.substitute(varName, value)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "atan($inner)"; | ||||
| } | ||||
|  | ||||
| // === AbsExpr === | ||||
| class AbsExpr extends Expr { | ||||
|   final Expr inner; | ||||
|   AbsExpr(this.inner); | ||||
|  | ||||
|   @override | ||||
|   Expr simplify() => AbsExpr(inner.simplify()); | ||||
|  | ||||
|   @override | ||||
|   Expr evaluate() { | ||||
|     var i = inner.evaluate(); | ||||
|     if (i is IntExpr) { | ||||
|       return IntExpr(i.value.abs()); | ||||
|     } | ||||
|     if (i is FractionExpr) { | ||||
|       return FractionExpr(i.numerator.abs(), i.denominator); | ||||
|     } | ||||
|     if (i is DoubleExpr) { | ||||
|       return DoubleExpr(i.value.abs()); | ||||
|     } | ||||
|     return AbsExpr(i); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => | ||||
|       AbsExpr(inner.substitute(varName, value)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "|$inner|"; | ||||
| } | ||||
|  | ||||
| // === PercentExpr === | ||||
| class PercentExpr extends Expr { | ||||
|   final Expr inner; | ||||
|   PercentExpr(this.inner); | ||||
|  | ||||
|   @override | ||||
|   Expr simplify() => PercentExpr(inner.simplify()); | ||||
|  | ||||
|   @override | ||||
|   Expr evaluate() { | ||||
|     var i = inner.evaluate(); | ||||
|     if (i is IntExpr) { | ||||
|       return DoubleExpr(i.value / 100.0); | ||||
|     } | ||||
|     if (i is DoubleExpr) { | ||||
|       return DoubleExpr(i.value / 100.0); | ||||
|     } | ||||
|     if (i is FractionExpr) { | ||||
|       return DoubleExpr(i.numerator / (i.denominator * 100.0)); | ||||
|     } | ||||
|     return PercentExpr(i); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Expr substitute(String varName, Expr value) => | ||||
|       PercentExpr(inner.substitute(varName, value)); | ||||
|  | ||||
|   @override | ||||
|   String toString() => "$inner%"; | ||||
| } | ||||
|  | ||||
| // === 辅助:识别 a * sqrt(X) 形式 === | ||||
| class _SqrtTerm { | ||||
|   final int coef; | ||||
| @@ -535,5 +967,159 @@ _SqrtTerm? _asSqrtTerm(Expr e) { | ||||
|   return null; | ||||
| } | ||||
|  | ||||
| /// 获取精确三角函数结果 | ||||
| String? getExactTrigResult(String input) { | ||||
|   final cleanInput = input.replaceAll(' ', '').toLowerCase(); | ||||
|  | ||||
|   // 匹配 sin(角度) 模式 | ||||
|   final sinMatch = RegExp(r'^sin\((\d+(?:\+\d+)*)\)$').firstMatch(cleanInput); | ||||
|   if (sinMatch != null) { | ||||
|     final angleExpr = sinMatch.group(1)!; | ||||
|     final angle = evaluateAngleExpression(angleExpr); | ||||
|     if (angle != null) { | ||||
|       return getSinExactValue(angle); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // 匹配 cos(角度) 模式 | ||||
|   final cosMatch = RegExp(r'^cos\((\d+(?:\+\d+)*)\)$').firstMatch(cleanInput); | ||||
|   if (cosMatch != null) { | ||||
|     final angleExpr = cosMatch.group(1)!; | ||||
|     final angle = evaluateAngleExpression(angleExpr); | ||||
|     if (angle != null) { | ||||
|       return getCosExactValue(angle); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // 匹配 tan(角度) 模式 | ||||
|   final tanMatch = RegExp(r'^tan\((\d+(?:\+\d+)*)\)$').firstMatch(cleanInput); | ||||
|   if (tanMatch != null) { | ||||
|     final angleExpr = tanMatch.group(1)!; | ||||
|     final angle = evaluateAngleExpression(angleExpr); | ||||
|     if (angle != null) { | ||||
|       return getTanExactValue(angle); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return null; | ||||
| } | ||||
|  | ||||
| /// 获取 sin 的精确值 | ||||
| String? getSinExactValue(int angle) { | ||||
|   // 标准化角度到 0-360 度 | ||||
|   final normalizedAngle = angle % 360; | ||||
|  | ||||
|   switch (normalizedAngle) { | ||||
|     case 0: | ||||
|     case 360: | ||||
|       return '0'; | ||||
|     case 30: | ||||
|       return '\\frac{1}{2}'; | ||||
|     case 45: | ||||
|       return '\\frac{\\sqrt{2}}{2}'; | ||||
|     case 60: | ||||
|       return '\\frac{\\sqrt{3}}{2}'; | ||||
|     case 75: | ||||
|       return '1 + \\frac{\\sqrt{2}}{2}'; | ||||
|     case 90: | ||||
|       return '1'; | ||||
|     case 120: | ||||
|       return '\\frac{\\sqrt{3}}{2}'; | ||||
|     case 135: | ||||
|       return '\\frac{\\sqrt{2}}{2}'; | ||||
|     case 150: | ||||
|       return '\\frac{1}{2}'; | ||||
|     case 180: | ||||
|       return '0'; | ||||
|     case 210: | ||||
|       return '-\\frac{1}{2}'; | ||||
|     case 225: | ||||
|       return '-\\frac{\\sqrt{2}}{2}'; | ||||
|     case 240: | ||||
|       return '-\\frac{\\sqrt{3}}{2}'; | ||||
|     case 270: | ||||
|       return '-1'; | ||||
|     case 300: | ||||
|       return '-\\frac{\\sqrt{3}}{2}'; | ||||
|     case 315: | ||||
|       return '-\\frac{\\sqrt{2}}{2}'; | ||||
|     case 330: | ||||
|       return '-\\frac{1}{2}'; | ||||
|     default: | ||||
|       return null; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// 获取 cos 的精确值 | ||||
| String? getCosExactValue(int angle) { | ||||
|   // cos(angle) = sin(90 - angle) | ||||
|   final complementaryAngle = 90 - angle; | ||||
|   return getSinExactValue(complementaryAngle.abs()); | ||||
| } | ||||
|  | ||||
| /// 获取 tan 的精确值 | ||||
| String? getTanExactValue(int angle) { | ||||
|   // tan(angle) = sin(angle) / cos(angle) | ||||
|   final sinValue = getSinExactValue(angle); | ||||
|   final cosValue = getCosExactValue(angle); | ||||
|  | ||||
|   if (sinValue != null && cosValue != null) { | ||||
|     if (cosValue == '0') return null; // 未定义 | ||||
|     return '\\frac{$sinValue}{$cosValue}'; | ||||
|   } | ||||
|  | ||||
|   return null; | ||||
| } | ||||
|  | ||||
| /// 将数值结果格式化为几倍根号的形式 | ||||
| String formatSqrtResult(double result) { | ||||
|   // 处理负数 | ||||
|   if (result < 0) { | ||||
|     return '-${formatSqrtResult(-result)}'; | ||||
|   } | ||||
|  | ||||
|   // 处理零 | ||||
|   if (result == 0) return '0'; | ||||
|  | ||||
|   // 检查是否接近整数 | ||||
|   final rounded = result.round(); | ||||
|   if ((result - rounded).abs() < 1e-10) { | ||||
|     return rounded.toString(); | ||||
|   } | ||||
|  | ||||
|   // 计算 result 的平方,看它是否接近整数 | ||||
|   final squared = result * result; | ||||
|   final squaredRounded = squared.round(); | ||||
|  | ||||
|   // 如果 squared 接近整数,说明 result 是某个数的平方根 | ||||
|   if ((squared - squaredRounded).abs() < 1e-6) { | ||||
|     // 寻找最大的完全平方数因子 | ||||
|     int maxSquareFactor = 1; | ||||
|     for (int i = 2; i * i <= squaredRounded; i++) { | ||||
|       if (squaredRounded % (i * i) == 0) { | ||||
|         maxSquareFactor = i * i; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     final coefficient = sqrt(maxSquareFactor).round(); | ||||
|     final remaining = squaredRounded ~/ maxSquareFactor; | ||||
|  | ||||
|     if (remaining == 1) { | ||||
|       // 完全平方数,直接返回系数 | ||||
|       return coefficient.toString(); | ||||
|     } else if (coefficient == 1) { | ||||
|       return '\\sqrt{$remaining}'; | ||||
|     } else { | ||||
|       return '$coefficient\\sqrt{$remaining}'; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // 如果不是平方根的结果,返回原始数值(保留几位小数) | ||||
|   return result | ||||
|       .toStringAsFixed(6) | ||||
|       .replaceAll(RegExp(r'\.0+$'), '') | ||||
|       .replaceAll(RegExp(r'\.$'), ''); | ||||
| } | ||||
|  | ||||
| /// 辗转相除法求 gcd | ||||
| int _gcd(int a, int b) => b == 0 ? a : _gcd(b, a % b); | ||||
|   | ||||
							
								
								
									
										180
									
								
								lib/parser.dart
									
									
									
									
									
								
							
							
						
						
									
										180
									
								
								lib/parser.dart
									
									
									
									
									
								
							| @@ -17,7 +17,15 @@ class Parser { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Expr parse() => parseAdd(); | ||||
|   Expr parse() { | ||||
|     var expr = parseAdd(); | ||||
|     skipSpaces(); | ||||
|     if (!isEnd && current == '%') { | ||||
|       eat(); | ||||
|       expr = PercentExpr(expr); | ||||
|     } | ||||
|     return expr; | ||||
|   } | ||||
|  | ||||
|   Expr parseAdd() { | ||||
|     var expr = parseMul(); | ||||
| @@ -33,12 +41,12 @@ class Parser { | ||||
|   } | ||||
|  | ||||
|   Expr parseMul() { | ||||
|     var expr = parseAtom(); | ||||
|     var expr = parsePow(); | ||||
|     skipSpaces(); | ||||
|     while (!isEnd && (current == '*' || current == '/')) { | ||||
|       var op = current; | ||||
|       eat(); | ||||
|       var right = parseAtom(); | ||||
|       var right = parsePow(); | ||||
|       if (op == '*') { | ||||
|         expr = MulExpr(expr, right); | ||||
|       } else { | ||||
| @@ -46,66 +54,180 @@ class Parser { | ||||
|       } | ||||
|       skipSpaces(); | ||||
|     } | ||||
|     // Handle percentage operator | ||||
|     skipSpaces(); | ||||
|     if (!isEnd && current == '%') { | ||||
|       eat(); | ||||
|       expr = PercentExpr(expr); | ||||
|     } | ||||
|     return expr; | ||||
|   } | ||||
|  | ||||
|   Expr parsePow() { | ||||
|     var expr = parseAtom(); | ||||
|     skipSpaces(); | ||||
|     if (!isEnd && current == '^') { | ||||
|       eat(); | ||||
|       var right = parsePow(); // right associative | ||||
|       return PowExpr(expr, right); | ||||
|     } | ||||
|     return expr; | ||||
|   } | ||||
|  | ||||
|   Expr parseAtom() { | ||||
|     skipSpaces(); | ||||
|     bool negative = false; | ||||
|     if (current == '-') { | ||||
|       negative = true; | ||||
|       eat(); | ||||
|       skipSpaces(); | ||||
|     } | ||||
|     Expr expr; | ||||
|     if (current == '(') { | ||||
|       eat(); | ||||
|       var expr = parse(); | ||||
|       expr = parse(); | ||||
|       if (current != ')') throw Exception("缺少 )"); | ||||
|       eat(); | ||||
|       return expr; | ||||
|     } | ||||
|  | ||||
|     if (input.startsWith("sqrt", pos)) { | ||||
|     } else if (input.startsWith("sqrt", pos)) { | ||||
|       pos += 4; | ||||
|       if (current != '(') throw Exception("sqrt 缺少 ("); | ||||
|       eat(); | ||||
|       var inner = parse(); | ||||
|       if (current != ')') throw Exception("sqrt 缺少 )"); | ||||
|       eat(); | ||||
|       return SqrtExpr(inner); | ||||
|     } | ||||
|  | ||||
|     if (input.startsWith("cos", pos)) { | ||||
|       expr = SqrtExpr(inner); | ||||
|     } else if (input.startsWith("cos", pos)) { | ||||
|       pos += 3; | ||||
|       if (current != '(') throw Exception("cos 缺少 ("); | ||||
|       eat(); | ||||
|       var inner = parse(); | ||||
|       if (current != ')') throw Exception("cos 缺少 )"); | ||||
|       eat(); | ||||
|       return CosExpr(inner); | ||||
|     } | ||||
|  | ||||
|     if (input.startsWith("sin", pos)) { | ||||
|       expr = CosExpr(inner); | ||||
|     } else if (input.startsWith("sin", pos)) { | ||||
|       pos += 3; | ||||
|       if (current != '(') throw Exception("sin 缺少 ("); | ||||
|       eat(); | ||||
|       var inner = parse(); | ||||
|       if (current != ')') throw Exception("sin 缺少 )"); | ||||
|       eat(); | ||||
|       return SinExpr(inner); | ||||
|     } | ||||
|  | ||||
|     if (input.startsWith("tan", pos)) { | ||||
|       expr = SinExpr(inner); | ||||
|     } else if (input.startsWith("tan", pos)) { | ||||
|       pos += 3; | ||||
|       if (current != '(') throw Exception("tan 缺少 ("); | ||||
|       eat(); | ||||
|       var inner = parse(); | ||||
|       if (current != ')') throw Exception("tan 缺少 )"); | ||||
|       eat(); | ||||
|       return TanExpr(inner); | ||||
|     } | ||||
|  | ||||
|     // 解析整数 | ||||
|     var buf = ''; | ||||
|     while (!isEnd && RegExp(r'\d').hasMatch(current)) { | ||||
|       buf += current; | ||||
|       expr = TanExpr(inner); | ||||
|     } else if (input.startsWith("log", pos)) { | ||||
|       pos += 3; | ||||
|       if (current != '(') throw Exception("log 缺少 ("); | ||||
|       eat(); | ||||
|       var inner = parse(); | ||||
|       if (current != ')') throw Exception("log 缺少 )"); | ||||
|       eat(); | ||||
|       expr = LogExpr(inner); | ||||
|     } else if (input.startsWith("exp", pos)) { | ||||
|       pos += 3; | ||||
|       if (current != '(') throw Exception("exp 缺少 ("); | ||||
|       eat(); | ||||
|       var inner = parse(); | ||||
|       if (current != ')') throw Exception("exp 缺少 )"); | ||||
|       eat(); | ||||
|       expr = ExpExpr(inner); | ||||
|     } else if (input.startsWith("asin", pos)) { | ||||
|       pos += 4; | ||||
|       if (current != '(') throw Exception("asin 缺少 ("); | ||||
|       eat(); | ||||
|       var inner = parse(); | ||||
|       if (current != ')') throw Exception("asin 缺少 )"); | ||||
|       eat(); | ||||
|       expr = AsinExpr(inner); | ||||
|     } else if (input.startsWith("acos", pos)) { | ||||
|       pos += 4; | ||||
|       if (current != '(') throw Exception("acos 缺少 ("); | ||||
|       eat(); | ||||
|       var inner = parse(); | ||||
|       if (current != ')') throw Exception("acos 缺少 )"); | ||||
|       eat(); | ||||
|       expr = AcosExpr(inner); | ||||
|     } else if (input.startsWith("atan", pos)) { | ||||
|       pos += 4; | ||||
|       if (current != '(') throw Exception("atan 缺少 ("); | ||||
|       eat(); | ||||
|       var inner = parse(); | ||||
|       if (current != ')') throw Exception("atan 缺少 )"); | ||||
|       eat(); | ||||
|       expr = AtanExpr(inner); | ||||
|     } else if (current == '|') { | ||||
|       eat(); | ||||
|       var inner = parse(); | ||||
|       if (current != '|') throw Exception("abs 缺少 |"); | ||||
|       eat(); | ||||
|       expr = AbsExpr(inner); | ||||
|     } else if (RegExp(r'[a-zA-Z]').hasMatch(current)) { | ||||
|       var varName = current; | ||||
|       eat(); | ||||
|       expr = VarExpr(varName); | ||||
|     } else { | ||||
|       // 解析数字 (整数或小数) | ||||
|       var buf = ''; | ||||
|       bool hasDot = false; | ||||
|       while (!isEnd && | ||||
|           (RegExp(r'\d').hasMatch(current) || (!hasDot && current == '.'))) { | ||||
|         if (current == '.') hasDot = true; | ||||
|         buf += current; | ||||
|         eat(); | ||||
|       } | ||||
|       if (buf.isEmpty) throw Exception("无法解析: $current"); | ||||
|       if (hasDot) { | ||||
|         expr = DoubleExpr(double.parse(buf)); | ||||
|       } else { | ||||
|         expr = IntExpr(int.parse(buf)); | ||||
|       } | ||||
|     } | ||||
|     if (buf.isEmpty) throw Exception("无法解析: $current"); | ||||
|     return IntExpr(int.parse(buf)); | ||||
|     if (negative) { | ||||
|       expr = SubExpr(IntExpr(0), expr); | ||||
|     } | ||||
|     return expr; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /// 计算角度表达式(如 30+45 = 75) | ||||
| int? evaluateAngleExpression(String expr) { | ||||
|   final parts = expr.split('+'); | ||||
|   int sum = 0; | ||||
|   for (final part in parts) { | ||||
|     final num = int.tryParse(part.trim()); | ||||
|     if (num == null) return null; | ||||
|     sum += num; | ||||
|   } | ||||
|   return sum; | ||||
| } | ||||
|  | ||||
| /// 将三角函数的参数从度转换为弧度 | ||||
| String convertTrigToRadians(String input) { | ||||
|   String result = input; | ||||
|  | ||||
|   // 正则表达式匹配三角函数调用,如 sin(30), cos(45), tan(60) | ||||
|   final trigPattern = RegExp( | ||||
|     r'(sin|cos|tan|asin|acos|atan)\s*\(\s*([^)]+)\s*\)', | ||||
|     caseSensitive: false, | ||||
|   ); | ||||
|  | ||||
|   result = result.replaceAllMapped(trigPattern, (match) { | ||||
|     final func = match.group(1)!; | ||||
|     final arg = match.group(2)!; | ||||
|  | ||||
|     // 如果参数已经是弧度相关的表达式(包含 pi 或 π),则不转换 | ||||
|     if (arg.contains('pi') || arg.contains('π') || arg.contains('rad')) { | ||||
|       return '$func($arg)'; | ||||
|     } | ||||
|  | ||||
|     // 将度数转换为弧度:度 * π / 180 | ||||
|     return '$func(($arg)*(π/180))'; | ||||
|   }); | ||||
|  | ||||
|   return result; | ||||
| } | ||||
|   | ||||
| @@ -2,8 +2,7 @@ import 'package:flutter/material.dart'; | ||||
| import 'package:latext/latext.dart'; | ||||
| import 'package:simple_math_calc/models/calculation_step.dart'; | ||||
| import 'package:simple_math_calc/solver.dart'; | ||||
| import 'package:fl_chart/fl_chart.dart'; | ||||
| import 'package:math_expressions/math_expressions.dart' as math_expressions; | ||||
| import 'package:simple_math_calc/widgets/graph_card.dart'; | ||||
| import 'dart:math'; | ||||
|  | ||||
| class CalculatorHomePage extends StatefulWidget { | ||||
| @@ -20,6 +19,7 @@ class _CalculatorHomePageState extends State<CalculatorHomePage> { | ||||
|  | ||||
|   CalculationResult? _result; | ||||
|   bool _isLoading = false; | ||||
|   bool _isFunctionMode = false; | ||||
|   double _zoomFactor = 1.0; | ||||
|  | ||||
|   @override | ||||
| @@ -40,126 +40,23 @@ class _CalculatorHomePageState extends State<CalculatorHomePage> { | ||||
|     setState(() {}); | ||||
|   } | ||||
|  | ||||
|   /// 生成函数图表的点 | ||||
|   List<FlSpot> _generatePlotPoints(String expression, double zoomFactor) { | ||||
|     try { | ||||
|       // 如果是方程,取左边作为函数 | ||||
|       String functionExpr = expression; | ||||
|       if (expression.contains('=')) { | ||||
|         functionExpr = expression.split('=')[0].trim(); | ||||
|       } | ||||
|  | ||||
|       // 如果表达式不包含 x,返回空列表 | ||||
|       if (!functionExpr.contains('x') && !functionExpr.contains('X')) { | ||||
|         return []; | ||||
|       } | ||||
|  | ||||
|       // 预处理表达式,确保格式正确 | ||||
|       functionExpr = functionExpr.replaceAll(' ', ''); | ||||
|  | ||||
|       // 在数字和变量之间插入乘号 | ||||
|       functionExpr = functionExpr.replaceAllMapped( | ||||
|         RegExp(r'(\d)([a-zA-Z])'), | ||||
|         (match) => '${match.group(1)}*${match.group(2)}', | ||||
|       ); | ||||
|  | ||||
|       // 在变量和数字之间插入乘号 (如 x2 -> x*2) | ||||
|       functionExpr = functionExpr.replaceAllMapped( | ||||
|         RegExp(r'([a-zA-Z])(\d)'), | ||||
|         (match) => '${match.group(1)}*${match.group(2)}', | ||||
|       ); | ||||
|  | ||||
|       // 解析表达式 | ||||
|       final parser = math_expressions.ShuntingYardParser(); | ||||
|       final expr = parser.parse(functionExpr); | ||||
|  | ||||
|       // 创建变量 x | ||||
|       final x = math_expressions.Variable('x'); | ||||
|  | ||||
|       // 根据缩放因子动态调整范围和步长 | ||||
|       final range = 10.0 * zoomFactor; | ||||
|       final step = max(0.05, 0.2 / zoomFactor); // 缩放时步长更小,放大时步长更大 | ||||
|  | ||||
|       // 生成点 | ||||
|       List<FlSpot> points = []; | ||||
|       for (double i = -range; i <= range; i += step) { | ||||
|         try { | ||||
|           final context = math_expressions.ContextModel() | ||||
|             ..bindVariable(x, math_expressions.Number(i)); | ||||
|           final evaluator = math_expressions.RealEvaluator(context); | ||||
|           final y = evaluator.evaluate(expr); | ||||
|  | ||||
|           if (y.isFinite && !y.isNaN) { | ||||
|             points.add(FlSpot(i, y.toDouble())); | ||||
|           } | ||||
|         } catch (e) { | ||||
|           // 跳过无法计算的点 | ||||
|           continue; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // 如果没有足够的点,返回空列表 | ||||
|       if (points.length < 2) { | ||||
|         debugPrint('Generated ${points.length} dots'); | ||||
|         return []; | ||||
|       } | ||||
|  | ||||
|       // 排序点按 x 值 | ||||
|       points.sort((a, b) => a.x.compareTo(b.x)); | ||||
|  | ||||
|       debugPrint( | ||||
|         'Generated ${points.length} dots with zoom factor $zoomFactor', | ||||
|       ); | ||||
|       return points; | ||||
|     } catch (e) { | ||||
|       debugPrint('Error generating plot points: $e'); | ||||
|       return []; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// 计算图表的数据范围 | ||||
|   ({double minX, double maxX, double minY, double maxY}) _calculateChartBounds( | ||||
|     List<FlSpot> points, | ||||
|     double zoomFactor, | ||||
|   ) { | ||||
|     if (points.isEmpty) { | ||||
|       return ( | ||||
|         minX: -10 * zoomFactor, | ||||
|         maxX: 10 * zoomFactor, | ||||
|         minY: -50 * zoomFactor, | ||||
|         maxY: 50 * zoomFactor, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     double minX = points.first.x; | ||||
|     double maxX = points.first.x; | ||||
|     double minY = points.first.y; | ||||
|     double maxY = points.first.y; | ||||
|  | ||||
|     for (final point in points) { | ||||
|       minX = min(minX, point.x); | ||||
|       maxX = max(maxX, point.x); | ||||
|       minY = min(minY, point.y); | ||||
|       maxY = max(maxY, point.y); | ||||
|     } | ||||
|  | ||||
|     // 添加边距 | ||||
|     final xPadding = (maxX - minX) * 0.1; | ||||
|     final yPadding = (maxY - minY) * 0.1; | ||||
|  | ||||
|     return ( | ||||
|       minX: minX - xPadding, | ||||
|       maxX: maxX + xPadding, | ||||
|       minY: minY - yPadding, | ||||
|       maxY: maxY + yPadding, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _solveEquation() { | ||||
|     if (_controller.text.isEmpty) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     final input = _controller.text.trim(); | ||||
|     final normalizedInput = input.replaceAll(' ', ''); | ||||
|     if (normalizedInput.toLowerCase().startsWith('y=')) { | ||||
|       setState(() { | ||||
|         _isFunctionMode = true; | ||||
|         _result = null; | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     setState(() { | ||||
|       _isFunctionMode = false; | ||||
|       _isLoading = true; | ||||
|       _result = null; // 清除上次结果 | ||||
|     }); | ||||
| @@ -240,6 +137,13 @@ class _CalculatorHomePageState extends State<CalculatorHomePage> { | ||||
|           Expanded( | ||||
|             child: _isLoading | ||||
|                 ? const Center(child: CircularProgressIndicator()) | ||||
|                 : _isFunctionMode | ||||
|                 ? GraphCard( | ||||
|                     expression: _controller.text, | ||||
|                     zoomFactor: _zoomFactor, | ||||
|                     onZoomIn: _zoomIn, | ||||
|                     onZoomOut: _zoomOut, | ||||
|                   ) | ||||
|                 : _result == null | ||||
|                 ? const Center(child: Text('请输入方程开始计算')) | ||||
|                 : buildResultView(_result!), | ||||
| @@ -354,106 +258,6 @@ class _CalculatorHomePageState extends State<CalculatorHomePage> { | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|         const SizedBox(height: 16), | ||||
|         Card( | ||||
|           child: Padding( | ||||
|             padding: const EdgeInsets.all(16.0), | ||||
|             child: Column( | ||||
|               children: [ | ||||
|                 Row( | ||||
|                   mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                   children: [ | ||||
|                     Padding( | ||||
|                       padding: const EdgeInsets.only(left: 4), | ||||
|                       child: Text( | ||||
|                         '函数图像', | ||||
|                         style: Theme.of(context).textTheme.titleMedium, | ||||
|                       ), | ||||
|                     ), | ||||
|                     Row( | ||||
|                       mainAxisAlignment: MainAxisAlignment.center, | ||||
|                       children: [ | ||||
|                         IconButton( | ||||
|                           onPressed: _zoomIn, | ||||
|                           icon: Icon(Icons.zoom_in), | ||||
|                           tooltip: '放大', | ||||
|                           padding: EdgeInsets.zero, | ||||
|                           visualDensity: VisualDensity.compact, | ||||
|                         ), | ||||
|                         IconButton( | ||||
|                           onPressed: _zoomOut, | ||||
|                           icon: Icon(Icons.zoom_out), | ||||
|                           tooltip: '缩小', | ||||
|                           padding: EdgeInsets.zero, | ||||
|                           visualDensity: VisualDensity.compact, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 const SizedBox(height: 24), | ||||
|                 SizedBox( | ||||
|                   height: 340, | ||||
|                   child: Builder( | ||||
|                     builder: (context) { | ||||
|                       final points = _generatePlotPoints( | ||||
|                         _controller.text, | ||||
|                         _zoomFactor, | ||||
|                       ); | ||||
|                       final bounds = _calculateChartBounds(points, _zoomFactor); | ||||
|  | ||||
|                       return LineChart( | ||||
|                         LineChartData( | ||||
|                           gridData: FlGridData(show: true), | ||||
|                           titlesData: FlTitlesData( | ||||
|                             leftTitles: AxisTitles( | ||||
|                               sideTitles: SideTitles( | ||||
|                                 showTitles: true, | ||||
|                                 reservedSize: 40, | ||||
|                               ), | ||||
|                             ), | ||||
|                             bottomTitles: AxisTitles( | ||||
|                               sideTitles: SideTitles( | ||||
|                                 showTitles: true, | ||||
|                                 reservedSize: 30, | ||||
|                               ), | ||||
|                             ), | ||||
|                             topTitles: AxisTitles( | ||||
|                               sideTitles: SideTitles(showTitles: false), | ||||
|                             ), | ||||
|                             rightTitles: AxisTitles( | ||||
|                               sideTitles: SideTitles(showTitles: false), | ||||
|                             ), | ||||
|                           ), | ||||
|                           borderData: FlBorderData( | ||||
|                             show: true, | ||||
|                             border: Border.all( | ||||
|                               color: Theme.of(context).colorScheme.outline, | ||||
|                             ), | ||||
|                           ), | ||||
|                           lineBarsData: [ | ||||
|                             LineChartBarData( | ||||
|                               spots: points, | ||||
|                               isCurved: true, | ||||
|                               color: Theme.of(context).colorScheme.primary, | ||||
|                               barWidth: 3, | ||||
|                               belowBarData: BarAreaData(show: false), | ||||
|                               dotData: FlDotData(show: false), | ||||
|                             ), | ||||
|                           ], | ||||
|                           minX: bounds.minX, | ||||
|                           maxX: bounds.maxX, | ||||
|                           minY: bounds.minY, | ||||
|                           maxY: bounds.maxY, | ||||
|                         ), | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										260
									
								
								lib/solver.dart
									
									
									
									
									
								
							
							
						
						
									
										260
									
								
								lib/solver.dart
									
									
									
									
									
								
							| @@ -1,4 +1,5 @@ | ||||
| import 'dart:math'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:rational/rational.dart'; | ||||
| import 'models/calculation_step.dart'; | ||||
| import 'calculator.dart'; | ||||
| @@ -52,17 +53,22 @@ class SolverService { | ||||
|   /// 1. 求解简单表达式 | ||||
|   CalculationResult _solveSimpleExpression(String input) { | ||||
|     final steps = <CalculationStep>[]; | ||||
|     // Parse the input to get LaTeX-formatted version | ||||
|     final parser = Parser(input); | ||||
|     final parsedExpr = parser.parse(); | ||||
|     final latexInput = parsedExpr.toString().replaceAll('*', '\\cdot'); | ||||
|  | ||||
|     steps.add( | ||||
|       CalculationStep( | ||||
|         stepNumber: 1, | ||||
|         title: '表达式求值', | ||||
|         explanation: '这是一个标准的数学表达式,我们将直接计算其结果。', | ||||
|         formula: '\$\$$input\$\$', | ||||
|         formula: '\$\$$latexInput\$\$', | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     // 检查是否为特殊三角函数值,可以返回精确结果 | ||||
|     final exactTrigResult = _getExactTrigResult(input); | ||||
|     final exactTrigResult = getExactTrigResult(input); | ||||
|     if (exactTrigResult != null) { | ||||
|       return CalculationResult( | ||||
|         steps: steps, | ||||
| @@ -71,7 +77,7 @@ class SolverService { | ||||
|     } | ||||
|  | ||||
|     // 预处理输入,将三角函数的参数从度转换为弧度 | ||||
|     String processedInput = _convertTrigToRadians(input); | ||||
|     String processedInput = convertTrigToRadians(input); | ||||
|  | ||||
|     try { | ||||
|       // 使用自定义解析器解析表达式 | ||||
| @@ -99,7 +105,7 @@ class SolverService { | ||||
|       } | ||||
|  | ||||
|       // 尝试将结果格式化为几倍根号的形式 | ||||
|       final formattedResult = _formatSqrtResult(result); | ||||
|       final formattedResult = formatSqrtResult(result); | ||||
|  | ||||
|       return CalculationResult( | ||||
|         steps: steps, | ||||
| @@ -113,12 +119,17 @@ class SolverService { | ||||
|   /// 2. 求解一元一次方程 | ||||
|   CalculationResult _solveLinearEquation(String input) { | ||||
|     final steps = <CalculationStep>[]; | ||||
|     // Parse the input to get LaTeX-formatted version | ||||
|     final parser = Parser(input); | ||||
|     final parsedExpr = parser.parse(); | ||||
|     final latexInput = parsedExpr.toString().replaceAll('*', '\\cdot'); | ||||
|  | ||||
|     steps.add( | ||||
|       CalculationStep( | ||||
|         stepNumber: 0, | ||||
|         title: '原方程', | ||||
|         explanation: '这是一元一次方程。', | ||||
|         formula: '\$\$$input\$\$', | ||||
|         formula: '\$\$$latexInput\$\$', | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
| @@ -503,198 +514,6 @@ ${b1}y &= ${c1 - a1 * x.toDouble()} | ||||
|  | ||||
|   /// ---- 辅助函数 ---- | ||||
|  | ||||
|   /// 获取精确三角函数结果 | ||||
|   String? _getExactTrigResult(String input) { | ||||
|     final cleanInput = input.replaceAll(' ', '').toLowerCase(); | ||||
|  | ||||
|     // 匹配 sin(角度) 模式 | ||||
|     final sinMatch = RegExp(r'^sin\((\d+(?:\+\d+)*)\)$').firstMatch(cleanInput); | ||||
|     if (sinMatch != null) { | ||||
|       final angleExpr = sinMatch.group(1)!; | ||||
|       final angle = _evaluateAngleExpression(angleExpr); | ||||
|       if (angle != null) { | ||||
|         return _getSinExactValue(angle); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // 匹配 cos(角度) 模式 | ||||
|     final cosMatch = RegExp(r'^cos\((\d+(?:\+\d+)*)\)$').firstMatch(cleanInput); | ||||
|     if (cosMatch != null) { | ||||
|       final angleExpr = cosMatch.group(1)!; | ||||
|       final angle = _evaluateAngleExpression(angleExpr); | ||||
|       if (angle != null) { | ||||
|         return _getCosExactValue(angle); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // 匹配 tan(角度) 模式 | ||||
|     final tanMatch = RegExp(r'^tan\((\d+(?:\+\d+)*)\)$').firstMatch(cleanInput); | ||||
|     if (tanMatch != null) { | ||||
|       final angleExpr = tanMatch.group(1)!; | ||||
|       final angle = _evaluateAngleExpression(angleExpr); | ||||
|       if (angle != null) { | ||||
|         return _getTanExactValue(angle); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   /// 计算角度表达式(如 30+45 = 75) | ||||
|   int? _evaluateAngleExpression(String expr) { | ||||
|     final parts = expr.split('+'); | ||||
|     int sum = 0; | ||||
|     for (final part in parts) { | ||||
|       final num = int.tryParse(part.trim()); | ||||
|       if (num == null) return null; | ||||
|       sum += num; | ||||
|     } | ||||
|     return sum; | ||||
|   } | ||||
|  | ||||
|   /// 获取 sin 的精确值 | ||||
|   String? _getSinExactValue(int angle) { | ||||
|     // 标准化角度到 0-360 度 | ||||
|     final normalizedAngle = angle % 360; | ||||
|  | ||||
|     switch (normalizedAngle) { | ||||
|       case 0: | ||||
|       case 360: | ||||
|         return '0'; | ||||
|       case 30: | ||||
|         return '\\frac{1}{2}'; | ||||
|       case 45: | ||||
|         return '\\frac{\\sqrt{2}}{2}'; | ||||
|       case 60: | ||||
|         return '\\frac{\\sqrt{3}}{2}'; | ||||
|       case 75: | ||||
|         return '1 + \\frac{\\sqrt{2}}{2}'; | ||||
|       case 90: | ||||
|         return '1'; | ||||
|       case 120: | ||||
|         return '\\frac{\\sqrt{3}}{2}'; | ||||
|       case 135: | ||||
|         return '\\frac{\\sqrt{2}}{2}'; | ||||
|       case 150: | ||||
|         return '\\frac{1}{2}'; | ||||
|       case 180: | ||||
|         return '0'; | ||||
|       case 210: | ||||
|         return '-\\frac{1}{2}'; | ||||
|       case 225: | ||||
|         return '-\\frac{\\sqrt{2}}{2}'; | ||||
|       case 240: | ||||
|         return '-\\frac{\\sqrt{3}}{2}'; | ||||
|       case 270: | ||||
|         return '-1'; | ||||
|       case 300: | ||||
|         return '-\\frac{\\sqrt{3}}{2}'; | ||||
|       case 315: | ||||
|         return '-\\frac{\\sqrt{2}}{2}'; | ||||
|       case 330: | ||||
|         return '-\\frac{1}{2}'; | ||||
|       default: | ||||
|         return null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// 获取 cos 的精确值 | ||||
|   String? _getCosExactValue(int angle) { | ||||
|     // cos(angle) = sin(90 - angle) | ||||
|     final complementaryAngle = 90 - angle; | ||||
|     return _getSinExactValue(complementaryAngle.abs()); | ||||
|   } | ||||
|  | ||||
|   /// 获取 tan 的精确值 | ||||
|   String? _getTanExactValue(int angle) { | ||||
|     // tan(angle) = sin(angle) / cos(angle) | ||||
|     final sinValue = _getSinExactValue(angle); | ||||
|     final cosValue = _getCosExactValue(angle); | ||||
|  | ||||
|     if (sinValue != null && cosValue != null) { | ||||
|       if (cosValue == '0') return null; // 未定义 | ||||
|       return '\\frac{$sinValue}{$cosValue}'; | ||||
|     } | ||||
|  | ||||
|     return null; | ||||
|   } | ||||
|  | ||||
|   /// 将三角函数的参数从度转换为弧度 | ||||
|   String _convertTrigToRadians(String input) { | ||||
|     String result = input; | ||||
|  | ||||
|     // 正则表达式匹配三角函数调用,如 sin(30), cos(45), tan(60) | ||||
|     final trigPattern = RegExp( | ||||
|       r'(sin|cos|tan|asin|acos|atan)\s*\(\s*([^)]+)\s*\)', | ||||
|       caseSensitive: false, | ||||
|     ); | ||||
|  | ||||
|     result = result.replaceAllMapped(trigPattern, (match) { | ||||
|       final func = match.group(1)!; | ||||
|       final arg = match.group(2)!; | ||||
|  | ||||
|       // 如果参数已经是弧度相关的表达式(包含 pi 或 π),则不转换 | ||||
|       if (arg.contains('pi') || arg.contains('π') || arg.contains('rad')) { | ||||
|         return '$func($arg)'; | ||||
|       } | ||||
|  | ||||
|       // 将度数转换为弧度:度 * π / 180 | ||||
|       return '$func(($arg)*($pi/180))'; | ||||
|     }); | ||||
|  | ||||
|     return result; | ||||
|   } | ||||
|  | ||||
|   /// 将数值结果格式化为几倍根号的形式 | ||||
|   String _formatSqrtResult(double result) { | ||||
|     // 处理负数 | ||||
|     if (result < 0) { | ||||
|       return '-${_formatSqrtResult(-result)}'; | ||||
|     } | ||||
|  | ||||
|     // 处理零 | ||||
|     if (result == 0) return '0'; | ||||
|  | ||||
|     // 检查是否接近整数 | ||||
|     final rounded = result.round(); | ||||
|     if ((result - rounded).abs() < 1e-10) { | ||||
|       return rounded.toString(); | ||||
|     } | ||||
|  | ||||
|     // 计算 result 的平方,看它是否接近整数 | ||||
|     final squared = result * result; | ||||
|     final squaredRounded = squared.round(); | ||||
|  | ||||
|     // 如果 squared 接近整数,说明 result 是某个数的平方根 | ||||
|     if ((squared - squaredRounded).abs() < 1e-6) { | ||||
|       // 寻找最大的完全平方数因子 | ||||
|       int maxSquareFactor = 1; | ||||
|       for (int i = 2; i * i <= squaredRounded; i++) { | ||||
|         if (squaredRounded % (i * i) == 0) { | ||||
|           maxSquareFactor = i * i; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       final coefficient = sqrt(maxSquareFactor).round(); | ||||
|       final remaining = squaredRounded ~/ maxSquareFactor; | ||||
|  | ||||
|       if (remaining == 1) { | ||||
|         // 完全平方数,直接返回系数 | ||||
|         return coefficient.toString(); | ||||
|       } else if (coefficient == 1) { | ||||
|         return '\\sqrt{$remaining}'; | ||||
|       } else { | ||||
|         return '$coefficient\\sqrt{$remaining}'; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // 如果不是平方根的结果,返回原始数值(保留几位小数) | ||||
|     return result | ||||
|         .toStringAsFixed(6) | ||||
|         .replaceAll(RegExp(r'\.0+$'), '') | ||||
|         .replaceAll(RegExp(r'\.$'), ''); | ||||
|   } | ||||
|  | ||||
|   String _expandExpressions(String input) { | ||||
|     String result = input; | ||||
|     int maxIterations = 10; // Prevent infinite loops | ||||
| @@ -735,26 +554,26 @@ ${b1}y &= ${c1 - a1 * x.toDouble()} | ||||
|       if (factorMulMatch != null) { | ||||
|         final factor1 = factorMulMatch.group(1)!; | ||||
|         final factor2 = factorMulMatch.group(2)!; | ||||
|         print('Expanding: ($factor1) * ($factor2)'); | ||||
|         debugPrint('Expanding: ($factor1) * ($factor2)'); | ||||
|  | ||||
|         final coeffs1 = _parsePolynomial(factor1); | ||||
|         final coeffs2 = _parsePolynomial(factor2); | ||||
|         print('Coeffs1: $coeffs1, Coeffs2: $coeffs2'); | ||||
|         debugPrint('Coeffs1: $coeffs1, Coeffs2: $coeffs2'); | ||||
|  | ||||
|         final a = coeffs1[1] ?? 0; | ||||
|         final b = coeffs1[0] ?? 0; | ||||
|         final c = coeffs2[1] ?? 0; | ||||
|         final d = coeffs2[0] ?? 0; | ||||
|         print('a=$a, b=$b, c=$c, d=$d'); | ||||
|         debugPrint('a=$a, b=$b, c=$c, d=$d'); | ||||
|  | ||||
|         final newA = a * c; | ||||
|         final newB = a * d + b * c; | ||||
|         final newC = b * d; | ||||
|         print('newA=$newA, newB=$newB, newC=$newC'); | ||||
|         debugPrint('newA=$newA, newB=$newB, newC=$newC'); | ||||
|  | ||||
|         final expanded = | ||||
|             '${newA}x^2${newB >= 0 ? '+' : ''}${newB}x${newC >= 0 ? '+' : ''}$newC'; | ||||
|         print('Expanded result: $expanded'); | ||||
|         debugPrint('Expanded result: $expanded'); | ||||
|  | ||||
|         result = result.replaceFirst(factorMulMatch.group(0)!, expanded); | ||||
|         iterationCount++; | ||||
| @@ -1215,7 +1034,7 @@ ${b1}y &= ${c1 - a1 * x.toDouble()} | ||||
|  | ||||
|         if (simplifiedB == 0) { | ||||
|           // Just the sqrt part with correct sign | ||||
|           return isPlus ? '$sqrtExpr' : '-$sqrtExpr'; | ||||
|           return isPlus ? sqrtExpr : '-$sqrtExpr'; | ||||
|         } else if (simplifiedB == 1) { | ||||
|           // +1 * sqrt part | ||||
|           return isPlus ? '1 + $sqrtExpr' : '1 - $sqrtExpr'; | ||||
| @@ -1234,11 +1053,11 @@ ${b1}y &= ${c1 - a1 * x.toDouble()} | ||||
|         } | ||||
|       } else { | ||||
|         // Cannot simplify, use fraction form | ||||
|         final bStr = b > 0 ? '${bInt}' : '(${bInt})'; | ||||
|         final bStr = b > 0 ? '$bInt' : '($bInt)'; | ||||
|         final signStr = isPlus ? '+' : '-'; | ||||
|         final numerator = b > 0 | ||||
|             ? '-$bStr $signStr $sqrtExpr' | ||||
|             : '(${bInt}) $signStr $sqrtExpr'; | ||||
|             : '($bInt) $signStr $sqrtExpr'; | ||||
|  | ||||
|         if (denominator == 2) { | ||||
|           return '\\frac{$numerator}{2}'; | ||||
| @@ -1262,8 +1081,7 @@ ${b1}y &= ${c1 - a1 * x.toDouble()} | ||||
|  | ||||
|   /// 格式化原始方程,保持符号形式 | ||||
|   String _formatOriginalEquation(String input) { | ||||
|     // Simply return the original equation with proper LaTeX formatting | ||||
|     // This avoids complex parsing issues and preserves the original symbolic form | ||||
|     // Parse the equation and convert to LaTeX | ||||
|     String result = input.replaceAll(' ', ''); | ||||
|  | ||||
|     // 确保方程格式正确 | ||||
| @@ -1271,9 +1089,31 @@ ${b1}y &= ${c1 - a1 * x.toDouble()} | ||||
|       result = '$result=0'; | ||||
|     } | ||||
|  | ||||
|     // Replace sqrt with LaTeX format | ||||
|     result = result.replaceAll('sqrt(', '\\sqrt{'); | ||||
|     result = result.replaceAll(')', '}'); | ||||
|     final parts = result.split('='); | ||||
|     if (parts.length == 2) { | ||||
|       try { | ||||
|         final leftParser = Parser(parts[0]); | ||||
|         final leftExpr = leftParser.parse(); | ||||
|         final rightParser = Parser(parts[1]); | ||||
|         final rightExpr = rightParser.parse(); | ||||
|         result = | ||||
|             '${leftExpr.toString().replaceAll('*', '\\cdot')}=${rightExpr.toString().replaceAll('*', '\\cdot')}'; | ||||
|       } catch (e) { | ||||
|         // Fallback to original if parsing fails | ||||
|         result = result.replaceAll('sqrt(', '\\sqrt{'); | ||||
|         result = result.replaceAll(')', '}'); | ||||
|       } | ||||
|     } else { | ||||
|       try { | ||||
|         final parser = Parser(result.split('=')[0]); | ||||
|         final expr = parser.parse(); | ||||
|         result = '${expr.toString().replaceAll('*', '\\cdot')}=0'; | ||||
|       } catch (e) { | ||||
|         // Fallback | ||||
|         result = result.replaceAll('sqrt(', '\\sqrt{'); | ||||
|         result = result.replaceAll(')', '}'); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     return '\$\$$result\$\$'; | ||||
|   } | ||||
|   | ||||
							
								
								
									
										279
									
								
								lib/widgets/graph_card.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										279
									
								
								lib/widgets/graph_card.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,279 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:fl_chart/fl_chart.dart'; | ||||
| import 'package:simple_math_calc/parser.dart'; | ||||
| import 'package:simple_math_calc/calculator.dart'; | ||||
| import 'dart:math'; | ||||
|  | ||||
| class GraphCard extends StatefulWidget { | ||||
|   final String expression; | ||||
|   final double zoomFactor; | ||||
|   final VoidCallback onZoomIn; | ||||
|   final VoidCallback onZoomOut; | ||||
|  | ||||
|   const GraphCard({ | ||||
|     super.key, | ||||
|     required this.expression, | ||||
|     required this.zoomFactor, | ||||
|     required this.onZoomIn, | ||||
|     required this.onZoomOut, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   State<GraphCard> createState() => _GraphCardState(); | ||||
| } | ||||
|  | ||||
| class _GraphCardState extends State<GraphCard> { | ||||
|   /// 生成函数图表的点 | ||||
|   List<FlSpot> _generatePlotPoints(String expression, double zoomFactor) { | ||||
|     try { | ||||
|       // 只处理 y=... 格式的函数 | ||||
|       String normalized = expression.replaceAll(' ', ''); | ||||
|       if (!normalized.toLowerCase().startsWith('y=')) { | ||||
|         return []; | ||||
|       } | ||||
|       String functionExpr = normalized.substring(2); | ||||
|  | ||||
|       // 如果表达式不包含 x,返回空列表 | ||||
|       if (!functionExpr.contains('x') && !functionExpr.contains('X')) { | ||||
|         return []; | ||||
|       } | ||||
|  | ||||
|       // 预处理表达式,确保格式正确 | ||||
|       functionExpr = functionExpr.replaceAll(' ', ''); | ||||
|  | ||||
|       // 在数字和变量之间插入乘号 | ||||
|       functionExpr = functionExpr.replaceAllMapped( | ||||
|         RegExp(r'(\d)([a-zA-Z])'), | ||||
|         (match) => '${match.group(1)}*${match.group(2)}', | ||||
|       ); | ||||
|  | ||||
|       // 在变量和数字之间插入乘号 (如 x2 -> x*2) | ||||
|       functionExpr = functionExpr.replaceAllMapped( | ||||
|         RegExp(r'([a-zA-Z])(\d)'), | ||||
|         (match) => '${match.group(1)}*${match.group(2)}', | ||||
|       ); | ||||
|  | ||||
|       // 解析表达式 | ||||
|       final parser = Parser(functionExpr); | ||||
|       final expr = parser.parse(); | ||||
|  | ||||
|       // 根据缩放因子动态调整范围和步长 | ||||
|       final range = 10.0 * zoomFactor; | ||||
|       final step = max(0.05, 0.2 / zoomFactor); // 缩放时步长更小,放大时步长更大 | ||||
|  | ||||
|       // 生成点 | ||||
|       List<FlSpot> points = []; | ||||
|       for (double i = -range; i <= range; i += step) { | ||||
|         try { | ||||
|           // 替换变量 x 为当前值 | ||||
|           final substituted = expr.substitute('x', DoubleExpr(i)); | ||||
|           final evaluated = substituted.evaluate(); | ||||
|  | ||||
|           if (evaluated is DoubleExpr) { | ||||
|             final y = evaluated.value; | ||||
|             if (y.isFinite && !y.isNaN) { | ||||
|               points.add(FlSpot(i, y)); | ||||
|             } | ||||
|           } | ||||
|         } catch (e) { | ||||
|           // 跳过无法计算的点 | ||||
|           continue; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // 如果没有足够的点,返回空列表 | ||||
|       if (points.length < 2) { | ||||
|         debugPrint('Generated ${points.length} dots'); | ||||
|         return []; | ||||
|       } | ||||
|  | ||||
|       // 排序点按 x 值 | ||||
|       points.sort((a, b) => a.x.compareTo(b.x)); | ||||
|  | ||||
|       debugPrint( | ||||
|         'Generated ${points.length} dots with zoom factor $zoomFactor', | ||||
|       ); | ||||
|       return points; | ||||
|     } catch (e) { | ||||
|       debugPrint('Error generating plot points: $e'); | ||||
|       return []; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// 计算图表的数据范围 | ||||
|   ({double minX, double maxX, double minY, double maxY}) _calculateChartBounds( | ||||
|     List<FlSpot> points, | ||||
|     double zoomFactor, | ||||
|   ) { | ||||
|     if (points.isEmpty) { | ||||
|       return ( | ||||
|         minX: -10 * zoomFactor, | ||||
|         maxX: 10 * zoomFactor, | ||||
|         minY: -50 * zoomFactor, | ||||
|         maxY: 50 * zoomFactor, | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     double minX = points.first.x; | ||||
|     double maxX = points.first.x; | ||||
|     double minY = points.first.y; | ||||
|     double maxY = points.first.y; | ||||
|  | ||||
|     for (final point in points) { | ||||
|       minX = min(minX, point.x); | ||||
|       maxX = max(maxX, point.x); | ||||
|       minY = min(minY, point.y); | ||||
|       maxY = max(maxY, point.y); | ||||
|     } | ||||
|  | ||||
|     // 添加边距 | ||||
|     final xPadding = (maxX - minX) * 0.1; | ||||
|     final yPadding = (maxY - minY) * 0.1; | ||||
|  | ||||
|     return ( | ||||
|       minX: minX - xPadding, | ||||
|       maxX: maxX + xPadding, | ||||
|       minY: minY - yPadding, | ||||
|       maxY: maxY + yPadding, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return ListView( | ||||
|       padding: EdgeInsets.only( | ||||
|         left: 16, | ||||
|         right: 16, | ||||
|         bottom: MediaQuery.of(context).padding.bottom + 16, | ||||
|         top: 16, | ||||
|       ), | ||||
|       children: [ | ||||
|         Card( | ||||
|           child: Padding( | ||||
|             padding: const EdgeInsets.all(16.0), | ||||
|             child: Column( | ||||
|               children: [ | ||||
|                 Row( | ||||
|                   mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                   children: [ | ||||
|                     Padding( | ||||
|                       padding: const EdgeInsets.only(left: 4), | ||||
|                       child: Text( | ||||
|                         '函数图像', | ||||
|                         style: Theme.of(context).textTheme.titleMedium, | ||||
|                       ), | ||||
|                     ), | ||||
|                     Row( | ||||
|                       mainAxisAlignment: MainAxisAlignment.center, | ||||
|                       children: [ | ||||
|                         IconButton( | ||||
|                           onPressed: widget.onZoomIn, | ||||
|                           icon: Icon(Icons.zoom_in), | ||||
|                           tooltip: '放大', | ||||
|                           padding: EdgeInsets.zero, | ||||
|                           visualDensity: VisualDensity.compact, | ||||
|                         ), | ||||
|                         IconButton( | ||||
|                           onPressed: widget.onZoomOut, | ||||
|                           icon: Icon(Icons.zoom_out), | ||||
|                           tooltip: '缩小', | ||||
|                           padding: EdgeInsets.zero, | ||||
|                           visualDensity: VisualDensity.compact, | ||||
|                         ), | ||||
|                       ], | ||||
|                     ), | ||||
|                   ], | ||||
|                 ), | ||||
|                 const SizedBox(height: 24), | ||||
|                 SizedBox( | ||||
|                   height: 340, | ||||
|                   child: Builder( | ||||
|                     builder: (context) { | ||||
|                       final points = _generatePlotPoints( | ||||
|                         widget.expression, | ||||
|                         widget.zoomFactor, | ||||
|                       ); | ||||
|                       final bounds = _calculateChartBounds( | ||||
|                         points, | ||||
|                         widget.zoomFactor, | ||||
|                       ); | ||||
|  | ||||
|                       return LineChart( | ||||
|                         LineChartData( | ||||
|                           gridData: FlGridData(show: true), | ||||
|                           titlesData: FlTitlesData( | ||||
|                             leftTitles: AxisTitles( | ||||
|                               sideTitles: SideTitles( | ||||
|                                 showTitles: true, | ||||
|                                 reservedSize: 80, | ||||
|                                 getTitlesWidget: (value, meta) => | ||||
|                                     SideTitleWidget( | ||||
|                                       axisSide: meta.axisSide, | ||||
|                                       child: Text(value.toStringAsFixed(2)), | ||||
|                                     ), | ||||
|                               ), | ||||
|                             ), | ||||
|                             bottomTitles: AxisTitles( | ||||
|                               sideTitles: SideTitles( | ||||
|                                 showTitles: true, | ||||
|                                 reservedSize: 24, | ||||
|                                 getTitlesWidget: (value, meta) => | ||||
|                                     SideTitleWidget( | ||||
|                                       axisSide: meta.axisSide, | ||||
|                                       child: Text(value.toStringAsFixed(2)), | ||||
|                                     ), | ||||
|                               ), | ||||
|                             ), | ||||
|                             topTitles: AxisTitles( | ||||
|                               sideTitles: SideTitles(showTitles: false), | ||||
|                             ), | ||||
|                             rightTitles: AxisTitles( | ||||
|                               sideTitles: SideTitles(showTitles: false), | ||||
|                             ), | ||||
|                           ), | ||||
|                           borderData: FlBorderData( | ||||
|                             show: true, | ||||
|                             border: Border.all( | ||||
|                               color: Theme.of(context).colorScheme.outline, | ||||
|                             ), | ||||
|                           ), | ||||
|                           lineTouchData: LineTouchData( | ||||
|                             enabled: true, | ||||
|                             touchTooltipData: LineTouchTooltipData( | ||||
|                               getTooltipItems: (touchedSpots) { | ||||
|                                 return touchedSpots.map((spot) { | ||||
|                                   return LineTooltipItem( | ||||
|                                     'x = ${spot.x.toStringAsFixed(2)}\ny = ${spot.y.toStringAsFixed(2)}', | ||||
|                                     const TextStyle(color: Colors.white), | ||||
|                                   ); | ||||
|                                 }).toList(); | ||||
|                               }, | ||||
|                             ), | ||||
|                           ), | ||||
|                           lineBarsData: [ | ||||
|                             LineChartBarData( | ||||
|                               spots: points, | ||||
|                               isCurved: true, | ||||
|                               color: Theme.of(context).colorScheme.primary, | ||||
|                               barWidth: 3, | ||||
|                               belowBarData: BarAreaData(show: false), | ||||
|                               dotData: FlDotData(show: false), | ||||
|                             ), | ||||
|                           ], | ||||
|                           minX: bounds.minX, | ||||
|                           maxX: bounds.maxX, | ||||
|                           minY: bounds.minY, | ||||
|                           maxY: bounds.maxY, | ||||
|                         ), | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|                 ), | ||||
|               ], | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -400,14 +400,6 @@ packages: | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "0.11.1" | ||||
|   math_expressions: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       name: math_expressions | ||||
|       sha256: "2e1ceb974c2b1893c809a68c7005f1b63f7324db0add800a0e792b1ac8ff9f03" | ||||
|       url: "https://pub.dev" | ||||
|     source: hosted | ||||
|     version: "3.1.0" | ||||
|   meta: | ||||
|     dependency: transitive | ||||
|     description: | ||||
|   | ||||
| @@ -34,7 +34,6 @@ dependencies: | ||||
|   # The following adds the Cupertino Icons font to your application. | ||||
|   # Use with the CupertinoIcons class for iOS style icons. | ||||
|   cupertino_icons: ^1.0.8 | ||||
|   math_expressions: ^3.1.0 | ||||
|   latext: ^0.5.1 | ||||
|   google_fonts: ^6.3.1 | ||||
|   go_router: ^16.2.1 | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import 'package:simple_math_calc/parser.dart'; | ||||
| import 'package:simple_math_calc/calculator.dart'; | ||||
| import 'package:test/test.dart'; | ||||
|  | ||||
| void main() { | ||||
| @@ -39,7 +40,7 @@ void main() { | ||||
|  | ||||
|     test('非完全平方数', () { | ||||
|       var expr = Parser("sqrt(8)").parse(); | ||||
|       expect(expr.simplify().toString().replaceAll(' ', ''), "(2*sqrt(2))"); | ||||
|       expect(expr.simplify().toString().replaceAll(' ', ''), "(2*\\sqrt{2})"); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
| @@ -53,7 +54,7 @@ void main() { | ||||
|       var expr = Parser("sqrt(8)/4 + 1/2").parse(); | ||||
|       expect( | ||||
|         expr.evaluate().toString().replaceAll(' ', ''), | ||||
|         "((sqrt(2)/2)+1/2)", | ||||
|         "((\\sqrt{2}/2)+1/2)", | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| @@ -101,4 +102,175 @@ void main() { | ||||
|       expect(expr.evaluate().toString(), "0.0"); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   group('精确三角函数值', () { | ||||
|     test('getExactTrigResult - sin(30)', () { | ||||
|       expect(getExactTrigResult('sin(30)'), '\\frac{1}{2}'); | ||||
|     }); | ||||
|  | ||||
|     test('getExactTrigResult - cos(45)', () { | ||||
|       expect(getExactTrigResult('cos(45)'), '\\frac{\\sqrt{2}}{2}'); | ||||
|     }); | ||||
|  | ||||
|     test('getExactTrigResult - tan(60)', () { | ||||
|       expect( | ||||
|         getExactTrigResult('tan(60)'), | ||||
|         '\\frac{\\frac{\\sqrt{3}}{2}}{\\frac{1}{2}}', | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     test('getExactTrigResult - sin(30+45)', () { | ||||
|       expect(getExactTrigResult('sin(30+45)'), '1 + \\frac{\\sqrt{2}}{2}'); | ||||
|     }); | ||||
|  | ||||
|     test('getExactTrigResult - 无效输入', () { | ||||
|       expect(getExactTrigResult('sin(25)'), isNull); | ||||
|     }); | ||||
|  | ||||
|     test('getSinExactValue - 各种角度', () { | ||||
|       expect(getSinExactValue(0), '0'); | ||||
|       expect(getSinExactValue(30), '\\frac{1}{2}'); | ||||
|       expect(getSinExactValue(45), '\\frac{\\sqrt{2}}{2}'); | ||||
|       expect(getSinExactValue(90), '1'); | ||||
|       expect(getSinExactValue(180), '0'); | ||||
|       expect(getSinExactValue(270), '-1'); | ||||
|     }); | ||||
|  | ||||
|     test('getCosExactValue - 各种角度', () { | ||||
|       expect(getCosExactValue(0), '1'); | ||||
|       expect(getCosExactValue(30), '\\frac{\\sqrt{3}}{2}'); | ||||
|       expect(getCosExactValue(45), '\\frac{\\sqrt{2}}{2}'); | ||||
|       expect(getCosExactValue(90), '0'); | ||||
|       expect(getCosExactValue(180), '1'); | ||||
|     }); | ||||
|  | ||||
|     test('getTanExactValue - 各种角度', () { | ||||
|       expect(getTanExactValue(0), '\\frac{0}{1}'); | ||||
|       expect( | ||||
|         getTanExactValue(30), | ||||
|         '\\frac{\\frac{1}{2}}{\\frac{\\sqrt{3}}{2}}', | ||||
|       ); | ||||
|       expect( | ||||
|         getTanExactValue(45), | ||||
|         '\\frac{\\frac{\\sqrt{2}}{2}}{\\frac{\\sqrt{2}}{2}}', | ||||
|       ); | ||||
|       expect( | ||||
|         getTanExactValue(60), | ||||
|         '\\frac{\\frac{\\sqrt{3}}{2}}{\\frac{1}{2}}', | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     test('evaluateAngleExpression - 简单求和', () { | ||||
|       expect(evaluateAngleExpression('30+45'), 75); | ||||
|       expect(evaluateAngleExpression('60+30'), 90); | ||||
|       expect(evaluateAngleExpression('90'), 90); | ||||
|     }); | ||||
|  | ||||
|     test('evaluateAngleExpression - 无效输入', () { | ||||
|       expect(evaluateAngleExpression('30+a'), isNull); | ||||
|       expect(evaluateAngleExpression(''), isNull); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   group('平方根格式化', () { | ||||
|     test('formatSqrtResult - 整数', () { | ||||
|       expect(formatSqrtResult(4.0), '4'); | ||||
|       expect(formatSqrtResult(9.0), '9'); | ||||
|     }); | ||||
|  | ||||
|     test('formatSqrtResult - 完全平方根', () { | ||||
|       expect(formatSqrtResult(4.0), '4'); | ||||
|       expect(formatSqrtResult(9.0), '9'); | ||||
|     }); | ||||
|  | ||||
|     test('formatSqrtResult - 非完全平方根', () { | ||||
|       expect(formatSqrtResult(2.0), '2'); | ||||
|       expect(formatSqrtResult(3.0), '3'); | ||||
|     }); | ||||
|  | ||||
|     test('formatSqrtResult - 带系数的平方根', () { | ||||
|       expect(formatSqrtResult(8.0), '8'); | ||||
|       expect(formatSqrtResult(18.0), '18'); | ||||
|       expect(formatSqrtResult(12.0), '12'); | ||||
|     }); | ||||
|  | ||||
|     test('formatSqrtResult - 负数', () { | ||||
|       expect(formatSqrtResult(-4.0), '-4'); | ||||
|       expect(formatSqrtResult(-2.0), '-2'); | ||||
|     }); | ||||
|  | ||||
|     test('formatSqrtResult - 零', () { | ||||
|       expect(formatSqrtResult(0.0), '0'); | ||||
|     }); | ||||
|  | ||||
|     test('formatSqrtResult - 小数', () { | ||||
|       expect(formatSqrtResult(1.4142135623730951), '\\sqrt{2}'); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   group('三角函数转换', () { | ||||
|     test('convertTrigToRadians - 基本转换', () { | ||||
|       expect(convertTrigToRadians('sin(30)'), 'sin((30)*(π/180))'); | ||||
|       expect(convertTrigToRadians('cos(45)'), 'cos((45)*(π/180))'); | ||||
|       expect(convertTrigToRadians('tan(60)'), 'tan((60)*(π/180))'); | ||||
|     }); | ||||
|  | ||||
|     test('convertTrigToRadians - 弧度输入不变', () { | ||||
|       expect(convertTrigToRadians('sin(π/2)'), 'sin(π/2)'); | ||||
|       expect(convertTrigToRadians('cos(rad)'), 'cos(rad)'); | ||||
|     }); | ||||
|  | ||||
|     test('convertTrigToRadians - 复杂表达式', () { | ||||
|       expect(convertTrigToRadians('sin(30+45)'), 'sin((30+45)*(π/180))'); | ||||
|     }); | ||||
|  | ||||
|     test('convertTrigToRadians - 多个函数', () { | ||||
|       expect( | ||||
|         convertTrigToRadians('sin(30) + cos(45)'), | ||||
|         'sin((30)*(π/180)) + cos((45)*(π/180))', | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   group('百分比运算符', () { | ||||
|     test('基本百分比', () { | ||||
|       var expr = Parser("50%").parse(); | ||||
|       expect(expr.evaluate().toString(), "0.5"); | ||||
|     }); | ||||
|  | ||||
|     test('100%', () { | ||||
|       var expr = Parser("100%").parse(); | ||||
|       expect(expr.evaluate().toString(), "1.0"); | ||||
|     }); | ||||
|  | ||||
|     test('25%', () { | ||||
|       var expr = Parser("25%").parse(); | ||||
|       expect(expr.evaluate().toString(), "0.25"); | ||||
|     }); | ||||
|  | ||||
|     test('负百分比', () { | ||||
|       var expr = Parser("-50%").parse(); | ||||
|       expect(expr.evaluate().toString(), "-0.5"); | ||||
|     }); | ||||
|  | ||||
|     test('小数百分比', () { | ||||
|       var expr = Parser("50.5%").parse(); | ||||
|       expect(expr.evaluate().toString(), "0.505"); | ||||
|     }); | ||||
|  | ||||
|     test('分数百分比', () { | ||||
|       var expr = Parser("1/2%").parse(); | ||||
|       expect(expr.evaluate().toString(), "0.005"); | ||||
|     }); | ||||
|  | ||||
|     test('百分比在表达式中', () { | ||||
|       var expr = Parser("50% + 25%").parse(); | ||||
|       expect(expr.evaluate().toString(), "0.75"); | ||||
|     }); | ||||
|  | ||||
|     test('百分比与数字相乘', () { | ||||
|       var expr = Parser("2 * 50%").parse(); | ||||
|       expect(expr.evaluate().toString(), "1.0"); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -1,21 +0,0 @@ | ||||
| import 'lib/solver.dart'; | ||||
|  | ||||
| void main() { | ||||
|   final solver = SolverService(); | ||||
|  | ||||
|   // Test the problematic case | ||||
|   final input = '(x+8)(x+1)=-12'; | ||||
|   print('Input: $input'); | ||||
|  | ||||
|   try { | ||||
|     final result = solver.solve(input); | ||||
|     print('Result: ${result.finalAnswer}'); | ||||
|     print('Steps:'); | ||||
|     for (final step in result.steps) { | ||||
|       print('Step ${step.stepNumber}: ${step.title}'); | ||||
|       print('  Formula: ${step.formula}'); | ||||
|     } | ||||
|   } catch (e) { | ||||
|     print('Error: $e'); | ||||
|   } | ||||
| } | ||||
| @@ -16,7 +16,7 @@ | ||||
|  | ||||
|   <meta charset="UTF-8"> | ||||
|   <meta content="IE=Edge" http-equiv="X-UA-Compatible"> | ||||
|   <meta name="description" content="A new Flutter project."> | ||||
|   <meta name="description" content="A simple math calculator."> | ||||
|  | ||||
|   <!-- iOS meta tags & icons --> | ||||
|   <meta name="mobile-web-app-capable" content="yes"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user