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