1

Task: Given a string of the form foo(bar) reverse the string inside the parentheses.

My solution has an error that I'm unable to understand the cause of.

Here's my code:

    const inputs = ["foo(bar)", "(bar)", "foo(bar)blim", "foo(foo(bar))blim", "foo(foo(bar)ddas)blim", "foo(foo(b(tu)ar)ddas)blim"]
const expected = ["foorab", "rab", "foorabblim", "foobaroofblim", "foosaddbaroofblim", "foosaddbutaroofblim"]

function reverseParans(input) {
    firstLeftParans = input.indexOf('(')
    lastRightParans = input.lastIndexOf(')')

    if (firstLeftParans == -1) {
        if (lastRightParans > -1) {
            return ['<MALFORMED>']
        } else {
            return input.reverse()
        }
    } else {
        if (lastRightParans == -1) {
            return ['<MALFORMED>']
        } else {
            left = input.slice(0, firstLeftParans)
            right = input.slice(lastRightParans+1).slice()
            middle = reverseParans(input.slice(firstLeftParans + 1, lastRightParans))
            return [...left, ...middle, ...right].reverse()
        }
    }
}

function process(str) {
    let input = str.split('');
    let firstLeftParans = input.indexOf('(');
    let lastRightParans = input.lastIndexOf(')');

    if (firstLeftParans == -1 && lastRightParans == -1) {
        return input
    } else if ((firstLeftParans > -1 && lastRightParans == -1) || (lastRightParans > -1 && firstLeftParans == -1)) {
        return "<MALFORMED>"
    }
    result = input.slice(0, firstLeftParans).concat(reverseParans(input.slice(firstLeftParans + 1, lastRightParans)), input.slice(lastRightParans + 1))
    return result.join('')
}

All inputs succeed except the last one with an extra level of nesting.

Input: foo(foo(b(tu)ar)ddas)blim   Result: foorabutarbblim   Expected:  foosaddbutaroofblim  Pass: false

I think I'm mistakenly mutating a value while making the recursive calls but I'm not sure where.

1

4 Answers 4

1

The second answer from "Thank you" can teach you a great deal about writing parsers. I highly recommend it.

And the basic problem here cannot be solved with any simple regular expression. The language of balanced parentheses is not a regular language; it is a context-free language. However, we can write a fairly simple version that uses recursive calls employing regular expression tests and replacements:

// or use the recursive version from @Thankyou
const reverse = (s) =>
  [...s] .reverse () .join ('')

const parens = /\(([^)(]*)\)/

const process = (s) => 
  parens .test (s)
    ? process (s .replace (parens, (_, inner) => reverse (inner)))
    : s


// Original sample cases
const inputs =   ["foo(bar)", "(bar)", "foo(bar)blim", "foo(foo(bar))blim", "foo(foo(bar)ddas)blim", "foo(foo(b(tu)ar)ddas)blim"]
const expected = ["foorab",   "rab",   "foorabblim",   "foobaroofblim",     "foosaddbaroofblim",     "foosaddbutaroofblim"      ]

console .log (expected .join (", "))
console .log (inputs .map (process) .join (", "))

//  Extended sample cases from @Thankyou
console. log ("hellodlrowyesonyangniy")
console .log (process ("hello(world)yes(no)yang(yin)"))
console .log ("abefhgdcijnlmkopqrxvwutsyz")
console .log (process ("ab(cd(ef(gh)))ij(k(lm)n)opqr(stu(vw)x)yz"))

parens is a regular expression matching a sub-string starting with an open parenthesis, then capturing a group of zero or more characters not including any close or open parentheses, followed by a close parenthesis. It breaks down like this:

// const parens = /\(([^)(]*)\)/
//                /            /  -- a regular expression matching a sub-string
//                 \(             -- starting with an open parenthesis 
//                   (      )     -- then capturing a group of 
//                         *      -- zero or more
//                    [^  ]       -- characters not including 
//                      )(        -- any close or open parentheses
//                           \)   -- followed by a close parenthesis

In other words, we can use this to find the innermost of any nested set of parentheses, and capture the content between them.

The function process uses this to test if there is any such sub-string. If there is not, we're done and we return the string intact. If there is we replace that set of parentheses with a reversed version of the contents and recursively call process with the result.

