mathsteps-experimental-fork
Version:
Step by step math solutions. Experimental Fork
765 lines (726 loc) • 28 kB
JavaScript
;
const config = require('../config.js');
const myNodeToString = require('../newServices/nodeServices/myNodeToString.js');
const parseText = require('../newServices/nodeServices/parseText.js');
const utilMadeFromMathJs = require('./utilMadeFromMathJs.js');
const ChangeTypes = require('../types/changeType/ChangeTypes.js');
const index = require('../node/index.js');
const msc = require('../utilRipFromMathJS/msc.js');
/* eslint-disable no-throw-literal,style/no-mixed-operators */
const SymbolNode = config.default.SymbolNode;
const OperatorNode = config.default.OperatorNode;
const FunctionNode = config.default.FunctionNode;
const AccessorNode = config.default.AccessorNode;
const ConstantNode = config.default.ConstantNode;
const equal = config.default.equal;
// We compile rules once and keep them for further reuse.
const RULES_CACHE = new Map();
function _typeof(obj) {
if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') {
// eslint-disable-next-line no-func-assign
_typeof = function _typeof(obj) {
return typeof obj
};
}
else {
// eslint-disable-next-line no-func-assign
_typeof = function _typeof(obj) {
return obj && typeof Symbol === 'function' && obj.constructor === Symbol && obj !== Symbol.prototype ? 'symbol' : typeof obj
};
}
return _typeof(obj)
}
let _lastsym = 0;
/* const simplifyCache = new Map()
function makeKeyForSimplifyCache(expr, rules, scope, options) {
const exprStr = makeKeyFromNode(expr)
const rulesKeyPart = rules.map(rule => rule.id || rule.name).join('|')
const scopePart = scope && Object.values(scope).length > 0 ? JSON.stringify(scope) : 'noScope'
const optionsPart = options && Object.values(options).length > 0 ? JSON.stringify(options) : 'noOptions'
return `${exprStr}|${rulesKeyPart}|${scopePart}|${optionsPart}`
// return `${exprStr}|${rulesKeyPart}|${scopePart}|${optionsPart}`
} */
/**
* Simplify an expression tree.
*
* A list of rules are applied to an expression, repeating over the list until
* no further changes are made.
* It's possible to pass a custom set of rules to the function as second
* argument. A rule can be specified as an object, string, or function:
*
* const rules = [
* { l: 'n1*n3 + n2*n3', r: '(n1+n2)*n3' },
* 'n1*n3 + n2*n3 -> (n1+n2)*n3',
* function (node) {
* // ... return a new node or return the node unchanged
* return node
* }
* ]
*
* String and object rules consist of a left and right pattern. The left is
* used to match against the expression and the right determines what matches
* are replaced with. The main difference between a pattern and a normal
* expression is that variables starting with the following characters are
* interpreted as wildcards:
*
* - 'n' - matches any Node
* - 'c' - matches any ConstantNode
* - 'v' - matches any Node that is not a ConstantNode
*
* The default list of rules is exposed on the function as `simplify.rules`
* and can be used as a basis to built a set of custom rules.
*
* For more details on the theory, see:
*
* - [Strategies for simplifying math expressions (Stackoverflow)](https://stackoverflow.com/questions/7540227/strategies-for-simplifying-math-expressions)
* - [Symbolic computation - Simplification (Wikipedia)](https://en.wikipedia.org/wiki/Symbolic_computation#Simplification)
*
* An optional `options` argument can be passed as last argument of `simplify`.
* There is currently one option available: `exactFractions`, a boolean which
* is `true` by default.
*
* Syntax:
*
* simplify(expr)
* simplify(expr, rules)
* simplify(expr, rules)
* simplify(expr, rules, scope)
* simplify(expr, rules, scope, options)
* simplify(expr, scope)
* simplify(expr, scope, options)
*
* Examples:
*
* math.simplify('2 * 1 * x ^ (2 - 1)') // Node '2 * x'
* math.simplify('2 * 3 * x', {x: 4}) // Node '24'
* const f = math.parse('2 * 1 * x ^ (2 - 1)')
* math.simplify(f) // Node '2 * x'
* math.simplify('0.4 * x', {}, {exactFractions: true}) // Node 'x * 2 / 5'
* math.simplify('0.4 * x', {}, {exactFractions: false}) // Node '0.4 * x'
*
* See also:
*
* derivative, parse, evaluate, rationalize
*
* @param {Node | string} expr
* The expression to be simplified
* @param {Array<{l: string, r: string} | string | Function>} [_rules]
* Optional list with custom rules
* @param scope
* @param options
* @return {Node} Returns the simplified form of `expr`
*/
function simplify(expr, _rules, scope, options) {
// const key = makeKeyForSimplifyCache(expr, rules, scope, options)
// if (simplifyCache.has(key)) {
// return simplifyCache.get(key)
// }
const steps = [];
const rules = buildRulesAndMistakeRules(_rules, scope, options);
let res = config.resolve(expr);
const visited = {};
let str = myNodeToString.myNodeToString(res, { parenthesis: 'all' });
let newRes = null;
options = options || {};
while (!visited[str]) {
visited[str] = true;
_lastsym = 0; // counter for placeholder symbols
for (let i = 0; i < rules.length; i++) {
if (typeof rules[i] === 'function') {
newRes = rules[i](res, options);
}
else {
utilMadeFromMathJs.flatten(res);
// handle full mistakes, which are the rules with mistakeL
if (rules[i].isMistake) {
let builtMistakes = [];
for (const mistakeRule of rules[i].mistakes) {
builtMistakes.push(applyRule(res, mistakeRule, scope, options));
}
builtMistakes = builtMistakes.filter(m => m).map(m => ({ ...m, isMistake: true }));
const firstMistake = builtMistakes.find(m => m);
newRes = { ...firstMistake, mistakes: builtMistakes, isMistake: true };
}
else {
newRes = applyRule(res, rules[i], scope, options);
}
}
// handle normal rule+mistakes here, which are the mistakeRules that don't specify a mistakeL
if (newRes && !rules[i].mistakeL && rules[i].mistakes && options.getMistakes) {
const mistakeReses = [];
for (const mistakeRule of rules[i].mistakes) {
const ruleToUse = {
...rules[i],
// r: mistakeRule?.r,
l: rules[i].l,
replaceFct: mistakeRule.replaceFct,
};
const mistakeResponse = applyRule(res, ruleToUse, scope, options);
if (mistakeResponse)
mistakeReses.push({ ...mistakeResponse, ruleApplied: { id: mistakeRule.id } });
}
newRes.mistakes = mistakeReses;
}
if (newRes) {
steps.push(newRes);
// TODO Had to change it to ChangeTypes only for some reason. Look at error - ['2/(4x) = 1', 'x = 1/2'] when off. I believe it has something to do with evaluateEquation type rules
const foundNewResIdInChangeTypes = Object.values(ChangeTypes.ChangeTypes).includes(newRes.ruleApplied?.id);
if (options.stopOnFirstStep && foundNewResIdInChangeTypes) {
return steps
}
res = newRes.nodeAfter;
utilMadeFromMathJs.unflattenl(res); // using left-heavy binary tree here since custom rule functions may expect it
}
}
str = myNodeToString.myNodeToString(res, { parenthesis: 'all' });
}
// simplifyCache.set(key, steps)
return steps
}
const SUPPORTED_CONSTANTS = {
true: true,
false: true,
e: true,
i: true,
Infinity: true,
LN2: true,
LN10: true,
LOG2E: true,
LOG10E: true,
NaN: true,
phi: true,
pi: true,
SQRT1_2: true,
SQRT2: true,
tau: true, // null: false,
// undefined: false,
// version: false,
// Array of strings, used to build the ruleSet.
// Each l (left side) and r (right side) are parsed by
// the expression parser into a node tree.
// Left hand sides are matched to subtrees within the
// expression to be parsed and replaced with the right
// hand side.
// TODO: Add support for constraints on constants (either in the form of a '=' expression or a callback [callback allows things like comparing symbols alphabetically])
// To evaluate lhs constants for rhs constants, use: { l: 'c1+c2', r: 'c3', evaluate: 'c3 = c1 + c2' }. Multiple assignments are separated by ';' in block format.
// It is possible to get into an infinite loop with conflicting rules
};
function _preprocessRuleNode(node) {
if (index.default.Type.isUnaryMinus(node)
&& index.default.Type.isSymbol(node.args[0]) && (node.args[0].name[0] === 'c')) {
// Negative constant node: -c.
// Encode negative sign as flag to make nodes match simpler.
node = node.args[0];
node.isNegative = true;
}
else {
// Non constant-node. Go on recurisively if possible.
if (node.args) {
node.args.forEach((childNode, idx) => {
node.args[idx] = _preprocessRuleNode(childNode);
});
}
}
return node
}
/**
* Parse the string array of rules into nodes
*
* Example syntax for rules:
*
* Position constants to the left in a product:
* { l: 'n1 * c1', r: 'c1 * n1' }
* n1 is any Node, and c1 is a ConstantNode.
*
* Apply difference of squares formula:
* { l: '(n1 - n2) * (n1 + n2)', r: 'n1^2 - n2^2' }
* n1, n2 mean any Node.
*
* Short hand notation:
* 'n1 * c1 -> c1 * n1'
*/
function _buildRules(_rules, scope, options) {
const rules = _rules.filter(rule => !rule.mistakeL);
if (rules.length === 0)
return []
let ruleSet = RULES_CACHE.get(rules);
if (!ruleSet) {
// Possible improvement: cache rules.
// Array of rules to be used to simplify expressions
ruleSet = [];
for (let i = 0; i < rules.length; i++) {
let rule = rules[i];
let newRule = void 0;
const ruleType = _typeof(rule);
switch (ruleType) {
case 'string':
{
const lr = rule.split('->');
if (lr.length === 2) {
rule = {
l: lr[0],
r: lr[1],
};
}
else {
throw new SyntaxError(`Could not parse rule: ${rule}`)
}
}
/* falls through */
case 'object': {
const ruleLeft = rule.l;
const ruleRight = rule.replaceFct ? '_rv' : rule.r;
newRule = {
l: parseText.parseText(ruleLeft),
r: parseText.parseText(ruleRight),
mistakes: rule.mistakes,
id: rule.id,
replaceFct: rule.replaceFct,
};
_preprocessRuleNode(newRule.l);
if (rule.context)
newRule.evaluate = rule.context;
if (rule.evaluate)
newRule.evaluate = parseText.parseText(rule.evaluate);
if (utilMadeFromMathJs.isAssociative(newRule.l)) {
const makeNode = utilMadeFromMathJs.createMakeNodeFunction(newRule.l);
const expandsym = _getExpandPlaceholderSymbol();
newRule.expanded = {};
newRule.expanded.l = makeNode([newRule.l.clone(), expandsym]); // Push the expandsym into the deepest possible branch.
// This helps to match the newRule against nodes returned from getSplits() later on.
utilMadeFromMathJs.flatten(newRule.expanded.l);
utilMadeFromMathJs.unflattenr(newRule.expanded.l);
newRule.expanded.r = makeNode([newRule.r, expandsym]);
}
break
}
case 'function': {
newRule = rule;
break
}
default: {
throw new TypeError(`Unsupported type of rule: ${ruleType}`)
}
}
ruleSet.push(newRule);
}
// Cache precompiled rules for further use.
RULES_CACHE.set(rules, ruleSet);
}
// Verify rules.
if (rules[0].id !== ruleSet[0].id) {
console.log('rule[0] :', rules[0]);
console.log('ruleSet[0]:', { l: ruleSet[0].l.toString(), r: ruleSet[0].r.toString(), id: ruleSet[0].id });
throw 'error: bad rule set'
}
return ruleSet
}
function _getExpandPlaceholderSymbol() {
return new SymbolNode(`_p${_lastsym++}`)
}
/**
* Returns a simplfied form of node, or the original node if no simplification was possible.
*
* @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} node
* @return {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} The simplified form of `expr`, or the original node if no simplification was possible.
*/
function applyRule(node, rule, scope, options) {
let rv = null;
// console.log('Entering applyRule(' + node.toString() + ')')
// Do not clone node unless we find a match
let res = node; // First replace our child nodes with their simplified versions
// If a child could not be simplified, the assignments will have
// no effect since the node is returned unchanged
// Try to match a rule against this node
let repl = rule.r;
let matches = _ruleMatch(rule.l, res, false, scope, options)[0]; // If the rule is associative operator, we can try matching it while allowing additional terms.
// This allows us to match rules like 'n+n' to the expression '(1+x)+x' or even 'x+1+x' if the operator is commutative.
if (!matches && rule.expanded) {
repl = rule.expanded.r;
matches = _ruleMatch(rule.expanded.l, res, false, scope, options)[0];
}
if (matches) {
// const before = res.toString({parenthesis: 'all'})
// Create a new node by cloning the rhs of the matched rule
// we keep any implicit multiplication state if relevant
const implicit = res.implicit;
res = repl.clone();
if (implicit && 'implicit' in repl)
res.implicit = true;
// Replace placeholders with their respective nodes without traversing deeper into the replaced nodes
if (rule.replaceFct)
matches.placeholders._rv = rule.replaceFct(node, matches.placeholders);
res = res.transform((node) => {
if (node.isSymbolNode && (msc.hasOwnProperty)(matches.placeholders, node.name))
return matches.placeholders[node.name].clone()
else
return node
});
rv = {
ruleApplied: rule,
nodeBefore: node,
nodeAfter: res,
matches,
};
}
// rv.mRv = mRv
return rv
// Original was:
// return res;
}
/**
* Get (binary) combinations of a flattened binary node
* e.g. +(node1, node2, node3) -> [
* +(node1, +(node2, node3)),
* +(node2, +(node1, node3)),
* +(node3, +(node1, node2))]
*
*/
function getSplits(node, context) {
const res = [];
let right, rightArgs;
const makeNode = utilMadeFromMathJs.createMakeNodeFunction(node);
if (utilMadeFromMathJs.isCommutative(node, context)) {
for (let i = 0; i < node.args.length; i++) {
rightArgs = node.args.slice(0);
rightArgs.splice(i, 1);
right = rightArgs.length === 1 ? rightArgs[0] : makeNode(rightArgs);
res.push(makeNode([node.args[i], right]));
}
}
else {
rightArgs = node.args.slice(1);
right = rightArgs.length === 1 ? rightArgs[0] : makeNode(rightArgs);
res.push(makeNode([node.args[0], right]));
}
return res
}
/**
* Returns the set union of two match-placeholders or null if there is a conflict.
*/
function mergeMatch(match1, match2) {
const res = {
placeholders: {}, // Some matches may not have placeholders; this is OK
};
if (!match1.placeholders && !match2.placeholders)
return res
else if (!match1.placeholders)
return match2
else if (!match2.placeholders)
return match1
// Placeholders with the same key must match exactly
for (const key in match1.placeholders) {
res.placeholders[key] = match1.placeholders[key];
if ((msc.hasOwnProperty)(match2.placeholders, key)) {
if (!_exactMatch(match1.placeholders[key], match2.placeholders[key]))
return null
}
}
for (const _key in match2.placeholders)
res.placeholders[_key] = match2.placeholders[_key];
return res
}
/**
* Combine two lists of matches by applying mergeMatch to the cartesian product of two lists of matches.
* Each list represents matches found in one child of a node.
*/
function combineChildMatches(list1, list2) {
const res = [];
if (list1.length === 0 || list2.length === 0)
return res
let merged;
for (let i1 = 0; i1 < list1.length; i1++) {
for (let i2 = 0; i2 < list2.length; i2++) {
merged = mergeMatch(list1[i1], list2[i2]);
if (merged)
res.push(merged);
}
}
return res
}
/**
* Combine multiple lists of matches by applying mergeMatch to the cartesian product of two lists of matches.
* Each list represents matches found in one child of a node.
* Returns a list of unique matches.
*/
function mergeChildMatches(childMatches) {
if (childMatches.length === 0)
return childMatches
const sets = childMatches.reduce(combineChildMatches);
const uniqueSets = [];
const unique = {};
for (let i = 0; i < sets.length; i++) {
const s = JSON.stringify(sets[i]);
if (!unique[s]) {
unique[s] = true;
uniqueSets.push(sets[i]);
}
}
return uniqueSets
}
/**
* Determines whether node matches rule.
*
* @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} rule
* @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} node
* @return {object} Information about the match, if it exists.
*/
function _ruleMatch(rule, node, isSplit, scope, options) {
// console.log('Entering _ruleMatch(' + JSON.stringify(rule) + ', ' + JSON.stringify(node) + ')')
// console.log('rule = ' + rule)
// console.log('node = ' + node)
// console.log('Entering _ruleMatch(' + rule.toString() + ', ' + node.toString() + ')')
let res = [{
placeholders: {},
}];
// console.log("------------------")
// console.log("MATCHING ", rule.toString(), ' | ', node.toString())
// console.log("MATCHING (1)", rule)
// console.log("MATCHING (2)", node)
if ((rule instanceof SymbolNode) && (rule.name[0] === 'a')) {
// Equation: Any node WITHOUT unknown variable.
if (options.unknownVariable == null) {
// Error - we want to match for unknown variable, but
// we don't know what symbol is used for (e.g. x).
throw 'error: unset unknown variable name'
}
else if (!index.default.Type.doesContainSymbol(node, options.unknownVariable)) {
// Matched - node does not contain unknown variable (x-like).
res[0].placeholders[rule.name] = node;
}
else {
// Mis-match: node contains unknown variable.
return []
}
}
else if ((rule instanceof SymbolNode) && (rule.name.substr(0, 2) === 'fx')) {
// Equation: Any node containing unknown variable f(x).
if (options.unknownVariable == null) {
// Error - we want to match for unknown variable, but
// we don't know what symbol is used for (e.g. x).
throw 'error: unset unknown variable name'
}
else if (index.default.Type.doesContainSymbol(node, options.unknownVariable)) {
// Matched - node containt any expression, which contains unkown
// variable (x-like).
res[0].placeholders[rule.name] = node;
}
else {
// Mis-match: node does not contain an unknown variable (x-like).
return []
}
}
else if (rule instanceof OperatorNode && node instanceof OperatorNode || rule instanceof FunctionNode && node instanceof FunctionNode) {
// If the rule is an OperatorNode or a FunctionNode, then node must match exactly
if (rule instanceof OperatorNode) {
if (rule.op !== node.op || rule.fn !== node.fn)
return []
}
else if (rule instanceof FunctionNode) {
if (rule.name !== node.name)
return []
} // rule and node match. Search the children of rule and node.
if (node.args.length === 1 && rule.args.length === 1 || !utilMadeFromMathJs.isAssociative(node) || isSplit) {
// Expect non-associative operators to match exactly
const childMatches = [];
for (let i = 0; i < rule.args.length; i++) {
const childMatch = _ruleMatch(rule.args[i], node.args[i], false, scope, options);
if (childMatch.length === 0) {
// Child did not match, so stop searching immediately
return []
} // The child matched, so add the information returned from the child to our result
childMatches.push(childMatch);
}
res = mergeChildMatches(childMatches);
}
else if (node.args.length >= 2 && rule.args.length === 2) {
// node is flattened, rule is not
// Associative operators/functions can be split in different ways so we check if the rule matches each
// them and return their union.
const splits = getSplits(node, rule.context);
let splitMatches = [];
for (let _i = 0; _i < splits.length; _i++) {
const matchSet = _ruleMatch(rule, splits[_i], true, scope, options); // recursing at the same tree depth here
splitMatches = splitMatches.concat(matchSet);
}
return splitMatches
}
else if (rule.args.length > 2) {
throw new Error(`Unexpected non-binary associative function: ${rule.toString()}`)
}
else {
// Incorrect number of arguments in rule and node, so no match
return []
}
}
else if (rule instanceof SymbolNode) {
// If the rule is a SymbolNode, then it carries a special meaning
// according to the first character of the symbol node name.
// c.* matches a ConstantNode
// n.* matches any node
if (rule.name.length === 0)
throw new Error('Symbol in rule has 0 length...!?')
if (SUPPORTED_CONSTANTS[rule.name]) {
// built-in constant must match exactly
if (rule.name !== node.name)
return []
}
else if (rule.name === 'x') {
// Equation: unknown variable.
if (options.unknownVariable == null) {
// Error - we want to match for unknown variable, but
// we don't know what symbol is used for (e.g. x).
throw 'error: unset unknown variable name'
}
// eslint-disable-next-line eqeqeq
else if (node.name == options.unknownVariable) {
// Matched - node is an unknown variable itself (x-like).
res[0].placeholders[rule.name] = node;
}
else {
// Mis-match: it's not an unknown variable node (x-like).
return []
}
}
else if (rule.name[0] === 'n' || rule.name.substring(0, 2) === '_p') {
// rule matches _anything_, so assign this node to the rule.name placeholder
// Assign node to the rule.name placeholder.
// Our parent will check for matches among placeholders.
res[0].placeholders[rule.name] = node;
}
else if (rule.name[0] === 'v') {
// rule matches any variable thing (not a ConstantNode)
if (!(config.isConstantNode)(node)) {
res[0].placeholders[rule.name] = node;
}
else {
// Mis-match: rule was expecting something other than a ConstantNode
return []
}
}
else if (rule.name[0] === 'c') {
// rule matches any ConstantNode
if (node instanceof ConstantNode) {
if (rule.isNegative) {
if (config.default.isNegative(node.value)) {
// -c : negative constant matched
// Possible improvement: Optimize it.
res[0].placeholders[rule.name] = index.default.Creator.unaryMinus(node);
}
else {
// We want negative constant, but non-negative found.
return []
}
}
else {
res[0].placeholders[rule.name] = node;
}
}
else {
// Mis-match: rule was expecting a ConstantNode
return []
}
}
else {
throw new Error(`Invalid symbol in rule: ${rule.name}`)
}
}
else if (rule instanceof AccessorNode) {
// Try exactly const.x match.
// Example: const.pi vs const.pi
if ((!(node instanceof AccessorNode))
|| (rule.object.name !== 'const')
|| (node.object.name !== 'const')
|| (rule.index.dimensions.length !== 1)
|| (node.index.dimensions.length !== 1)
|| (rule.index.dimensions[0].value !== node.index.dimensions[0].value)) {
return []
}
}
else if (rule instanceof ConstantNode) {
// Literal constant must match exactly
if (!equal(rule.value, node.value))
return []
}
else {
// Some other node was encountered which we aren't prepared for, so no match
return []
} // It's a match!
return res
}
/**
* Determines whether p and q (and all their children nodes) are identical.
*
* @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} p
* @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} q
* @return {object} Information about the match, if it exists.
*/
function _exactMatch(p, q) {
if (p instanceof ConstantNode && q instanceof ConstantNode) {
if (!equal(p.value, q.value))
return false
}
else if (p instanceof SymbolNode && q instanceof SymbolNode) {
if (p.name !== q.name)
return false
}
else if (p instanceof OperatorNode && q instanceof OperatorNode || p instanceof FunctionNode && q instanceof FunctionNode) {
if (p instanceof OperatorNode) {
if (p.op !== q.op || p.fn !== q.fn)
return false
}
else if (p instanceof FunctionNode) {
if (p.name !== q.name)
return false
}
if (p.args.length !== q.args.length)
return false
for (let i = 0; i < p.args.length; i++) {
if (!_exactMatch(p.args[i], q.args[i]))
return false
}
}
else if (p instanceof AccessorNode && q instanceof AccessorNode) {
// Possible improvement: Handle multidimentional arrays?
if ((p.object.name !== q.object.name)
|| (p.index.dimensions.length !== 1)
|| (q.index.dimensions.length !== 1)
|| (p.index.dimensions[0].value !== q.index.dimensions[0].value)) {
return false
}
}
else {
return false
}
return true
}
function buildRulesAndMistakeRules(_rules, scope, options) {
if (!_rules || _rules.length === 0)
return _rules
const normalRules = _rules.filter(rule => !rule.mistakeL);
const normalBuiltRules = _buildRules(normalRules);
if (!options.getMistakes)
return normalBuiltRules
// This does not include mistakes that are just attached to a rule, like commonRulesMistakes. It only includes rules that have a mistakeL
let mistakeRulesWithMistakeL = _rules.filter(rule => rule.mistakeL);
mistakeRulesWithMistakeL = mistakeRulesWithMistakeL.map?.((rule) => {
return rule.mistakes.map((mistake) => {
const res = _buildRules([mistake])[0];
if (!res) {
return null
}
return { ...res, isMistake: true, mistakeL: rule.l }
}).filter(builtRule => builtRule)
});
// combine rules
if (mistakeRulesWithMistakeL.length > 0) {
const firstMistake = mistakeRulesWithMistakeL[0][0];
const newMistakeRule = { ...firstMistake, mistakes: mistakeRulesWithMistakeL[0], isMistake: true };
normalBuiltRules.push(newMistakeRule);
return normalBuiltRules
}
else {
return normalBuiltRules
}
}
module.exports = simplify;