VBA необходимо добавить столбец для подсчета вхождений в последовательности и сброса при появлении заданного значения

#excel #vba

#excel #vba

Вопрос:

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

Я хочу создать макрос в VBA, который добавляет два новых столбца в таблицу данных и заполняет каждую ячейку количеством, созданным из данных в другом столбце. Таким образом, он по существу подсчитывает последовательность. Например, ниже я добавил столбец счетчика, который сначала увеличивает счетчик при появлении игрока в столбце победитель. Затем он сбрасывается, когда значение оценки = 0-0, и запускается снова.

Затем столбец последовательности будет выбирать наибольшее число в каждой последовательности.

Причина, по которой я хочу это сделать, заключается в том, что затем я могу запустить COUNTIFS в таблице, чтобы узнать, сколько раз игрок набирал очки подряд — например, 3pts в строке = 2, 2pts в строке = 3, 1pt в строке = 7.

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

Спасибо,

Саймон

 
 ------- ---------- --------- ---------- 
| Score |  Winner  | Counter | Sequence |
 ------- ---------- --------- ---------- 
| 0-0   | Player 1 |       1 |          |
| 15-0  | Player 1 |       2 |          |
| 30-0  | Player 1 |       3 |          |
| 40-0  | Player 1 |       4 |        4 |
| 0-0   | Player 1 |       1 |        1 |
| 15-0  | Player 2 |       1 |          |
| 15-15 | Player 2 |       2 |        2 |
| 15-30 | Player 1 |       1 |          |
| 30-30 | Player 1 |       2 |          |
| 40-30 | Player 1 |       3 |        3 |
| 0-0   | Player 2 |       1 |          |
| 0-15  | Player 2 |       2 |        2 |
| 0-30  | Player 1 |       1 |        1 |
| 15-30 | Player 2 |       1 |          |
| 15-40 | Player 2 |       2 |        2 |
| 0-0   | Player 2 |       1 |          |
 ------- ---------- --------- ---------- 


  

Комментарии:

1. Если вы собираетесь использовать функции рабочего листа (например COUNTIFS ), почему вы должны использовать VBA??

2. @RonRosenfeld мне нужно выполнить некоторую предварительную обработку, иначе формулы в электронной таблице станут массивными.

Ответ №1:

Сценарий, который я создал, может быть ужасно оптимизирован, но я попробовал.

Это должно работать при условии, что нужные вам данные находятся на первом листе (листы (1)) вашей книги. И предполагая, что структура данных соответствует приведенному вами примеру (colA = ОЦЕНКА, ColB = ПОБЕДИТЕЛЬ, ColC = СЧЕТЧИК, colD = Последовательность).

Для выполнения скрипта просто запустите вспомогательную процедуру main(). Он вызывает addcounter sub (который работает с ColC, COUNTER), а затем вызывает addseq (colD, SEQUENCE)

Для COUNTER я в значительной степени добавил счетчик, который будет сбрасываться всякий раз, когда оба результата будут равны 0 или номер игрока изменится с 1 на 2 или наоборот.

Для ПОСЛЕДОВАТЕЛЬНОСТИ он будет учитываться всякий раз, когда игра заканчивается (когда в счете 40) или происходит смена игрока.

Я запустил скрипт на своем компьютере и получил тот же результат в вашем примере.

Попробуйте и свяжитесь со мной в комментариях, чтобы исправить возможные проблемы.

Пожалуйста, смотрите Код ниже,

 Option Explicit

Dim wb As Workbook
Dim tRow As Long

Dim sRng As Range
Dim cel As Range

Dim tArr() As String
Dim contar As Long
Dim i As Long

Dim a As String
Dim b As String

Sub main()

'sets wb as active workbook
Set wb = ThisWorkbook

'calls procedure that sets up COUNTER col
Call addcounter

'calls second procedure that fills the SEQUENCE col
Call addseq

End Sub



Private Sub addcounter()

'counts last row to obtain ubound of table range
tRow = crow(1, 1)

'sets range based on total row count and col a, excluding header, starts from A2
Set sRng = wb.Sheets(1).Range("A2:A" amp; tRow)

'bad practice, but i used on error to ignore any data type error when comparing variables
On Error Resume Next

