【问题标题】:recursive tree-like parsing with VBA使用 VBA 进行递归树状解析
【发布时间】:2018-08-24 21:21:43
【问题描述】:

我有以下输入和输出数据(Sheet1 从 1-19,Sheet2 从 21+,然后是输出) https://ethercalc.org/bzrwyz8bsail(请注意,孩子们向右对齐,而不是脚本格式的 2 个空格)

我有以下 VBA 脚本,它解析父母和项目并写入工作表 2:

Sub newlist()
    Dim w1 As Worksheet
    Dim w2 As Worksheet
    Dim Ide As String
    Dim k As Long
    Dim kk As Long
    Dim n As Long
    Dim entity As String

    Set w1 = Sheets("Sheet1")
    Set w2 = Sheets("Sheet2")
    w2.Cells(1, 1).Value = w1.Cells(1, 8).Value
    w2.Cells(1, 2).Value = w1.Cells(1, 10).Value
    c = 0
    Ide = Cells(1, 1).Value
    w1.Activate
    n = Cells(Rows.Count, 1).End(xlUp).row
    k = 3
    kk = 1
    For i = 2 To n
        If w1.Cells(i, 8).Value = Ide Then
            entity= w1.Cells(i, 10).Value
            entityString = "  " & entity
            w2.Cells(kk + 1, 1).Value = entityString
            kk = kk + 1
            k = k + 1
        Else
            kk = kk + 1
            k = 3
            Ide = w1.Cells(i, 8).Value
            entity= w1.Cells(i, 10).Value
            w2.Cells(kk, 1).Value = Ide
            kk = kk + 1
            entityString = "  " & entity
            w2.Cells(kk, 1).Value = entityString
        End If
        Next
    End Sub

从输出中可以看出,自己是孩子的父母并没有写在他们的父母之下。例如,第一个爸爸的孩子应该写在根的孩子爸爸下面,名字不能重复两次。另一个例子是 Echo 的孩子应该如何在 Echo 之下,而不是被重复。

我将如何使用递归来处理这个问题?好像迭代没有效果。

【问题讨论】:

  • 是的,在 VBA 中可以递归。
  • @QHarr 我不确定如何实现它来解决我的问题,我什至不知道是否可以使用递归来做我正在做的事情
  • 另外,你应该养成声明变量的习惯。您在这段代码中没有声明它们,这使得代码很难遵循,而且效率低下,并且经常由于隐式转换导致类型不匹配错误。
  • @DavidZemens 我没有意识到 VBA 可以有这些技术细节,谢谢你的建议,我会更新帖子。

标签: vba excel


【解决方案1】:

我在递归方面真的很弱,但这里试一试。下面的输出来自Debug.Print 语句:

Root
  Lima
    Delta
    Echo
      Foxtrot
      Golf
      Hotel
      India
      Juliett
      Kilo
  Mike
  November
  Oscar
    Papa
      Alpha
      Bravo
      Charlie
    Quebec

Sheet 2 上的输出带有缩进级别:

ProcessItem 方法在For Each v In dict(name) 循环内调用自身时会发生递归:

Option Explicit
Sub newlist()
    Dim w1 As Worksheet, w2 As Worksheet
    Dim num_rows
    Dim parent As Range, parentName As String
    Dim parentRange As Range, childrenRange As Range
    Dim childCount As Long
    Dim p As Variant

    Dim dict As Object
    Set dict = CreateObject("Scripting.Dictionary")

    Set w1 = Sheets("Sheet6")
    num_rows = w1.Cells(Rows.Count, 1).End(xlUp).row
    'If there's no parentName column, we can't continue.
    If w1.Rows(1).Find("parentName") Is Nothing Then Exit Sub
    Set parentRange = w1.Rows(1).Find("parentName").Offset(1).Resize(num_rows - 1, 1)
    'If there's no Root level, how do we know where to start?
    If parentRange.Find("Root") Is Nothing Then Exit Sub

    For Each parent In parentRange
        If Not dict.Exists(parent.Value) Then
            childCount = Application.WorksheetFunction.CountIf(parentRange, parent.Value)
            Set childrenRange = parent.Offset(, 2).Resize(childCount, 1)
            dict.Add parent.Value, Application.Transpose(Application.Transpose(childrenRange.Value))
        End If
    Next
    Set w2 = Sheets.Add
    ' Recursive method to traverse our dictionary, beginning at Root element.
    Call ProcessItem("Root", dict, w2, 2)

    w2.Cells(1, 1).Value = w1.Cells(1, 8).Value
    w2.Cells(1, 2).Value = w1.Cells(1, 10).Value

