ChatGPT не умеет писать макросы

от автора

Ты попался на кликбейт, олуу… ну нет, на самом деле не совсем.

Итак, все вы знаете (а если не знаете, то пора выбираться из вакуума в котором вы находитесь) про такую нейросеть как ChatGPT. Мне все никак не удавалось попробовать его поюзать в связи с некоторыми сложностями доступа, и вот вчера наконец повезло. Ну и естественно я начал проверять его знания VBA?

И как ни странно они неплохие, но запустить сразу без правок написанный нейронкой скрипт удавалось далеко не всегда.
То переменную не объявит:

Function GetRightBoundary(rng As Range) As Long     Dim cols As Collection     Set cols = New Collection      For Each col In rng.Columns ' При этом он знает про Option Explicit.         If WorksheetFunction.CountA(col) > 0 Then cols.Add col.Column     Next col     GetRightBoundary = WorksheetFunction.Max(cols) End Function

То передает неверный тип данных в функцию (например в метод Max из WoksheetFunction в примере выше он передает объект Collection).

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

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

Я решил сгенерировать с помощью ChatGPT класс, которому нужно будет передавать некий диапазон (а точнее UsedRange), а он в свою очередь в этом диапазоне по определенному алгоритму будет искать таблицу и если найдет, то вернет уже ее диапазон.

Сначала пробуем попросить придумать алгоритм для нахождения таблицы:

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

Для решения этой задачи можно использовать методы Find или FindNext, чтобы найти первую ячейку с данными и последнюю ячейку с данными в таблице. Затем можно использовать метод Intersect, чтобы исключить любую информацию в заголовке таблицы. Если заголовок таблицы не найден, то будет возвращен весь диапазон.

Вот пример кода функции, которая решает эту задачу:

Function FindTableRange(sheet As Worksheet) As Range     Dim firstCell As Range     Dim lastCell As Range     Dim tableRange As Range          ' Ищем первую ячейку с данными в таблице     Set firstCell = sheet.Cells.Find(What:="*", _                                       After:=sheet.Cells(1, 1), _                                       LookAt:=xlPart, _                                       SearchOrder:=xlByRows, _                                       SearchDirection:=xlNext, _                                       MatchCase:=False)                                            If Not firstCell Is Nothing Then         ' Ищем последнюю ячейку с данными в таблице         Set lastCell = sheet.Cells.Find(What:="*", _                                          After:=sheet.Cells(1, 1), _                                          LookAt:=xlPart, _                                          SearchOrder:=xlByRows, _                                          SearchDirection:=xlPrevious, _                                          MatchCase:=False)                                                   ' Используем метод Intersect, чтобы найти диапазон таблицы без заголовка         If Not Intersect(firstCell.EntireRow, sheet.UsedRange) Is Nothing And _            Not Intersect(lastCell.EntireRow, sheet.UsedRange) Is Nothing Then             Set tableRange = sheet.Range(firstCell, lastCell).Offset(1).Resize(lastCell.row - firstCell.row)         Else             Set tableRange = sheet.UsedRange         End If     Else         Set tableRange = sheet.UsedRange     End If          Set FindTableRange = tableRange End Function

Неплохо, но этот вариант не работает так как нужно.

К примеру в таком виде он вернет выделенный диапазон A3:S28, когда сама таблица находится в диапазоне C9:S28:

После нескольких безуспешных попыток, я решил помочь нейронке, и придумал следующий алгоритм:

Необходимо найти границы диапазона таблицы.
Для нахождения верхней границы, нужно перебрать все колонки переданного диапазона, найти в них номера первых непустых строк и поместить их в массив. Медиана номеров этих строк – верхняя граница таблицы.
Для нахождения нижней границы, нужно перебрать все колонки переданного диапазона, найти в них номера последних непустых строк и поместить их в массив. Максимум из номеров этих строк – нижняя граница таблицы.
Для нахождения левой границы, нужно перебрать все строки переданного диапазона, найти в них номера первых непустых колонок и поместить их в массив. Медиана номеров этих колонок – левая граница таблицы.
Для нахождения правой границы, нужно перебрать все строки переданного диапазона, найти в них номера последних непустых колонок и поместить их в массив. Максимум из номеров этих колонок – правая граница таблицы.

