Управление коллекциями и массивами в Excel VBA для учета отсутствующих значений и обработки ошибок - PullRequest
0 голосов
/ 28 сентября 2018

Я не знал, как объяснить вопрос, поэтому я приложу изображения для объяснения моей ситуации.Вот вид моего листа Excel:

Мой лист Excel

image

Подсвеченные ячейки содержат несколько значений, называемых ID , и связаны с нимис соответствующими версиями в столбцах рядом с ними.Я использую следующий макрос (подробности с подробным объяснением здесь) , чтобы разбить эти значения на несколько строк на одном листе.

Option Explicit

Private Const ID_IDX As Long = 0
Private Const VER_IDX As Long = 1
Private Const RNG_IDX As Long = 2

Private Sub RunMe()
    Dim data As Variant, cols As Variant, items As Variant
    Dim r As Long, c As Long, i As Long, n As Long
    Dim ids() As String, vers() As String
    Dim addItems As Collection, concatItems As Collection
    Dim dataRng As Range, rng As Range
    Dim writeID() As Variant, writeVer() As Variant, writeConcat() As Variant
    Dim dataStartRow As Long

    On Error Resume Next

    'Define the range we're interested in and read into an array.
    With Sheet1 'adjust for your worksheet object
        Set dataRng = Application.InputBox(prompt:="Select the Range of cells:", Type:=8)
       End With
    data = dataRng.Value2
    dataStartRow = 2

    'Find the two target columns
    cols = AcquireIdAndVerCol(data, 3, 8)
    If IsEmpty(cols) Then
        MsgBox "Unable to find Id and Ver columns."
        Exit Sub
    End If

    With dataRng
        'Add a column next to the version number column.
        .Columns(cols(VER_IDX)).Offset(, 1).Insert Shift:=xlShiftToRight, CopyOrigin:=xlFormatFromLeftOrAbove

        'Add a column to our range.
        'This is to cover the case that the rightmost column is the version number column.
        Set dataRng = .Resize(, .Columns.Count + 1)
    End With

    'Find the rows that need to be split and concatenate the target strings.
    Set addItems = New Collection
    Set concatItems = New Collection
    For r = dataStartRow To UBound(data, 1)

        ids = Split(data(r, cols(ID_IDX)), vbLf)
        vers = Split(data(r, cols(VER_IDX)), vbLf)
        n = IIf(UBound(ids) >= UBound(vers), UBound(ids), UBound(vers))

        If n = 0 Then 'it's just one line of text.

            'Add concatenated text to list.
            concatItems.Add data(r, cols(ID_IDX)) & " " & data(r, cols(VER_IDX))

        ElseIf n > 0 Then 'it's multiple lines of text.

            'Transpose the id array.
            ReDim writeID(1 To UBound(ids) + 1, 1 To 1)
            For i = 0 To UBound(ids)
                writeID(i + 1, 1) = ids(i)
            Next
            'Transpose the version array.
            ReDim writeVer(1 To UBound(vers) + 1, 1 To 1)
            For i = 0 To UBound(ids)
                writeVer(i + 1, 1) = vers(i)
            Next

            'Add concatenated text to list.
            For i = 0 To n
                concatItems.Add (IIf(UBound(ids) <= n And UBound(vers) <= n, ids(i) & " " & vers(i), Empty))
            Next

            'Add the range to be split to the collection.
            addItems.Add Array(writeID, writeVer, dataRng.Rows(r + 1).Resize(n))

        Else 'it's an empty cell

            'Add empty item to concatenated list in order to keep alignment.
            concatItems.Add Empty

        End If

    Next

    Application.ScreenUpdating = False

    'Split the ranges in the list.
    If addItems.Count > 0 Then
        For Each items In addItems
            'Add the rows.
            With items(RNG_IDX)
                .Insert Shift:=xlDown, CopyOrigin:=xlFormatFromLeftOrAbove
                Set rng = .Offset(-.Rows.Count - 1).Resize(.Rows.Count + 1)
                'Note: format your rng Range obect as desired here.
            End With
            'Write the id and version values.
            rng.Columns(cols(ID_IDX)).Value = items(ID_IDX)
            rng.Columns(cols(VER_IDX)).Value = items(VER_IDX)
        Next
    End If

    'Write the concatenated values.
    If concatItems.Count > 0 Then
        ReDim writeConcat(1 To concatItems.Count + dataStartRow - 1, 1 To 1)
        'Header to array.
        writeConcat(1, 1) = "Concat values"
        'Values from the collection to array.
        i = dataStartRow
        For Each items In concatItems
            writeConcat(i, 1) = items
            i = i + 1
        Next
        'Output array to range.
        With dataRng.Columns(cols(VER_IDX) + 1)
            .Value = writeConcat
            .AutoFit
        End With
    End If

    Application.ScreenUpdating = True
