前言
近期我接到一个项目,需要根据施工时间、施工效率和施工比例的规则,推导出完整施工工期中所有施工时间的需求。经过需求分析,这实际上涉及解方程的过程。 假设规则如下:P10O1 = 0.8 * P10,其中P10O1是施工时间,0.8是施工效率,P10是P10O1这个工序的上一级工艺的总施工时间。 为了满足需求,需要求解方程 P10 = P10O1 / 0.8,以便计算出P10的值,然后可以进行后续所有工序的时间计算。
具体实现
第一步,推导出方程求解公式
为什么会有这一步,因为在需求中,只有一个工序的施工时间是已知的,比如说P10O1 = 0.8 * P10,需要根据P10O1来计算出P10的值,然后才能进行后续所有工序的时间计算。 algebra.js能轻松地推导出方程的解析公式。通过解析方程,我们可以得到类似于"P10 = P10O1 / 0.8"这样的表达式,其中P10O1是已知的施工时间,0.8是施工效率,P10是我们需要求解的变量。 接下来,我们实现了一个函数reverseEquation,它接受方程和目标变量作为参数,并返回求解结果。
export function reverseEquation(equation: string, target: string) {
// 判断是否为方程式
if (!equation.includes("=")) {
throw new Error("公式必须包含“=”");
}
// 去除空格
equation = equation.replace(/\s/g, "");
target = target.replace(/\s/g, "");
try {
const e = algebra.parse(equation) as algebra.Equation;
const expression = e.solveFor(target);
if (expression) {
return `${target} = ${expression.toString()}`;
}
} catch (error) {
throw new Error(equation + "公式不包含目标变量");
}
}
核心代码只有两行。
// 解析方程
const e = algebra.parse('P10O1=0.8*P10')
// 求解方程
e.solveFor('P10')
reverseEquation('P10O1=0.8*P10', 'P10') // P10 = 5/4P10O1
reverseEquation('P10O1=0.8*(X-P1-P10)', 'P10') // P10 = X - P1 - 5/4P10O1
第二步,计算出所有的一元一次方程
为了解决一组方程,我们还编写了另一个函数solveEquations。它接受一组方程字符串作为输入,并返回变量的解。这个函数通过多次迭代解析方程,直到所有的表达式都得到解析为止。
/**
* 解决一组方程式并返回变量的解
* @param equationStrs 方程式字符串数组
* @returns 变量的解
*/
export function solveEquations(equationStrs: string[]) {
// 存储变量和对应的分数值
const variables: Record<string, algebra.Fraction> = {};
// 存储解析后的结果
const result: Record<string, number> = {};
// 存储无法解析为分数的其他表达式
const otherExpression: Record<string, algebra.Expression> = {};
// 解析方程式
function solve(result: Record<string, number> = {}) {
for (let index = 0; index < equationStrs.length; index++) {
const equationStr = equationStrs[index];
const [key] = equationStr.replace(replaceSpaceReg, "").split("=");
// 如果变量已经有解,则跳过当前方程式
if (variables[key]) {
continue;
}
const equation = algebra.parse(equationStr) as algebra.Equation & {
eval: (variables: Object) => algebra.Equation;
};
const eq = equation.eval(variables);
const value = eq.solveFor(key);
if (!variables[key] && value) {
if (value instanceof algebra.Fraction) {
variables[key] = value;
delete otherExpression[key];
} else if (value instanceof algebra.Expression) {
otherExpression[key] = value;
}
}
}
// 对解析后的分数值进行取整
Object.entries(variables).forEach(([k, v]) => {
const value = v.valueOf();
if (typeof value === "number") {
result[k] = value;
}
});
}
solve(result);
let count = 0;
const MAX_PARSE_COUNT = 5;
// 解析多次,直到所有表达式都解析完毕, 设置最大解析次数为5次
while (Object.keys(otherExpression).length > 0 && count < MAX_PARSE_COUNT) {
solve(result);
count++;
}
// 如果还有未解析的表达式,打印警告
if (Object.keys(otherExpression).length > 0) {
Object.entries(otherExpression).forEach(([k, v]) => {
const equationStr = equationStrs.find((equationStr) => {
const [key] = equationStr.replace(replaceSpaceReg, "").split("=");
return key === k;
});
console.warn("无法解析的表达式", k, equationStr);
});
}
return result;
}
核心代码只有三行。
// 解析方程
const e = algebra.parse('P10O1=0.8*P10')
// 导入已知变量
const eq = equation.eval(variables);
// 求解方程
const value = eq.solveFor(key);
// demo
const equationStrs = [
"P10O1 = 4.5",
"X = 20",
"P10 = 5/4P10O1",
"P1=1",
"P1O1=1",
"P2=0.2*(X-P1-P10)",
"P2Lag=0.2*(X-P1-P10)",
"P2O1=0.6*P2",
"P2O2=0.7*P2",
"P2O2Lag=0.2*P2",
"P2O3=0.1*P2",
"P3=0.2*(X-P1-P10)",
"P3Lag=0.2*(X-P1-P10)",
"P3O1=0.6*P3",
"P3O2=0.7*P3",
"P3O2Lag=0.2*P3",
"P3O3=0.1*P3",
"P4=0.2*(X-P1-P10)",
"P4Lag=0.1*(X-P1-P10)",
"P4O1=0.6*P4",
"P4O2=0.8*P4",
"P4O2Lag=0.2*P4",
"P5=0.2*(X-P1-P10)",
"P5Lag=0.1*(X-P1-P10)",
"P5O1=0.6*P5",
"P5O2=0.8*P5",
"P5O2Lag=0.2*P5",
"P6=0.3*(X-P1-P10)",
"P6Lag=0.1*(X-P1-P10)",
"P6O1=0.1*P6",
"P6O2=0.6*P5",
"P6O3=0.8*P6",
"P6O3Lag=0.1*P6",
"P7=0.4*(X-P1-P10)",
"P7Lag=0.2*(X-P1-P10)",
"P7O1=0.3*P7",
"P7O2=0.8*P7",
"P7O2Lag=0.1*P7",
"P7O3=0.1*P7",
"P8=0.3*(X-P1-P10)",
"P8Lag=0.3*(X-P1-P10)",
"P8O1=0.3*P8",
"P8O2=0.9*P8",
"P8O2Lag=0.1*P8",
"P9=0.25*(X-P1-P10)",
"P9Lag=0.15*(X-P1-P10)",
"P9O1=0.3*P9",
"P9O2=0.8*P9",
"P9O2Lag=0.1*P9",
"P9O3=0.1*P9",
"P10O1=0.8*P10",
"P10O2=0.8*P10",
"P10O3=0.8*P10",
"P10O4=P10-0.5",
"P10O4Lag=0.5"
]
const result = solveEquations(equationStrs)
/**
result = {
"P10O1": 4.5,
"X": 20,
"P10": 5.625,
"P1": 1,
"P1O1": 1,
"P2": 2.675,
"P2Lag": 2.675,
"P2O1": 1.605,
"P2O2": 1.8725,
"P2O2Lag": 0.535,
"P2O3": 0.2675,
"P3": 2.675,
"P3Lag": 2.675,
"P3O1": 1.605,
"P3O2": 1.8725,
"P3O2Lag": 0.535,
"P3O3": 0.2675,
"P4": 2.675,
"P4Lag": 1.3375,
"P4O1": 1.605,
"P4O2": 2.14,
"P4O2Lag": 0.535,
"P5": 2.675,
"P5Lag": 1.3375,
"P5O1": 1.605,
"P5O2": 2.14,
"P5O2Lag": 0.535,
"P6": 4.0125,
"P6Lag": 1.3375,
"P6O1": 0.40125,
"P6O2": 1.605,
"P6O3": 3.21,
"P6O3Lag": 0.40125,
"P7": 5.35,
"P7Lag": 2.675,
"P7O1": 1.605,
"P7O2": 4.28,
"P7O2Lag": 0.535,
"P7O3": 0.535,
"P8": 4.0125,
"P8Lag": 4.0125,
"P8O1": 1.20375,
"P8O2": 3.61125,
"P8O2Lag": 0.40125,
"P9": 3.34375,
"P9Lag": 2.00625,
"P9O1": 1.003125,
"P9O2": 2.675,
"P9O2Lag": 0.334375,
"P9O3": 0.334375,
"P10O2": 4.5,
"P10O3": 4.5,
"P10O4": 5.125,
"P10O4Lag": 0.5
}
*/
需要注意的是,algebra.js不支持被除数为变量的方程,如10 = 100 / x,像这样的会报错。
总结
在整个解决过程中,我们使用了一些简单的核心代码,如方程的解析、方程求解和变量的存储。通过这些步骤,我们能够得到每个阶段的施工时间的解析结果。