【问题标题】:how to build parent-child data table in excel?如何在excel中建立父子数据表?
【发布时间】:2016-03-29 06:07:45
【问题描述】:

我有这种方式的数据:

Parent  |  Data
---------------
Root    | AAA  
AAA     | BBB  
AAA     | CCC  
AAA     | DDD  
BBB     | EEE  
BBB     | FFF  
CCC     | GGG  
DDD     | HHH  

这需要转换成下面喜欢的时尚。这基本上需要以excel电子表格结尾。如何将上述数据转换为以下数据:

级别

1   |  2  | 3

AAA | BBB |  
AAA | BBB | EEE  
AAA | BBB | FFF  
AAA | CCC |  
AAA | CCC | GGG  
AAA | DDD |  
AAA | DDD | HHH  

【问题讨论】:

  • 您的源数据是否已经在 Excel 中?
  • 是的,我也喜欢将结果添加到 Excel。
  • 有趣 - 我怀疑这个问题在 sql 中会容易得多。我会着迷于在 Excel 中看到一个优雅的解决方案——看着它,使用各种查找公式似乎会很混乱;虽然我不确定如何使用这些。你看到了吗post
  • 是的,我看到了那个帖子,但是它以不同的方式构建列。这可以与枢轴一起使用,还有许多其他可能性。
  • 它会走多远?我已经完成了一些其他级联树宏,它们可以处理任何深度的输出,但在您的最终布局中没有。 Have a look here 看看这些是否适合您。如果没有,我会看看我能想到什么。

标签: excel vba


【解决方案1】:

我昨晚深夜开始并完成了下面的答案。在寒冷的白天,它至少需要一些扩展。

Sheet2,源数据,宏运行前:

Sheet3,结果,宏运行后:

该方法的基础是创建将每个子项链接到其父项的数组。然后,宏从每个孩子开始沿着其祖先的链向上生长一个字符串:孩子,父母|孩子,祖父母|父母|孩子,...排序后,这是可以保存的结果。

对于示例数据,步骤 1 和 3 可以合并,因为所有名称和行都按字母顺序排列。在一个步骤中构建名称列表并将它们链接到另一个步骤中可以生成一个简单的宏,而不管顺序如何。经过反思,我不确定是否有必要进行第 2 步(对名称进行排序)。第 5 步,对祖先姓名列表进行排序是必要的。输出后无法对 Sheet3 进行排序,因为可能超过三个级别。


我不确定这是否算作一个优雅的解决方案,但它非常简单。

我已将源数据放入工作表 Sheet2 并输出到 Sheet3。

有7个阶段:

  1. 构建包含每个名称的子数组。
  2. 排序数组子项。我提供了一个简单的排序,足以进行演示。如果您有足够的名字来要求它,可以在 Internet 上找到更好的分类。
  3. 构建数组 Parent 使得 Parent(N) 是 Child(N) 的父级的 Child 内的索引。
  4. 按照数组 Parent 中的指针从子级到父级到祖父级构建数组 ParentName... 在执行此操作时,确定最大级别数。
  5. 对数组 ParentName 进行排序。
  6. 在输出表中构建标题行。
  7. 将 ParentName 复制到输出表。

我相信我已经包含了足够多的 cmets 以使代码易于理解。

