瀏覽代碼

近似值改成按"小数位数"截断(Symja 的 N 忽略位数参数);键盘行高跟着排数算

- **Symja 的 `N[expr, k]` 根本不看第二个参数**(实测 4 位和 15 位打印一模一样),
  `NumberForm` / `SetPrecision` 也没实现(原样返回)。改用
  `N[Round[expr, 1/10^k]]` 真截到 k 位小数,文本和 LaTeX 都跟着变干净,
  顺带把 `3.5999999999999996` 这种浮点尾巴一起解决了(现在是 3.6)
- 设置项随之改成"近似值小数位数",范围 0~15、默认 10(原来写的"有效位数"
  名不副实);计算器的"近似"和工具页所有"(近似)"共用同一个值
- 键盘行高按**实际排数**算:之前写死 9 排,缩到 8 排后底下空出一条

Co-Authored-By: Claude Code <noreply@anthropic.com>
nonvioce 3 周之前
父節點
當前提交
db4516217a

+ 4 - 2
lib/calculator_page.dart

@@ -548,9 +548,11 @@ class _CalculatorPageState extends State<CalculatorPage> {
           ],
           ],
         ];
         ];
 
 
-        // 7 行 + 6 个间隙,再扣掉 Padding 上下各 6 ——
+        // 行数**按实际排数算**(改键盘时加减一排不用回来改这里 ——
+        // 之前写死 9,缩到 8 排之后底下就空出一条)。
         // box.maxHeight 是 padding **之前**的高度,漏掉会溢出。
         // box.maxHeight 是 padding **之前**的高度,漏掉会溢出。
