Reputation: 127
I want to create a function that generates mathematical expressions like ( 21 + 13 ) * 56
using random numbers from 1 to 100.
The function must take a level parameter. The level determines the length of the generated equation, for example:
// level 2
75 - 54 = 21
62 + 15 = 77
88 / 22 = 4
93 + 22 = 115
90 * 11 = 990
// level 3
( 21 + 13 ) * 56 = 1904
82 - 19 + 16 = 79
51 * ( 68 - 2 ) = 3366
So far I can create equations without brackets but I need help that would give me a reliable solution. This is what I have done so far:
var level = 3;
var x = ['/', '*', '-', '+'];
function randomNumberRange(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
var a = '';
for (var i = 0; i < level; i++) {
if (i !== level - 1) {
var n1 = randomNumberRange(1, 100);
var m = randomNumberRange(0, x.length);
var str = x[m];
a += n1;
a += ' ' + str + ' ';
} else {
a += n1;
}
}
Upvotes: 7
Views: 8143
Reputation: 12323
Here an implementation that works for +
, -
, *
, /
, %
, ^
, parentheses and functions (min, max, sin, cos, tan, log). You can also easily add support for more functions like sqrt, asin, acos...
const operatorsKeys = ['+', '-', '*', '/', '%', '^'];
const functions = {
min: { arity: 2 },
max: { arity: 2 },
sin: { arity: 1 },
cos: { arity: 1 },
tan: { arity: 1 },
log: { arity: 1 }
};
const functionsKeys = Object.keys(functions);
// ⚠️ High probability that the expression calculation is NaN because of 'log(-1)', '-1 ^ 0.1', '1 % 0', '1 / 0 * 0'
function getRandomMathExpression(nbNodes: number): string {
assert(nbNodes > 0, 'nbNodes must be > 0');
if (nbNodes === 1) {
//return getRandomInt(-9, 9).toString();
return getRandomFloat(-100, 100, { decimalPlaces: 2 }).toString();
}
const operator = operatorsKeys[getRandomInt(0, operatorsKeys.length - 1)];
const func = functionsKeys[getRandomInt(0, functionsKeys.length - 1)];
const nbNodesLeft = Math.floor(nbNodes / 2);
const nbNodesRight = Math.ceil(nbNodes / 2);
const left = getRandomMathExpression(nbNodesLeft);
const right = getRandomMathExpression(nbNodesRight);
let expr;
if (Math.random() < 0.5) {
// eval("-1 ** 2") => eval("(-1) ** 2")
// Fix "SyntaxError: Unary operator used immediately before exponentiation expression..."
expr = operator === '^' ? `(${left}) ${operator} ${right}` : `${left} ${operator} ${right}`;
expr = Math.random() < 0.5 ? `(${expr})` : expr;
} else {
expr =
functions[func]!.arity === 2
? `${func}(${left}, ${right})`
: `${func}(${left}) ${operator} ${right}`;
}
return expr;
}
// Exported for testing purposes only
// https://stackoverflow.com/a/45736131
export function getNumberWithDecimalPlaces(num: number, decimalPlaces: number) {
const power = 10 ** decimalPlaces;
return Math.floor(num * power) / power;
}
type GetRandomNumberOptions = {
/**
* The number of digits to appear after the decimal point.
* https://ell.stackexchange.com/q/141863
*/
decimalPlaces?: number;
};
// min included, max excluded
export function getRandomFloat(min: number, max: number, options: GetRandomNumberOptions = {}) {
const { decimalPlaces } = options;
const num = Math.random() * (max - min) + min;
if (decimalPlaces === undefined) {
return num;
}
return getNumberWithDecimalPlaces(num, decimalPlaces);
}
// min/max included
export function getRandomInt(min: number, max: number) {
// https://stackoverflow.com/a/7228322
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Examples / unit tests:
function convertMathExpressionToEval(expr: string) {
let evalExpr = expr.replaceAll('^', '**');
functionsKeys.forEach(func => (evalExpr = evalExpr.replaceAll(func, `Math.${func}`)));
return evalExpr;
}
test('getRandomMathExpression()', () => {
const numberRegex = /-?\d+(\.\d+)?/g;
for (let i = 0; i < 100; i++) {
// 13.69
// -97.11
{
const expr = getRandomMathExpression(1);
expect(expr).toMatch(/^-?\d+(\.\d+)?$/);
expect(eval(convertMathExpressionToEval(expr))).toEqual(expect.any(Number));
}
// cos(-20.85) * 65.04
// max(50.44, 66.98)
// (-13.33 / 70.81)
// -51.48 / -83.07
{
const expr = getRandomMathExpression(2);
expect(expr.match(numberRegex)).toHaveLength(2);
expect(eval(convertMathExpressionToEval(expr))).toEqual(expect.any(Number));
}
// min(-91.65, min(99.88, -33.67))
// (-77.28 % sin(-52.18) + -20.19)
// (67.58 % -32.31 * -7.73)
// (28.33) ^ (-32.59) ^ -80.54
{
const expr = getRandomMathExpression(3);
expect(expr.match(numberRegex)).toHaveLength(3);
expect(eval(convertMathExpressionToEval(expr))).toEqual(expect.any(Number));
}
// cos(max(24.57, 84.07)) ^ tan(51.78) - -45.52
// (min(-40.91, -67.48) * sin(-25.99) ^ -29.35)
// cos(1.61 - -22.15) % (-70.39 * 0.98)
// ((30.91) ^ -63.24) + 76.72 / 61.07
{
const expr = getRandomMathExpression(4);
expect(expr.match(numberRegex)).toHaveLength(4);
expect(eval(convertMathExpressionToEval(expr))).toEqual(expect.any(Number));
}
// tan((24.97) ^ 55.61) ^ (-46.74 % -31.38 * 84.34)
// max(tan(-7.78) + -2.43, max(35.48, (6.13 % 25.54)))
// ((5.66 / 23.21) - (-22.93 % 96.56 * 52.12))
// (((-40.93 % 13.72)) ^ (29.48 * 57.34 + 13.26))
{
const expr = getRandomMathExpression(5);
expect(expr.match(numberRegex)).toHaveLength(5);
expect(eval(convertMathExpressionToEval(expr))).toEqual(expect.any(Number));
}
}
// Torture test, should not throw
for (let i = 0; i < 100; i++) {
const expr = getRandomMathExpression(1000);
expect(expr.match(numberRegex)).toHaveLength(1000);
// The longer the expression, the more likely it will result in a NaN
expect(eval(convertMathExpressionToEval(expr))).toEqual(expect.any(Number));
}
});
More here: https://gist.github.com/tkrotoff/b0b1d39da340f5fc6c5e2a79a8b6cec0
Upvotes: 0
Reputation: 9319
I picked up the idea of @plamut to create a binary tree, where each node represents an operator with a left and a right side.
For instance, the equation 2 * (3 + 4)
can be seen as
*
/ \
2 +
/ \
3 4
You can represent this quite straight forward using objects as follows:
var TreeNode = function(left, right, operator) {
this.left = left;
this.right = right;
this.operator = operator;
this.toString = function() {
return '(' + left + ' ' + operator + ' ' + right + ')';
}
}
Then you can create a recursive function to build such trees, where one sub-tree would have half of the desired total number of nodes (= length of equation):
function buildTree(numNodes) {
if (numNodes === 1)
return randomNumberRange(1, 100);
var numLeft = Math.floor(numNodes / 2);
var leftSubTree = buildTree(numLeft);
var numRight = Math.ceil(numNodes / 2);
var rightSubTree = buildTree(numRight);
var m = randomNumberRange(0, x.length);
var str = x[m];
return new TreeNode(leftSubTree, rightSubTree, str);
}
Here's a JSFiddle with a working example.
Maybe you still want to care about special cases, like avoiding brackets at top level, but that shouldn't be too hard from here.
Upvotes: 6