Sign up to request clarification or add additional context in comments.

1 Comment

Great usage example of regex and explanation of their caveats! Relying on regex engine as you have here really shows how to keep complexity out of the programmer's way. And more robust than the naive program I first shared, too :D
1

Some other answers on this question produce an incorrect result when there are multiple sets of unnested parens -

console.log(process("hello(world)yes(no)yang(yin)"))
// helloniy(gnaynosey)dlrow

console.log(process("ab(cd(ef(gh)))ij(k(lm)n)opqr(stu(vw)x)yz"))
// abxefn<MALFORMED>ji)))hgopqr(stu(vwdcyz

// OH NO!

lex

Let's try another way of approaching the problem. First lexer creates a stream of lexemes -

function* lexer(s = "")
{ const leftParen =
    { type: "leftParen" }

  const rightParen =
    { type: "rightParen" }

  const str = value =>
    ({ type: "str", value })
  
  let r =
    ""
  
  for (const c of s)
    if (c === "(")
      (yield str(r), yield leftParen, r = "")
    else if (c === ")")
      (yield str(r), yield rightParen, r = "")
    else
      r = r + c

  yield str(r)
}

Let's take a look at how lexer works -

lexer("this(is(very(nested)))but(nothere)") // => ...
{ type: "str", value: "this" }
{ type: "leftParen" }
{ type: "str", value: "is" }
{ type: "leftParen" }
{ type: "str", value: "very" }
{ type: "leftParen" }
{ type: "str", value: "nested" }
{ type: "rightParen" }
{ type: "str", value: "" }
{ type: "rightParen" }
{ type: "str", value: "" }
{ type: "rightParen" }
{ type: "str", value: "but" }
{ type: "leftParen" }
{ type: "str", value: "nothere" }
{ type: "rightParen" }
{ type: "str", value: "" }

parse

Next, parser takes the lexemes and produces an abstract syntax tree -

function parser(lexemes)
{ const concat = _ =>
    ({ type: "concat", values: [] })
  
  const rev = _ =>
    ({ type: "rev", values: [] })

  const r =
    [ concat() ]

  for (const t of lexemes)
    if (t.type === "str")
      r[0].values.unshift(t)
    else if (t.type === "leftParen")
      r.unshift(rev())
    else if (t.type === "rightParen")
      if (r.length <= 1) 
        throw Error("unexpected ')'")
      else
        r[1].values.unshift(r.shift())
    else
      throw Error("unexpected lexeme")

  if (r.length > 1)
    throw Error("expected ')'")
  else
    return r[0]
}

Let's see the effect of parser now -

parser(lexer("this(is(very(nested)))but(nothere)")) // => ...
{ type: "concat"
, values: 
    [ { type: "str", value: "" }
    , { type: "rev"
      , values:
          [ { type: "str", value: "nothere" }
          ]
      }
    , { type: "str", value: "but" },
      { type: "rev"
      , values: 
          [ { type: "str", value: "" }
          , { type: "rev"
            , values:
                [ { type: "str", value: "" }
                , { type: "rev"
                  , values:
                      [ { type: "str", value: "nested" } 
                      ]
                  }
                , { type: "str", value: "very" }
                ]
            }
          , { type: "str", value: "is" }
          ]
      }
    , { type: "str", value: "this" }
    ]
}

eval

Finally eval recursively evaluates the AST and spits out a value -

function eval(e)
{ if (e.type === "str")
    return e.value
  else if (e.type === "concat")
    return reverse(e.values).map(eval).join("")
  else if (e.type === "rev")
    return e.values.map(_ => reverse(eval(_))).join("")
  else
    throw Error(`unexpected expression: ${e}`)
}

Let's see how eval handles our example -

eval(parser(lexer("this(is(very(nested)))but(nothere)"))) // => ...
"thisverydetsensibuterehton"

process

Now process is simply a combination of lexer, parser, and eval -

const process = (s = "") =>
  eval(parser(lexer(s)))

The program works just like before -

const inputs =
  ["foo(bar)", "(bar)", "foo(bar)blim", "foo(foo(bar))blim", "foo(foo(bar)ddas)blim", "foo(foo(b(tu)ar)ddas)blim"]

