Для цикла с динамическим диапазоном / границами

#excel #vba #for-loop

#excel #vba #для цикла #динамический #диапазон

Вопрос:

Я хочу использовать приведенный ниже код для перебора диапазона ячеек и проверки, находится ли «тестовое слово» в одной из этих ячеек. Если да, то вставляется новая строка, и все ячейки под ней перемещаются на одну ячейку вниз. Теперь проблема в том, что я использую j = Range("A:A").End(xlDown).Row для определения второго счетчика цикла for, и этот счетчик не меняется при перемещении путем вставки новых строк каждый раз, когда найдено «Тестовое слово». Таким образом, ячейки со значениями, которые необходимо проверить, выходят за границы и «пропускают» цикл.

 Sub Macro1()

    Dim i As Integer
    Dim j As Integer
        
    j = Range("A:A").End(xlDown).Row

    For i = 1 To j

        If Range("A" amp; i) = "Testword" Then
            Range("A" amp; i   1).Insert
    
        End If
    
    Next i

End Sub
 

Ответ №1:

Попробуйте следующий способ, пожалуйста. Это будет быстрее, если сразу вставить все в конец:

 Sub Macro1()
    Dim i As Long, j As Long, rngIns As Range
        
    j = Range("A" amp; rows.count).End(xlUp).row 'it will work even with gaps in A:A column

    For i = 1 To j
        If Range("A" amp; i) = "Testword" Then
            If rngIns Is Nothing Then
                Set rngIns = Range("A" amp; i   1)
            Else
                Set rngIns = Union(rngIns, Range("A" amp; i   1))
            End If
        End If
    Next i
    If Not rngIns Is Nothing Then
        'solving cases of consecutive rows keeping the searched string:
        If InStr(rngIns.Address(0, 0), ":") > 0 Then _
                   Set rngIns = makeDiscontinuu(rngIns)
        rngIns.EntireRow.Insert
    End If
End Sub

Function makeDiscontinuu(rng As Range) As Range
   Dim a As Range, c As Range, strAddress As String
   For Each a In rng.Areas
        If a.cells.count = 1 Then
            strAddress = strAddress amp; a.Address(0, 0) amp; ","
        Else
            For Each c In a.cells
                strAddress = strAddress amp; c.Address(0, 0) amp; ","
            Next c
        End If
   Next a
   Set makeDiscontinuu = Range(left(strAddress, Len(strAddress) - 1))
End Function
 

Комментарии:

1. Я не думал, что это будет иметь такое большое значение… Но я провел тест, чтобы увидеть. ~ 35 строк данных, где 14 — это «testrow». Обратный цикл Forloop занял 4,11 секунды. Цикл while занял 3,84 секунды. Этот ответ 0,34 секунды. Писать так много кода выглядит нелогично. Однако …. установка для обновления экрана и вычислений значения false / manual и запуск forloop дали мне 0,48 секунды.

2. Время цикла с false / manual составляет 0,4 секунды. И на самом деле это занимает то же время с добавлением или без добавления каких-либо строк. Я допустил опечатку на листе, поэтому вместо Testword написано testword. Когда я запускал код с опечаткой, ничего не изменилось (очевидно), но время все еще составляло 0,4 секунды.

3. @Andreas: Разница может быть намного больше, если диапазон обрабатываемых данных действительно велик и вхождения тоже. И даже быстрее, если случай последовательных строк очень редок. Для такого случая это можно сделать еще быстрее, выполнив итерацию по массиву и построив диапазон ячеек в соответствии с номером строки итерации. Но не большой выигрыш…

4. На самом деле. Я позволю себе не согласиться… 10072 строки копипасты предыдущего тестового сценария. Цикл while занял 11,2 секунды. ваш код вообще не был завершен. Он разбился при наборе makeDiskcontinuu = range …. (последняя строка функции). Не уверен, что строка стала длинной или что случилось … 1004 (переведенный) диапазон методов в object _global не удался. (или что-то в этом роде). Но это заняло гораздо больше времени, чем 11 секунд. Возможно, ~ 30, когда он разбился … Straddr имеет 24 365 символов, когда он разбился, все выглядит нормально, но в этот момент что-то не работает. Возможно, диапазон имеет предел (?)

5. @Andreas: Да. Адресная строка имеет ограничение в 256 символов. Вот почему я предпочел адрес (0, 0), чтобы сохранить некоторые символы … 🙂 Можете ли вы провести некоторый тест скорости без появления последовательных строк?

Ответ №2:

 Sub Macro1()

    Dim i As Integer
    Dim j As Integer
        
    j = Range("A:A").End(xlDown).Row

    For i = j To 1 Step -1

        If Range("A" amp; i) = "Testword" Then
            Range("A" amp; i   1).Insert
    
        End If
    
    Next i

End Sub
 

Ответ №3:

Идите назад…

 Sub Macro1()

    Dim i As Integer
    Dim j As Integer
        
    j = Range("A:A").End(xlDown).Row

    For i = j To 1 step -1

        If Range("A" amp; i) = "Testword" Then
            Range("A" amp; i  1).Insert
    
        End If
    
    Next i

End Sub
 

Или используйте цикл while (менее эффективный)

 Sub Macro1()

    Dim i As Integer

    i = 1
    while i < Range("A" amp; rows.count).End(xlUp).Row

        If Range("A" amp; i) = "Testword" Then
            Range("A" amp; i   1).Insert
    
        End If
        i = i  1
    wend

