// tools/cuo_wei_xiang_jian.dart import 'package:flutter/material.dart'; import 'tool_kit.dart'; /// 错位相减法。 /// /// 通项 aₙ = (a·n + b)·qⁿ⁺ᵐ 的数列,前 n 项和可以写成 /// Sₙ = (A·n + B)·qⁿ − B /// 其中 A = a·q^(m+1)/(q−1)、B = (b·q^(m+1) − A)/(q−1)。 /// /// B 的表达式里把 A 的定义整个代进去,省掉"先算一次、把结果拼回表达式"这种来回, /// Symja 各自求值一次就够。 class CuoWeiXiangJianPage extends StatefulWidget { const CuoWeiXiangJianPage({super.key}); @override State createState() => _CuoWeiXiangJianPageState(); } class _CuoWeiXiangJianPageState extends State { final _a = ToolField('a (系数)', hint: '输入分数如 1/2,或小数'); final _b = ToolField('b (常数)', hint: '输入分数如 1/2,或小数'); final _q = ToolField('q (公比)', hint: '输入分数如 1/2,或小数'); final _m = ToolField('m (指数偏移)', hint: '整数,如 1'); @override Widget build(BuildContext context) => ToolScaffold( title: '错位相减法', fields: [_a, _b, _q, _m], compute: (session) async { final a = _need(_a); final b = _need(_b); final q = _need(_q); final m = int.tryParse(_need(_m)); if (m == null) { throw Exception('「m (指数偏移)」必须是整数'); } // va/vb/vq/vm 这些前缀名是为了避开 Symja 的内置名(E、D、N 等)。 final bind = 'va=$a,vb=$b,vq=$q,vm=$m'; final aExpr = 'With[{$bind}, va*vq^(vm+1)/(vq-1)]'; // B 里把 A 的定义整个代进去,不依赖上一条结果。 final bExpr = 'With[{$bind}, (vb*vq^(vm+1)-(va*vq^(vm+1)/(vq-1)))/(vq-1)]'; final aRes = await session.compute(aExpr); final bRes = await session.compute(bExpr); // q 也回炉一次:用户可能填 `0.5`,Symja 会规范成 `1/2`,渲染才好看 final qv = (await session.compute('With[{$bind}, vq]')).text; final av = aRes.text; final bv = bRes.text; // 结果那条:Sₙ = (A·n + B)·qⁿ − B,把算出来的 A、B、q 代进去显示。 // **值全是 Symja 算的**,这里只负责把数字摆进公式、分数写成 \frac。 final s = sFormula(a: av, b: bv, q: qv); final aApprox = await session.compute( approxExpr('Rationalize[$aExpr]', session.approximateDigits), ); final bApprox = await session.compute( approxExpr('Rationalize[$bExpr]', session.approximateDigits), ); return [ ToolText('结果 Sₙ = (A·n + B)·qⁿ − B', s.plain, tex: s.tex), ToolText('A', av, tex: aRes.tex), if (aApprox.text != av) ToolText('A(近似)', aApprox.text), ToolText('B', bv, tex: bRes.tex), if (bApprox.text != bv) ToolText('B(近似)', bApprox.text), ]; }, ); /// 取一个标量输入框:空着或填了多个值都直接报中文错。 String _need(ToolField f) { if (f.count == 0) throw Exception('请填写「${f.label}」'); if (f.count > 1) throw Exception('「${f.label}」只能填一个数'); return f.text; } } /// 拼出 Sₙ = (A·n + B)·qⁿ − B 的两种写法(给人看的纯文本 / 给渲染的 LaTeX)。 /// /// 抽成顶层函数是为了能直接测:**尾巴那项是 −B**,B 取负值时该写成 `+ |B|`, /// 括号里才是 `A·n + B`(用 B 自己的符号)。这里把两者写成一样过一次, /// 结果 B = 2 时尾巴显示成 `+ 2`,整个公式是错的。 /// /// [a]、[b]、[q] 都是 Symja 算出来的数值原文(`-4`、`1/2` …),这里只排版。 @visibleForTesting ({String plain, String tex}) sFormula({ required String a, required String b, required String q, }) => ( plain: 'S(n) = ($a·n ${_signed(b, _plain)})·$q^n ' '${_signed(_neg(b), _plain)}', tex: 'S_{n} = (${texNum(a)}\\cdot n ${_signed(b, texNum)})\\cdot ' '{${texNum(q)}}^{n} ${_signed(_neg(b), texNum)}', ); /// `+ 4` / `- \frac{1}{2}`:把带符号的数接到前一项后面, /// 不要写成 `+ (-4)` 那种。 String _signed(String v, String Function(String) fmt) { final t = v.trim(); return t.startsWith('-') ? '- ${fmt(t.substring(1))}' : '+ ${fmt(t)}'; } String _plain(String v) => v; /// 取相反数。值都是 Symja 给的数字文本,翻个符号就行。 String _neg(String v) { final t = v.trim(); return t.startsWith('-') ? t.substring(1) : '-$t'; }