Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение9 мин
Количество просмотров9.8K

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

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

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

Теги:
Хабы:
Всего голосов 6: ↑2 и ↓4-2
Комментарии18

Публикации

Истории

Ближайшие события

12 – 13 июля
Геймтон DatsDefense
Онлайн
19 сентября
CDI Conf 2024
Москва