const expected =
  ["foorab", "rab", "foorabblim", "foobaroofblim", "foosaddbaroofblim", "foosaddbutaroofblim"]

const result =
  inputs.map(process) // <-- call process for each input

console.log(expected.join(", "))
console.log(result.join(", "))

Output -

foorab, rab, foorabblim, foobaroofblim, foosaddbaroofblim, foosaddbutaroofblim
foorab, rab, foorabblim, foobaroofblim, foosaddbaroofblim, foosaddbutaroofblim

Except now our program works for more complicated inputs -

console.log(process("hello(world)yes(no)yang(yin)"))
// hellodlrowyesonyangniy

console.log(process("ab(cd(ef(gh)))ij(k(lm)n)opqr(stu(vw)x)yz"))
// abefhgdcijnlmkopqrxvwutsyz

You just made your own programming language!

char = "a" | "b" | "c" | "d" ...
     | "A" | "B" | "C" | "D" ...
     | "1" | "2" | "3" | "4" ...
     | "!" | "@" | "#" | "$" ...
     | ...

string = ""
       | char + string

expr = string
     | expr + "(" + expr + ")" + expr

Verify the results in your own browser -

const reverse = (s = "") =>
  s.length > 1
    ? reverse(s.slice(1)).concat(s.slice(0, 1))
    : s

function* lexer(s = "")
{ const leftParen =
    { type: "leftParen" }

  const rightParen =
    { type: "rightParen" }

  const str = value =>
    ({ type: "str", value })
  
  let r =
    ""
  
  for (const c of s)
    if (c === "(")
      (yield str(r), yield leftParen, r = "")
    else if (c === ")")
      (yield str(r), yield rightParen, r = "")
    else
      r = r + c

  yield str(r)
}

function parser(lexemes)
{ const concat = _ =>
    ({ type: "concat", values: [] })
  
  const rev = _ =>
    ({ type: "rev", values: [] })

  const r =
    [ concat() ]

  for (const t of lexemes)
    if (t.type === "str")
      r[0].values.unshift(t)
    else if (t.type === "leftParen")
      r.unshift(rev())
    else if (t.type === "rightParen")
      if (r.length <= 1) 
        throw Error("unexpected ')'")
      else
        r[1].values.unshift(r.shift())
    else
      throw Error("unexpected lexeme")

  if (r.length > 1)
    throw Error("expected ')'")
  else
    return r[0]
}

function eval(e)
{ if (e.type === "str")
    return e.value
  else if (e.type === "concat")
    return reverse(e.values).map(eval).join("")
  else if (e.type === "rev")
    return e.values.map(_ => reverse(eval(_))).join("")
  else
    throw Error(`unexpected expression: ${e}`)
}

const process = (s = "") =>
  eval(parser(lexer(s)))
  
const inputs =
  ["foo(bar)", "(bar)", "foo(bar)blim", "foo(foo(bar))blim", "foo(foo(bar)ddas)blim", "foo(foo(b(tu)ar)ddas)blim"]

const expected =
  ["foorab", "rab", "foorabblim", "foobaroofblim", "foosaddbaroofblim", "foosaddbutaroofblim"]

console.log(expected.join(", "))
console.log(inputs.map(process).join(", "))
console.log("hellodlrowyesonyangniy")
console.log(process("hello(world)yes(no)yang(yin)"))
console.log("abefhgdcijnlmkopqrxvwutsyz")
console.log(process("ab(cd(ef(gh)))ij(k(lm)n)opqr(stu(vw)x)yz"))

1 Comment

An excellent primer of lexing/parsing! See my answer for a very different alternative using recursive regex tests/replacements.
0

You messed up your scoping, because you didn't declare your variables as being local to reverseParans. This modified code works:

const inputs = ["foo(bar)", "(bar)", "foo(bar)blim", "foo(foo(bar))blim", "foo(foo(bar)ddas)blim", "foo(foo(b(tu)ar)ddas)blim"]
const expected = ["foorab", "rab", "foorabblim", "foobaroofblim", "foosaddbaroofblim", "foosaddbutaroofblim"]

