mathsteps-experimental-fork
Version:
Step by step math solutions. Experimental Fork
251 lines (248 loc) • 9.34 kB
JavaScript
import { isOperatorNode, isConstantNode, isSymbolNode } from '../../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 (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 (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 (isConstantNode(n) || 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" || 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 };
}
}
export { combineMakeMinusNegativeTerms, combineNumberVarTimesTerms, flattenAndIndexTrackAST, getAddedAndRemovedTerms, getTermDifferencesViaIndexChanges, getTermDifferencesViaRemovals, makeCountTerms, mathNodePrecedence, termToString };