Как в 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 шт):
Использование функций листа в коде - такая операция медленнее, чем работа в собственных 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
О добавленном в вопрос коде.
Не прописан диапазон реагирования на изменения на листе. Надо добавить:
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 минуты.