Search code examples
excelvbafinddo-loops

Using Excel VBA Range.Find to find first and last addresses of a range of congruent data containing similar values and break a Do Loop?


I have been struggling to get a Do/While loop using Range.Find and Range.FindNext methods to break out of an infinite loop. I have researched all of this and found the tons of duplicate questions asking generically "how do I stop the infinite loop?" but these do not answer the specific condition I am looking for since I do not want to simply stop the Do loop when the .find function finds the last iteration and returns to the first iteration found of the matching text (i.e. using Loop While Not testFind is Nothing And nxtAddr <> firstAddr type construct).

let's look at some data examples. I have roughly 8000+++ rows of data on any given file I'm working on. In the file, I'm searching for dates/times, specifying a date and specific hour (for example - "20210715 12" as my query):

Row 37: 20210715 14:07:21  ---- This row is the last row of a previous set of congruent data.
        (Row 37 and all other data above minus header rows are hidden for user's viewing pleasure.)
Row 38: 20210715 12:48:20  ---- This row is the first row of the congruent data - I CARE ABOUT THIS DATA
Row 39: 20210715 12:47:20
Row 40~85: Date stays same, hour stays the same, rest of time increments down
Row 86: 20210715 12:07:15
Row 87: 20210715 12:07:13  ---- This row is the final row of the congruent data - I CARE ABOUT THIS DATA
Row 88: 20210715 11:54:20  ---- This row is the first row of a new set of congruent data.

A typical Do/While loop for this can be found in multiple places, including duplicates right here on SO. These examples (here, here, or here for example) do NOT prevent .find or .findnext methods from looping through the entire rest of my range in order to assure there are "no more matches."

Referencing THIS SO article, and THIS SO article, I stumbled upon a hypothesis of whether the range could be dynamically shifted through the congruent data without Range.Resize (which does not just shift the range down the rows - it increases the size of the current range being tested). . .

Is this possible, and does it work? I did happen to run into a very eye-opening facepalm moment, when I stumbled upon this incredibly simple and eloquent SO solution of returning the final row matching the same text by changing the SearchDirection=xlNext to SearchDirection=xlPrevious and thought "EUREKA!"

BUT

This solution of using xlPrevious in the Range.find method STILL FORCES A LOOP THROUGH ALL OF THE REMAINING ROWS BELOW THE ENTRY UNTIL IT FINDS THE MATCH!!!

Bottom Line: Can the loop be broken once it searches through a section of congruent data as shown above, ending on the final Row, without having to iterate through the remaining 7900+++ rows of data?

For those who may ask - I specifically also do NOT want to use a For i = rows.count To 1 Step -1 construct answer either, as again, I am NOT wanting to loop through all of the cells to find the last iteration!!!


Solution

  • I have found the answer! It most definitely IS POSSIBLE to do this, but does NOT use .FindNext as the solution. Referencing the articles I found here on SO, I improvised on the collective methods came up with the following solution which does exactly as I want it to.

    Note: the StartTimer and StopTimer uses a method to calculate the time it takes to perform a function and gives me the output down to the second (not completely accurate, but accurate enough) -- you can find functions for this here.

    This section starts the process of finding the first iteration of my query and then hides the rows before the data (for clean visibility's sake):

    'Note - the values of topRow, dateCol, and r were determined earlier in code, r = rows of used data (NOT USING "USEDRANGE").
    With Range(Cells(topRow, dateCol), Cells(r, dateCol))
      Set test = .Find("20210715 12", LookIn:=xlValues, Lookat:=xlPart, _
        searchorder:=xlByColumns, searchdirection:=xlNext, MatchCase:=False)
      If Not test Is Nothing Then
        'sets the address of cell above first found match to _not_ hide the first matching cell
        strtFndCel = test.Offset(-1, 0).Address
    
        'sets the first found cell's address
        strtFnd = test.Address
    
        'sets the next cell's address to specify for future search range.
        strtFndNxt = test.Offset(1, 0).Address
    
        Debug.Print "The First Cell Address containing my query = " & strtFnd
    
        Set strtRng = Range(Cells(topRow, 1), strtFndCel)
        strtRng.Rows.Hidden = True
    
      End If
    End With
    

    And this section performs the Do loop without any use of While or Until and stops the .Find function from looping through the remaining cells of data in my file.

    (SOLUTION)

    Dim findrng As Range
    
    'Uses the strtFndNxt value to set the defined range for the next query of congruent data
    Set findrng = Range(strtFnd, strtFndNxt)
    
    Call StartTimer
    
    Do
      Set test = Nothing
      Set test = findrng.Find("20210715 12", LookIn:=xlValues, Lookat:=xlPart, _
        searchdirection:=xlNext, MatchCase:=False)
      If test Is Nothing Then
        Exit Do
      Else
        'Note - when looping, the "lastFnd" address increments by 2, but will adequately stop 
        'on the appropriate cell regardless of even or odd number of rows.
        lastFnd = test.Address
        strtFnd = test.Offset(1, 0).Address
        strtFndNxt = test.Offset(2, 0).Address
    
        Debug.Print "The Current Address containing my query = " & lastFnd
    
        Set findrng = Range(strtFnd, strtFndNxt)
      End If
    Loop
    
    Debug.Print "The Last Address containing my query using breaking loop Method = " & lastFnd
    
    Call StopTimer
    

    Now - in order to be as thorough and fair as I possibly could, I did test the xlPrevious method as listed in my question, and it does produce the same address for the final cell containing the match.

    BUT

    The xlPrevious method did take longer to iterate through the remaining 7900+++ cells. In this case the timing took about 2 seconds which I found to still be impressive, so it is relatively quick, however, if the range of data expands, this will take longer over time, or, if the range given is more than just a single column it would take even longer with the same given data in this file (tested and found to be roughly 10 seconds. . . same result of the matching cell address).

    The only time I could see the Solution being unhelpful is in situations where you may have more rows and then another block of congruent data matching the same query as before, but even in this case, you could put in a portion of code to make the search query start again, but using the after parameter to specify the search begins after the last found cell. In my data, there will never be a situation where the same data will be found again after another block of congruent data which does not match the query. Again, for fairness, here is the method using the xlPrevious by comparison as specified in final SO article I listed in the question.

    Call StartTimer
    
    'topRow, dateCol, and r were determined earlier in code, r = rows of used data (NOT USING "USEDRANGE").
    With Range(Cells(topRow, dateCol), Cells(r, dateCol))
      Set test = .Find("20210715 12", LookIn:=xlValues, Lookat:=xlPart, _
        searchdirection:=xlPrevious, MatchCase:=False)
      If Not test Is Nothing Then
        lstFnd = test.Address
    
      End If
    End With
      Debug.Print "The Last Address containing my query using xlPrevious Method = " & lstFnd & vbCrLf & "Range Searched is on single column only."
    
    Call StopTimer
    
    'Uses the entire working range to perform the same query
    'instead of on just a single column, but searches by columns
    'to make the efforts a little more expedient.
    
    Call StartTimer
    
    'workrng was specified in previous code - NOT USING "USEDRANGE" methods.
    With workrng
    
      'Note: removing SearchOrder to use default behavior made NO DIFFERENCE in Timing!
      Set test = .Find("20210715 12", LookIn:=xlValues, Lookat:=xlPart, SearchOrder:=xlByColumns, _
        searchdirection:=xlPrevious, MatchCase:=False)
      If Not test Is Nothing Then
        lstFnd = test.Address
    '    Debug.Print strtFnd
    '    Debug.Print strtFndNxt
    
      End If
    End With
      Debug.Print "The Last Address containing my query using xlPrevious Method = " & lstFnd & vbCrLf & "Range Searched is entire working range."
    
    Call StopTimer
    

    LASTLY - Some Results

    Here is what printed off of my Immediate Pane from the testing:

    The First Cell Address containing my query = $A$38
    The Starting Time is: 14:51:33
    
    The Current Address containing my query = $A$39
    The Current Address containing my query = $A$41
    .
    .
    .
    The Current Address containing my query = $A$47
    The Current Address containing my query = $A$49
    .
    .
    .
    The Current Address containing my query = $A$85
    The Current Address containing my query = $A$87
    The Last Address containing my query using breaking loop Method = $A$87
    The End Time is: 14:51:33
    
    The time it took to run this process is: 00:00:00
    
    
    The Starting Time is: 14:51:33
    
    The Last Address containing my query using xlPrevious Method = $A$87
    Range Searched is on single column only.
    The End Time is: 14:51:35
    The time it took to run this process is: 00:00:02
    
    
    The Starting Time is: 14:51:35
    
    The Last Address containing my query using xlPrevious Method = $A$87
    Range Searched is entire working range.
    The End Time is: 14:51:45
    The time it took to run this process is: 00:00:10