End Sub

Private Function AcquireIdAndVerCol(data As Variant, minCol As Long, maxCol As Long) As Variant
    Dim result(1) As Long
    Dim r As Long, c As Long, i As Long
    Dim items() As String

    'Check we're not operating outside bounds of data array.
    If minCol < LBound(data, 2) Then minCol = LBound(data, 2)
    If minCol > UBound(data, 2) Then minCol = UBound(data, 2)
    If maxCol < LBound(data, 2) Then maxCol = LBound(data, 2)
    If maxCol > UBound(data, 2) Then maxCol = UBound(data, 2)

    'Loop through data to find the two columns.
    'Once found, leave the function.
    For r = 1 To UBound(data, 1)
        For c = minCol To maxCol
            items = Split(data(r, c), vbLf)
            For i = 0 To UBound(items)
                If result(ID_IDX) = 0 Then
                    If IsDocId(items(i)) Then
                        result(ID_IDX) = c
                        If result(VER_IDX) = 0 Then
                            Exit For
                        Else
                            AcquireIdAndVerCol = result
                            Exit Function
                        End If
                    End If
                End If
                If result(VER_IDX) = 0 Then
                    If IsDocVer(items(i)) Then
                        result(VER_IDX) = c
                        If result(ID_IDX) = 0 Then
                            Exit For
                        Else
                            AcquireIdAndVerCol = result
                            Exit Function
                        End If
                    End If
                End If
            Next
        Next
    Next

End Function
Private Function IsDocId(val As String) As Boolean
    Dim n As Long

    n = TryClng(val)
    IsDocId = (n > 9999 And n <= 999999999)
End Function

Private Function IsDocVer(val As String) As Boolean
    Dim n As Long, m As Long
    Dim items() As String

    items = Split(val, ".")
    If UBound(items) <> 1 Then Exit Function

    n = TryClng(items(0))
    m = TryClng(items(1))

    IsDocVer = (n > 0 And n <= 99) And (m >= 0 And m <= 9)
End Function

'-------------------------------------------------------------------
'Converts a variant to a Long or returns a fail value as a Long
'if the conversion failed.
'-------------------------------------------------------------------
Private Function TryClng(expr As Variant, Optional fail As Long = -1) As Long
    Dim n As Long

    n = fail
    On Error Resume Next
    n = CLng(expr)
    On Error GoTo 0

    TryClng = n
End Function

Это дает следующий вывод с добавленным столбцом с именем, Concat Values ​​, который содержит объединенные значения Id's и соответствующие Версии :

Вывод

image

Проблема:

Работает без сбоев, если все идентификаторы имеют соответствующие версии, указанные влист отдельно, как я уже говорил выше.Однако в тех случаях, когда существует только один номер версии и он связан с 4 или более идентификаторами, т. Е. Один и тот же номер версии применим ко всем идентификаторам, например:

Common Version for separate ID's

Вывод в столбце Значения Concat становится дезориентированным, потому что мы используем массив для вывода значений Concat, а массив не вмещает отсутствующие версии для соответствующих идентификаторов.Выглядит это так:

Смещенные значения строк

image

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

РЕДАКТИРОВАТЬ: Я постараюсь перечислить все возможные случаи и ожидаемый результат, включая сценарии наихудшего случая:

Вот ссылка на мой лист Excel.

Обычные сценарии

  1. Количество идентификаторов = Количество версий (Отлично работает, значения Concat выровнены в соответствующих строках в столбцах) *Number of Id's  = Number of Versions:*

  2. Несколько идентификаторов - одна версия (В таких случаяхВерсия №, применимая ко всем идентификаторам, одинаковая , т. е. одна Версия должна применяться ко всем идентификаторам.)

Проблема: Макрос выполняет задачу разделения столбцов на строки, за исключением части, в которой значения Concat смещены. *Multiple Id's - Single Version:*

Сценарии наихудшего случая

Несколько идентификаторов - несколько версий, но меньше, чем общее количество идентификаторов # 1099 * (В таких случаях версии должны соответствовать самым верхним идентификаторам и заполнять идентификаторы ниже пробелами ) Проблема: Макрос выполняет задачу разбиения столбцов на строки, за исключением части, в которой значения Concat смещены. *Multiple Id's - Multiple Versions - Type 1:* Здесь 4 идентификаторам присвоены только 3 версии, поэтому 3 верхним идентификаторам назначены 3 версии, а для 4-го идентификатора нет версии, связанной с ним. Аналогично, *Multiple Id's - Multiple Versions - Type 2:* Здесь 4 идентификаторам присвоены только 2 версии, поэтому 2 верхним идентификаторам назначены 2 версии, а для 3-го и 4-го идентификаторов нет версии, связанной с ними. Несколько идентификаторов - без версии (В таких случаях столбцы должны разбиваться на строки на основе # ID, а соответствующие строки версии должны заполняться пробелами ) Проблема: Макрос выполняет задачу разделения столбцов на строки, за исключением части, в которой Конкатные значения смещены. Multiple Id's - No Version