'iterate through all the cells in sRng set up above
For Each cel In sRng

    'splits string under SCORE, and removes the dash, and stores the data in tArr array
    tArr = Split(cel.Value, "-")
    
    'a and b variables obtain the player numbers for the winner change check
    a = Replace(cel.Offset(0, 1).Value, "Player", "")
    b = Replace(cel.Offset(-1, 1).Value, "Player", "")
    
    'check for sum of the both scores, if zero, it resets the count
    i = CLng(tArr(0))   CLng(tArr(1))

    'checks for counter reset, it checks for sum of both score 0 or if player 1 amp; 2 changed
    If i = 0 Or pcheck(a, b) = False Then
    
        contar = 1
    
        cel.Offset(0, 2).Value = contar

    Else
    
        contar = contar   1
    
        cel.Offset(0, 2).Value = contar
    
    End If

Next cel

Set sRng = Nothing
Set cel = Nothing

End Sub



Private Sub addseq()

'counts last row to obtain ubound of table range
tRow = crow(1, 1)


Set sRng = wb.Sheets(1).Range("A2:A" amp; tRow)

On Error Resume Next
contar = 1

'iterate through all the cells in sRng set up above
For Each cel In sRng

    tArr = Split(cel.Value, "-")
    
    a = Replace(cel.Offset(0, 1).Value, "Player", "")
    b = Replace(cel.Offset(1, 1).Value, "Player", "")
    
    
    If cel.Offset(1, 0) = "" Then
        
        Set sRng = Nothing
        Set cel = Nothing

        Exit Sub
    
    ElseIf (tArr(0) = 40 Or tArr(1) = 40) Or pcheck(a, b) = False Then
    
        cel.Offset(0, 3).Value = contar
    
        contar = 1
    
    Else

        contar = contar   1

    End If
    
Next cel

Set sRng = Nothing
Set cel = Nothing

End Sub

'The Functions used are below:

Private Function crow(s As Variant, c As Integer) As Long


    crow = Sheets(s).Cells(Rows.Count, c).End(xlUp).Row


End Function



Private Function pcheck(ByVal a As Long, ByVal b As Long) As Boolean


    If a = b Then
    
        pcheck = True
    
    Else
    
        pcheck = False
    
    End If

End Function
  

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

Спасибо!

Комментарии:

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

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

Ответ №2:

Получить последовательность счетчиков

  • Вы запускаете только первую процедуру, runCounterSequence .
  • Вторая процедура, getCounterSequence , вызывается первой, когда это необходимо.
  • В этом примере A2 представляет первую ячейку диапазона данных из двух столбцов и ColumnOffset = 2 означает, что результат того же размера будет записан, начиная с ячейки C2 .

Код

 Option Explicit

Sub runCounterSequence()
    
    ' Constants
    Const wsName As String = "Sheet1"
    Const FirstDataCell As String = "A2"
    Const ColumnOffset As Long = 2
    Dim wb As Workbook
    Set wb = ThisWorkbook ' The workbook containing this code.
    
    ' Define worksheet
    Dim ws As Worksheet
    On Error Resume Next
    Set ws = wb.Worksheets(wsName)
    On Error GoTo 0
    
    ' Validate worksheet.
    If ws Is Nothing Then
        GoTo ProcExit
    End If
    
    ' Transform values from Source Range to Data Array ('Data').
    Dim Data As Variant
    Data = getCounterSequence(ws, FirstDataCell)
    
    ' Validate Data Array.
    If IsEmpty(Data) Then
        GoTo ProcExit
    End If
    
    ' Define Target First Data Row Range ('rng').
    Dim rng As Range
    Set rng = ws.Range(FirstDataCell).Offset(, ColumnOffset).Resize(, 2)
    
    ' Write values from Data Array to Target Range.
    With cel
        ' Clear contents from Target First Data Row Range to the bottom.
        .Resize(ws.Rows.Count - rng.Row   1).ClearContents
        ' Write values from Data Array to Target Range.
        .Resize(UBound(Data, 1)).Value = Data
    End With
    
    ' Inform user.
    MsgBox "Counter amp; Sequence written.", vbInformation, "Success"

ProcExit:

