| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- // 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<CuoWeiXiangJianPage> createState() => _CuoWeiXiangJianPageState();
- }
- class _CuoWeiXiangJianPageState extends State<CuoWeiXiangJianPage> {
- 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 <ToolOutput>[
- 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';
- }
|