Ответы [ 2 ]

0 голосов
/ 10 октября 2018

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

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

Сначала добавьте дополнительное объявление в RunMe Sub:

Dim curVer As String

, а затем вам просто нужно настроить ElseIf n > 0 дело.Замените код следующим:

    ElseIf n > 0 Then 'it's multiple lines of text.

        'Resize the output arrays to max ('n')
        ReDim writeID(1 To n + 1, 1 To 1)
        ReDim writeVer(1 To n + 1, 1 To 1)

        'Loop through the arrays to align id and versions.
        For i = 0 To n
            If i <= UBound(ids) Then
                writeID(i + 1, 1) = ids(i)
            End If
            If i <= UBound(vers) Then
                curVer = vers(i)
            End If
            writeVer(i + 1, 1) = curVer
        Next

        'Add concatenated text to list.
        For i = 0 To n
            concatItems.Add writeID(i + 1, 1) & " " & writeVer(i + 1, 1)
        Next

        'Add the range to be split to the collection.
        addItems.Add Array(writeID, writeVer, dataRng.Rows(r + 1).Resize(n))
0 голосов
/ 30 сентября 2018

Слишком много кода, чтобы я мог его прочитать, но я нашел решение, если правильно понял вашу проблему.

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

Sub Test()
    Dim xRange As Range
    Dim xArrRange() As Variant
    Dim xNewArrRange() As Variant
    Dim xNewArrRangeResize() As Variant
    Dim xNumberColumns As Long
    Dim i As Long
    Dim j As Long
    Dim k As Long
    Dim l As Long
    Dim ii As Long
    Dim jj As Long

    Set xRange = Range("A2:C5")
    xNumberColumns = 3

    xArrRange = xRange.Value2


    ReDim xNewArrRange(xRange.Rows.Count + 10, xNumberColumns)          ' "xNumberColumns - 1" to have the number of columns
                                                                        ' "xNumberColumns" is one more

    For i = LBound(xArrRange, 1) To UBound(xArrRange, 1)
        Dim xTempArrVer As Variant
        Dim xTempArrID As Variant
        xTempArrVer = Split(xArrRange(i, 3), vbLf)

        If UBound(xTempArrVer) = -1 Then                                ' If there are no version, initialize it with ""
            ReDim xTempArrVer(0)
            xTempArrVer(0) = ""
        End If

        xTempArrID = Split(xArrRange(i, 2), vbLf)

        For j = LBound(xTempArrID, 1) To UBound(xTempArrID, 1)
            If j > UBound(xTempArrVer, 1) Then
                l = UBound(xTempArrVer, 1)
            Else
                l = j
            End If

            xNewArrRange(k, 0) = xArrRange(i, 1)
            xNewArrRange(k, 1) = xTempArrID(j)
            xNewArrRange(k, 2) = xTempArrVer(l)

            If xTempArrVer(l) <> "" Then
                xNewArrRange(k, 3) = xTempArrID(j) & " " & xTempArrVer(l)
            Else
                xNewArrRange(k, 3) = xTempArrID(j)
            End If

            k = k + 1

            If k + 1 > UBound(xNewArrRange, 1) Then
                ReDim Preserve xNewArrRange(UBound(xNewArrRange, 1) + 30, xNumberColumns)
            End If

        Next j
    Next i

    ReDim xNewArrRangeResize(k - 1, xNumberColumns)         ' "xNumberColumns - 1" to have the number of columns
                                                            ' "xNumberColumns" is one more

    For ii = LBound(xNewArrRangeResize, 1) To UBound(xNewArrRangeResize, 1)
        For jj = LBound(xNewArrRangeResize, 2) To UBound(xNewArrRangeResize, 2)
            xNewArrRangeResize(ii, jj) = xNewArrRange(ii, jj)
        Next jj
    Next ii

    Range(Cells(2, 6), Cells(UBound(xNewArrRangeResize, 1) + 1, 6 + UBound(xNewArrRangeResize, 2))).Value2 = xNewArrRangeResize

    Debug.Print "Finish"


End Sub

Этот код производит это:

enter image description here

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

Редактировать: я вижу, что чего-то не хватаетно это потому, что я неправильно рассчитал этот диапазон.

Range(Cells(2, 6), Cells(UBound(xNewArrRangeResize, 1) + 1, 6 + UBound(xNewArrRangeResize, 2))).Value2 = xNewArrRangeResize
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...