End Sub

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Purpose:      ?
' Remarks:      The result is a 2D two-column one-based array.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Function getCounterSequence(Sheet As Worksheet, _
                            Optional ByVal FirstDataCell As String = "A1") _
         As Variant
    
    ' Define Data Range ('rng')
    
    ' Validate worksheet.
    If Sheet Is Nothing Then
        GoTo ProcExit
    End If
    ' Validate First Data Cell (and define its row).
    On Error GoTo ProcExit
    Dim FirstRow As Long
    FirstRow = Sheet.Range(FirstDataCell).Row
    On Error GoTo 0
    ' Continue...
    Dim FirstCol As Long
    FirstCol = Sheet.Range(FirstDataCell).Column
    Dim LastRow As Long
    LastRow = Sheet.Cells(Sheet.Rows.Count, FirstCol).End(xlUp).Row
    ' Validate Last Non-Empty Row.
    If FirstRow > LastRow Then
        GoTo ProcExit
    End If
    ' Continue...
    Dim rng As Range
    Set rng = Sheet.Range(FirstDataCell).Resize(LastRow - FirstRow   1, 2)
    Dim DataCount As Long
    DataCount = rng.Rows.Count
    
    ' Write values from Data Range to Data Array ('Data').
    
    Dim Data As Variant
    Data = rng.Value
    
    ' Modify values in Data Array.
    
    ' Modify first row.
    Dim Previous As String
    Data(1, 1) = 1
    Previous = Data(1, 2)
    Data(1, 2) = Empty
    If DataCount = 1 Then
        GoTo writeResult
    End If
    ' Modify remaining rows.
    Dim i As Long
    For i = 2 To DataCount
        If Data(i, 1) = "0-0" Then
            Data(i, 1) = 1
            Previous = Data(i, 2)
            Data(i - 1, 2) = Data(i - 1, 1)
        Else ' Data(i, 1) <> "0-0"
            If Data(i, 2) = Previous Then
                Data(i, 1) = Data(i - 1, 1)   1
            Else ' Data(i, 2) <> Previous
                Data(i, 1) = 1
                Previous = Data(i, 2)
                Data(i - 1, 2) = Data(i - 1, 1)
            End If
        End If
        Data(i, 2) = Empty
    Next i
 
    ' Write result.

writeResult:
    getCounterSequence = Data
    
ProcExit:

End Function
  

Ответ №3:

Если вам нужно выполнить много предварительной обработки, это может быть проще, Power Query чем VBA , но вот VBA процедура, которая сложнее, чем необходимо (см. Упрощенную версию Ниже), но она должна дать вам представление о том, как использовать классы, чтобы, возможно, дать вам несколько советов о том, какдля предварительной обработки ваших данных, как вы упомянули в комментарии.

  • Я использовал объект Dictionary для сортировки и помощи в создании необходимых последовательностей.
  • Я также использовал класс (объект) для хранения элементов для каждой последовательности.
  • Я использовал массив vba для хранения и обработки данных, поскольку это НАМНОГО быстрее, чем несколько последовательностей чтения / записи на листе.

Обязательно либо установите ссылку Tools/References Microsoft Scripting Runtime , либо преобразуйте ссылки в позднюю привязку.

  • Добавьте модуль класса и переименуйте его cScores

Модуль класса

 Option Explicit
Private pScore As String
Private pScores As Dictionary
Private pWinner As String

Public Property Get Score() As String
    Score = pScore
End Property
Public Property Let Score(Value As String)
    pScore = Value
End Property

Public Property Get Scores() As Dictionary
    Set Scores = pScores
End Property
Public Function addScoresItem(Value)
    If pScores.Exists(Value) Then
        MsgBox "Duplicate key will not be added"
    Else
        pScores.Add Value, Value
    End If
End Function

Public Property Get Winner() As String
    Winner = pWinner
End Property
Public Property Let Winner(Value As String)
    pWinner = Value
End Property

Private Sub Class_Initialize()
    Set pScores = New Dictionary
End Sub

  
  • Добавьте обычный модуль
 'Set reference to Microsoft Scripting Runtime
Option Explicit
Sub PlayerScoring()
    Dim wsSrc As Worksheet
    Dim vData As Variant
    Dim lKey As Long
    Dim dS As Dictionary, cS As cScores
    Dim I As Long, J As Long, v
    
