Using a stack, keep track of all of the pairs as you traverse the string:
def find_matching_parens(s, braces=None):
openers = braces or {"(": ")"}
closers = {v: k for k, v in openers.items()}
stack = []
result = []
for i, c in enumerate(s):
if c in openers:
stack.append([c, i])
elif c in closers:
if not stack:
raise ValueError(f"tried to close brace without an open at position {i}")
pair, idx = stack.pop()
result.append([idx, i])
if pair != closers[c]:
raise ValueError(f"mismatched brace at position {i}")
if stack:
raise ValueError(f"no closing brace at position {i}")
return result
if __name__ == "__main__":
print(find_matching_parens("(foo(bar)()baz(a(fz()asdf)))"))
Output:
[[4, 8], [9, 10], [19, 20], [16, 25], [14, 26], [0, 27]]
If you just want the matching bracket for a specific index, you can use this modification to the above function:
def find_matching_paren(s, i, braces=None):
openers = braces or {"(": ")"}
closers = {v: k for k, v in openers.items()}
stack = []
result = []
if s[i] not in openers:
raise ValueError(f"char at index {i} was not an opening brace")
for ii in range(i, len(s)):
c = s[ii]
if c in openers:
stack.append([c, ii])
elif c in closers:
if not stack:
raise ValueError(f"tried to close brace without an open at position {i}")
pair, idx = stack.pop()
if pair != closers[c]:
raise ValueError(f"mismatched brace at position {i}")
if idx == i:
return ii
if stack:
raise ValueError(f"no closing brace at position {i}")
return result
if __name__ == "__main__":
print(find_matching_paren("(foo(barbaz(a(fz()asdf))))", 4)) # => 24