Option Explicit
Sub CreateParentChildSheet()

  Dim Child() As String
  Dim ChildCrnt As String
  Dim InxChildCrnt As Long
  Dim InxChildMax As Long
  Dim InxParentCrnt As Long
  Dim LevelCrnt As Long
  Dim LevelMax As Long
  Dim Parent() As Long
  Dim ParentName() As String
  Dim ParentNameCrnt As String
  Dim ParentSplit() As String
  Dim RowCrnt As Long
  Dim RowLast As Long

  With Worksheets("Sheet2")
    RowLast = .Cells(Rows.Count, 1).End(xlUp).Row
    ' If row 1 contains column headings, if every child has one parent
    ' and the ultimate ancester is recorded as having a parent of "Root",
    ' there will be one child per row
    ReDim Child(1 To RowLast - 1)

    InxChildMax = 0
    For RowCrnt = 2 To RowLast
      ChildCrnt = .Cells(RowCrnt, 1).Value
      If LCase(ChildCrnt) <> "root" Then
        Call AddKeyToArray(Child, ChildCrnt, InxChildMax)
      End If
      ChildCrnt = .Cells(RowCrnt, 2).Value
      If LCase(ChildCrnt) <> "root" Then
        Call AddKeyToArray(Child, ChildCrnt, InxChildMax)
      End If
    Next

    ' If this is not true, one of the assumptions about the
    ' child-parent table is false
    Debug.Assert InxChildMax = UBound(Child)

    Call SimpleSort(Child)

    ' Child() now contains every child plus the root in
    ' ascending sequence.

    ' Record parent of each child
      ReDim Parent(1 To UBound(Child))
      For RowCrnt = 2 To RowLast
        If LCase(.Cells(RowCrnt, 1).Value) = "root" Then
          ' This child has no parent
          Parent(InxForKey(Child, .Cells(RowCrnt, 2).Value)) = 0
        Else
          ' Record parent for child
          Parent(InxForKey(Child, .Cells(RowCrnt, 2).Value)) = _
                           InxForKey(Child, .Cells(RowCrnt, 1).Value)
        End If
      Next

  End With

  ' Build parent chain for each child and store in ParentName
  ReDim ParentName(1 To UBound(Child))

  LevelMax = 1

  For InxChildCrnt = 1 To UBound(Child)
    ParentNameCrnt = Child(InxChildCrnt)
    InxParentCrnt = Parent(InxChildCrnt)
    LevelCrnt = 1
    Do While InxParentCrnt <> 0
      ParentNameCrnt = Child(InxParentCrnt) & "|" & ParentNameCrnt
      InxParentCrnt = Parent(InxParentCrnt)
      LevelCrnt = LevelCrnt + 1
    Loop
    ParentName(InxChildCrnt) = ParentNameCrnt
    If LevelCrnt > LevelMax Then
      LevelMax = LevelCrnt
    End If
  Next

  Call SimpleSort(ParentName)

  With Worksheets("Sheet3")
    For LevelCrnt = 1 To LevelMax
      .Cells(1, LevelCrnt) = "Level " & LevelCrnt
    Next
    ' Ignore entry 1 in ParentName() which is for the root
    For InxChildCrnt = 2 To UBound(Child)
      ParentSplit = Split(ParentName(InxChildCrnt), "|")
      For InxParentCrnt = 0 To UBound(ParentSplit)
        .Cells(InxChildCrnt, InxParentCrnt + 1).Value = _
                                                ParentSplit(InxParentCrnt)
      Next
    Next

  End With

End Sub

Sub AddKeyToArray(ByRef Tgt() As String, ByVal Key As String, _
                                                  ByRef InxTgtMax As Long)

  ' Add Key to Tgt if it is not already there.

  Dim InxTgtCrnt As Long

  For InxTgtCrnt = LBound(Tgt) To InxTgtMax
    If Tgt(InxTgtCrnt) = Key Then
      ' Key already in array
      Exit Sub
    End If
  Next
  ' If get here, Key has not been found
  InxTgtMax = InxTgtMax + 1
  If InxTgtMax <= UBound(Tgt) Then
    ' There is room for Key
    Tgt(InxTgtMax) = Key
  End If

End Sub

Function InxForKey(ByRef Tgt() As String, ByVal Key As String) As Long

  ' Return index entry for Key within Tgt

  Dim InxTgtCrnt As Long

  For InxTgtCrnt = LBound(Tgt) To UBound(Tgt)
    If Tgt(InxTgtCrnt) = Key Then
      InxForKey = InxTgtCrnt
      Exit Function
    End If
  Next

  Debug.Assert False        ' Error

