david warne
david warne

Reputation: 127

Generate a random math equation using random numbers and operators in JavaScript

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

Answers (2)

tanguy_k
tanguy_k

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

Cedric Reichenbach
Cedric Reichenbach

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

Related Questions