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

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

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

Комментарии (16)


  1. astronom
    27.05.2023 20:09
    +1

    Говорят, и в C# он не очень умеет и тп языки, а вот на Python очень неплохой выдает код, если заранее просить его писать как senior-помидор с типизацией и с примерами кода)

    И не пробовали делать запросы сразу на английском?


    1. ArtCapCorn Автор
      27.05.2023 20:09

      на английском не пробовал, тут на русском-то порой сложно было изъясниться ????

      А вот про сеньора помидора надо было попробовать


      1. csharpreader
        27.05.2023 20:09
        +3

        Зря. Без английского там делать нечего, тем более по коду. А с английским и С# очень сносно делает. Да, пару строк из пятидесяти может «забыть», но дописать сотню символов проще (и даже что-то поправить на готовой «рыбе»), чем писать всю бадягу с нуля. Очень, очень ускоряет рутину. Я говорю не про влажные фантазии, а про ежедневную работу.


        1. ArtCapCorn Автор
          27.05.2023 20:09

          Надо будет попробовать


      1. aGGre55or
        27.05.2023 20:09
        +2

        Никакой разницы между английским и русским запросом нет. Но и по-русски надо писать грамотно. Вы пишите "Начало и конец ее не известны." Ну т.е. они известны. Потому что неизвестны - пишется слитно. Ж)


        1. ArtCapCorn Автор
          27.05.2023 20:09

          Ого, проглядел…


        1. Metotron0
          27.05.2023 20:09
          +3

          А вы пишете "пишите" :)


        1. Guul
          27.05.2023 20:09

          Раза в три разница есть.
          Токенизациия различается, что видно невооружённым взглядом. В английском чуть ли не каждое слово в один токен влезает. В русском хорошо если в три войдёт.


    1. xkb45bkc4
      27.05.2023 20:09
      +3

      а вот на Python очень неплохой выдает код,

      "Перепиши код на JS" это каждый второй запрос у меня.


  1. aborouhin
    27.05.2023 20:09
    +5

    Думаю, скромные успехи ChatGPT с VB вызваны тем, что программисты на VB не очень склонны свои исходники держать в общедоступных репозиториях (чему, понятное дело, способствует отсутствие средств для этого в IDE). По рейтингу TIOBE VB на 20 месте, а по статистике Github не входит даже в топ-50. Ну и на чём бедной нейросетке прикажете учиться?..

    P.S. И да, к рекомендациям выше про английский язык присоединюсь. Русский в ChatGPT - это сразу радикальное снижение качества по любой теме. Ну а уж русский язык в программировании - вообще на любителя история.


    1. ArtCapCorn Автор
      27.05.2023 20:09
      -1

      Вы немного путаете VBA и VB. Разница между ними достаточная. Но суть от этого не меняется, кода на VBA, скорее всего, еще меньше в общедоступных репозиториях (хотя мне кажется, что его обучали не только на репозиториях, а еще и на ответах stack overflow).

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


      1. aborouhin
        27.05.2023 20:09

        Да нет, не путаю, в давние времена достаточно поупражнялся в нём в обоих вариантах :) Просто VB (который не .Net - тот в рейтингах выделен отдельно, а классический, до 6.0) сейчас только в виде VBA в основном в живой природе и существует.


      1. IvanSTV
        27.05.2023 20:09
        +1

        Относительно VBA. Репозиториями для VBA пользуюся в основном люди весьма продвинутые, в то время как основная масса кода пишется как правило, дилетантами, потому что по своему характеру он узкоприкладной. Причем сильно отличается и сложность. и качество кода. Если обучать на репозиториях, то само собой нейросеть будет выдавать периодически дичь - и материала немного, и используются достаточно сложные методы. Если на StackOverflow, то там будет получше - обычно ответ, написанный на бегу, будет попроще, а объем охватываемых задач пошире.

        В компании, где работаю, встал вопрос о переписывании макросов с VBA на JS для Р7, и один активный товарищ внедрил в мозг менеджменту, что нейросеть с этим процессом будет в помощь и почти все напишет, и все до сих пор достаточно к этому легкомысленно к этому относятся. Я по-всякому пробовал давать запросы (на английском и частично на русском, когда пытался допытать его относительно объектной базы Р7), результат примертно такой:

        • объектной базы Р7 нейросеть не знает и даже не понимает, что это, даже с объяснениями, объектную базу опенсорсного проекта OnlyOffice(идентичную), тоже, хотя тот лежит на Гитхабе

        • написать код на JS может только в самом наипростейшем типа "напиши обход map по ключу" . Перебирать объекты с вложенными объектами не написал - может, я плохо объяснял, но обычный пользователь, решивший написать макрос на JS с нейросетью тоже вряд ли объяснится понятно.

        • Попытки давать элементы объектной базы вводными дают чушь. Самая лютая дичь получается, когда скармливаешь описание объектов, данных в описаниях. Как говорится, наглядно видно, чем робот хуже человека - я хоть могу угадывать, что они в описании пропустили, нейросеть не умеет. Когда описываешь своими словами - лучше, но пользоваться кодом нельзя. Такое срабатывает для отдельных конструкций, но объяснять дольше, чем написать самому, и смысла в этом немного.

        Одним словом, чатГПТ способен облегчить только поиск методов языка, давать куски кода в качестве справочника - например, сортировку двумерного массива на JS написал вполне удовлетворительно. Все остальное он не обучен.

        В этой истории интересно даже не то, что там нейросеть умеет или нет. а то, что уже есть люди, которые готовы из факта того, что она что-то может в код иногда, выводить управленческие решения :)


  1. arheops
    27.05.2023 20:09
    +4

    Просто оно не умеет вообще писать. Качество увеличивается с популярностью и доступностью оналайн разных исходников.
    Для VB, доступность, очевидно, близка к нулю.
    Но даже для питона чат галюцинирует достаточно часто. Он, скорее, дает идеи которые потом нужно "дописать"


  1. cat_chi
    27.05.2023 20:09
    +1

    А с чего он должен уметь что-то писать? ChatGPT вообще не умеет писать код, если что. Даже если вам кажется, что это не так.

    У него просто физически нет цикла "сделать -> проверить -> отрефлексировать", т.к. нет никаких инструментов для проверки кода. А написание кода – это и есть этот цикл, а вовсе не генерация текста, как кому-то может показаться.


  1. AntoineLarine
    27.05.2023 20:09

    Астрологи объявили год ChatGPT. Поток шлака на тему "нейросеть работает за меня" увеличился на порядок. Блин, но зачем это на Хабре?