UNPKG

mathsteps-experimental-fork

Version:

Step by step math solutions. Experimental Fork

263 lines (258 loc) 9.72 kB
'use strict'; Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const config = require('../../config.js'); function termToString(terms) { terms = terms.sort((a, b) => a.index - b.index); let lastDepth = 0; let result = ""; let openParentheses = 0; for (let i = 0; i < terms.length; i++) { const term = terms[i]; const nextTerm = terms[i + 1] ?? null; const currentDepth = term?.parenthesisDepth ?? 0; while (currentDepth > lastDepth) { result += "("; openParentheses++; lastDepth++; } while (currentDepth < lastDepth) { result += ")"; openParentheses--; lastDepth--; } result += term.value; if (!nextTerm && currentDepth > 0) { while (openParentheses > 0) { result += ")"; openParentheses--; } } } while (openParentheses > 0) { result += ")"; openParentheses--; } return result.replace(/\s+/g, ""); } function flattenAndIndexTrackAST(node) { const terms = []; let currentIndex = 0; function traverse({ n, parentOperator = null, operandPosition = null, depth = 0, parenthesisDepth = 0, inParentheses = false // Track if current node is within parentheses }) { const newDepth = depth + 1; let needsParentheses = false; if (config.isOperatorNode(n) && parentOperator) { const currentPrecedence = mathNodePrecedence(n); const parentPrecedence = mathNodePrecedence(parentOperator); if (currentPrecedence < parentPrecedence || currentPrecedence === parentPrecedence && operandPosition === "right") { needsParentheses = true; parenthesisDepth++; } } const currentInParentheses = inParentheses || needsParentheses; if (config.isOperatorNode(n)) { if (n.args.length === 2) { traverse({ n: n.args[0], parentOperator: n, operandPosition: "left", depth: newDepth, inParentheses: currentInParentheses, parenthesisDepth }); terms.push({ type: "operator", value: n.op, index: currentIndex, isInParentheses: currentInParentheses, depth: newDepth, parenthesisDepth }); currentIndex += n.op.length; traverse({ n: n.args[1], parentOperator: n, operandPosition: "right", depth: newDepth, inParentheses: currentInParentheses, parenthesisDepth }); } else if (n.args.length === 1) { terms.push({ type: "operator", value: n.op, index: currentIndex, isInParentheses: currentInParentheses, depth: newDepth, parenthesisDepth }); currentIndex += n.op.length; traverse({ n: n.args[0], parentOperator: n, operandPosition: "left", depth: newDepth, inParentheses: currentInParentheses, parenthesisDepth }); } } else if (config.isConstantNode(n) || config.isSymbolNode(n)) { const termStr = n.toString(); const operation = parentOperator ? parentOperator.op : null; terms.push({ type: "term", value: termStr, index: currentIndex, isInParentheses: currentInParentheses, depth: newDepth, parenthesisDepth, operationAppliedToTerm: { operation, position: operandPosition // 'left' or 'right' } }); currentIndex += termStr.length; } } traverse({ n: node }); return terms; } function mathNodePrecedence(node) { if (typeof node === "string" || config.isOperatorNode(node)) { const precedenceMap = { "^": 4, "*": 3, "/": 3, "+": 2, "+-": 2, "-": 2, "=": 1 }; const op = typeof node === "string" ? node : node.op; return precedenceMap[op] || 0; } else { return 5; } } function makeCountTerms(terms) { const counts = {}; for (const { type, value } of terms) { const key = `${type}:${value}`; counts[key] = (counts[key] || 0) + 1; } return terms.map((term) => { term.count = counts[`${term.type}:${term.value}`]; return term; }); } function isNumberOrDecimal(value) { return value.match(/^\d+\.?\d*$/) !== null; } function combineNumberVarTimesTerms(terms) { for (let i = 0; i < terms.length; i++) { const number = terms[i]; const operator = terms?.[i + 1]; const variable = terms?.[i + 2]; const isVarNotInParenthesis = variable && (variable.depth === number.depth || variable.depth === number.depth + 1 && variable.depth === number.depth); if (number.type === "term" && isNumberOrDecimal(number.value) && operator?.value === "*" && operator.type === "operator" && variable?.type === "term" && variable?.value.match(/[a-zA-Z]/) && isVarNotInParenthesis) { terms[i].value = terms[i].value + terms[i + 2].value; terms.splice(i + 1, 2); for (let j = i + 1; j < terms.length; j++) { terms[j].index = j; } } } return terms; } function combineMakeMinusNegativeTerms(terms) { for (let i = 0; i < terms.length; i++) { if (terms[i - 1]?.type === "term" && (terms[i].type === "operator" && terms[i].value === "-") && terms[i + 1]?.type === "term" && !terms[i + 1]?.value.includes("-")) { terms[i + 1].value = `${terms[i].value}${terms[i + 1].value}`; terms[i] = { type: "operator", value: "+", index: terms[i].index }; for (let j = i; j < terms.length; j++) { terms[j].index = j; } } } return terms; } function getTermDifferencesViaRemovals(a, b) { const issues = []; const combinedBA_B = [...b]; for (let i = 0; i < a.length; i++) { if (!b.some((item) => item.value === a[i].value)) combinedBA_B.push({ ...a[i], count: 0 }); } const diffBA = combinedBA_B.map((item, index) => { const aCount = a.find((aItem) => aItem.value === item.value)?.count || 0; const bCount = b.find((bItem) => bItem.value === item.value)?.count || 0; return { ...item, diff: bCount - aCount }; }).sort((a2, b2) => a2.index - b2.index); const removedTermsFlattened = []; const addedTermsFlattened = []; for (let i = 0; i < diffBA.length; i++) { if (diffBA[i].diff > 0) { for (let j = 0; j < diffBA[i].diff; j++) addedTermsFlattened.push(diffBA[i]); } else if (diffBA[i].diff < 0) { for (let j = 0; j < -diffBA[i].diff; j++) removedTermsFlattened.push(diffBA[i]); } } const removedTermsFlattenedNoOp = removedTermsFlattened.filter((item) => item.type !== "operator"); const addedTermsFlattenedNoOp = addedTermsFlattened.filter((item) => item.type !== "operator"); if (removedTermsFlattenedNoOp.length !== 2 || addedTermsFlattenedNoOp.length !== 1) { issues.push("NO_2_REMOVED_OR_NOT_1_ADDED"); } const removedOps = removedTermsFlattened.filter((item) => item.type === "operator"); function getRemovedTerm1Index() { const removedTerm1Value = removedTermsFlattenedNoOp[0].value; return a.find( (item) => ( // We are getting the removed term1 from the original expression. // TODO, I am not sure this is working right. This whole function is a mess. item.value === removedTerm1Value && !b.some((bItem) => bItem.value === item.value && bItem.index === item.index) ? item.index : null ) )?.index ?? removedTermsFlattenedNoOp[0].index; } const term1Index = getRemovedTerm1Index(); const op = term1Index + 1 ? a.find((item) => item.index === term1Index + 1 && item.type === "operator") : null; if (!op || removedOps.length === 0) issues.push("NO_OP"); const removed = removedTermsFlattened.map((term) => ({ term })).sort((a2, b2) => a2.term.index - b2.term.index); const added = addedTermsFlattened.map((term) => ({ term })).sort((a2, b2) => a2.term.index - b2.term.index); const opFound = op ? { term: op } : null; return { removed, added, issues, opFound }; } function getTermDifferencesViaIndexChanges(fromTerms, toTerms) { const removedTerms = []; const addedTerms = []; let fromIndex = 0; let toIndex = 0; while (fromIndex < fromTerms.length || toIndex < toTerms.length) { const fromTerm = fromTerms[fromIndex]; const toTerm = toTerms[toIndex]; if (fromTerm && toTerm && fromTerm.value === toTerm.value) { fromIndex++; toIndex++; } else if (fromTerm && (!toTerm || fromTerm.value !== toTerm.value)) { removedTerms.push({ term: fromTerm }); fromIndex++; } else if (toTerm && (!fromTerm || fromTerm.value !== toTerm.value)) { addedTerms.push({ term: toTerm }); toIndex++; } } return { added: addedTerms, removed: removedTerms }; } function getAddedAndRemovedTerms(fromTerms, toTerms) { const { removed, added, issues, opFound } = getTermDifferencesViaRemovals(fromTerms, toTerms); if (added.length > 0 && removed.length > 0 && issues.length === 0) { return { removed, added, opFound }; } else { const { removed: viaIndexRemoved, added: viaIndexAdded } = getTermDifferencesViaIndexChanges(fromTerms, toTerms); return viaIndexRemoved?.length && viaIndexAdded?.length ? { removed: viaIndexRemoved, added: viaIndexAdded, opFound: null } : { removed: [], added: [], opFound: null }; } } exports.combineMakeMinusNegativeTerms = combineMakeMinusNegativeTerms; exports.combineNumberVarTimesTerms = combineNumberVarTimesTerms; exports.flattenAndIndexTrackAST = flattenAndIndexTrackAST; exports.getAddedAndRemovedTerms = getAddedAndRemovedTerms; exports.getTermDifferencesViaIndexChanges = getTermDifferencesViaIndexChanges; exports.getTermDifferencesViaRemovals = getTermDifferencesViaRemovals; exports.makeCountTerms = makeCountTerms; exports.mathNodePrecedence = mathNodePrecedence; exports.termToString = termToString;