function reverseParans(input) {
    let firstLeftParans = input.indexOf('(')
    let lastRightParans = input.lastIndexOf(')')

    if (firstLeftParans == -1) {
        if (lastRightParans > -1) {
            return ['<MALFORMED>']
        } else {
            return input.reverse()
        }
    } else {
        if (lastRightParans == -1) {
            return ['<MALFORMED>']
        } else {
            let left = input.slice(0, firstLeftParans)
            let right = input.slice(lastRightParans+1).slice()
            let middle = reverseParans(input.slice(firstLeftParans + 1, lastRightParans))
            return [...left, ...middle, ...right].reverse()
        }
    }
}

function process(str) {
    let input = str.split('');
    let firstLeftParans = input.indexOf('(');
    let lastRightParans = input.lastIndexOf(')');

    if (firstLeftParans == -1 && lastRightParans == -1) {
        return input
    } else if ((firstLeftParans > -1 && lastRightParans == -1) || (lastRightParans > -1 && firstLeftParans == -1)) {
        return "<MALFORMED>"
    }
    result = input.slice(0, firstLeftParans).concat(reverseParans(input.slice(firstLeftParans + 1, lastRightParans)), input.slice(lastRightParans + 1))
    return result.join('')
}

1 Comment

Ah, that's just embarrassing. Thanks for the quick review!
0

Recursion by mathematical induction can simplify your program. The numbered comments below correspond to the numbers in the code -

  1. If neither left nor right paren is found; return the input string, s
  2. (induction) if only one paren is found; return malformed string result
  3. (induction) both left and right paren are found; return the string portion before the left paren, plus the recursive result, plus the string portion after the right paren
function process(s = "")
{ const l = s.indexOf("(")          // "left"
  const r = s.lastIndexOf(")")      // "right"
  if (l === -1 && r === -1)
    return s                        // 1
  else if (l === -1 || r === -1)
    return "<MALFORMED>"            // 2
  else
    return s.substring(0, l)        // 3
      + reverse(process(s.substring(l + 1, r)))
      + s.substr(r + 1)
}

We can define reverse as -

const reverse = (s = "") =>
  s.length
    ? reverse(s.substr(1)) + s.substr(0, 1)
    : ""
const inputs =
  ["foo(bar)", "(bar)", "foo(bar)blim", "foo(foo(bar))blim", "foo(foo(bar)ddas)blim", "foo(foo(b(tu)ar)ddas)blim"]

const expected =
  ["foorab", "rab", "foorabblim", "foobaroofblim", "foosaddbaroofblim", "foosaddbutaroofblim"]

const result =
  inputs.map(process) // <-- call process for each input

console.log(expected.join(", "))
console.log(result.join(", "))

Output -

foorab, rab, foorabblim, foobaroofblim, foosaddbaroofblim, foosaddbutaroofblim
foorab, rab, foorabblim, foobaroofblim, foosaddbaroofblim, foosaddbutaroofblim

Expand the snippet to verify the result in your browser -

const reverse = (s = "") =>
  s.length
    ? reverse(s.substr(1)) + s.substr(0, 1)
    : ""

function process(s = "")
{ const l = s.indexOf("(")
  const r = s.lastIndexOf(")")
  if (l === -1 && r === -1)
    return s
  else if (l === -1 || r === -1)
    return "<MALFORMED>"
  else
    return s.substring(0, l)
      + reverse(process(s.substring(l + 1, r)))
      + s.substr(r + 1)
}

const inputs =
  ["foo(bar)", "(bar)", "foo(bar)blim", "foo(foo(bar))blim", "foo(foo(bar)ddas)blim", "foo(foo(b(tu)ar)ddas)blim"]

const expected =
  ["foorab", "rab", "foorabblim", "foobaroofblim", "foosaddbaroofblim", "foosaddbutaroofblim"]

const result =
  inputs.map(process) // <-- call process for each input

console.log(expected.join(", "))
console.log(result.join(", "))

1 Comment

An similar alternative that solves the problems fixed by your other answer: function process(s = "") { const r = s .indexOf (")") const l = s .slice (0, r) .lastIndexOf ("(") if (l === -1 && r === -1) return s else if (l > r) return "<MALFORMED>" else return process ( s .substring (0, l) + reverse (s .substring (l + 1, r)) + s .substr (r + 1) ) } My answer does the same thing, but uses regex rather than (last)indexOf.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.