1

I'm trying to extract the last group bounded by brackets from the string. The string could be like one of these variants:

String                             ' Extracted sub
--------------                     ' -------------
Some (text)                        ' text
Some text                          ' "" or "Some text"
Some (text) (here)                 ' here
Some (text) (is situated (here))   ' is situated (here)
Some text (is situated (here))     ' is situated (here)
Some (text) (is (situated (here))) ' is (situated (here))

I need the substring between the last closing bracket and the corresponding opening bracket.

All variants of Split, Mid, InStr and InStrRev I tested...

0m3r
  • 12,286
  • 15
  • 35
  • 71
Dennis S.
  • 47
  • 7

3 Answers3

2

Not sure what have you tried, but the idea is (hope it's logical in writing):

  1. Locate the location of last (right most) )
  2. Locate the location of right most (
  3. Calculate how many ) inside the substring from #2 to #1
  4. Look at the characters before #2, increase #3 when ) is met, decrease #3 when ( is met, stop when #3 becomes zero.
  5. The desired output is the substring from (#4+1) for (#1-1-#4) characters
  6. If unmatched pairs found, it will return "ERROR: UNMATCHED BRACKETS!"


Code below tested in Excel (updated for sample YowE3K in comment)

Option Explicit

Function LastOutmostBracketText(ByVal InputText As String) As String
    Dim lRightMostCloseBracket As Long, CloseBracketCount As Long
    Dim lRightMostOpenBracket As Long
    Dim sTmp As String

    Dim sOutput As String
    If InStr(1, InputText, "(", vbTextCompare) > 0 And InStr(1, InputText, ")", vbTextCompare) > 0 Then
        ' Find the Last Close Bracket
        lRightMostCloseBracket = InStrRev(InputText, ")")
        ' Find the Last Open Bracket
        lRightMostOpenBracket = InStrRev(InputText, "(")
        If (lRightMostCloseBracket - lRightMostOpenBracket) > 1 Then
            ' Count how many Close brackets within the last Open and last Close bracket
            sTmp = Mid(InputText, lRightMostOpenBracket, lRightMostCloseBracket - lRightMostOpenBracket)
            CloseBracketCount = Len(sTmp) - Len(Replace(sTmp, ")", ""))
            ' Find the matching Open Bracket by looking at previous characters
            Do Until CloseBracketCount = 0 Or lRightMostOpenBracket = 1
                If lRightMostOpenBracket > 0 Then lRightMostOpenBracket = lRightMostOpenBracket - 1
                sTmp = Mid(InputText, lRightMostOpenBracket, 1)
                Select Case sTmp
                    Case "(":   CloseBracketCount = CloseBracketCount - 1
                    Case ")":   CloseBracketCount = CloseBracketCount + 1
                End Select
            Loop
            If lRightMostOpenBracket = 1 And CloseBracketCount > 0 Then
                sOutput = "ERROR: UNMATCHED BRACKETS!" & vbCrLf & InputText
            Else
                sOutput = Mid(InputText, lRightMostOpenBracket + 1, lRightMostCloseBracket - 1 - lRightMostOpenBracket)
            End If
        End If
    End If
    LastOutmostBracketText = sOutput
End Function
PatricK
  • 6,375
  • 1
  • 21
  • 25
  • 1
    Error case: `LastOutmostBracketText("abc ((def) ghi (jkl) (mno (pqr)) stu) vwx")` (You have to increase the number calculated in your step 3 if you encounter any more close brackets while performing step 4. Or you can just find the last close bracket and then increase a counter whenever you find a close bracket, including that last one, and decrease the counter whenever you find an open bracket, and stop when you get to zero.) – YowE3K Jan 08 '18 at 02:40
  • 1
    Interesting @YowE3K! Need more thinking now... I think will need to go through the characters at some stage of my method. – PatricK Jan 08 '18 at 02:45
  • Going character-by-character will certainly be the easiest method to understand. I'm also wondering whether something could be done using recursive calls - my brain is telling me that there might be something in that, but it's too tired to tell me exactly what it is thinking of. (And I also have a feeling that a RegEx might be able to do this very easily - but I have never used them.) – YowE3K Jan 08 '18 at 02:54
  • Much better! :) – YowE3K Jan 08 '18 at 03:33
  • YowE3K - you are Genius! – Dennis S. Jan 08 '18 at 21:29
1