End Sub
Private Sub ProcessItem(name As String, dict As Object, ws As Worksheet, row_num As Long, Optional indent As Long = 0)
Dim output As String, v
' add spaces to indent the output string:
output = WorksheetFunction.Rept(" ", indent) & name
Debug.Print output
' write output to the new worksheet:
ws.Cells(row_num, 1).Value = output
row_num = row_num + 1
If Not dict.Exists(name) Then
    'we're at a terminal element, a child with no children.
    Exit Sub
Else
    For Each v In dict(name)
        ' ## RECURSION ##
        Call ProcessItem(CStr(v), dict, ws, row_num, indent + 2)
    Next
End If

End Sub

跟进:

如果您还想跟踪父名称(例如“parent.child”),那么我认为您可以这样做(未经测试):

像这样进行初始调用 - 您实际上不需要在函数调用中命名参数,但我这样标记它只是为了说明:

Call ProcessItem(parentName:="", "Root", dict, w2, 2)

那么函数需要稍微修改一下:

Private Sub ProcessItem(parentName as String, name As String, dict As Object, ws As Worksheet, row_num As Long, Optional indent As Long = 0)
Dim output As String, v
output = IIF(parentName = "", name, parentName & "." & name)
output = WorksheetFunction.Rept(" ", indent) & output
Debug.Print output
' write output to the new worksheet:
ws.Cells(row_num, 1).Value = output
row_num = row_num + 1
If Not dict.Exists(name) Then
    'we're at a terminal element, a child with no children.
    Exit Sub
Else
    For Each v In dict(name)
        ' ## RECURSION ##
        Call ProcessItem(name, CStr(v), dict, ws, row_num, indent + 2)
    Next
End If

End Sub

【讨论】:

  • 这是一个有趣的解决方案,我认为使用递归辅助函数是不可能的。另外,我不知道您可以按名称而不是索引来引用列。出于好奇,Find 命令将如何处理具有 2 个相同列名的工作表,或者您将如何从代码中处理它?
  • 我的错误,我忘记了我将数据修改为更简单 - “-220”是我的根的实际常量 ID,而 entityID 是 itemID 的同义词。撇开命名不谈,似乎即使在保持 2 列偏移量的情况下,用另一个常量字符串(“-220”)替换根,用“parentID”替换“parentName”也不会在第一个 -220 之后写入任何内容。还有什么需要改变的吗?
  • @DavidZemens,一个很好且有启发性的解决方案 +。 放大备注:如果Set childrenRange 语句只计算一个单元格,则无法列出该子项。此后使用临时数组 (Dim temp) 的解决方法以避免出现问题:If childCount = 1 Then 后跟 ReDim temp(1 To 1, 1 To 1): temp(1, 1) = childrenRange.Valuedict.Add parent.Value, temp 在最终 Else 之前的特殊添加通过您已经显示的双转置将子项添加到字典.
  • @ÉvaristeGalois 我为此添加了一些示例代码,但尚未对其进行测试。如果您在实施时遇到其他问题,最好提出一个新问题:)
  • 也可能相关,请参考@T.M.上面的评论,我在答案中提供的代码中没有实现。
猜你喜欢
  • 1970-01-01
  • 2015-05-21
  • 1970-01-01
  • 2019-02-13
  • 1970-01-01
  • 1970-01-01
  • 2020-12-22
  • 2017-07-01
  • 1970-01-01
相关资源
最近更新 更多