Для реализации этой задачи в VBA можно использовать следующий код:

Function FindTableRange(tableRange As Range) As Range      Dim numRows As Long     Dim numCols As Long     Dim row As Long     Dim col As Long     Dim topRow As Long     Dim bottomRow As Long     Dim leftCol As Long     Dim rightCol As Long     Dim topRows() As Long     Dim bottomRows() As Long     Dim leftCols() As Long     Dim rightCols() As Long          numRows = tableRange.rows.Count     numCols = tableRange.Columns.Count          ReDim topRows(1 To numCols)     ReDim bottomRows(1 To numCols)     ReDim leftCols(1 To numRows)     ReDim rightCols(1 To numRows)          For col = 1 To numCols         topRows(col) = tableRange.rows.Count         bottomRows(col) = 1         For row = 1 To numRows             If Not IsEmpty(tableRange(row, col)) Then                 If row < topRows(col) Then topRows(col) = row                 If row > bottomRows(col) Then bottomRows(col) = row                 If leftCols(row) = 0 Then leftCols(row) = col                 rightCols(row) = col             End If         Next row     Next col          topRow = WorksheetFunction.Median(topRows)     bottomRow = WorksheetFunction.Max(bottomRows)     leftCol = WorksheetFunction.Median(leftCols)     rightCol = WorksheetFunction.Max(rightCols)          Set FindTableRange = Intersect(tableRange.Worksheet.Range(tableRange.Cells(topRow, leftCol), tableRange.Cells(bottomRow, rightCol)), tableRange.Worksheet.UsedRange)      End Function

Бинго! Функция выдает верный результат. Но мне этого мало. Я хочу класс, и рефакторинг, и чтоб по красоте.

Не без корректировок, конечно, но спустя минут 30-40 (возможно меньше/больше, не помню), я наконец получил приемлемый результат.
Далее я решил попросить сгенерировать тесты используя Rubberduck:

Хорошо, я могу помочь написать unit-тесты для этого класса с использованием Rubberduck.

Перед тем, как начать написание тестов, необходимо убедиться, что Rubberduck установлен и настроен в Visual Studio.

После этого можно приступить к написанию тестов. Ниже приведен пример тестового класса с несколькими тестами для функции FindTableRange:

Option Explicit '@TestModule '@Folder "Tests" Private Assert As Object @ModuleInitializee Public Sub ModuleInitialize() Set Assert = CreateObject("Rubberduck.AssertClass") End Sub @ModuleCleanupp Public Sub ModuleCleanup() Set Assert = Nothing End Sub @TestMethodd Public Sub Test_FindTableRange_SimpleTable() 'Arrange Dim rng As Range Set rng = ThisWorkbook.Sheets("Sheet1").Range("A1:C3") 'Act Dim result As Range Set result = FindTableRange(rng)  'Assert Assert.AreEqual "A2:C3", result.Address  End Sub @TestMethodd Public Sub Test_FindTableRange_EmptyTable() 'Arrange Dim rng As Range Set rng = ThisWorkbook.Sheets("Sheet1").Range("A1:C1") 'Act Dim result As Range Set result = FindTableRange(rng)  'Assert Assert.IsNull result  End Sub @TestMethodd Public Sub Test_FindTableRange_TableWithEmptyRowsAndColumns() 'Arrange Dim rng As Range Set rng = ThisWorkbook.Sheets("Sheet1").Range("A1:D5") rng.Cells(2, 2).ClearContents rng.Cells(4, 3).ClearContents 'Act Dim result As Range Set result = FindTableRange(rng)  'Assert Assert.AreEqual "B3:C4", result.Address  End Sub @TestMethodd Public Sub Test_FindTableRange_TableWithHeader() 'Arrange Dim rng As Range Set rng = ThisWorkbook.Sheets("Sheet1").Range("A1:C4") rng.Cells(1, 1).Value = "Header1" rng.Cells(1, 2).Value = "Header2" rng.Cells(1, 3).Value = "Header3" 'Act Dim result As Range Set result = FindTableRange(rng)  'Assert Assert.AreEqual "A2:C4", result.Address  End Sub @TestMethodd Public Sub Test_FindTableRange_TableWithMultipleRanges() 'Arrange Dim rng As Range Set rng = ThisWorkbook.Sheets("Sheet1").Range("A1:C3") Dim rng2 As Range Set rng2 = ThisWorkbook.Sheets("Sheet1").Range("D1:F3") Dim multiRange As Range Set multiRange = Union(rng, rng2) 'Act Dim result As Range Set result = FindTableRange(multiRange)  'Assert Assert.AreEqual "A2:F3", result.Address  End Sub 