-        final h = (box.maxHeight - 12 - 8 * 6) / 9;
+        final h =
+            (box.maxHeight - 12 - (rows.length - 1) * 6) / rows.length;
         return Padding(
         return Padding(
           padding: const EdgeInsets.all(6),
           padding: const EdgeInsets.all(6),
           child: Column(
           child: Column(

+ 26 - 18
lib/settings.dart

@@ -8,7 +8,8 @@ import 'session_store.dart';
 /// 存成单独一个 `settings.json`(见 [SessionStore]),不跟会话存档混在一起 ——
 /// 存成单独一个 `settings.json`(见 [SessionStore]),不跟会话存档混在一起 ——
 /// 会话存档每次求值都会重写,设置却是偶尔改一次的东西。
 /// 会话存档每次求值都会重写,设置却是偶尔改一次的东西。
 class AppSettings extends ChangeNotifier {
 class AppSettings extends ChangeNotifier {
-  AppSettings({SessionStore? store}) : _store = store ?? SessionStore(fileName: 'settings.json');
+  AppSettings({SessionStore? store})
+    : _store = store ?? SessionStore(fileName: 'settings.json');
 
 
   final SessionStore _store;
   final SessionStore _store;
 
 
@@ -16,10 +17,10 @@ class AppSettings extends ChangeNotifier {
   static const int minPort = 1024;
   static const int minPort = 1024;
   static const int maxPort = 65535;
   static const int maxPort = 65535;
 
 
-  /// 近似值保留几位有效数字(工具页和计算器的"近似"都听这个)。
-  static const int defaultApproxDigits = 15;
-  static const int minDigits = 2;
-  static const int maxDigits = 20;
+  /// 近似值保留几位**小数**(工具页和计算器的"近似"都听这个)。
+  static const int defaultApproxDigits = 10;
+  static const int minDigits = 0;
+  static const int maxDigits = 15;
 
 
   int _mcpPort = defaultMcpPort;
   int _mcpPort = defaultMcpPort;
   int _approxDigits = defaultApproxDigits;
   int _approxDigits = defaultApproxDigits;
@@ -78,10 +79,10 @@ class AppSettings extends ChangeNotifier {
   }
   }
 
 
   Future<void> _save() => _store.save(<String, dynamic>{
   Future<void> _save() => _store.save(<String, dynamic>{
-        'mcpPort': _mcpPort,
-        'approxDigits': _approxDigits,
-        'eInk': _eInk,
-      });
+    'mcpPort': _mcpPort,
+    'approxDigits': _approxDigits,
+    'eInk': _eInk,
+  });
 }
 }
 
 
 /// 弹出设置面板。控制台顶栏的齿轮按钮走这里。
 /// 弹出设置面板。控制台顶栏的齿轮按钮走这里。
@@ -103,8 +104,9 @@ class _SettingsSheet extends StatefulWidget {
 
 
 class _SettingsSheetState extends State<_SettingsSheet> {
 class _SettingsSheetState extends State<_SettingsSheet> {
   late final AppSettings _settings = context.read<AppSettings>();
   late final AppSettings _settings = context.read<AppSettings>();
-  late final TextEditingController _port =
-      TextEditingController(text: '${_settings.mcpPort}');
+  late final TextEditingController _port = TextEditingController(
+    text: '${_settings.mcpPort}',
+  );
   String? _portError;
   String? _portError;
 
 
   @override
   @override
@@ -116,8 +118,10 @@ class _SettingsSheetState extends State<_SettingsSheet> {
   void _applyPort() {
   void _applyPort() {
     final v = int.tryParse(_port.text.trim());
     final v = int.tryParse(_port.text.trim());
     if (v == null || v < AppSettings.minPort || v > AppSettings.maxPort) {
     if (v == null || v < AppSettings.minPort || v > AppSettings.maxPort) {
-      setState(() => _portError =
-          '端口要在 ${AppSettings.minPort}~${AppSettings.maxPort} 之间');
+      setState(
+        () => _portError =
+            '端口要在 ${AppSettings.minPort}~${AppSettings.maxPort} 之间',
+      );
       return;
       return;
     }
     }
     setState(() => _portError = null);
     setState(() => _portError = null);
@@ -163,10 +167,12 @@ class _SettingsSheetState extends State<_SettingsSheet> {
             const SizedBox(height: 8),
             const SizedBox(height: 8),
             Row(
             Row(
               children: [
               children: [
-                const Text('近似值位数'),
+                const Text('近似值小数位数'),
                 const Spacer(),
                 const Spacer(),
-                Text('$digits',
-                    style: const TextStyle(fontFamily: 'monospace')),
+                Text(
+                  '$digits',
+                  style: const TextStyle(fontFamily: 'monospace'),
+                ),
               ],
               ],
             ),
             ),
             Slider(
             Slider(
@@ -177,8 +183,10 @@ class _SettingsSheetState extends State<_SettingsSheet> {
               label: '$digits',
               label: '$digits',
               onChanged: (v) => _settings.setApproxDigits(v.round()),
               onChanged: (v) => _settings.setApproxDigits(v.round()),
             ),
             ),
-            const Text('工具页的「(近似)」和计算器的「近似」都用这个位数。',
-                style: TextStyle(fontSize: 12)),
+            const Text(
+              '工具页的「(近似)」和计算器的「近似」都用这个位数。',
+              style: TextStyle(fontSize: 12),
+            ),
             const SizedBox(height: 8),
             const SizedBox(height: 8),
             SwitchListTile(
             SwitchListTile(
               contentPadding: EdgeInsets.zero,
               contentPadding: EdgeInsets.zero,

+ 46 - 34
lib/symja.dart

@@ -27,7 +27,7 @@ class SymjaEntry {
   final DateTime at;
   final DateTime at;
 
 
   SymjaEntry({required this.input, this.result, this.error, DateTime? at})
   SymjaEntry({required this.input, this.result, this.error, DateTime? at})
-      : at = at ?? DateTime.now();
+    : at = at ?? DateTime.now();
 
 
   bool get ok => result != null;
   bool get ok => result != null;
 }
 }
@@ -97,7 +97,7 @@ class SymjaSession extends ChangeNotifier {
   /// 也是符号计算的意义所在,所以默认不开。开起来时给表达式套一层 `N[expr, 30]`。
   /// 也是符号计算的意义所在,所以默认不开。开起来时给表达式套一层 `N[expr, 30]`。
   bool approximate = false;
   bool approximate = false;
 
 
-  /// 近似模式下的有效位数。
+  /// 近似模式下保留几位**小数**。
   ///
   ///
   /// 15 位是刻意选的:再高就放不下了。`N[1/3, 30]` 会给出三十多个字符的长串,
   /// 15 位是刻意选的:再高就放不下了。`N[1/3, 30]` 会给出三十多个字符的长串,
   /// 手机上怎么看都显示不全(用户实测反馈)。15 位比一般科学计算器还多,
   /// 手机上怎么看都显示不全(用户实测反馈)。15 位比一般科学计算器还多,
@@ -114,7 +114,8 @@ class SymjaSession extends ChangeNotifier {
   String _applyMode(String expr) {
   String _applyMode(String expr) {
     if (!approximate) return expr;
     if (!approximate) return expr;
     if (_assignment.hasMatch(expr)) return expr;
     if (_assignment.hasMatch(expr)) return expr;
-    return 'N[$expr, $approximateDigits]';
+    // 保留 k 位小数。用 Round 而不是 N[expr, k]:Symja 的 N 忽略第二个参数
+    return 'N[Round[$expr, 1/10^$approximateDigits]]';
   }
   }
 
 
   int _seq = 0;
   int _seq = 0;
@@ -145,14 +146,17 @@ class SymjaSession extends ChangeNotifier {
     notifyListeners();
     notifyListeners();
 
 
     try {
     try {
-      final m = await symjaChannel.invokeMapMethod<String, dynamic>('evaluate', {
-        'taskId': id,
-        'expr': _applyMode(input),
-        'timeoutMs': timeout.inMilliseconds,
-        'wantTeX': wantTeX,
-        // 显式声明这不是预览:预览不更新 `%`,正式求值要更新
-        'preview': false,
-      });
+      final m = await symjaChannel.invokeMapMethod<String, dynamic>(
+        'evaluate',
+        {
+          'taskId': id,
+          'expr': _applyMode(input),
+          'timeoutMs': timeout.inMilliseconds,
+          'wantTeX': wantTeX,
+          // 显式声明这不是预览:预览不更新 `%`,正式求值要更新
+          'preview': false,
+        },
+      );
 
 
       if (m == null || m['ok'] != true) {
       if (m == null || m['ok'] != true) {
         _record(input, error: '${m?['error'] ?? '原生侧返回了空结果'}');
         _record(input, error: '${m?['error'] ?? '原生侧返回了空结果'}');
@@ -187,13 +191,16 @@ class SymjaSession extends ChangeNotifier {
     final input = expr.trim();
     final input = expr.trim();
     if (input.isEmpty) return null;
     if (input.isEmpty) return null;
     try {
     try {
-      final m = await symjaChannel.invokeMapMethod<String, dynamic>('evaluate', {
-        'taskId': ++_seq,
-        'expr': _applyMode(input),
-        'timeoutMs': 4000,
-        'wantTeX': true,
-        'preview': true,
-      });
+      final m = await symjaChannel.invokeMapMethod<String, dynamic>(
+        'evaluate',
+        {
+          'taskId': ++_seq,
+          'expr': _applyMode(input),
+          'timeoutMs': 4000,
+          'wantTeX': true,
+          'preview': true,
+        },
+      );
       if (m == null || m['ok'] != true) return null;
       if (m == null || m['ok'] != true) return null;
       return SymjaResult(
       return SymjaResult(
         text: '${m['text']}',
         text: '${m['text']}',
@@ -223,18 +230,21 @@ class SymjaSession extends ChangeNotifier {
     final input = expr.trim();
     final input = expr.trim();
     if (input.isEmpty) throw const SymjaException('表达式为空');
     if (input.isEmpty) throw const SymjaException('表达式为空');
     try {
     try {
-      final m = await symjaChannel.invokeMapMethod<String, dynamic>('evaluate', {
-        'taskId': ++_seq,
-        // 工具页传 applyMode: false —— 它在表达式里自己写 N[...],
-        // 不该被计算器顶栏那个"精确 / 近似"开关二次改写(否则"精确值"
-        // 那一条也会变成近似值,两条输出就没区别了)。
-        'expr': applyMode ? _applyMode(input) : input,
-        'timeoutMs': timeout.inMilliseconds,
-        'wantTeX': wantTeX,
-        // preview=true 同时意味着原生侧不更新 lastOutput,
-        // 所以工具算完不会顶掉用户在计算器上按 ANS 拿到的东西。
-        'preview': true,
-      });
+      final m = await symjaChannel.invokeMapMethod<String, dynamic>(
+        'evaluate',
+        {
+          'taskId': ++_seq,
+          // 工具页传 applyMode: false —— 它在表达式里自己写 N[...],
+          // 不该被计算器顶栏那个"精确 / 近似"开关二次改写(否则"精确值"
+          // 那一条也会变成近似值,两条输出就没区别了)。
+          'expr': applyMode ? _applyMode(input) : input,
+          'timeoutMs': timeout.inMilliseconds,
+          'wantTeX': wantTeX,
+          // preview=true 同时意味着原生侧不更新 lastOutput,
+          // 所以工具算完不会顶掉用户在计算器上按 ANS 拿到的东西。
+          'preview': true,
+        },
+      );
       if (m == null || m['ok'] != true) {
       if (m == null || m['ok'] != true) {
         throw SymjaException('${m?['error'] ?? '未知错误'}');
         throw SymjaException('${m?['error'] ?? '未知错误'}');
       }
       }
@@ -359,10 +369,12 @@ class SymjaSession extends ChangeNotifier {
   /// 从存档恢复历史。不触发求值。
   /// 从存档恢复历史。不触发求值。
   void restoreHistory(Iterable<({String input, String text, int ms})> entries) {
   void restoreHistory(Iterable<({String input, String text, int ms})> entries) {
     for (final e in entries) {
     for (final e in entries) {
-      history.add(SymjaEntry(
-        input: e.input,
-        result: SymjaResult(text: e.text, tex: null, elapsedMs: e.ms),
-      ));
+      history.add(
+        SymjaEntry(
+          input: e.input,
+          result: SymjaResult(text: e.text, tex: null, elapsedMs: e.ms),
+        ),
+      );
     }
     }
     while (history.length > maxHistory) {
     while (history.length > maxHistory) {
       history.removeAt(0);
       history.removeAt(0);

+ 6 - 2
lib/tools/circle_equation.dart

@@ -59,11 +59,15 @@ class _CircleEquationPageState extends State<CircleEquationPage> {
 
 
       // 精确值 + 近似值都要:系数和圆心半径都是分数,小数形式有时更直观
       // 精确值 + 近似值都要:系数和圆心半径都是分数,小数形式有时更直观
       final defsN = parseSymjaList(
       final defsN = parseSymjaList(
-        (await session.compute('N[Rationalize[$defExpr], ${session.approximateDigits}]')).text,
+        (await session.compute(
+          approxExpr('Rationalize[$defExpr]', session.approximateDigits),
+        )).text,
       );
       );
       final center = parseSymjaList((await session.compute(centerExpr)).text);
       final center = parseSymjaList((await session.compute(centerExpr)).text);
       final centerN = parseSymjaList(
       final centerN = parseSymjaList(
-        (await session.compute('N[Rationalize[$centerExpr], ${session.approximateDigits}]')).text,
+        (await session.compute(
+          approxExpr('Rationalize[$centerExpr]', session.approximateDigits),
+        )).text,
       );
       );
 
 
       String at(List<Object> l, int i) => i < l.length ? '${l[i]}' : '';
       String at(List<Object> l, int i) => i < l.length ? '${l[i]}' : '';

+ 5 - 2
lib/tools/convolution_calculator.dart

@@ -151,7 +151,10 @@ class _ConvolutionCalculatorPageState extends State<ConvolutionCalculatorPage> {
         if (ps == null) continue;
         if (ps == null) continue;
         final total = await session.compute('Total[{${ps.join(', ')}}]');
         final total = await session.compute('Total[{${ps.join(', ')}}]');
         final totalN = await session.compute(
         final totalN = await session.compute(
-          'N[Rationalize[Total[{${ps.join(', ')}}]], ${session.approximateDigits}]',
+          approxExpr(
+            'Rationalize[Total[{${ps.join(', ')}}]]',
+            session.approximateDigits,
+          ),
         );
         );
         groupSums.add(total.text);
         groupSums.add(total.text);
         // 精确分数 + 小数:概率这种"看一眼量级"的值,两者都有用
         // 精确分数 + 小数:概率这种"看一眼量级"的值,两者都有用
@@ -163,7 +166,7 @@ class _ConvolutionCalculatorPageState extends State<ConvolutionCalculatorPage> {
       final grand = await session.compute('Total[{${groupSums.join(', ')}}]');
       final grand = await session.compute('Total[{${groupSums.join(', ')}}]');
       final grandN = await session.compute(
       final grandN = await session.compute(
         'N[Rationalize[Total[{${groupSums.join(', ')}}]], '
         'N[Rationalize[Total[{${groupSums.join(', ')}}]], '
-          '${session.approximateDigits}]',
+        '${session.approximateDigits}]',
       );
       );
       outputs.add(ToolText('概率总和', withApprox(grand.text, grandN.text)));
       outputs.add(ToolText('概率总和', withApprox(grand.text, grandN.text)));
       return outputs;
       return outputs;

+ 6 - 2
lib/tools/cuo_wei_xiang_jian.dart

@@ -55,8 +55,12 @@ class _CuoWeiXiangJianPageState extends State<CuoWeiXiangJianPage> {
       // **值全是 Symja 算的**,这里只负责把数字摆进公式、分数写成 \frac。
       // **值全是 Symja 算的**,这里只负责把数字摆进公式、分数写成 \frac。
       final s = sFormula(a: av, b: bv, q: qv);
       final s = sFormula(a: av, b: bv, q: qv);
 
 
-      final aApprox = await session.compute('N[Rationalize[$aExpr], ${session.approximateDigits}]');
-      final bApprox = await session.compute('N[Rationalize[$bExpr], ${session.approximateDigits}]');
+      final aApprox = await session.compute(
+        approxExpr('Rationalize[$aExpr]', session.approximateDigits),
+      );
+      final bApprox = await session.compute(
+        approxExpr('Rationalize[$bExpr]', session.approximateDigits),
+      );
 
 
       return <ToolOutput>[
       return <ToolOutput>[
         ToolText('结果 Sₙ = (A·n + B)·qⁿ − B', s.plain, tex: s.tex),
         ToolText('结果 Sₙ = (A·n + B)·qⁿ − B', s.plain, tex: s.tex),

+ 18 - 15
lib/tools/regression.dart

@@ -36,15 +36,16 @@ class _RegressionPageState extends State<RegressionPage> {
       final y = _y.list;
       final y = _y.list;
 
 
       // 十个量一条表达式算齐(隐式乘号一律写出来)。
       // 十个量一条表达式算齐(隐式乘号一律写出来)。
-      final statsExpr =
-          'N[With[{x=$x, y=$y}, '
-          'Module[{xb, yb, sxx, syy, sxy, b, aa, se2}, '
-          'xb=Mean[x]; yb=Mean[y]; '
-          'sxx=Total[(x-xb)^2]; syy=Total[(y-yb)^2]; '
-          'sxy=Total[(x-xb)*(y-yb)]; '
-          'b=sxy/sxx; aa=yb-b*xb; se2=Total[(y-(aa+b*x))^2]; '
-          '{xb, yb, sxx, syy, sxy, sxy/Sqrt[sxx*syy], b, aa, se2, 1-se2/syy}]], '
-              '${session.approximateDigits}]';
+      final statsExpr = approxExpr(
+        'With[{x=$x, y=$y}, '
+        'Module[{xb, yb, sxx, syy, sxy, b, aa, se2}, '
+        'xb=Mean[x]; yb=Mean[y]; '
+        'sxx=Total[(x-xb)^2]; syy=Total[(y-yb)^2]; '
+        'sxy=Total[(x-xb)*(y-yb)]; '
+        'b=sxy/sxx; aa=yb-b*xb; se2=Total[(y-(aa+b*x))^2]; '
+        '{xb, yb, sxx, syy, sxy, sxy/Sqrt[sxx*syy], b, aa, se2, 1-se2/syy}]]',
+        session.approximateDigits,
+      );
       final stats = parseSymjaList((await session.compute(statsExpr)).text)
       final stats = parseSymjaList((await session.compute(statsExpr)).text)
           .map((e) => _clean('$e'))
           .map((e) => _clean('$e'))
           .toList();
           .toList();
@@ -84,12 +85,14 @@ class _RegressionPageState extends State<RegressionPage> {
       }
       }
 
 
       // 回归直线两端点(在 x 的最小/最大值处取值)。
       // 回归直线两端点(在 x 的最小/最大值处取值)。
-      final endsExpr =
-          'N[With[{x=$x, y=$y}, '
-          'Module[{xb, yb, sxy, sxx, b, aa}, '
-          'xb=Mean[x]; yb=Mean[y]; sxx=Total[(x-xb)^2]; '
-          'sxy=Total[(x-xb)*(y-yb)]; b=sxy/sxx; aa=yb-b*xb; '
-          '{Min[x], Max[x], aa+b*Min[x], aa+b*Max[x]}]], 10]';
+      final endsExpr = approxExpr(
+        'With[{x=$x, y=$y}, '
+        'Module[{xb, yb, sxy, sxx, b, aa}, '
+        'xb=Mean[x]; yb=Mean[y]; sxx=Total[(x-xb)^2]; '
+        'sxy=Total[(x-xb)*(y-yb)]; b=sxy/sxx; aa=yb-b*xb; '
+        '{Min[x], Max[x], aa+b*Min[x], aa+b*Max[x]}]]',
+        session.approximateDigits,
+      );
       final ends = parseSymjaList((await session.compute(endsExpr)).text)
       final ends = parseSymjaList((await session.compute(endsExpr)).text)
           .map(asNumber)
           .map(asNumber)
           .toList();
           .toList();

+ 8 - 2
lib/tools/symmetry.dart

@@ -84,7 +84,9 @@ class _SymmetryPageState extends State<SymmetryPage> {
               'Module[{t=(A*x+B*y+C)/(A^2+B^2)}, {x-2*A*t, y-2*B*t}]]';
               'Module[{t=(A*x+B*y+C)/(A^2+B^2)}, {x-2*A*t, y-2*B*t}]]';
           final r = parseSymjaList((await session.compute(expr)).text);
           final r = parseSymjaList((await session.compute(expr)).text);
           final rN = parseSymjaList(
           final rN = parseSymjaList(
-            (await session.compute('N[Rationalize[$expr], ${session.approximateDigits}]')).text,
+            (await session.compute(
+              approxExpr('Rationalize[$expr]', session.approximateDigits),
+            )).text,
           );
           );
           return [ToolText('对称点', '(${_pair(r, rN, 0)}, ${_pair(r, rN, 1)})')];
           return [ToolText('对称点', '(${_pair(r, rN, 0)}, ${_pair(r, rN, 1)})')];
         }
         }
@@ -120,7 +122,11 @@ class _SymmetryPageState extends State<SymmetryPage> {
   /// 精确值 + 近似值:每个分量各自配上自己的小数。
   /// 精确值 + 近似值:每个分量各自配上自己的小数。
   Future<List<Object>> _withApprox(SymjaSession session, String expr) async {
   Future<List<Object>> _withApprox(SymjaSession session, String expr) async {
     final r = parseSymjaList((await session.compute(expr)).text);
     final r = parseSymjaList((await session.compute(expr)).text);
-    final n = parseSymjaList((await session.compute('N[Rationalize[$expr], ${session.approximateDigits}]')).text);
+    final n = parseSymjaList(
+      (await session.compute(
+        approxExpr('Rationalize[$expr]', session.approximateDigits),
+      )).text,
+    );
     return [
     return [
       for (var i = 0; i < r.length; i++)
       for (var i = 0; i < r.length; i++)
         withApprox('${r[i]}', i < n.length ? '${n[i]}' : ''),
         withApprox('${r[i]}', i < n.length ? '${n[i]}' : ''),

+ 13 - 4
lib/tools/tool_kit.dart

@@ -391,6 +391,13 @@ class _OutputCard extends StatelessWidget {
       Text(text, style: const TextStyle(fontFamily: 'monospace', fontSize: 16));
       Text(text, style: const TextStyle(fontFamily: 'monospace', fontSize: 16));
 }
 }
 
 
+/// 近似值的 Symja 写法:保留 k 位**小数**。
+///
+/// 为什么不是 `N[expr, k]`:**Symja 的 N 忽略第二个参数**(实测 4 位和 15 位打印
+/// 一模一样)。`NumberForm` / `SetPrecision` 也没实现(会原样返回 numberform(...))。
+/// `Round[值, 1/10^k]` 是真把值截到 k 位小数,文本和 LaTeX 都跟着变干净。
+String approxExpr(String expr, int digits) => 'N[Round[$expr, 1/10^$digits]]';
+
 /// 精确值 + 近似值两行输出。
 /// 精确值 + 近似值两行输出。
 ///
 ///
 /// 工具页原先在 Dart 里既给分数又给小数(`1/3 (≈0.3333)`)。精确值走
 /// 工具页原先在 Dart 里既给分数又给小数(`1/3 (≈0.3333)`)。精确值走
@@ -410,8 +417,9 @@ Future<List<ToolOutput>> exactAndApprox(
   // `3.5999999999999996` 这种尾巴,先还原成分数再取小数就干净了(3.6)。
   // `3.5999999999999996` 这种尾巴,先还原成分数再取小数就干净了(3.6)。
   // 位数听设置里那个滑动条(计算器的"近似"也是同一个值)。
   // 位数听设置里那个滑动条(计算器的"近似"也是同一个值)。
   final approx = await session.compute(
   final approx = await session.compute(
-      'N[Rationalize[$expr], ${session.approximateDigits}]',
-      applyMode: false);
+    approxExpr('Rationalize[$expr]', session.approximateDigits),
+    applyMode: false,
+  );
   final same = _sameValue(exact.text, approx.text);
   final same = _sameValue(exact.text, approx.text);
   return [
   return [
     ToolText(label, exact.text, tex: exact.tex),
     ToolText(label, exact.text, tex: exact.tex),
@@ -444,8 +452,9 @@ Future<ToolOutput> numeric(
   String expr,
   String expr,
 ) async {
 ) async {
   final r = await session.compute(
   final r = await session.compute(
-      'N[Rationalize[$expr], ${session.approximateDigits}]',
-      applyMode: false);
+    approxExpr('Rationalize[$expr]', session.approximateDigits),
+    applyMode: false,
+  );
   return ToolText(label, r.text, tex: null);
   return ToolText(label, r.text, tex: null);
 }
 }
 
 

+ 2 - 2
test/widget_test.dart

@@ -446,7 +446,7 @@ void main() {
       await press(tester, '✓');
       await press(tester, '✓');
       final committed = calls.where(
       final committed = calls.where(
           (c) => c.method == 'evaluate' && c.arguments['preview'] == false);
           (c) => c.method == 'evaluate' && c.arguments['preview'] == false);
-      expect(committed.last.arguments['expr'], 'N[Sin[x], 15]');
+      expect(committed.last.arguments['expr'], 'N[Round[Sin[x], 1/10^10]]');
     });
     });
 
 
     testWidgets('近似模式不碰赋值表达式(否则赋值会被变成求值)', (tester) async {
     testWidgets('近似模式不碰赋值表达式(否则赋值会被变成求值)', (tester) async {
@@ -808,7 +808,7 @@ void main() {
 
 
       expect(
       expect(
         calls.lastWhere((c) => c.method == 'evaluate').arguments['expr'],
         calls.lastWhere((c) => c.method == 'evaluate').arguments['expr'],
-        'N[2, $digits]',
+        'N[Round[2, 1/10^$digits]]',
       );
       );
     });
     });