Как в VBA применять WorksheetFunction к части (отрезку или прямоугольной подобласти) массива?

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

Dim x(3)
x(0) = 1
x(1) = 5
x(2) = -1
x(3) = 3
y = Application.WorksheetFunction.Min(x)
MsgBox y

А что делать, если нужно подобным образом обращаться к различным отрезкам массива? Например, в цикле считать минимум в меняющихся пределах части массива?

Создавать для этого собственную функцию - напрягать интерпретатор частым разбором всё тех же строк VBA-кода, что весьма непроизводительно.

Аналогичный вопрос в отношении любой прямоугольной части двумерного массива.

Добавлено к сообщению позднее.

Для теста скорости исходный код упрощён.

    'Начало скрипта
Private Sub Worksheet_Change(ByVal Target As Range)
If Range("K1").Value = "" Then
    Range("K1").Value = "delete"
    Range("L1").Value = Now()
    Range("L1").NumberFormat = "dd/mm/yyyy hh:mm:ss"
    Range("B:B").Clear
    Range("L2").ClearContents
    n = Range("C1").Value
    m = Range("D1").Value
    iMethod = Range("E1").Value
    A = Range("A1:A" & n)
    Dim b
    ReDim b(1 To n, 1 To 1)
    If iMethod = 1 Then
        For i = 1 To n
            If i <= m Then
                b(i, 1) = Application.WorksheetFunction.Min(Range("A1:A" & i + m))
            ElseIf i > n - m Then
                b(i, 1) = Application.WorksheetFunction.Min(Range("A" & i - m & ":A" & n))
            Else
                b(i, 1) = Application.WorksheetFunction.Min(Range("A" & i - m & ":A" & i + m))
            End If
        Next
        Range("B1:B" & n).Value = b
    ElseIf iMethod = 2 Then
        For i = 1 To n
            If i <= m Then
                b(i, 1) = "=MIN(A1:A" & i + m & ")"
            ElseIf i > n - m Then
                b(i, 1) = "=MIN(A" & i - m & ":A" & n & ")"
            Else
                b(i, 1) = "=MIN(A" & i - m & ":A" & i + m & ")"
            End If
        Next
        Range("B1:B" & n).Formula = b
        b = Range("B1:B" & n).Value
    Else
        MsgBox "Under construction."
    End If
    Range("L2").Value = Now()
    Range("L2").NumberFormat = "dd/mm/yyyy hh:mm:ss"
End If

End Sub 'Конец скрипта

Клетки A1:A1000000 заполнены для теста случайными числами от 1 до 100000 (сохранены "как значения", чтобы не тратилось время на новую генерацию при каждом неосторожном чихе), в клетку C1 помещено число 1000000, в клетку D1 - число 500, в клетку E1 - одно из чисел 1 или 2 (для двух тестов поочерёдно), в клетку K1 - слово "delete" (запуск скрипта по её очищению). При E1=1 тест работал больше 2 минут, при E1=2 - порядка минуты.


Ответы (3 шт):

Автор решения: vikttur_Stop_RU_war_in_UA

Использование функций листа в коде - такая операция медленнее, чем работа в собственных VBA-дебрях. Подобные попытки ускорить вычисления могут привести к противоположному результату.

Для решения конкретной задачи нужно видеть... конкретную задачу :)

В общем виде:

Sub test()
    Dim arr_
    
    arr_ = Range("A2:D7").Value
    
    With Application
        Debug.Print .Min(.Index(arr_, 4, 0)) ' минимальное в "строке" массива
        Debug.Print .Sum(.Index(arr_, 0, 2)) ' сумма в "столбце" массива
    End With
End Sub

Для прямоугольной области решения с помощью функций листа у меня нет.

Часто решение проблем находится в миксованном решении - формулы на листе + VBA

→ Ссылка
Автор решения: vikttur_Stop_RU_war_in_UA

О добавленном в вопрос коде.

Не прописан диапазон реагирования на изменения на листе. Надо добавить:

If Application.Intersect(Range("K1"), Target) Is Nothing Then Exit Sub

Грубая ошибка - нет отключения событий на время работы макроса. Событие Worksheet_Changeсрабатывает каждый раз при изменении в ячейках. В показанном примере это почти не сказывается на производительности, т.к. стоит заглушка в K1 (при этом изменения все равно вызывают проверку условия), но если игнорировать такую "мелочь", можно получить большие тормоза, зацикливания... В общем, чтобы избежать головной боли в будущем, события нужно отключать, если работа кода предполагает работу с листом:

Application.EnableEvents = False

Если в коде работы с листом много, нужно отключать обновление экрана, т.к. перерисовка картинки медленная:

Application: .ScreenUpdating = False

VBA - язык со строгой типизацией. Не нужно забывать декларировать все используемые переменные.

Worksheet_Change - событие листа и его желательно не замусоривать лишними операциями. Тем более, когда в коде есть переменные - при любом изменении на листе срабатывает событие и сразу под все переменные выделяется память. А так как у Вас переменным не присвоены типы (все типа Variant), то объем памяти откусывается под тип, занимающий наибольший кусок оперативки.

Событие сработало и передало управление макросу в общем модуле:

Private Sub Worksheet_Change(ByVal Target As Range)
    If Application.Intersect(Range("K1"), Target) Is Nothing Then Exit Sub
    
    With Application: .ScreenUpdating = False: .EnableEvents = False: End With
    Range("L1").Value = Now()

    Call test

    Range("L2").Value = Now()
    With Application: .ScreenUpdating = True: .EnableEvents = True: End With
End Sub

А еще лучше - поменьше использовать события и запускать обработку иным способом(например, повесить макрос на кнопку).

Теперь о работе двух вариантов. Если исключить выгрузку на лист после циклов, второй вариант отрабатывает меньше секунды. Т.е. его основная нагрузка - запись на лист формул. В первом варианте наоборот - медленная запись в массив из-за вычислений функций.

→ Ссылка
Автор решения: Алексей Р

Если нужно посчитать скользящий минимум, среднее, сумму, то для высокой производительности лучше использовать инкрементальный подход, т.е. не считать каждый раз все "окно", а удалять самый старый элемент и добавлять самый новый. Как-то так:

Function generate(n)
    ReDim a(n)
    For i = LBound(a) To UBound(a)
        a(i) = CInt(Rnd * 10000)
    Next
    generate = a
End Function


Sub rolling_min(a)
    Min = a(LBound(a))
    For i = LBound(a) + 1 To UBound(a)
        If a(i) < Min Then
            Min = a(i)
        End If
'        Debug.Print a(i), Min
    Next
End Sub

Sub rolling_avg(a, winsize)
    cnt = 1
    Sum = 0
    first = 0
    For i = LBound(a) To UBound(a)
        If cnt < winsize Then
            Sum = Sum + a(i)
'            Debug.Print a(i), "-"
        Else
            Sum = Sum - first + a(i)
            first = a(i - winsize + 1)
'            Debug.Print a(i), Sum / winsize
        End If
        cnt = cnt + 1
    Next
End Sub


Sub test1()
    t = Timer
    n = 10 ^ 8
    a = generate(n)
    Debug.Print "a", "rolling_min"
    rolling_min a
    Debug.Print "a", "rolling_avg"
    rolling_avg a, 3
    Debug.Print "Элементов: ", n, "время выполнения,c = ", Timer - t
End Sub
a             rolling_min
a             rolling_avg
Элементов:     100000000    время выполнения,c =         30,79297 

В коде генерится 100 млн элементов и считаются скользящий минимум и скользящее среднее (без вывода на экран - закомментировано) за 0,5 минуты.

→ Ссылка