'Set source and results worksheet and ranges
'read source data into VBA array for faster processing
Set wsSrc = Worksheets("Sheet1")
With wsSrc
    'this is one of many ways to find the relevant range.
    vData = .Cells(1, 1).CurrentRegion.Resize(columnsize:=4)
    vData(1, 3) = "Counter"
    vData(1, 4) = "Sequence"
End With


'split up the relevant data for each sequence
'and store in dictionary so as to easily count
Set dS = New Dictionary
lKey = 0
For I = 2 To UBound(vData)
    Set cS = New cScores
    If vData(I, 1) = "0-0" Or vData(I, 2) <> vData(I - 1, 2) Then lKey = lKey   1
    If Not dS.Exists(lKey) Then
        With cS
            .Score = vData(I, 1)
            .Winner = vData(I, 2)
            .addScoresItem .Score
            dS.Add Key:=lKey, Item:=cS
        End With
    Else
        dS(lKey).addScoresItem vData(I, 1)
    End If
Next I

'Populate results
I = 2
For Each v In dS
    With dS(v)
        'populate counter
        For J = 0 To .Scores.Count
        
            'check for last entry
            If I   J > UBound(vData) Then
                J = J   1
                Exit For
            End If
            
            vData(I   J, 3) = J   1
        Next J
        'populate sequence
        vData(I   J - 2, 4) = .Scores.Count
        I = I   J - 1
    End With
Next v
    
Dim rRes As Range
Set rRes = wsSrc.Cells(1, 12).Resize(rowsize:=UBound(vData, 1), columnsize:=UBound(vData, 2))
With rRes
    .EntireColumn.Clear
    .Value = vData
    .Style = "output"
    .EntireColumn.AutoFit
End With

End Sub

  
  • Измените ссылки на лист и результаты, чтобы отразить вашу таблицу данных
  • Как написано, результаты НЕ будут перезаписывать оригинал.
    • Как только вы убедитесь, что все работает нормально, вы можете изменить ссылку так, чтобы она работала.

введите описание изображения здесь

Редактировать:

Приведенный выше код можно упростить. Я написал это с помощью модуля класса, чтобы дать вам представление о том, что можно сделать, поэтому, возможно, вы можете сделать что-то подобное с тем, что вам нужно предварительно обработать.

Но чтобы получить ТОЛЬКО те результаты, которые вам нужны, вам просто нужно сохранить соответствующие значения для каждой группировки в словаре, а затем вывести результаты, аналогичные описанному выше методу.

например: добавьте только этот обычный модуль

 Option Explicit
Sub PlayerScoring()
    Dim wsSrc As Worksheet, rRes As Range
    Dim vData As Variant, vRes As Variant
    Dim lKey As Long
    Dim dS As Dictionary
    Dim I As Long, J As Long, v
    
'Set source and results worksheet and ranges
'read source data into VBA array for faster processing
Set wsSrc = Worksheets("Sheet1")
With wsSrc
    'this is one of many ways to find the relevant range.
    vData = Range(.Cells(1, 1), .Cells(.Rows.Count, 1).End(xlUp)).Resize(columnsize:=2)
    ReDim vRes(1 To UBound(vData, 1), 1 To UBound(vData, 2))
        vRes(1, 1) = "Counter"
        vRes(1, 2) = "Sequence"
    Set rRes = Range(.Cells(1, 3), .Cells(UBound(vData, 1), 4))
End With


'split up the relevant data for each sequence
'and store in dictionary so as to easily count
Set dS = New Dictionary
lKey = 0
For I = 2 To UBound(vData)
    If vData(I, 1) = "0-0" Or vData(I, 2) <> vData(I - 1, 2) Then lKey = lKey   1
    If Not dS.Exists(lKey) Then
            dS.Add Key:=lKey, Item:=1
    Else
        dS(lKey) = dS(lKey)   1
    End If
Next I

'Populate results
I = 2
For Each v In dS
    With dS(v)
        'populate counter
        For J = 0 To dS(v)
        
            'check for last entry
            If I   J > UBound(vData) Then
                J = J   1
                Exit For
            End If
            
            vRes(I   J, 1) = J   1
        Next J
        'populate sequence
        vRes(I   J - 2, 2) = dS(v)
        I = I   J - 1
    End With
Next v
    

With rRes
    .EntireColumn.Clear
    .Value = vRes
    .EntireColumn.AutoFit
End With

End Sub

  

Комментарии:

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