End Function
Sub SimpleSort(ByRef Tgt() As String)

  ' On return, the entries in Tgt are in ascending order.

  ' This sort is adequate to demonstrate the creation of a parent-child table
  ' but much better sorts are available if you google for "vba sort array".

  Dim InxTgtCrnt As Long
  Dim TempStg As String

  InxTgtCrnt = LBound(Tgt) + 1
  Do While InxTgtCrnt <= UBound(Tgt)
    If Tgt(InxTgtCrnt - 1) > Tgt(InxTgtCrnt) Then
      ' The current entry belongs before the previous entry
      TempStg = Tgt(InxTgtCrnt - 1)
      Tgt(InxTgtCrnt - 1) = Tgt(InxTgtCrnt)
      Tgt(InxTgtCrnt) = TempStg
      ' Check the new previous enty against its previous entry if there is one.
      InxTgtCrnt = InxTgtCrnt - 1
      If InxTgtCrnt = LBound(Tgt) Then
        ' Prevous entry is start of array
        InxTgtCrnt = LBound(Tgt) + 1
      End If
    Else
      ' These entries in correct sequence
      InxTgtCrnt = InxTgtCrnt + 1
    End If
  Loop

End Sub

【讨论】:

  • 嗨。您可以更改此设置以使父项仅出现一次吗?我的意思是在你的例子中不是重复的 AAA,它第一次只显示一次。同样,BBB 只在第 2 行显示一次。那么 final child 只从第 3 行开始?
【解决方案2】:

我有一个使用TreeView object 的更简单的解决方案。如果您不介意节点顺序不同并使用 MSCOMCTL.OCX,请使用以下代码。

需要注册 MSOCOMCTL.OCX。

考虑以下数据:

使用 TreeView(添加到 UserForm 以进行可视化,代码未显示):

转储树数据的代码(普通模块,使用TreeToText):

Option Explicit

Private oTree As TreeView

Private Sub CreateTree()
    On Error Resume Next ' <-- To keep running even error occurred
    Dim oRng As Range, sParent As String, sChild As String

    Set oRng = ThisWorkbook.Worksheets("Sheet1").Range("A2") ' <-- Change here to match your Root cell
    Do Until IsEmpty(oRng)
        sParent = oRng.Value
        sChild = oRng.Offset(0, 1).Value
        If InStr(1, sParent, "root", vbTextCompare) = 1 Then
            oTree.Nodes.Add Key:=sChild, Text:=sChild
        Else
            oTree.Nodes.Add Relative:=oTree.Nodes(sParent).Index, Relationship:=tvwChild, Key:=sChild, Text:=sChild
        End If
        '--[ ERROR HANDLING HERE ]--
        ' Invalid (Repeating) Child will have the Row number appended
        If Err.Number = 0 Then
            Set oRng = oRng.Offset(1, 0) ' Move to Next Row
        Else
            oRng.Offset(0,1).Value = sChild & " (" & oRng.Row & ")"
            Err.Clear
        End If
    Loop
    Set oRng = Nothing
End Sub

Sub TreeToText()
    Dim oRng As Range, oNode As Node, sPath As String, oTmp As Variant

    ' Create Tree from Data
    Set oTree = New TreeView
    CreateTree
    ' Range to dump Tree Data
    Set oRng = ThisWorkbook.Worksheets("Sheet1").Range("D2") ' <-- Change here
    For Each oNode In oTree.Nodes
        sPath = oNode.FullPath
        If InStr(1, sPath, oTree.PathSeparator, vbTextCompare) > 0 Then
            oTmp = Split(sPath, oTree.PathSeparator)
            oRng.Resize(, UBound(oTmp) + 1).Value = oTmp
            Set oRng = oRng.Offset(1, 0)
        End If
    Next
    Set oRng = Nothing
    Set oTree = Nothing
End Sub

代码输出(硬编码到 D2):

如果您有非常大的数据,最好先将 Range 加载到内存中。

【讨论】:

  • 有趣的方法。当一个孩子有一个以上的父母时,如何修改它来处理这种情况?例如,将“EEE”作为父项,将“FFF”作为子项添加到数据中:因为它会引发类似“运行时错误'35602':不是唯一键”的错误
  • @user3598756 我添加了重复 Child 的错误处理 - 假设解决方案是将行号附加到它的值。因此,在您的示例中,以EEE 为父级的FFF 将变为FFF (10)(假设它位于第10 行)。
  • 嗨。您可以修改它以使树在输出中没有重复的项目吗?我的意思是在 D 列中重复 AAA,只在顶部显示一次(就像在你的树截图中一样)。可以为 excel 表中的硬编码输出完成吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-11-22
  • 1970-01-01
  • 2013-02-20
  • 1970-01-01
  • 2016-02-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多