使用algebra.js轻松解决施工时间推导问题:优化方程求解方法

139 阅读4分钟

前言

近期我接到一个项目,需要根据施工时间、施工效率和施工比例的规则,推导出完整施工工期中所有施工时间的需求。经过需求分析,这实际上涉及解方程的过程。 假设规则如下: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,像这样的会报错。

总结

在整个解决过程中,我们使用了一些简单的核心代码,如方程的解析、方程求解和变量的存储。通过这些步骤,我们能够得到每个阶段的施工时间的解析结果。