使用 LibreOffice Calc 中的自定义列表创建一个宏来对行进行排序

Make a macro to sort a row using a custom list in LibreOffice Calc

我需要对包含以下格式的单元格的列进行排序:“TITLE 文本”。我知道可能的标题列表,但不知道文本,所以我想做的是以非字母顺序的自定义顺序(例如:PLA、ARG、FHI、BRT)对标题进行排序。问题是标题和文本在同一个单元格中。 因此,例如,这是我可能想要处理的数据屏幕:

如果单元格与列表成员不完全匹配,我该如何排序? 而且,如果可能的话,如何使用宏而不是手动来做到这一点?

不是很难。我将尝试解释这是如何完成的。

首先,我们需要想办法将要排序的单元格范围传递给宏。有不同的方法——直接在宏代码中写入地址,将其作为参数传递给 UDF,从当前 selection 中获取。我们使用第三种方法 - 它不是最容易编码的,但它适用于任何数据集。

使用当前 selection 的主要困难在于 selection 可以是单个单元格(无需排序)、一系列单元格(并且可能是几列 - 如何对此进行排序?)或多个单元格范围(如果您按住 CTRL 键并 select 几个未连接的范围)。

一个好的宏应该能处理所有这些情况。但是现在我们不是在写一个好的宏,我们正在熟悉解决此类问题的原理(由于StackOfflow是程序员的资源,这里的答案是帮助您自己编写代码,而不是得到ready-made 程序免费 )。因此,我们将忽略单个单元格并 多个范围 - 我们将停止执行宏。而且,如果selected范围内不止一列,那我们也不会做任何事情。

此外,如果select编辑了整列,我们将要排序的范围限制在已用区域。这将对真实数据进行排序,但不会对百万个空单元格进行排序。

执行所有这些操作的代码如下所示:

Sub SortByTitles()
Dim oCurrentSelection As Variant
Dim oSortRange As Variant
Dim oSheet As Variant
Dim oCursor As Variant
Dim oDataArray As Variant
Dim sList As String 
    sList = "PLA,ARG,FHI,BRT"
    oCurrentSelection = ThisComponent.getCurrentSelection()
Rem Is it one singl cell?
    If oCurrentSelection.supportsService("com.sun.star.sheet.SheetCell") Then Exit Sub 
Rem Is it several ranges of cells?
    If oCurrentSelection.supportsService("com.sun.star.sheet.SheetCellRanges") Then Exit Sub 
Rem Is this one range of cells? (It can be a graphic item or a control. 
Rem Or it may not even be a Calc spreadsheet at all)
    If Not oCurrentSelection.supportsService("com.sun.star.sheet.SheetCellRange") Then Exit Sub 
Rem Is there only one column selected?
    If oCurrentSelection.getColumns().getCount() <> 1 Then Exit Sub 
Rem Is the current selection outside of the used area?  
    oSheet = oCurrentSelection.getSpreadsheet()
    oCursor = oSheet.createCursor()
    oCursor.gotoEndOfUsedArea(True)
    oSortRange = oCursor.queryIntersection(oCurrentSelection.getRangeAddress())
    If oSortRange.getCount() <> 1 Then Exit Sub 
Rem Redim oSortRange as single range (not any ranges)
    oSortRange = oSortRange.getByIndex(0)
Rem Get data from oSortRange
    oDataArray = oSortRange.getDataArray()
Rem Paste sorted data to the same place:
    oSortRange.setDataArray(getSorted(oDataArray, Split(sList,",")))
End Sub

程序最后一行提到的getSorted()函数必须以两个数组为参数——待排序单元格的值和排序列表 — 和 return 一个排序值数组。

这里应该提到处理来自单元格区域的数据的一个方面。如果在 Excel 中从范围接收数据后我们得到一个 two-dimensional 数组,那么在 OpenOffice/LibreOffice 中我们得到一个 one-dimensional “数组的数组”,其中的每个元素都是一个one-dimensional 一行的单元格值数组。写入范围是从完全相同的结构,从“数组的数组”中完成的。 getSorted()函数的第一个参数是oDataArray——就是这样一个数组的数组,这个在处理数据的时候会需要考虑到。

getSorted() 函数有什么作用?它将根据 oDataArray 值构建一个按 Headers 排序的“树”。事实上,这不是一棵树——它是所有 Headers 以及所有带有这些 Headers 的值的升序排列数组。值也是一个排序数组。然后该函数将从树中 select 列表中列出的那些标题并将它们从树中删除。如果在所有操作之后,一些元素仍然留在排序树中,它们将显示在最后。