Эти тесты проверяют различные случаи использования функции FindTableRange, включая простые таблицы, пустые таблицы, таблицы с пустыми строками и столбцами, таблицы с заголовками и таблицы с несколькими диапазонами.

Тесты можно запустить с помощью Rubberduck, выбрав класс тестов в окне "Unit Test Explorer" и нажав кнопку "Run".

Не без вопросиков, конечно, но немного подредактировав, можно получить адекватные тесты.
Ну и на последок я попросил сгенерировать документацию методов в стиле VB.Net:

Документация к методам класса:

Option Explicit  ''' <summary> ''' Этот класс содержит методы для поиска диапазона таблицы на листе Excel. ''' </summary>  ''' <summary> ''' Находит диапазон таблицы в заданном диапазоне Range. ''' </summary> ''' <param name="rng">Диапазон Range, в котором нужно найти таблицу.</param> ''' <returns>Диапазон Range, представляющий таблицу.</returns> ''' <example> ''' Пример использования: ''' <code> ''' Dim tableRange As Range ''' Set tableRange = FindTableRange(Range("A1:F10")) ''' </code> ''' </example> Public Function FindTableRange(ByVal rng As Range) As Range     Dim top As Long, bottom As Long, left As Long, right As Long          If WorksheetFunction.CountA(rng) = 0 Then         Set FindTableRange = Nothing         Exit Function     End If          top = FindBoundary(rng, False, xlNext)     bottom = FindBoundary(rng, False, xlPrevious)     left = FindBoundary(rng, True, xlNext)     right = FindBoundary(rng, True, xlPrevious)          If top > 0 And bottom > 0 And left > 0 And right > 0 Then         Set FindTableRange = rng.Parent.Range(rng.Cells(top, left), rng.Cells(bottom, right))     End If End Function  ''' <summary> ''' Находит границы таблицы в заданном диапазоне Range по заданному направлению. ''' </summary> ''' <param name="rng">Диапазон Range, в котором нужно найти границы таблицы.</param> ''' <param name="isRow">Указывает, ищутся ли границы строк.</param> ''' <param name="Direction">Направление поиска границ таблицы.</param> ''' <returns>Число, представляющее границу таблицы.</returns> Private Function FindBoundary(ByVal rng As Range, ByVal isRow As Boolean, ByVal Direction As XlSearchDirection) As Long     Dim Arr() As Long     Arr = FindBoundaryArray(rng, isRow, Direction)          If UBound(Arr) > 0 Then         If isRow Then             FindBoundary = WorksheetFunction.Median(Arr)         Else             FindBoundary = WorksheetFunction.Max(Arr)         End If     Else         FindBoundary = 0     End If End Function  ''' <summary> ''' Находит массив границ таблицы в заданном диапазоне Range по заданному направлению. ''' </summary> ''' <param name="rng">Диапазон Range, в котором нужно найти границы таблицы.</param> ''' <param name="isRow">Указывает, ищутся ли границы строк.</param> ''' <param name="Direction">Направление поиска границ таблицы.</param> ''' <returns>Массив чисел, представляющий границы таблицы.</returns> Private Function FindBoundaryArray(ByVal rng As Range, ByVal isRow As Boolean, ByVal Direction As XlSearchDirection) As Variant     Dim i As Long     Dim Arr() As Long          If WorksheetFunction.CountA(rng) = 0 Then         FindBoundaryArray = 0         Exit Function     End If          If isRow And Direction = xlNext Then         ReDim Arr(1 To rng.rows.Count)         For i = 1 To rng.rows.Count             Arr(i) = FindLeftBoundary(rng, i)         Next i     ElseIf isRow And Direction = xlPrevious Then         ReDim Arr(1 To rng.rows.Count)         For i = 1 To rng.rows.Count             Arr(i) = FindRightBoundary(rng, i)         Next i     ElseIf Not isRow And Direction = xlNext Then         ReDim Arr(1 To rng.Columns.Count)         For i = 1 To rng.Columns.Count             Arr(i) = FindTopBoundary(rng, i)         Next i     ElseIf Not isRow And Direction = xlPrevious Then         ReDim Arr(1 To rng.Columns.Count)         For i = 1 To rng.Columns.Count             Arr(i) = FindBottomBoundary(rng, i)         Next i     End If          FindBoundaryArray = Arr End Function  ''' <summary> ''' Находит правую границу таблицы в заданной строке диапазона Range. ''' </summary> ''' <param name="rng">Диапазон Range, в котором нужно найти границу таблицы.</param> ''' <param name="rowIdx">Индекс строки, в которой нужно найти границу таблицы.</param> ''' <returns>Число, представляющее правую границу таблицы.</returns> Private Function FindRightBoundary(ByVal rng As Range, ByVal rowIdx As Long) As Long     Dim colIdx As Long     For colIdx = rng.Columns.Count To 1 Step -1         If Not IsEmpty(rng(rowIdx, colIdx)) Then             FindRightBoundary = colIdx             Exit Function         End If     Next colIdx     FindRightBoundary = 0 End Function  ''' <summary> ''' Находит нижнюю границу таблицы в заданном столбце диапазона Range. ''' </summary> ''' <param name="rng">Диапазон Range, в котором нужно найти границу таблицы.</param> ''' <param name="colIdx">Индекс столбца, в котором нужно найти границу таблицы.</param> ''' <returns>Число, представляющее нижнюю границу таблицы.</returns> Private Function FindBottomBoundary(ByVal rng As Range, ByVal colIdx As Long) As Long     Dim rowIdx As Long     For rowIdx = rng.rows.Count To 1 Step -1         If Not IsEmpty(rng(rowIdx, colIdx)) Then             FindBottomBoundary = rowIdx             Exit Function         End If     Next rowIdx     FindBottomBoundary = 0 End Function  ''' <summary> ''' Находит левую границу таблицы в заданной строке диапазона Range. ''' </summary> ''' <param name="rng">Диапазон Range, в котором нужно найти границу таблицы.</param> ''' <param name="rowIdx">Индекс строки, в которой нужно найти границу таблицы.</param> ''' <returns>Число, представляющее левую границу таблицы.</returns> Private Function FindLeftBoundary(ByVal rng As Range, ByVal rowIdx As Long) As Long     Dim colIdx As Long     For colIdx = 1 To rng.Columns.Count Step 1         If Not IsEmpty(rng(rowIdx, colIdx)) Then             FindLeftBoundary = colIdx             Exit Function         End If     Next colIdx     FindLeftBoundary = 0 End Function  ''' <summary> ''' Находит верхнюю границу таблицы в заданном столбце диапазона Range. ''' </summary> ''' <param name="rng">Диапазон Range, в котором нужно найти границу таблицы.</param> ''' <param name="colIdx">Индекс столбца, в котором нужно найти границу таблицы.</param> ''' <returns>Число, представляющее верхнюю границу таблицы.</returns> Private Function FindTopBoundary(ByVal rng As Range, ByVal colIdx As Long) As Long     Dim rowIdx As Long     For rowIdx = 1 To rng.rows.Count Step 1         If Not IsEmpty(rng(rowIdx, colIdx)) Then             FindTopBoundary = rowIdx             Exit Function         End If     Next rowIdx     FindTopBoundary = 0 End Function

Написание документации мы обсуждали с подписчиками в моем телеграм канале (не подписывайтесь).


ссылка на оригинал статьи https://habr.com/ru/articles/738038/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *