Brian
Brian

Reputation: 1989

How can I replace all instances of 'x' within a string as long as 'x' is within Math.sin(), Math.cos(), or Math.tan()?

I am trying to write a JavaScript replaceAll() with a RegEx that will replace every value of x with period*x, as long as it is within Math.sin() or Math.cos() or Math.tan()

I tried this:

let fx = 'Math.tan(Math.sin(Math.cos(x)*x)*x)';
const periodRegEx = /(Math.(sin|cos|tan)\(.*?)(x)([^\)].*?)(\))/gi;
// capture group with 'Math.sin(' or 'Math.cos(' or 'Math.tan('
// capture group with 'x'
// capture group with any character except for ')'
// capture group with ')'
let newFx = fx.replaceAll(periodRegEx,('period*'+\2));

But that is getting me an `illegal escape sequence error. This:

let newFx = fx.replaceAll(periodRegEx,('period*'+'\2'));

Is giving me nothing, and this:

let newFx = fx.replaceAll(periodRegEx,('period*'+$2));

Is giving me a $2 not defined error.

What I am looking for is this as the output of the replaceAll():

'Math.tan(Math.sin(Math.cos(period*x)*period*x)*period*x)'

Upvotes: 2

Views: 99

Answers (3)

jmcgriz
jmcgriz

Reputation: 3353

Doing this with a single regex expression is going to be difficult, but you can accomplish it with a recursive replace function that will handle the matches one at a time, applying placeholders, then loop back through the array of matches in reverse.

let fx = 'Math.tan(Math.sin(Math.cos(x)*x)*x)';

let matches = []

function replaceX(str){
  const rgxmatch = /Math\.(?:sin|cos|tan)\([^)(]*x[^)(]*\)/.exec(str),
    match = rgxmatch ? rgxmatch[0] : null
  
  if(match){
    matches.push(match.replace(/\bx\b/g, 'period*x'))
    return replaceX(str.replace(match, `_PLACEHOLDER_${matches.length}`))
  }
  else {
    return fillInMatches(str)
  }
}

function fillInMatches(str){
  let len = matches.length
  
  while(len > 0){
    str = str.replace(`_PLACEHOLDER_${len}`, matches[len-1])
    --len
  }
  return str
}

const newStr = replaceX(fx)

console.log(newStr)

Upvotes: 0

Wojciech Kaczmarek
Wojciech Kaczmarek

Reputation: 91

It is most likely unfeasible to achieve what you need with RegEx-only oneliner, having in mind that:

  1. functions may be nested, as Poul Bak mentioned;
  2. several independent trig function calls may be present in an expression;
  3. both within and outside of trig function arguments other expressions in parentheses may be present.

A simple solution in plain JS without additional libraries would be to extract parts of expression that start with a trig function call, perform replacement only on them and concatenate back with the remainder of the formula. Note that due to (3) also using /\((.*?x[^\)]*?)\)/ to extract function argument will not always be sufficient. One example of such solution would be:

let fx = 'Math.tan(Math.sin(Math.cos((x-1)*x)*x)*x)*(x+1)+Math.sin(x)';
let newFx = periodSubstitute(fx,'x','period*x');
console.log(newFx)

function periodSubstitute(formula, variable, replacement) {
  const trigFunctionRegex = /Math.(sin|cos|tan)\(/;
  let trigFunction = trigFunctionRegex.exec(formula);
  if (trigFunction === null)
    return formula;
  else {
    let start = formula.indexOf(trigFunction[0]) + trigFunction[0].length;
    let end = closingParenthesis(formula, start);
    let substitute = formula.substring(start, end).replaceAll(variable, replacement);
    return formula.substring(0, start) + substitute + periodSubstitute(formula.substring(end), variable, replacement);
  }
}

function closingParenthesis(formula, from) {
  let open = 0;
  for (let index = from; index < formula.length; index++) {
    const char = formula[index];
    if (char == '(')
      open++;
    if (char == ')') {
      if (open == 0)
        return index;
      else
        open--;
    }
  }
  return 0;
}

Upvotes: 2

Alexander Nenashev
Alexander Nenashev

Reputation: 23761

If your code is JS (seems) you can parse and replace it with acorn and generate back JS code with astring:

let fx = 'Math.tan(Math.sin(Math.cos(x)*x)*x)';

const parsed = acorn.Parser.parse(fx, {ecmaVersion: 'es6'});

const replace = (node, inside = false) => {
  if(node.callee?.object?.name === 'Math' && ['tan', 'cos', 'sin'].includes(node.callee?.property?.name)){
    replace(node.arguments, true);
  } else if(node.name === 'x' && inside){
    node.name = 'period * x';
  } else if(Array.isArray(node)){
    node.forEach(node => replace(node, inside));
  } else if(typeof node === 'object' && node){
    for(const k in node){
      replace(node[k], inside);
    }
  }
}
replace(parsed);

const code = astring.generate(parsed);

console.log(code)
<script src="https://cdnjs.cloudflare.com/ajax/libs/acorn/8.11.3/acorn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/astring.min.js"></script>

Upvotes: 0

Related Questions