该函数会将结果累加到一个与原始数组大小相同的单独数组中。换句话说,该算法将使用比原始排序范围多三倍的内存——源数据、树和结果数组。该函数会将结果累积到一个与原始数组大小相同的单独数组中。换句话说,该算法将使用比原始排序范围多三倍的内存——源数据、树和结果数组。

您可以尝试节省资源,将结果直接写入原始数组。但我强烈建议不要这样做。

事实上,一个数组单元格可能包含的不是一个值,而是一个值的引用,在编码不准确的情况下,你不会得到一个大的排序数组,而是一个具有相同值的大数组(最后一个单元格)。

我故意不对以下所有代码发表评论 - 如果您无需评论就可以阅读并理解这些代码,那么您将了解如何对操作进行编程以处理来自范围的数据:

Function getSorted(aData As Variant, aList As Variant) As Variant
Dim aRes As Variant 
Dim i As Long, pos As Long, j As Long, k As Long, m As Long, uB As Long 
Dim aTemp As Variant
    aTemp = Array()
    ReDim aRes(LBound(aData) To UBound(aData))
    For i = LBound(aData) To UBound(aData)
        pos = InStr(aData(i)(0), " ")
        If pos > 0 Then 
            AddToArray(Left(aData(i)(0),pos-1), aData(i)(0), aTemp)
        Else 
            AddToArray(aData(i)(0), aData(i)(0), aTemp)
        EndIf 
    Next i
    m = LBound(aData) - 1
    For i = LBound(aList) To UBound(aList)
        k = getIndex(aList(i), aTemp)
        If k > -1 Then 
            uB = UBound(aTemp) - 1
            For j = LBound(aTemp(k)(1)) To UBound(aTemp(k)(1))
                m = m + 1
                aRes(m) = Array(aTemp(k)(1)(j))
            Next j
            For j = k To uB
                aTemp(j) = aTemp(j+1)
            Next j
            ReDim Preserve aTemp(uB)
        EndIf 
    Next i
    For k = LBound(aTemp) To UBound(aTemp)
        For j = LBound(aTemp(k)(1)) To UBound(aTemp(k)(1))
            m = m + 1
            aRes(m) = Array(aTemp(k)(1)(j))
        Next j
    Next k
    getSorted = aRes
End Function

为了构建排序树,使用了两个子例程 - AddToArray()InsertToArray()。它们非常相似——前八行是正常的二分查找,剩下的 10-12 行是在数组末尾找不到元素时、找到时和中间找不到元素时的动作数组的:

Sub AddToArray(key As Variant, value As Variant, aData As Variant)
Dim l&, r&, m&, N&, i&
    l=LBound(aData)
    r=UBound(aData)+1
    N=r
    While (l<r)
        m=l+Int((r-l)/2)
        If aData(m)(0)<key Then 
            l=m+1
        Else
            r=m
        EndIf
    Wend
    If r=N Then
        ReDim Preserve aData(0 To N)
        aData(N) = Array(key, Array(value))
    ElseIf  aData(r)(0)=key Then
        InsertToArray(value, aData(r)(1))
    Else
        ReDim Preserve aData(0 To N)
        For i = N-1 To r Step -1
            aData(i+1)=aData(i)
        Next i
        aData(r) = Array(key, Array(value))
    EndIf
End Sub

Sub InsertToArray(key As Variant, aData As Variant)
Dim l&, r&, m&, N&, i&
    l=LBound(aData)
    r=UBound(aData)+1
    N=r
    While (l<r)
        m=l+Int((r-l)/2)
        If aData(m)<key Then 
            l=m+1
        Else
            r=m
        EndIf
    Wend
    If r=N Then
        ReDim Preserve aData(0 To N)
        aData(N) = key
    Else
        ReDim Preserve aData(0 To N)
        For i = N-1 To r Step -1
            aData(i+1)=aData(i)
        Next i
        aData(r) = key
    EndIf
End Sub

getIndex() 函数 使用相同的二进制搜索。如果可以找到它,它将 return 数组中元素的索引,否则 -1:

Function getIndex(key As Variant, aData As Variant) As Long
Dim l&, r&, m&, N&
    l=LBound(aData)
    r=UBound(aData)+1
    N=r
    While (l<r)
        m=l+Int((r-l)/2)
        If aData(m)(0)<key Then 
            l=m+1
        Else
            r=m
        EndIf
    Wend
    If r=N Then
        getIndex = -1
    ElseIf  aData(r)(0)=key Then
        getIndex = r
    Else
        getIndex = -1
    EndIf
End Function

这就是解决任务所需的全部内容:

带代码的演示文件 - SortByTitle.ods