diff --git a/.DS_Store b/.DS_Store index 0fa7973..7210422 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/README.md b/README.md index adbb2f8..3a104d3 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ - an api - a large set of test data +- aims to determine if two mathematical expressions are equal, either having the same form or not - have a public website where you can add some math and check if it works - also the link should be shareable @@ -13,6 +14,7 @@ yarn demo Then go to `http://localhost:$PORT/demo` + ## tests ```shell @@ -27,9 +29,9 @@ There is one test that runs fixture data located here: `src/fixtures/latex-equal yarn jest src/__tests__/latex-equal.spec.ts -t src/fixtures/latex-equal/7119.ts --reporters default ``` + ## Next Steps -* Check that we have a test case for any outstanding jira tickets relating to math-validation. * Check that api we expose will support what is needed. * Do triage on the test failures, add a note to failing test so we can build a picture of the work needed @@ -52,27 +54,55 @@ yarn jest src/__tests__/latex-equal.spec.ts -t src/fixtures/latex-equal/7119.ts * more advanced literal validation (todo) * block input that is clearly too large/unrelated (eg: a user can type in gobbledy-gook - we should just abort if we see that) + +## Capabilities + +It can determine mathematical equivalence between: + +- linear equations in one variable +- linear equations in two variables +- 2-way inequalities in one or two variables +- compound inequalities in one variable +- trigonometric identities and functions +- inverse trigonometric functions +- similar notation for logarithms and based logarithms + +It can also handle degrees, radians and gradians + + ### things that'd be great (but we may have to park until we have more time) * a faster latex parser * faster math evaluation + ## modes There are 2 modes - literal and symbolic -Literal: needs to more advanced than the legacy literal implementation which was essentially a string check. +Literal: is at its most basic a tuned version of a string validation + +By default - ignores spaces and parentheses as long as they do not change the meaning of operations (ex. “a+7 +b” will validate against “ ((a) + (7))+b ”) + - ignores leading zeros: “0.1” will validate against “.1” + - accepts commas for decimal marks. For example “1,000” will be equivalent with 1000 + +Literal Validation offers two configuration options that can be used to validate some variety of forms for an expression: + +Ignore trailing zeros option; allows the evaluation to accept zeros to the right of the decimal place “4.5” will validate against “4.50000000000000” +Ignore order option; makes validation indifferent to the variables order, as long as it does not change operations meaning. In this case “a+7 +b*c” will validate against “7 + a+bc”, but not against “ac+7+b”; without it “a+7 +b” will not validate against “7 + a+b” + +Symbolic: attempts to decide if expressions are mathematically equivalent or not + +By default, it offers all configurations presented for literal validation, exceeding it by quite a lot +In order to check equivalence between 2 expressions, we have to reduce both expressions to the simplest one. Then distribute all coefficients, combine any like terms on each side of the expression, and arrange them in the same order. -Symbolic: ### Notes * `@babel/runtime` is a devDependency if you ever need to link this repo to another package for testing ## TODO -* strip logs on compile * set up api that is compatible w/ ui component options -* start going through the tests, build up literal + symbolic a bit att the start * derivatives kind of work and kind of not - how to use? ### CI diff --git a/src/__tests__/compare-compound-inequations.spec.ts b/src/__tests__/compare-compound-inequations.spec.ts new file mode 100644 index 0000000..4c551e4 --- /dev/null +++ b/src/__tests__/compare-compound-inequations.spec.ts @@ -0,0 +1,34 @@ +import { AstToMathJs } from "../conversion/ast-to-mathjs"; +import { LatexToAst } from "../conversion/latex-to-ast"; + +import { splitInequality } from "../symbolic/compare-compound-inequations"; + +const lta = new LatexToAst(); +const atm = new AstToMathJs(); + +describe("splitInequality", () => { + it.each` + compoundInequality | leftPart | rightPart + ${"20>x>7"} | ${"20>x"} | ${"x>7"} + ${" 5 ≥ -4 + x > -1 - 1"} | ${" 5 ≥ -4 + x"} | ${"-4 + x > -1 - 1"} + ${"x/x 20"} | ${"2<4x"} | ${"4x>20"} + ${"-3 < 2x+5 < 17"} | ${"-3<2x+5"} | ${"2x+5 < 17"} + ${"-3 = 6x = -3"} | ${"-3 = 6x"} | ${"6x = -3"} + ${"a≥b≥c "} | ${"a≥b"} | ${"b≥c"} + ${"a+2≥b-10≥c-100 "} | ${"a+2≥b-10"} | ${"b-10≥c-100"} + ${"a≠b≠c "} | ${"a≠b"} | ${"b≠c"} + `( + "$compoundInequality => $leftPart, $rightPart", + ({ compoundInequality, leftPart, rightPart }) => { + const inequality = atm.convert(lta.convert(compoundInequality)); + const broken = splitInequality(inequality); + + const leftSide = atm.convert(lta.convert(leftPart)); + const rightSide = atm.convert(lta.convert(rightPart)); + + expect(broken.left).toEqual(leftSide); + expect(broken.right).toEqual(rightSide); + } + ); +}); diff --git a/src/__tests__/compare-equations.spec.ts b/src/__tests__/utils.spec.ts similarity index 62% rename from src/__tests__/compare-equations.spec.ts rename to src/__tests__/utils.spec.ts index 848029a..e067b58 100644 --- a/src/__tests__/compare-equations.spec.ts +++ b/src/__tests__/utils.spec.ts @@ -1,18 +1,77 @@ import { AstToMathJs } from "../conversion/ast-to-mathjs"; import { LatexToAst } from "../conversion/latex-to-ast"; +import { simplify } from "../symbolic"; + import { - getUnknowns, + getVariables, getCoefficients, setXToOne, solveLinearEquation, -} from "../symbolic/compare-equations"; + expressionsCanBeCompared, + transformEqualityInExpression, +} from "../symbolic/utils"; const lta = new LatexToAst(); const atm = new AstToMathJs(); -describe("getUnknowns", () => { +describe("expressionsCanBeCompared", () => { + it('equations: "x = x" and "2=2" - should return false: equations can not be compared because second equation does not have a variable', () => { + const firstEquation = atm.convert(lta.convert("x=x")); + const secondEquation = atm.convert(lta.convert("2=2")); + const result = expressionsCanBeCompared(firstEquation, secondEquation); + + expect(result).toEqual(false); + }); + + it('equations: "x = x" and "\\log x=2" - should return false: equations can not be compared because second equation contains a function', () => { + const firstEquation = atm.convert(lta.convert("x=x")); + const secondEquation = atm.convert(lta.convert("\\log x=2")); + const result = expressionsCanBeCompared(firstEquation, secondEquation); + + expect(result).toEqual(false); + }); + + it('equations: "5z = 0" and "2y+3=m" - should return true: both equations have variables and does not contain functions', () => { + const firstEquation = atm.convert(lta.convert("x=x")); + const secondEquation = atm.convert(lta.convert("2y+3=m")); + const result = expressionsCanBeCompared(firstEquation, secondEquation); + + expect(result).toEqual(true); + }); + + it('equations: "x" and "y" - should return true: both expressions have variables and does not contain functions', () => { + const firstEquation = atm.convert(lta.convert("x")); + const secondEquation = atm.convert(lta.convert("y")); + const result = expressionsCanBeCompared(firstEquation, secondEquation); + + expect(result).toEqual(true); + }); +}); + +describe("transformEqualityInExpression", () => { it.each` - expression | unknowns + equation | transformedExpression + ${"x+5= 2x+3"} | ${"2-x"} + ${"5-2(3-m)= 4m+10"} | ${"-5-4m-2(3-m)"} + ${"a=2b+3"} | ${"a-2b-3"} + `( + "$equation => $transformedExpression", + ({ equation, transformedExpression }) => { + const equationToTransform = atm.convert(lta.convert(equation)); + const expression = simplify( + atm.convert(lta.convert(transformedExpression)) + ); + + const result = transformEqualityInExpression(equationToTransform); + + expect(result.equals(expression)).toEqual(true); + } + ); +}); + +describe("getVariables", () => { + it.each` + expression | variables ${"x"} | ${["x"]} ${"x +1"} | ${["x"]} ${"((x^2 + x) / x) - 1"} | ${["x"]} @@ -21,11 +80,11 @@ describe("getUnknowns", () => { ${"((y^2 + z) / x) - 1"} | ${["x", "y", "z"]} ${"109h"} | ${["h"]} ${"m+n+10"} | ${["m", "n"]} - `("$expression => $unknowns", ({ expression, unknowns }) => { + `("$expression => $variables", ({ expression, variables }) => { const equation = atm.convert(lta.convert(expression)); - const unknownsName = getUnknowns(equation); + const variablesName = getVariables(equation); - expect(unknownsName).toEqual(unknowns); + expect(variablesName).toEqual(variables); }); }); @@ -33,18 +92,19 @@ describe("getCoefficients", () => { it.each` expression | coefficients ${"x+0"} | ${[0, 1]} + ${"2x^2 = 2x"} | ${[1, 0]} ${"x +1"} | ${[1, 1]} ${"((x^2 + x) / x) - 1"} | ${[0, 0, 1]} - ${"1+2"} | ${[1, 0]} - ${"a +1+c"} | ${[1, 0]} + ${"1+2"} | ${[]} + ${"a +1+c"} | ${[]} ${"y^2+5y - 1"} | ${[-1, 5, 1]} ${"2y^2+4y"} | ${[0, 4, 2]} ${"109h"} | ${[0, 109]} - ${"m+n+10"} | ${[1, 0]} + ${"m+n+10"} | ${[]} ${"x-x"} | ${[0, 0]} ${"x + 5 - 3 + x - 6 - x + 2"} | ${[-2, 1]} ${"2x-x"} | ${[0, 1]} - ${"x - x - 2"} | ${[1, 0]} + ${"x - x - 2"} | ${[]} `("$expression => $coefficients", ({ expression, coefficients }) => { const equation = atm.convert(lta.convert(expression)); const coefficientsList = getCoefficients(equation); @@ -61,11 +121,11 @@ describe("getCoefficients", () => { expect(coefficientsList).toEqual([0, 0]); }); - it('equation: "1 = -2" - if equation has no coefficient for x it will return coefficients [1, 0]', () => { + it('equation: "1 = -2" - if equation has no coefficient for x but can be rationalized it will return an empty array', () => { const equation = atm.convert(lta.convert("1+2")); const coefficientsList = getCoefficients(equation); - expect(coefficientsList).toEqual([1, 0]); + expect(coefficientsList).toEqual([]); }); it('equation: "m + n = - 2" - if equation has more than one variable, will return coefficients [1, 0]', () => { @@ -148,7 +208,7 @@ describe("solveLinearEquation", () => { expect(result).toEqual(-Infinity); }); - it('equation: "2x^2 = 2x" - has no solution', () => { + it('equation: "2y^2+4y" - solution is -2', () => { const coefficients = [0, 4, 2]; const result = solveLinearEquation(coefficients); diff --git a/src/conversion/latex-to-ast.ts b/src/conversion/latex-to-ast.ts index 2cc0266..50702a0 100644 --- a/src/conversion/latex-to-ast.ts +++ b/src/conversion/latex-to-ast.ts @@ -298,10 +298,10 @@ export const latex_rules = [ ["\\\\lnot(?![a-zA-Z])", "NOT"], ["=", "="], + ["≠", "NE"], ["\\\\neq(?![a-zA-Z])", "NE"], ["\\\\ne(?![a-zA-Z])", "NE"], ["\\\\not\\s*=", "NE"], - ["≠", "NE"], ["\\\\leq(?![a-zA-Z])", "LE"], ["\\\\le(?![a-zA-Z])", "LE"], ["\\\\geq(?![a-zA-Z])", "GE"], @@ -689,7 +689,11 @@ export class LatexToAst { var lhs = this.expression(params); let relationalToken = (token) => - token === "<" || token === "LE" || token === ">" || token === "GE"; + token === "<" || + token === "LE" || + token === ">" || + token === "GE" || + token === "NE"; while ( this.token.token_type === "=" || @@ -733,6 +737,9 @@ export class LatexToAst { case "GE": case "ge": return "largerEq"; + case "NE": + case "ne": + return "unequal"; } }; diff --git a/src/difference.ts b/src/difference.ts index 04035ae..1c9ee42 100644 --- a/src/difference.ts +++ b/src/difference.ts @@ -6,7 +6,7 @@ const log = debug("difference"); export const differenceIsTooGreat = (a: RawAst, b: RawAst) => { const smallest = Math.min(a.toString().length, b.toString().length); const biggest = Math.max(a.toString().length, b.toString().length); - const errorAcceptance = 6; + const errorAcceptance = 17; const limit = (1 / smallest) * 100 + 10 + errorAcceptance; const diff = biggest - smallest; diff --git a/src/fixtures/latex-equal/symbolic/2-way-inequalities-with-variables.ts b/src/fixtures/latex-equal/symbolic/2-way-inequalities-with-variables.ts new file mode 100644 index 0000000..0824ab5 --- /dev/null +++ b/src/fixtures/latex-equal/symbolic/2-way-inequalities-with-variables.ts @@ -0,0 +1,93 @@ +export default { + mode: "symbolic", + tests: [ + // 2-way inequalities in one variable + { + target: "2x<4", + eq: ["4>2x", "x+x < 2*2", "x<2"], + ne: ["x*(-1)<2*(-1)"], + }, + { + target: "2x≤4", + eq: ["4≥2x", "x+x ≤ 2*2", "x≤2"], + ne: ["x<2", "3x≤4", "x*(-1)≤2*(-1)", "2x≥4", "2x=4"], + }, + { + target: "2x>4", + eq: ["4<2x", "x+x > 2*2"], + ne: ["x*(-1)>2*(-1)", "2x<4", "2x=4"], + }, + { + target: "2x≥4", + eq: ["4≤2x", "x+x ≥ 2*2"], + ne: ["x*(-1)≥2*(-1)"], + }, + { + target: "2x+y≤4+y", + eq: ["4≥2x", "x+x ≤ 2*2", "4x≤8", "2x+y-y-4≤0"], + ne: ["x<2", "3x≤4", "x<3", "-2x-y≤-4-y"], + }, + { + target: "\\frac{2x+4}{10}>4", + eq: ["\\frac{x+2}{5}>4", "\\frac{x}{5}+\\frac{2}{5}>4"], + ne: ["\\frac{x+3}{5}>4", "\\frac{2x}{5}+\\frac{2}{5}>4"], + }, + { + target: "\\frac{(2x-3)+9(4)}{4}≥\\frac{9+4x}{3}", + eq: [ + "\\frac{2x-3+36}{4}≥\\frac{9+4x}{3}", + "\\frac{2x+33}{4}≥\\frac{9+4x}{3}", + ], + ne: [ + "\\frac{2x+3+36}{4}≥\\frac{9+4x}{3}", + "\\frac{2x-33}{4}≥\\frac{9+4x}{3}", + ], + }, + { + target: "\\frac{5x-2}{3}-\\frac{7x-3}{5}>\\frac{x}{4}", + eq: ["\\frac{5(5x-2)-3(7x-3)}{15}>\\frac{x}{4}"], + ne: ["\\frac{5.1(5x-2)-3(7x-3)}{15}>\\frac{x}{4}"], + }, + { + target: "\\frac{x}{4}+1>\\frac{1}{2}", + eq: ["\\frac{x*4}{4}+1*4>\\frac{1*4}{2}", "x+4>2", "x>-2"], + ne: ["x>-1", "x≥-2"], + }, + { + target: "\\frac{-3}{4}m-\\frac{1}{8} ≤ - \\frac{1}{4}", + eq: [ + "8*(-\\frac{3}{4}m-\\frac{1}{8}) ≤ - \\frac{1}{4} * 8", + "8(-\\frac{3}{4}m)-8(\\frac{1}{8})≤ 8(-\\frac{1}{4})", + "-6m-1≤-2", + "-6m-1+1≤-2+1", + "m ≥ \\frac{1}{6}", + ], + ne: [ + "8(-\\frac{5}{4}m)-8(\\frac{1}{8})≤ 8(-\\frac{1}{4})", + "8(-\\frac{3}{4}m)-8(\\frac{2}{8})≤ 8(-\\frac{1}{4})", + ], + }, + // 2-way inequalities in two variables + { + target: "2x+3y≤4+y", + eq: ["4+y≥2x+3y", "x+x +3y≤ 2*2 +y", "4x+6y≤8+2y"], + ne: ["x+y<2", "3x+2y≤4", "x+4y≤3", "-2x-3y≤-4-y"], + }, + { + target: "5.5x + 8y≥100", + eq: [ + "11x+16y≥200", + "5.5x + 8y+10≥100+10", + "5.5x + 8y+z≥100+z", + "(5.5x + 8y)*2≥100*2", + "5.5x + 8y -100≥0", + "(5.5x + 8y)≥100", + ], + }, + { + target: "2x + 3y≥7", + eq: ["x + 1.5y≥3.5", "3x + 6y≥14-x", "-2x-3y≤-7"], + ne: ["-2x-3y≥-7", "3x + 6y≤14-x", "x + 1.5y≤3.5", "x + 1.5y=3.5"], + }, + ], +}; diff --git a/src/fixtures/latex-equal/symbolic/3-way-inequalities-with-variables.ts b/src/fixtures/latex-equal/symbolic/3-way-inequalities-with-variables.ts new file mode 100644 index 0000000..77425fa --- /dev/null +++ b/src/fixtures/latex-equal/symbolic/3-way-inequalities-with-variables.ts @@ -0,0 +1,77 @@ +export default { + mode: "symbolic", + tests: [ + { + target: "1<2x ≤ 3", + eq: ["3 ≥ 2x>1", "x/x0"], + ne: ["0 < 4x ≤ 22", "66 ≥ 6x>1"], + }, + { + target: "-4 < 2x - 8 ≤ 10", + eq: [ + "-2 < x - 4 ≤ 5", + "-4/4 < (2x - 8)/4 ≤ 10/4", + "-1 < x/2 - 2 ≤ 5/2", + " 5 ≥ -4 + x > -1 - 1", + ], + }, + { + target: "-3 < 2x +5 < 17", + eq: [ + "-3-5 < 2x +5-5 < 17-5", + "-8 < 2x < 12", + "\\frac{-8}{2}<\\frac{2x}{2}<\\frac{12}{2}", + ], + }, + { + target: "-5 ≤ 3 - 2z ≤ 5", + eq: ["-8 ≤ -2z ≤ 2", "4 ≥ z ≥ -1"], + }, + { + target: "57.06 ≤ 24.74 + 1.54b ≤ 171.02", + eq: [ + "57.06 - 24.74 ≤ 24.74 - 24.74 + 1.54b ≤ 171.02- 24.74", + "32.32 ≤1.54b ≤146.28 ", + ], + }, + { + target: "1 < 2x < 10", + eq: ["2 < 4x < 20"], + ne: [ + "2 = 4x = 20", + "1 <2x >10" + ], + }, + { + target: "a≠b≠c", + ne: ["c≠d≠e"] + }, + { + target: "a=b=c", + ne: ["c=d=e"] + }, + { + target: "-99 ≤ 9-12r ≤ 45", + eq: ["-108≤ -12r ≤ 36", "-108/-12≤ -12r/-12 ≤ 36/-12","9 ≥ r ≥ -3", "-3≤ r≤ 9", ], + ne: [ + "-108≤ -12r < 36", + "-108≤ -12r > 36", + "-107/-12≤ -12r/-12 ≤ 36/-12" + ], + }, + ], +}; diff --git a/src/fixtures/latex-equal/symbolic/7119-mixed-numbers.ts b/src/fixtures/latex-equal/symbolic/7119-mixed-numbers.ts index c4fa96f..bb0a50b 100644 --- a/src/fixtures/latex-equal/symbolic/7119-mixed-numbers.ts +++ b/src/fixtures/latex-equal/symbolic/7119-mixed-numbers.ts @@ -26,5 +26,13 @@ export default { "\\frac{10}{2} - 0.5", ], }, + { + target: "4\\frac{1}{2} + b + ca", + eq: [ + "\\frac{10}{2} + ac + b- 0.5" + ], + ne: ["\\frac{10}{2} + ac + b + 0.5"], + }, ], }; + diff --git a/src/fixtures/latex-equal/symbolic/equiv-symbolic.ts b/src/fixtures/latex-equal/symbolic/equiv-symbolic.ts index 0269578..b1d78ad 100644 --- a/src/fixtures/latex-equal/symbolic/equiv-symbolic.ts +++ b/src/fixtures/latex-equal/symbolic/equiv-symbolic.ts @@ -45,30 +45,14 @@ export default { { // equation with no variable target: "2+2=4", - // they should not be equal ne: ["1=1", "0.5=1/2", "9x=3(3x)"], }, { // equation with no variable target: "4=4", eq: ["2+2=2+2"], - // they should not be equal ne: ["1=1", "0.5=1/2", "9x=3(3x)"], }, - // { - // // 2-way inequality with variable(s) - // target: "2x<4", - // eq: ["4>2x", "x+x < 2*2", "x<2"], - // }, - { - // 3-way inequality with variable(s) - target: "1<2x ≤ 3", - eq: [ - "3 ≥ 2x>1", - "x/x { + switch (signName) { + case "smaller": + return "<"; + case "smallerEq": + return "<="; + case "larger": + return ">"; + case "largerEq": + return ">="; + case "equal": + return "=="; + case "unequal": + return "!="; + } +}; + +// splitInequality takes in a RelationalNode with 2 conditionals and 3 params and returns 2 operatorNodes each containing a 2-way inequality +export const splitInequality = (compoundInequality: any): NodePair => ({ + left: new m.OperatorNode( + operation(compoundInequality.conditionals[0]), + compoundInequality.conditionals[0], + [compoundInequality.params[0], compoundInequality.params[1]] + ), + right: new m.OperatorNode( + operation(compoundInequality.conditionals[1]), + compoundInequality.conditionals[1], + [compoundInequality.params[1], compoundInequality.params[2]] + ), +}); + +// get interval inferior/superior limits +export const getLimit = ( + expressionsPair: NodePair, + limitType: string +): number => { + const expressionR = transformEqualityInExpression(expressionsPair.right); + const expressionL = transformEqualityInExpression(expressionsPair.left); + + const xFirstInequality = solveLinearEquation(getCoefficients(expressionR)); + const xSecondInequality = solveLinearEquation(getCoefficients(expressionL)); + + if (limitType === "inferior") { + return Math.min(xFirstInequality, xSecondInequality); + } + + return Math.max(xFirstInequality, xSecondInequality); +}; + +export const compareCompoundInequations = ( + firstInequation: any, + secondInequation: any +) => { + + if (!expressionsCanBeCompared(firstInequation, secondInequation)) { + return false; + } + + const firstInequalityVariablesName = getVariables(firstInequation); + const secondInequalityVariablesName = getVariables(secondInequation); + + if ( + !equationsHaveTheSameVariables( + firstInequalityVariablesName, + secondInequalityVariablesName + ) && + firstInequalityVariablesName?.length === 1 + ) { + return false; + } + + const firstInequalities = splitInequality(firstInequation); + const secondInequalities = splitInequality(secondInequation); + + // find out interval for inequality solution; + // it does not matter whether we have an open, or half-open/half-close interval, the signs are already compared and at this point they match + let firstInequalitiesSolution: Range = { + min: getLimit(firstInequalities, "inferior"), + max: getLimit(firstInequalities, "superior"), + }; + + let secondInequalitiesSolution: Range = { + min: getLimit(secondInequalities, "inferior"), + max: getLimit(secondInequalities, "superior"), + }; + + // if interval limits are the same for both inequalities then we can say that inequalities are equivalent + return ( + firstInequalitiesSolution.min === secondInequalitiesSolution.min && + firstInequalitiesSolution.max === secondInequalitiesSolution.max + ); +}; diff --git a/src/symbolic/compare-equations.ts b/src/symbolic/compare-equations.ts index 3582f5f..447a325 100644 --- a/src/symbolic/compare-equations.ts +++ b/src/symbolic/compare-equations.ts @@ -1,149 +1,39 @@ import { mathjs } from "../mathjs"; import { MathNode } from "mathjs"; -import { isMathEqual, simplify } from "."; +import { + getVariables, + equationsHaveTheSameVariables, + getCoefficients, + solveLinearEquation, + setXToOne, + transformEqualityInExpression, + expressionsCanBeCompared, +} from "./utils"; const m: any = mathjs; -// check if equation is valid and find out the number of unknowns and their name -export const getUnknowns = (equation: MathNode) => { - let variableNames: string[] = []; - - equation.traverse(function (node, path, parent) { - if ( - node.isSymbolNode && - node?.name.length === 1 && - !variableNames.includes(node.name) - ) { - variableNames.push(node.name); - } - }); - - variableNames.sort(); - - return variableNames; -}; - -export const getCoefficients = (equation: MathNode) => { - let result: number[] = []; - - try { - const rationalizedEquation = m.rationalize(equation, {}, true); - result = rationalizedEquation.coefficients; - } catch (e) {} - - result = result.length === 0 ? [1, 0] : result; - - return result; -}; - -export const setXToOne = (equation: any, unknownName: string) => { - let result: MathNode; - - result = equation.transform(function (node, path, parent) { - if (node.isSymbolNode && node.name === unknownName) { - - return new m.ConstantNode(1); - } else { - - return node; - } - }); - - return result; -}; - -// solve x -export const solveLinearEquation = (coefficients: number[]) => { - let result: number; - - // TO DO: solve quadratic equation - if (coefficients.length === 3 && coefficients[0] === 0 ) { - coefficients = coefficients.splice(1, 2); - } - - if (coefficients.length === 2) { - if (coefficients[0] === 0 && coefficients[1] === 0) { - result = Infinity; - } else if (coefficients[0] === 0) { - result = 0; - } else { - // equation with no solution : if coefficient for x is 0 => division by zero => result == -Infinity - result = m.divide(coefficients[0], -1 * coefficients[1]); - } - } - - return result; -}; - -export const equationsHaveTheSameUnknowns = ( - firstEquationUnknowns: string[], - secondEquationUnknowns: string[] -) => { - return ( - Array.isArray(firstEquationUnknowns) && - Array.isArray(secondEquationUnknowns) && - firstEquationUnknowns.length === secondEquationUnknowns.length && - firstEquationUnknowns.every( - (unknonwn, index) => unknonwn === secondEquationUnknowns[index] - ) - ); -}; - export const compareEquations = ( firstEquation: MathNode, - secondEquation: MathNode + secondEquation: MathNode, + isInequality: boolean ) => { - let noFunctionOrArray: boolean = true; - let firstSymbolNode: boolean = false; - let symbolNode: boolean = false; let equivalence: boolean = false; - firstEquation.traverse(function (node, path, parent) { - noFunctionOrArray = - noFunctionOrArray || node.isFunctionNode || node.isArrayNode; - firstSymbolNode = firstSymbolNode || node.isSymbolNode; + if (expressionsCanBeCompared(firstEquation, secondEquation)) { + let firstExpression = transformEqualityInExpression(firstEquation); + let secondExpression = transformEqualityInExpression(secondEquation); - return node; - }); - - secondEquation.traverse(function (node, path, parent) { - if (node.isFunctionNode || node.isArrayNode) { - noFunctionOrArray = false; - } - - if (node.isSymbolNode && firstSymbolNode) symbolNode = true; - - return node; - }); - - // move the terms of the equations to the left hand side - if (noFunctionOrArray && symbolNode) { - let firstExpression = new m.OperatorNode( - "-", - "subtract", - firstEquation.args - ); - let secondExpression = new m.OperatorNode( - "-", - "subtract", - secondEquation.args - ); - - // remove added/subtracted numbers/variables from both sides of the equation - firstExpression = simplify(firstExpression); - secondExpression = simplify(secondExpression); - - if (isMathEqual(firstExpression, secondExpression)) { + if (firstExpression.equals(secondExpression)) { return true; } - let firstEquationUnknownsName = getUnknowns(firstExpression); - let secondEquationUnknownsName = getUnknowns(secondExpression); + let firstEquationVariablesName = getVariables(firstExpression); + let secondEquationVariablesName = getVariables(secondExpression); if ( - !equationsHaveTheSameUnknowns( - firstEquationUnknownsName, - secondEquationUnknownsName + !equationsHaveTheSameVariables( + firstEquationVariablesName, + secondEquationVariablesName ) ) { return false; @@ -153,18 +43,23 @@ export const compareEquations = ( let secondEquationCoefficients: number[]; // if both equations are linear in one variable then we solve "x" for both. If x has the same value then equations are equivalent - if (firstEquationUnknownsName.length === 1) { + if (firstEquationVariablesName.length === 1) { firstEquationCoefficients = getCoefficients(firstExpression); secondEquationCoefficients = getCoefficients(secondExpression); - equivalence = - solveLinearEquation(firstEquationCoefficients) === - solveLinearEquation(secondEquationCoefficients); + const solutionForFirstEquation = solveLinearEquation( + firstEquationCoefficients + ); + const solutionForSecondEquation = solveLinearEquation( + secondEquationCoefficients + ); + + equivalence = solutionForFirstEquation === solutionForSecondEquation; } // if both equations are linear in two variabled then we give value "1" for both "x". Doing this we get a linear equation in one variable "y". Then we solve "y" for both. If y has the same value then equations are equivalent - if (firstEquationUnknownsName.length === 2) { - let x = firstEquationUnknownsName[0]; + if (firstEquationVariablesName.length === 2) { + let x = firstEquationVariablesName[0]; // solve expression for x=1 let expraNoX = setXToOne(firstExpression, x); @@ -182,6 +77,23 @@ export const compareEquations = ( // if y has the same value, for the same x then the expressions should be equivalent equivalence = yFromFirstExpression === yFromSecondExpression; } + + // determine equivalence between 2-way inequalities with 1 or 2 variables: + // we treat 2-way inequalities the same way as linear equations in 1 or 2 variables; we find out the solutions that solve the inequality then compare them + // we have one distinct case, when multiplying both parts of an inequality with a negative number, the sign must change direction + if (equivalence && isInequality) { + // check if direction should be changed + return !( + (m.isPositive(firstEquationCoefficients[0]) && + m.isNegative(firstEquationCoefficients[1]) && + m.isNegative(secondEquationCoefficients[0]) && + m.isPositive(secondEquationCoefficients[1])) || + (m.isNegative(firstEquationCoefficients[0]) && + m.isPositive(firstEquationCoefficients[1]) && + m.isPositive(secondEquationCoefficients[0]) && + m.isNegative(secondEquationCoefficients[1])) + ); + } } return equivalence; diff --git a/src/symbolic/index.ts b/src/symbolic/index.ts index a193d79..96ff068 100644 --- a/src/symbolic/index.ts +++ b/src/symbolic/index.ts @@ -3,6 +3,7 @@ import { mathjs } from "../mathjs"; import { MathNode } from "mathjs"; import { sort } from "../node-sort"; import { compareEquations } from "./compare-equations"; +import { compareCompoundInequations } from "./compare-compound-inequations"; const m: any = mathjs; const log = logger("mv:symbolic"); @@ -74,8 +75,6 @@ const normalize = (a: string | MathNode | any) => { containsArrayNode = true; node.items = node.items.map((item) => simplify(item)); } - - return node; }); if (r.fn === "equal") { @@ -149,9 +148,59 @@ export const isMathEqual = (a: any, b: any) => { if (equality) { return true; } + // if both expressions are equations if (as.fn === "equal" && bs.fn === "equal") { - equality = compareEquations(as, bs); + return compareEquations(as, bs, false); + } + + // if both expressions are inequalities treat greater sign as equal sign + if ( + (as.fn === "larger" && bs.fn === "larger") || + (as.fn === "largerEq" && bs.fn === "largerEq") + ) { + // solving a 2 way inequality is the same as solving an equation, we can treat the greater sign as an equal sign + // the difference is that solution should be plotted on a number line (or interval) + // if we have the same signs and the same direction, the interval will always be the same, starting with the solution + as.fn = "equal"; + bs.fn = "equal"; + as.op = "="; + bs.op = "="; + + return compareEquations(as, bs, true); + } + + // check for compound inequalities/3-way inequalities + if ( + //@ts-ignore + as?.conditionals?.length === bs?.conditionals?.length && + //@ts-ignore + as?.conditionals?.length === 2 && + //@ts-ignore + as?.conditionals?.toString() === bs?.conditionals?.toString() + ) { + const params = [ + "smaller", + "smallerEq", + "larger", + "largerEq", + "equal", + "unequal", + ]; + + const paramsIncludedA = params.some((param) => + //@ts-ignore + as.conditionals.includes(param) + ); + + const paramsIncludedB = params.some((param) => + //@ts-ignore + bs.conditionals.includes(param) + ); + + if (paramsIncludedA && paramsIncludedB) { + return compareCompoundInequations(as, bs); + } } return equality; diff --git a/src/symbolic/utils.ts b/src/symbolic/utils.ts new file mode 100644 index 0000000..d63eb0f --- /dev/null +++ b/src/symbolic/utils.ts @@ -0,0 +1,134 @@ +import { mathjs } from "../mathjs"; +import { MathNode } from "mathjs"; +import { simplify as customSimplify } from "./"; +const { simplify } = mathjs; + +const m: any = mathjs; + +// expressions can be compared if we have at least one symbol node and has no function node or array +export const expressionsCanBeCompared = ( + firstEquation: MathNode, + secondEquation: MathNode +): boolean => { + let noFunctionOrArray: boolean = true; + let firstSymbolNode: boolean = false; + let symbolNode: boolean = false; + + firstEquation.traverse(function (node, path, parent) { + noFunctionOrArray = + noFunctionOrArray || node.isFunctionNode || node.isArrayNode; + firstSymbolNode = firstSymbolNode || node.isSymbolNode; + }); + + secondEquation.traverse(function (node, path, parent) { + if (node.isFunctionNode || node.isArrayNode) { + noFunctionOrArray = false; + } + + if (node.isSymbolNode && firstSymbolNode) symbolNode = true; + }); + + return noFunctionOrArray && symbolNode; +}; + +// move the terms of the equations to the left hand side +export const transformEqualityInExpression = (equality: MathNode) => + // remove added/subtracted numbers/variables from both sides of the equation + customSimplify(new m.OperatorNode("-", "subtract", equality.args)); + +// check if equation is valid and find out the number of variables and their name +export const getVariables = (equation: MathNode) => { + let variableNames: string[] = []; + + equation.traverse(function (node, path, parent) { + if ( + node.isSymbolNode && + node?.name.length === 1 && + !variableNames.includes(node.name) + ) { + variableNames.push(node.name); + } + }); + + return variableNames.sort(); +}; + +export const getCoefficients = (equation: MathNode) => { + // coefficients will be determined if equation has only one variable + + try { + const rationalizedEquation = m.rationalize(equation, {}, true); + return rationalizedEquation.coefficients; + } catch (e) { + // rationalize may fail if variable is isolated in a fraction + // we give it another try to rationalize after applying a new round of simplify to separate the variable + equation = simplify(equation, [ + { l: "(n1-n2)/n3", r: "n1/n3-n2/n3" }, + { l: "(n1+n2)/n3", r: "n1/n3+n2/n3" }, + { l: "(n1-n2)*n3/n4", r: "(n1*n3)/n4-(n2*n3)/n4" }, + { l: "(n1+n2)*n3/n4", r: "(n1*n3)/n4+(n2*n3)/n4" }, + ]); + + try { + const rationalizedEquation = m.rationalize(equation, {}, true); + return rationalizedEquation.coefficients; + } catch (e) {} + } + + return [1, 0]; +}; + +export const setXToOne = (equation: any, variableName: string) => + equation.transform(function (node, path, parent) { + if (node.isSymbolNode && node.name === variableName) { + return new m.ConstantNode(1); + } + + return node; + }); + +// TO DO: solve quadratic equation + +// solve x +export const solveLinearEquation = (coefficients: number[]) => { + let result: number; + + if (!coefficients) { + return undefined; + } + + if (coefficients.length === 3 && coefficients[0] === 0) { + coefficients = coefficients.splice(1, 2); + } + + if (coefficients.length === 2) { + if (coefficients[0] === 0 && coefficients[1] === 0) { + return Infinity; + } + + if (coefficients[0] === 0) { + return 0; + } + + // equation with no solution : if coefficient for x is 0 => division by zero => result == -Infinity + result = + Math.round(m.divide(coefficients[0], -1 * coefficients[1]) * 10000) / + 10000; + } + + return result; +}; + +export const equationsHaveTheSameVariables = ( + firstEquationVariables: string[], + secondEquationVariables: string[] +) => { + return ( + Array.isArray(firstEquationVariables) && + Array.isArray(secondEquationVariables) && + firstEquationVariables.length === secondEquationVariables.length && + firstEquationVariables.every( + (variable, index) => variable === secondEquationVariables[index] + ) + ); +};