I suspect that regular expressions could indeed do this better but this is the codethat I knew how to write (now incorporating @YowE3K's "error case" and his more awake - despite his professed tiredness - understanding of how brackets within brackets should be treated):-

Private Function LastBracket(ByVal Txt As String) As String
    ' 08 Jan 2018

    Dim Fun As String
    Dim x As Integer, y As Integer
    Dim n As Integer, m As Integer

    For n = 0 To Len(Txt) - 1
        Fun = Fun & Mid(Txt, Len(Txt) - n, 1)
    Next n

    n = InStr(Fun, ")")                         ' remove trailing text
    If n Then
        Fun = Mid(Fun, n)
    Else
        Exit Function                           ' no bracket found
    End If

    Do
        n = InStr(m + 1, Fun, "(")
        If n Then
            Txt = Left(Fun, n)
            m = n
            x = Len(Txt) - Len(Replace(Txt, "(", ""))
            y = Len(Txt) - Len(Replace(Txt, ")", ""))
        Else
            Exit Function                       ' paired bracket not found
        End If
    Loop Until x = y

    Fun = Txt
    Txt = ""
    For n = 1 To Len(Fun) - 2
        Txt = Txt & Mid(Fun, Len(Fun) - n, 1)
    Next n

    LastBracket = Txt
End Function

It will return a null string if there is no bracketed text or the brackets are empty. Here are the tests I ran.

Private Sub TestUnpack()
    Debug.Print "Result = "; LastBracket("Some; Text")
    Debug.Print "Result = "; LastBracket("Some; Text()")
    Debug.Print "Result = "; LastBracket("Some(Text)")
    Debug.Print "Result = "; LastBracket("Some(Text)(here)")
    Debug.Print "Result = "; LastBracket("Some (text) (might be (here))")
    Debug.Print "Result = "; LastBracket("Some (text) (might be (situated (here)))")
    Debug.Print "Result = "; LastBracket("Some (text) (might be (situated (here))) not here")
    Debug.Print "Result = "; LastBracket("abc ((def) ghi (jkl) (mno (pqr)) stu) vwx")
End Sub
Variatus
  • 14,293
  • 2
  • 14
  • 30
  • Error case: `LastBracket("abc ((def) ghi (jkl) (mno (pqr)) stu) vwx")` – YowE3K Jan 08 '18 at 03:28
  • Doesn't the update just return `"pqr"` for "my" test case? (I would expect it should return `"(def) ghi (jkl) (mno (pqr)) stu"` given the OP's test case of `"Some (text) (is (situated (here)))"` returning `"is (situated (here))"`.) – YowE3K Jan 08 '18 at 03:43
  • @YeoE3K You are right, as usual. I'll take this back to the drawing board. – Variatus Jan 08 '18 at 03:46
  • If you get something working (and don't decide to give up beforehand!) you'll certainly get a +1 from me. (But I probably won't notice any updates to the answer now until my tomorrow, so be patient if you don't see the rep addition straight away.) – YowE3K Jan 08 '18 at 03:48
  • 1
    Old story @YowE3K - the carrot and the stick, lol: I think you will enjoy my solution. – Variatus Jan 08 '18 at 05:23
  • I see that it works now. Now to sit down and work out **how** it works! :D – YowE3K Jan 08 '18 at 06:26
  • RegEx will work (https://regex101.com/r/XzIIzC/1) but seemingly NOT in VBA as the MS VBScript Regular Expressions engine does not seem to support recursion. **Credit** - *RegEx lifted from here: https://stackoverflow.com/a/3851098/3451115* – SlowLearner Jan 08 '18 at 11:56
0

And a slightly different (but mostly the same) approach using CSet:

Sub test()
    Dim i As Integer
    Dim str As String
    Dim rng As Range
    Dim l As Integer
    For i = 1 To ActiveDocument.Sentences.Count
        Set rng = ActiveDocument.Paragraphs.Item(i).Range
        rng.End = rng.End - 1
        l = Len(rng.Text)
        rng.Collapse wdCollapseEnd
        Do
            rng.MoveStartUntil cset:="(", Count:=-l
            rng.Start = rng.Start - 1
            str = rng.Text
        Loop While Len(Replace(str, "(", vbNullString)) <> Len(Replace(str, ")", vbNullString))
        Debug.Print str
        str = vbNullString
    Next i
End Sub

Oh, I was too lazy to remove the outer brackets, but that should not be too problematic I hope ;-)

SlowLearner
  • 3,086
  • 24
  • 54