skip to Main Content

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)'

3

Answers


  1. 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>
    Login or Signup to reply.
  2. 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;
    }
    
    Login or Signup to reply.
  3. 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(/bxb/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)
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search