End Sub
 

Это будет обновлять цикл на каждой итерации.

Ответ №4:

@Андреас, пожалуйста, посмотри на эту пару кодов. Более сложный код по-прежнему работает быстрее, но происходит что-то странное. Я имею в виду, что если вы запускаете каждый код после сохранения (каким-то образом обновите приложение / VBA / что угодно …), время выполнения будет минимальным. Затем, после каждой пары запусков, добавляется несколько дополнительных секунд (как минимум миллисекунд). Поэтому, пожалуйста, попробуйте следующие фрагменты кода и убедитесь, что они ведут себя так же при вашей установке.

Я протестировал оба кода в диапазоне из 8700 строк с 1216 искомыми вхождениями строк:

 Sub Macro1Simple()
    Dim i As Long, j As Long', dTime As Double
    
    j = Range("A" amp; rows.count).End(xlUp).row 'changed the way of calculation, for the case of inserted rows existence. 
    On Error Resume Next
     Range("A1:A" amp; j).SpecialCells(xlCellTypeBlanks).EntireRow.Delete
    On Error GoTo 0
    
    j = Range("A" amp; rows.count).End(xlUp).row
    'dTime = MicroTimer 'to measure duration
    
    Application.ScreenUpdating = False
    Application.Calculation = xlCalculationManual
    For i = j To 1 Step -1

        If Range("A" amp; i) = "Testword" Then
            Range("A" amp; i   1).Insert
        End If
    
    Next i
    Application.ScreenUpdating = True
    Application.Calculation = xlCalculationAutomatic
    'MsgBox (MicroTimer - dTime) * 1000 amp; " [ms]"
    'Refresh
End Sub
 

Сложная версия:

 Sub Macro1_Complicated()
    Dim i As Long, j As Long, rngIns As Range, arrRng() As Range', dTime As Double
        
    j = Range("A" amp; rows.count).End(xlUp).row 'it will work even with gaps in A:A column
    On Error Resume Next
     Range("A1:A" amp; j).SpecialCells(xlCellTypeBlanks).EntireRow.Delete: '  Stop
    On Error GoTo 0
    
    j = Range("A" amp; rows.count).End(xlUp).row
    'dTime = MicroTimer 'to measure duration
    
    For i = 1 To j
        If Range("A" amp; i) = "Testword" Then
            If rngIns Is Nothing Then
                Set rngIns = Range("A" amp; i   1)
            Else
                Set rngIns = Union(rngIns, Range("A" amp; i   1))
            End If
        End If
    Next i
    If Not rngIns Is Nothing Then
        If InStr(rngIns.Address(0, 0), ":") > 0 Then _
                   arrRng = makeDiscontNoLimit(rngIns)
        With Application
            .ScreenUpdating = False
            .Calculation = xlCalculationManual
        End With
        For i = 0 To UBound(arrRng)
            arrRng(i).EntireRow.Insert
        Next i
         With Application
            .ScreenUpdating = True
            .Calculation = xlCalculationAutomatic
        End With
    End If
    'MsgBox (MicroTimer - dTime) * 1000 amp; " [ms]"
    'Refresh
End Sub

Function makeDiscontNoLimit(rng As Range) As Variant
   Dim a As Range, c As Range, strAddress As String, strAddr() As Range, i As Long, k As Long
   Dim arNo As Long
   
   ReDim strAddr(100)
   For Each a In rng.Areas
        If a.cells.count = 1 Then
            strAddress = strAddress amp; a.Address(0, 0) amp; ","
            If Len(strAddress) >= 256 Then
                For i = Len(strAddress) - 1 To 1 Step -1
                    If Mid(strAddress, i, 1) = "," Then
                        Set strAddr(k) = Range(left(strAddress, i - 1)): k = k   1
                        strAddress = Mid(strAddress, i   1, Len(strAddress) - i): Exit For
                    End If
                Next i
            End If
        Else
            For Each c In a.cells
                strAddress = strAddress amp; c.Address(0, 0) amp; ","
                If Len(strAddress) >= 256 Then
                    For i = Len(strAddress) - 1 To 1 Step -1
                        If Mid(strAddress, i, 1) = "," Then
                            Set strAddr(k) = Range(left(strAddress, i - 1)): k = k   1
                            strAddress = Mid(strAddress, i   1, Len(strAddress) - i): Exit For
                        End If
                    Next i
                End If
            Next c
        End If
   Next a
   If Not strAddr(0) Is Nothing Then
        Set strAddr(k) = Range(left(strAddress, Len(strAddress) - 1))
        ReDim Preserve strAddr(k)
        makeDiscontNoLimit = strAddr: Exit Function
   Else
        Set strAddr(0) = Range(left(strAddress, Len(strAddress) - 1))
        ReDim Preserve strAddr(0)
        makeDiscontNoLimit = strAddr
   End If
End Function
 

Я мог бы создать функцию для обработки повторяющейся части ввода диапазона (ограниченного адреса) в массив, но я только хотел, чтобы она работала в больших диапазонах.

Итак, чтобы избежать увеличения продолжительности после каждого запуска, я попытался Refresh , но без какого-либо результата. Можете ли вы представить способ заставить VBA работать с одинаковой эффективностью для всех запусков?

Я измерил время, используя API getFrequency в сочетании с getTickCount . но я не думаю, что это вопрос времени измерения. Я не разместил функции API и необходимые Sub , чтобы вы могли использовать свой способ измерения…