Regex (Perl / PCRE / Boost / Pythonregex
), 45 44 45 44 43 bytes
s/(\(|^)\K(\((((?2)|[^()])*)\))(?=\)|$)/$3/
Try it online! - Perl v5.28.2 / Attempt This Online! - Perl v5.36+
Try it online! - PCRE1
Try it online! - PCRE2 v10.33
Try it online! - Boost
Try it online! - Python import regex
This is a single regex substitution to be repeatedly applied until it has nothing to match. However in Boost, it is applied until there is no change (since its substitution interface apparently doesn't allow detecting whether any replacements were done).
This golf down from the 45 byte version is similar to Neil's .NET regex, in that it is flanked with (?<=\(|^)
and (?=\)|$)
(or equivalent). When initially writing a PCRE regex answer to this challenge, the thought occurred to me to do this, but I dismissed the idea, assuming that empty capture groups would need to be used to XNOR the flanking conditions (i.e. force them to only match if they agree on ^
and $
or \(
and \)
), by doing (?<=\(()|^)
...(?=(?(1)\)|$))
or at least (?<=\(()|^)
...(?=\1\)|$)
.
Then later on, after Neil posted his answer, I tried porting that approach to mine anyway. I found it to indeed return some incorrect results unless forced to agree using an empty capture group, but at that point I'd already gone with the (as it turned out incorrect) golf of [^)]
in my regex. As it turns out, with [^()]
, the flanking approach is fully robust in recursive versions of the regex, without requiring any additional XNORing logic or atomic grouping.
s/ # Begin substitution - match the following:
(\(|^) # Assert either we're at the start of the string, or there
# is a (redundant) left-adjacent opening parenthesis.
\K # Keep everything matched up to this point out of the match.
# This is a more efficient way of doing the same thing as
# lookbehind in many situations.
( # Define recursive subroutine (?2)
\( # Match an opening parenthesis
( # At the outermost level, $3 = the following capture, i.e.
# with the parentheses matched outside it discarded.
(
(?2) # Call (?2) recursively
| # or
[^()] # Match any character other than a parenthesis.
# It's not safe to reduce this to "[^)]" because if,
# after popping out to the top level and exiting this
# loop, the "\)" fails to match because there's more
# inside the top-level parentheses pair matched in this
# regex, it will backtrack and can incorrectly match an
# opening parenthesis here.
)* # Iterate the above as many times as possible, min 0
)
\) # Match a closing parenthesis
)
(?=\)|$) # Assert that either we're at the end of the string, or
# there is a redundant right-adjacent closing parenthesis.
/ # Substitution - replace with the following:
$3
/ # End substitution - no flags used
I also intend to write a single-use s/
...//g
substitution that will erase all the parentheses in one go, but that will be significantly more complicated and less efficient (as it will need to emulate variable-length lookbehind to erase the closing parentheses). As such, it would be demonstrable in regex101, which can only apply a substitution once. (I've done this for .NET, but intend to for PCRE2 as well.)
Regex (PCRE / Ruby), 48 49 45 44 bytes
s/(\(|^)\K(\(((\g<2>|[^()])*)\))(?=\)|$)/\3/
Try it online! - PCRE1
Try it online! - PCRE2
Try it online! - Ruby
As of 45 bytes and smaller, this is an absolutely straight port of the regex above. Previously, it needed to work around the difference in Ruby subroutine capture group behavior.
\$\large\textit{Functions}\$
Ruby, 75 76 74 72 71 66 62 bytes
-5 bytes thanks to Steffan
-4 bytes thanks to Dingus
->s{0while s[/(\(|^)\K(\(((\g<2>|[^()])*)\))(?=\)|$)/]&&=$3;s}
Try it online!
Julia v1.6.1+, 84 85 84 83 bytes
Attempt This Online! (now the same as below)
Julia v0.7+, 86 87 84 83 bytes
f(s,p=0)=s==p ? s : f(replace(s,r"(\(|^)\K(\((((?2)|[^()])*)\))(?=\)|$)"=>s"\3"),s)
Try it online!
Julia's implementation of PCRE substitution appears to be deficient, not actually using pcre2_substitute()
(the giveaway is that $1
$2
etc. capture group syntax is not supported). This is a shame, because PCRE2 has some advanced conditional replacement features (enabled by PCRE2_SUBSTITUTE_EXTENDED
in its C interface). The explanation, though, is that it was originally integrated with PCRE1, which has no built-in substitution API – so presumably when they switched to PCRE2, they kept the substitution code they'd already written for use with PCRE1.
R v4.1.0+, 90 91 89 88 bytes
f=\(s,p=0)if(p==s)s else f(sub(r'{(\(|^)\K(\((((?2)|[^()])*)\))(?=\)|$)}','\\3',s,,1),s)
Attempt This Online!
R is apparently also not using PCRE2's built-in substitution engine, otherwise $2
would work as the replacement argument.
Python (with regex
), 123 108 107 106 107 106 105 bytes
-15 bytes thanks to Steffan
import regex
f=lambda s,p=0:s==p and s or f(regex.sub('(\(|^)\K(\((((?2)|[^()])*)\))(?=\)|$)',r'\3',s),s)
Try it online!
For golf reasons, this continues substituting until the string is unchanged, not until there are no matches (with this regex either of those two methods will work).
Alternative 105 bytes, using essentially the same technique as suggested by VisualMelon:
import regex
def f(s):
for i in s:s=regex.sub('(\(|^)\K(\((((?2)|[^()])*)\))(?=\)|$)',r'\3',s)
return s
Try it online!
lambda s:[s:=regex.sub('(\(|^)\K(\((((?2)|[^()])*)\))(?=\)|$)',r'\3',s)for i in s][-1]
import regex
Can't Try it online! - Confirmed to work on my machine, but regex
is not installed on TIO or ATO.
Uses essentially the same technique as suggested by VisualMelon.
PHP, 108 109 108 107 106 104 bytes
-1 bytes (→ 106) thanks to Steffan with the added bonus of now being an anonymous function
-2 bytes (→ 104) with no "Undefined variable" warning
function($s){while($p!=$s=preg_replace('/(\(|^)\K(\((((?2)|[^()])*)\))(?=\)|$)/','$3',$p=$s));return$s;}
Try it online!
\$\large\textit{Full programs}\$
Perl -p
, 52 51 50 51 50 49 bytes
-1 byte thanks to dingledooper
and Sisyphus
1while s;(\(|^)\K(\((((?2)|[^()])*)\))(?=\)|$);$3
Attempt This Online!
PHP -F
, 103 104 103 102 100 98 bytes
-2 bytes (→ 100) thanks to Steffan
-2 bytes (→ 98) with the added bonus of getting rid of the "Undefined variable" warning
<?for($s=$argn;$p!=$s=preg_replace('/(\(|^)\K(\((((?2)|[^()])*)\))(?=\)|$)/','$3',$p=$s););echo$s;
Try it online!
"x(((y)(z)))": "x((y)(z))"
\$\endgroup\$Parenthesis are guaranteed to be matched in the input
was intended to cover your issue. \$\endgroup\$(foo(bar))baz
,foo((bar)baz)
\$\endgroup\$