@jacksontian/equation-resolver 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/nodejs.yml +31 -0
- package/README.md +114 -0
- package/bin/cli.js +64 -0
- package/eslint.config.js +63 -0
- package/lib/evaluator.js +308 -0
- package/lib/fraction.js +123 -0
- package/lib/index.js +6 -0
- package/lib/lexer.js +115 -0
- package/lib/parser.js +118 -0
- package/lib/semantic-checker.js +56 -0
- package/package.json +34 -0
- package/test/evaluator.test.js +230 -0
- package/test/fraction.test.js +435 -0
- package/test/lexer.test.js +418 -0
- package/test/parser.test.js +264 -0
- package/test/semantic-checker.test.js +45 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
|
|
3
|
+
|
|
4
|
+
name: Node.js CI
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: [ "main" ]
|
|
9
|
+
pull_request:
|
|
10
|
+
branches: [ "main" ]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
strategy:
|
|
18
|
+
matrix:
|
|
19
|
+
node-version: [18.x, 20.x, 22.x, 24.x]
|
|
20
|
+
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
25
|
+
uses: actions/setup-node@v4
|
|
26
|
+
with:
|
|
27
|
+
node-version: ${{ matrix.node-version }}
|
|
28
|
+
cache: 'npm'
|
|
29
|
+
- run: npm ci
|
|
30
|
+
- run: npm run build --if-present
|
|
31
|
+
- run: npm test
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# 方程求解器
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@jacksontian/equation-resolver)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](https://github.com/JacksonTian/equation-resolver)
|
|
7
|
+
|
|
8
|
+
一个使用词法分析、语法分析和语义分析实现的方程求解程序。
|
|
9
|
+
|
|
10
|
+
## 功能特性
|
|
11
|
+
|
|
12
|
+
- 支持线性方程求解
|
|
13
|
+
- **支持多元一次方程组求解**(用分号分隔多个方程)
|
|
14
|
+
- 支持隐式乘法(如 `2x`, `x(`, `)x`, `xy`, `x2`)
|
|
15
|
+
- 支持括号表达式
|
|
16
|
+
- 支持除法表达式(如 `8 / 2x = 2`)
|
|
17
|
+
- 使用分数(Fraction)类进行精确计算,避免浮点数精度丢失
|
|
18
|
+
- 使用高斯消元法求解线性方程组
|
|
19
|
+
|
|
20
|
+
## 安装
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @jacksontian/equation-resolver -g
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 使用方法
|
|
27
|
+
|
|
28
|
+
### 命令行工具
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# REPL 模式(交互式求解)
|
|
32
|
+
solve
|
|
33
|
+
|
|
34
|
+
# 在 REPL 中输入方程求解
|
|
35
|
+
> 2x = 4
|
|
36
|
+
x = 2
|
|
37
|
+
|
|
38
|
+
> x + y = 5; x - y = 1
|
|
39
|
+
x = 3
|
|
40
|
+
y = 2
|
|
41
|
+
|
|
42
|
+
> exit # 退出
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 作为模块使用
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
import { Lexer, Parser, SemanticChecker, Evaluator } from '@jacksontian/equation-resolver';
|
|
49
|
+
|
|
50
|
+
// 单变量方程
|
|
51
|
+
function solve(input) {
|
|
52
|
+
const lexer = new Lexer(input);
|
|
53
|
+
const parser = new Parser(lexer);
|
|
54
|
+
const ast = parser.parse();
|
|
55
|
+
const checker = new SemanticChecker(ast);
|
|
56
|
+
checker.check();
|
|
57
|
+
const evaluator = new Evaluator(ast);
|
|
58
|
+
return evaluator.solve();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 使用示例
|
|
62
|
+
const result = solve('2x + 3 = 7');
|
|
63
|
+
console.log(result); // { x: 2 }
|
|
64
|
+
|
|
65
|
+
// 多元一次方程组(用分号分隔)
|
|
66
|
+
const systemResult = solve('x + y = 5; x - y = 1');
|
|
67
|
+
console.log(systemResult); // { x: 3, y: 2 }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 测试
|
|
71
|
+
|
|
72
|
+
项目使用 Node.js 内置的 `test` 模块(Node.js 18+)进行测试。
|
|
73
|
+
|
|
74
|
+
### 运行测试
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm test
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 测试覆盖率
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# 生成 HTML 和文本覆盖率报告
|
|
84
|
+
npm run test:cov
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
HTML 报告会生成在 `coverage/index.html`。
|
|
88
|
+
|
|
89
|
+
## 测试用例
|
|
90
|
+
|
|
91
|
+
测试用例覆盖了以下场景:
|
|
92
|
+
|
|
93
|
+
- 基础线性方程(加法、减法、乘法)
|
|
94
|
+
- **多元一次方程组**(二元、三元等)
|
|
95
|
+
- 除法表达式
|
|
96
|
+
- 括号和隐式乘法
|
|
97
|
+
- 复杂分数方程
|
|
98
|
+
- 小数系数
|
|
99
|
+
- 负数
|
|
100
|
+
- 多变量方程
|
|
101
|
+
- 错误情况(恒等式、矛盾方程、语法错误等)
|
|
102
|
+
|
|
103
|
+
## 技术实现
|
|
104
|
+
|
|
105
|
+
- **词法分析器(Lexer)**:将输入字符串转换为 token 流,支持数字、变量、运算符、括号等
|
|
106
|
+
- **语法分析器(Parser)**:使用递归下降解析器构建抽象语法树(AST),支持隐式乘法
|
|
107
|
+
- **语义检查器(SemanticChecker)**:检查方程是否包含变量,验证 AST 的有效性
|
|
108
|
+
- **求值器(Evaluator)**:遍历 AST 求解方程,支持单方程和方程组
|
|
109
|
+
- **分数类(Fraction)**:使用分数进行精确计算,避免浮点数精度丢失
|
|
110
|
+
- **方程组求解**:使用高斯消元法求解多元一次方程组,支持部分主元选择
|
|
111
|
+
|
|
112
|
+
## 许可证
|
|
113
|
+
|
|
114
|
+
The MIT License
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import readline from 'readline';
|
|
4
|
+
import { Lexer } from '../lib/lexer.js';
|
|
5
|
+
import { Parser } from '../lib/parser.js';
|
|
6
|
+
import { SemanticChecker } from '../lib/semantic-checker.js';
|
|
7
|
+
import { Evaluator } from '../lib/evaluator.js';
|
|
8
|
+
|
|
9
|
+
function main() {
|
|
10
|
+
// 进入 REPL 模式
|
|
11
|
+
const rl = readline.createInterface({
|
|
12
|
+
input: process.stdin,
|
|
13
|
+
output: process.stdout,
|
|
14
|
+
prompt: '> '
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
console.log('解方程 REPL 模式');
|
|
18
|
+
console.log('输入方程求解(用分号分隔多个方程可求解方程组),输入 exit 或 quit 退出\n');
|
|
19
|
+
rl.prompt();
|
|
20
|
+
|
|
21
|
+
rl.on('line', (line) => {
|
|
22
|
+
const input = line.trim();
|
|
23
|
+
|
|
24
|
+
if (input === 'exit' || input === 'quit' || input === 'q') {
|
|
25
|
+
rl.close();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (input === '') {
|
|
30
|
+
rl.prompt();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// 词法分析
|
|
36
|
+
const lexer = new Lexer(input);
|
|
37
|
+
|
|
38
|
+
// 语法分析
|
|
39
|
+
const parser = new Parser(lexer);
|
|
40
|
+
const ast = parser.parse();
|
|
41
|
+
|
|
42
|
+
const checker = new SemanticChecker(ast);
|
|
43
|
+
checker.check();
|
|
44
|
+
|
|
45
|
+
const evaluator = new Evaluator(ast);
|
|
46
|
+
const result = evaluator.solve();
|
|
47
|
+
for (const [variable, value] of Object.entries(result)) {
|
|
48
|
+
console.log(`${variable} = ${value}`);
|
|
49
|
+
}
|
|
50
|
+
console.log();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('错误:', error.message, '\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
rl.prompt();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
rl.on('close', () => {
|
|
59
|
+
console.log('\n再见!');
|
|
60
|
+
process.exit(0);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main();
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
js.configs.recommended,
|
|
5
|
+
{
|
|
6
|
+
languageOptions: {
|
|
7
|
+
ecmaVersion: 2024,
|
|
8
|
+
sourceType: 'module',
|
|
9
|
+
globals: {
|
|
10
|
+
console: 'readonly',
|
|
11
|
+
process: 'readonly',
|
|
12
|
+
Buffer: 'readonly',
|
|
13
|
+
__dirname: 'readonly',
|
|
14
|
+
__filename: 'readonly',
|
|
15
|
+
global: 'readonly',
|
|
16
|
+
module: 'readonly',
|
|
17
|
+
require: 'readonly',
|
|
18
|
+
exports: 'readonly',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
rules: {
|
|
22
|
+
'no-unused-vars': ['error', {
|
|
23
|
+
argsIgnorePattern: '^_',
|
|
24
|
+
varsIgnorePattern: '^_',
|
|
25
|
+
}],
|
|
26
|
+
'no-console': 'off',
|
|
27
|
+
'no-undef': 'error',
|
|
28
|
+
'no-redeclare': 'error',
|
|
29
|
+
'no-constant-condition': 'error',
|
|
30
|
+
'no-empty': 'error',
|
|
31
|
+
'no-extra-semi': 'error',
|
|
32
|
+
'no-func-assign': 'error',
|
|
33
|
+
'no-inner-declarations': 'error',
|
|
34
|
+
'no-sparse-arrays': 'error',
|
|
35
|
+
'no-unreachable': 'error',
|
|
36
|
+
'use-isnan': 'error',
|
|
37
|
+
'valid-typeof': 'error',
|
|
38
|
+
'no-var': 'error',
|
|
39
|
+
'prefer-const': 'error',
|
|
40
|
+
'prefer-arrow-callback': 'error',
|
|
41
|
+
'arrow-body-style': ['error', 'as-needed'],
|
|
42
|
+
'no-duplicate-imports': 'error',
|
|
43
|
+
'no-useless-constructor': 'error',
|
|
44
|
+
'no-useless-return': 'error',
|
|
45
|
+
'prefer-template': 'error',
|
|
46
|
+
'prefer-destructuring': ['error', {
|
|
47
|
+
array: false,
|
|
48
|
+
object: true,
|
|
49
|
+
}],
|
|
50
|
+
'object-shorthand': 'error',
|
|
51
|
+
'prefer-rest-params': 'error',
|
|
52
|
+
'prefer-spread': 'error',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
ignores: [
|
|
57
|
+
'node_modules/**',
|
|
58
|
+
'coverage/**',
|
|
59
|
+
'*.min.js',
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
package/lib/evaluator.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { Fraction } from './fraction.js';
|
|
2
|
+
|
|
3
|
+
// 单方程求值器
|
|
4
|
+
class EquationEvaluator {
|
|
5
|
+
constructor(ast) {
|
|
6
|
+
this.ast = ast;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// 将方程转换为标准形式: left - right = 0
|
|
10
|
+
toStandardForm() {
|
|
11
|
+
const {left} = this.ast;
|
|
12
|
+
const {right} = this.ast;
|
|
13
|
+
return {
|
|
14
|
+
type: 'BINARY_OP',
|
|
15
|
+
op: '-',
|
|
16
|
+
left,
|
|
17
|
+
right
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 使用给定的变量值计算表达式(支持多变量)
|
|
22
|
+
evaluateWithValues(node, variableValues) {
|
|
23
|
+
if (node.type === 'NUMBER') {
|
|
24
|
+
return Fraction.fromString(node.value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (node.type === 'VARIABLE') {
|
|
28
|
+
const value = variableValues[node.name] || 0;
|
|
29
|
+
return Fraction.fromNumber(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (node.type === 'BINARY_OP') {
|
|
33
|
+
const left = this.evaluateWithValues(node.left, variableValues);
|
|
34
|
+
const right = this.evaluateWithValues(node.right, variableValues);
|
|
35
|
+
|
|
36
|
+
// 处理 Infinity 情况
|
|
37
|
+
if (left === Infinity || right === Infinity) {
|
|
38
|
+
if (node.op === '/') {
|
|
39
|
+
// Infinity / Infinity = 0 (未定式,这里简化为 0)
|
|
40
|
+
if (right === Infinity) {
|
|
41
|
+
return new Fraction(0, 1);
|
|
42
|
+
}
|
|
43
|
+
// a / Infinity = 0 (a 为有限数)
|
|
44
|
+
return Infinity;
|
|
45
|
+
}
|
|
46
|
+
if (node.op === '*') {
|
|
47
|
+
// Infinity * a = Infinity (a 为有限数)
|
|
48
|
+
return Infinity;
|
|
49
|
+
}
|
|
50
|
+
if (node.op === '+' || node.op === '-') {
|
|
51
|
+
// Infinity + a = Infinity, Infinity - a = Infinity (a 为有限数)
|
|
52
|
+
return Infinity;
|
|
53
|
+
}
|
|
54
|
+
// 其他操作符不应该出现 Infinity,但为了安全返回 Infinity
|
|
55
|
+
return Infinity;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
switch (node.op) {
|
|
59
|
+
case '+': return left.add(right);
|
|
60
|
+
case '-': return left.subtract(right);
|
|
61
|
+
case '*': return left.multiply(right);
|
|
62
|
+
case '/':
|
|
63
|
+
// 除以零返回 Infinity,与 Fraction.divide 保持一致
|
|
64
|
+
// 这样在计算系数时可以使用备用方法
|
|
65
|
+
return left.divide(right);
|
|
66
|
+
default:
|
|
67
|
+
throw new Error(`Unknown operator: ${node.op}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
throw new Error(`Unknown node type: ${node.type}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 获取指定变量的系数(支持多变量方程),返回 Fraction
|
|
75
|
+
getVariableCoefficientFraction(variable, allVariables) {
|
|
76
|
+
const standardForm = this.toStandardForm();
|
|
77
|
+
|
|
78
|
+
// 使用两个不同的变量值来计算系数
|
|
79
|
+
const values1 = {};
|
|
80
|
+
const values0 = {};
|
|
81
|
+
|
|
82
|
+
allVariables.forEach(v => {
|
|
83
|
+
if (v === variable) {
|
|
84
|
+
values1[v] = 1;
|
|
85
|
+
values0[v] = 0;
|
|
86
|
+
} else {
|
|
87
|
+
values1[v] = 0;
|
|
88
|
+
values0[v] = 0;
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const val1 = this.evaluateWithValues(standardForm, values1);
|
|
93
|
+
const val0 = this.evaluateWithValues(standardForm, values0);
|
|
94
|
+
|
|
95
|
+
if (val0 === Infinity || !val0.isFinite()) {
|
|
96
|
+
const values2 = {};
|
|
97
|
+
allVariables.forEach(v => {
|
|
98
|
+
if (v === variable) {
|
|
99
|
+
values2[v] = 2;
|
|
100
|
+
} else {
|
|
101
|
+
values2[v] = 0;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
const val2 = this.evaluateWithValues(standardForm, values2);
|
|
105
|
+
return val2.subtract(val1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return val1.subtract(val0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 获取指定变量的系数(支持多变量方程),返回数字(向后兼容)
|
|
112
|
+
getVariableCoefficient(variable, allVariables) {
|
|
113
|
+
return this.getVariableCoefficientFraction(variable, allVariables).toNumber();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 获取常数项(支持多变量方程),返回 Fraction
|
|
117
|
+
getConstantTermFraction(allVariables) {
|
|
118
|
+
const standardForm = this.toStandardForm();
|
|
119
|
+
|
|
120
|
+
const values = {};
|
|
121
|
+
allVariables.forEach(v => {
|
|
122
|
+
values[v] = 0;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const val0 = this.evaluateWithValues(standardForm, values);
|
|
126
|
+
|
|
127
|
+
if (val0 === Infinity || !val0.isFinite()) {
|
|
128
|
+
const values1 = {};
|
|
129
|
+
allVariables.forEach(v => {
|
|
130
|
+
values1[v] = 1;
|
|
131
|
+
});
|
|
132
|
+
const val1 = this.evaluateWithValues(standardForm, values1);
|
|
133
|
+
|
|
134
|
+
// 计算所有变量的系数和
|
|
135
|
+
let totalCoeff = new Fraction(0, 1);
|
|
136
|
+
allVariables.forEach(v => {
|
|
137
|
+
const coeff = this.getVariableCoefficientFraction(v, allVariables);
|
|
138
|
+
totalCoeff = totalCoeff.add(coeff);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return val1.subtract(totalCoeff);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return val0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 获取常数项(支持多变量方程),返回数字(向后兼容)
|
|
148
|
+
getConstantTerm(allVariables) {
|
|
149
|
+
return this.getConstantTermFraction(allVariables).toNumber();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 语义分析器/求值器
|
|
154
|
+
export class Evaluator {
|
|
155
|
+
constructor(ast) {
|
|
156
|
+
this.ast = ast;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 从 AST 中提取所有不同的变量名
|
|
160
|
+
extractAllVariables(node) {
|
|
161
|
+
const variables = new Set();
|
|
162
|
+
|
|
163
|
+
if (node.type === 'VARIABLE') {
|
|
164
|
+
variables.add(node.name);
|
|
165
|
+
} else if (node.type === 'BINARY_OP') {
|
|
166
|
+
this.extractAllVariables(node.left).forEach(v => variables.add(v));
|
|
167
|
+
this.extractAllVariables(node.right).forEach(v => variables.add(v));
|
|
168
|
+
} else if (node.type === 'EQUATION') {
|
|
169
|
+
this.extractAllVariables(node.left).forEach(v => variables.add(v));
|
|
170
|
+
this.extractAllVariables(node.right).forEach(v => variables.add(v));
|
|
171
|
+
} else if (node.type === 'EQUATION_SYSTEM') {
|
|
172
|
+
node.equations.forEach(eq => {
|
|
173
|
+
this.extractAllVariables(eq).forEach(v => variables.add(v));
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return variables;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 求解方程组(实例方法)
|
|
181
|
+
solve() {
|
|
182
|
+
const {equations} = this.ast;
|
|
183
|
+
const allVariables = Array.from(this.extractAllVariables(this.ast)).sort();
|
|
184
|
+
|
|
185
|
+
if (equations.length < allVariables.length) {
|
|
186
|
+
throw new Error(`Equation count (${equations.length}) is less than variable count (${allVariables.length})`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 构建增广矩阵(直接使用 Fraction 保持精度)
|
|
190
|
+
const matrix = [];
|
|
191
|
+
const evaluators = equations.map(eq => new EquationEvaluator(eq));
|
|
192
|
+
for (const evaluator of evaluators) {
|
|
193
|
+
const row = [];
|
|
194
|
+
for (const variable of allVariables) {
|
|
195
|
+
const coeff = evaluator.getVariableCoefficientFraction(variable, allVariables);
|
|
196
|
+
row.push(coeff);
|
|
197
|
+
}
|
|
198
|
+
const constant = evaluator.getConstantTermFraction(allVariables);
|
|
199
|
+
row.push(constant.negate()); // 移项到右边
|
|
200
|
+
matrix.push(row);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 使用高斯消元法求解(矩阵已经是 Fraction 数组)
|
|
204
|
+
const isSingleEquation = equations.length === 1;
|
|
205
|
+
const solution = Evaluator.gaussianElimination(matrix, isSingleEquation);
|
|
206
|
+
|
|
207
|
+
// 构建结果对象
|
|
208
|
+
const result = {};
|
|
209
|
+
allVariables.forEach((variable, index) => {
|
|
210
|
+
result[variable] = solution[index];
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 高斯消元法求解线性方程组(使用 Fraction 进行精确计算)
|
|
217
|
+
static gaussianElimination(matrix, isSingleEquation = false) {
|
|
218
|
+
const n = matrix.length;
|
|
219
|
+
const m = matrix[0].length - 1; // 变量数量
|
|
220
|
+
|
|
221
|
+
// 将矩阵转换为 Fraction 矩阵(如果还不是 Fraction)
|
|
222
|
+
const fracMatrix = matrix.map(row =>
|
|
223
|
+
row.map(val => val instanceof Fraction ? val : Fraction.fromNumber(val))
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// 前向消元
|
|
227
|
+
for (let i = 0; i < Math.min(n, m); i++) {
|
|
228
|
+
// 找到主元(绝对值最大的行)
|
|
229
|
+
let maxRow = i;
|
|
230
|
+
let maxAbs = Math.abs(fracMatrix[i][i].toNumber());
|
|
231
|
+
for (let k = i + 1; k < n; k++) {
|
|
232
|
+
const absVal = Math.abs(fracMatrix[k][i].toNumber());
|
|
233
|
+
if (absVal > maxAbs) {
|
|
234
|
+
maxRow = k;
|
|
235
|
+
maxAbs = absVal;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 交换行
|
|
240
|
+
[fracMatrix[i], fracMatrix[maxRow]] = [fracMatrix[maxRow], fracMatrix[i]];
|
|
241
|
+
|
|
242
|
+
// 如果主元为 0,跳过
|
|
243
|
+
if (fracMatrix[i][i].isZero()) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 消元
|
|
248
|
+
for (let k = i + 1; k < n; k++) {
|
|
249
|
+
if (fracMatrix[k][i].isZero()) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const factor = fracMatrix[k][i].divide(fracMatrix[i][i]);
|
|
253
|
+
for (let j = i; j <= m; j++) {
|
|
254
|
+
fracMatrix[k][j] = fracMatrix[k][j].subtract(factor.multiply(fracMatrix[i][j]));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 回代(使用 Fraction 进行精确计算)
|
|
260
|
+
const solution = new Array(m);
|
|
261
|
+
|
|
262
|
+
for (let i = Math.min(n, m) - 1; i >= 0; i--) {
|
|
263
|
+
if (fracMatrix[i][i].isZero()) {
|
|
264
|
+
if (!fracMatrix[i][m].isZero()) {
|
|
265
|
+
throw new Error('Equation system has no solution');
|
|
266
|
+
}
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let sum = fracMatrix[i][m];
|
|
271
|
+
for (let j = i + 1; j < m; j++) {
|
|
272
|
+
sum = sum.subtract(fracMatrix[i][j].multiply(solution[j]));
|
|
273
|
+
}
|
|
274
|
+
solution[i] = sum.divide(fracMatrix[i][i]);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 检查是否有未确定的变量和恒等式
|
|
278
|
+
let allZeroRows = 0;
|
|
279
|
+
|
|
280
|
+
for (let i = 0; i < n; i++) {
|
|
281
|
+
let hasNonZero = false;
|
|
282
|
+
for (let j = 0; j < m; j++) {
|
|
283
|
+
if (!fracMatrix[i][j].isZero()) {
|
|
284
|
+
hasNonZero = true;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (!hasNonZero) {
|
|
289
|
+
if (!fracMatrix[i][m].isZero()) {
|
|
290
|
+
throw new Error('Equation system has no solution');
|
|
291
|
+
} else {
|
|
292
|
+
// 所有系数为 0 且常数项为 0,这是恒等式
|
|
293
|
+
allZeroRows++;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 检查是否所有行的系数都为 0(恒等式情况)
|
|
299
|
+
// 对于单方程,如果系数全为 0 且常数项为 0,则有无穷解
|
|
300
|
+
if (allZeroRows > 0 && isSingleEquation && m === 1) {
|
|
301
|
+
throw new Error('Equation has infinite solutions');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 将 Fraction 数组转换为数字数组
|
|
305
|
+
return solution.map(sol => sol.toNumber());
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
package/lib/fraction.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// 分数(有理数)类,用于精确计算,避免浮点数精度丢失
|
|
2
|
+
class Fraction {
|
|
3
|
+
constructor(numerator, denominator = 1) {
|
|
4
|
+
if (denominator === 0) {
|
|
5
|
+
throw new Error('分母不能为0');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// 处理符号
|
|
9
|
+
if (denominator < 0) {
|
|
10
|
+
numerator = -numerator;
|
|
11
|
+
denominator = -denominator;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 约分
|
|
15
|
+
const gcd = this.gcd(Math.abs(numerator), Math.abs(denominator));
|
|
16
|
+
this.numerator = numerator / gcd;
|
|
17
|
+
this.denominator = denominator / gcd;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 最大公约数
|
|
21
|
+
gcd(a, b) {
|
|
22
|
+
if (b === 0) return a;
|
|
23
|
+
return this.gcd(b, a % b);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 从字符串创建分数(支持整数和小数)
|
|
27
|
+
static fromString(str) {
|
|
28
|
+
if (str.includes('.')) {
|
|
29
|
+
// 小数:如 "3.14" -> 314/100
|
|
30
|
+
const parts = str.split('.');
|
|
31
|
+
const integerPart = parts[0];
|
|
32
|
+
const decimalPart = parts[1];
|
|
33
|
+
const denominator = Math.pow(10, decimalPart.length);
|
|
34
|
+
const numerator = parseInt(integerPart) * denominator +
|
|
35
|
+
(integerPart.startsWith('-') ? -1 : 1) * parseInt(decimalPart);
|
|
36
|
+
return new Fraction(numerator, denominator);
|
|
37
|
+
} else {
|
|
38
|
+
// 整数
|
|
39
|
+
return new Fraction(parseInt(str, 10), 1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 从数字创建分数(用于变量值等)
|
|
44
|
+
static fromNumber(num) {
|
|
45
|
+
if (Number.isInteger(num)) {
|
|
46
|
+
return new Fraction(num, 1);
|
|
47
|
+
}
|
|
48
|
+
// 对于浮点数,转换为分数
|
|
49
|
+
const str = num.toString();
|
|
50
|
+
return Fraction.fromString(str);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 加法
|
|
54
|
+
add(other) {
|
|
55
|
+
if (typeof other === 'number') {
|
|
56
|
+
other = Fraction.fromNumber(other);
|
|
57
|
+
}
|
|
58
|
+
const num = this.numerator * other.denominator + other.numerator * this.denominator;
|
|
59
|
+
const den = this.denominator * other.denominator;
|
|
60
|
+
return new Fraction(num, den);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 减法
|
|
64
|
+
subtract(other) {
|
|
65
|
+
if (typeof other === 'number') {
|
|
66
|
+
other = Fraction.fromNumber(other);
|
|
67
|
+
}
|
|
68
|
+
const num = this.numerator * other.denominator - other.numerator * this.denominator;
|
|
69
|
+
const den = this.denominator * other.denominator;
|
|
70
|
+
return new Fraction(num, den);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 乘法
|
|
74
|
+
multiply(other) {
|
|
75
|
+
if (typeof other === 'number') {
|
|
76
|
+
other = Fraction.fromNumber(other);
|
|
77
|
+
}
|
|
78
|
+
const num = this.numerator * other.numerator;
|
|
79
|
+
const den = this.denominator * other.denominator;
|
|
80
|
+
return new Fraction(num, den);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 除法
|
|
84
|
+
divide(other) {
|
|
85
|
+
if (typeof other === 'number') {
|
|
86
|
+
other = Fraction.fromNumber(other);
|
|
87
|
+
}
|
|
88
|
+
if (other.numerator === 0) {
|
|
89
|
+
return Infinity;
|
|
90
|
+
}
|
|
91
|
+
const num = this.numerator * other.denominator;
|
|
92
|
+
const den = this.denominator * other.numerator;
|
|
93
|
+
return new Fraction(num, den);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 取负
|
|
97
|
+
negate() {
|
|
98
|
+
return new Fraction(-this.numerator, this.denominator);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 转换为数字(用于最终结果)
|
|
102
|
+
toNumber() {
|
|
103
|
+
return this.numerator / this.denominator;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 检查是否为有限值
|
|
107
|
+
isFinite() {
|
|
108
|
+
return isFinite(this.numerator) && isFinite(this.denominator);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 检查是否为零
|
|
112
|
+
isZero() {
|
|
113
|
+
return this.numerator === 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 获取数值(用于比较)
|
|
117
|
+
valueOf() {
|
|
118
|
+
return this.toNumber();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { Fraction };
|
|
123
|
+
|