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