プルダウンリストを作成する ④ 表示する順序を指定 の続き

先日は、プルダウンリストを作成する際に、リストの表示順序を明示する方法を考えてみた。
infoment.hatenablog.com

この中で、二次元配列のソートを突貫工事で作成したところ、なかなかの欠陥物件になってしまった。
※数字を文字に置き換える過程で、正しくソートされなくなる。等々。

そこで今日は、前回、前々回の内容と合わせて、この問題解決に挑戦する。
f:id:Infoment:20190624203012p:plain

元となる表がコチラ。
f:id:Infoment:20190624211906p:plain

以下は全て配列を用いて行っているが、視覚化のためシート上で表現している。
まず空欄に、記入済み通し番号の続きを上から順に付す。
f:id:Infoment:20190624212120p:plain

この表示列の最大値を求め、さらにその桁数を求める。
このとき表示列が全て文字列ならば、最大値は0となる。
f:id:Infoment:20190624212414p:plain

求めた桁数分、10を掛け算する。
f:id:Infoment:20190624213046p:plain

表の各行を、「,」でつなげる。
f:id:Infoment:20190624213256p:plain

ソートする列の各値に、先程求めた「100」を加えたものを準備する。この時、足される側が数字でない場合は、その文字のままとする。
例)
1 ⇒ 101
a ⇒ aのまま
f:id:Infoment:20190624213525p:plain

求めた値を、先程の先頭に追加する。
f:id:Infoment:20190624214140p:plain

これを、昇順でソートする。
先頭の数値は桁が揃えてあるため、1 ⇒ 10 ⇒ 2 のような逆転は起きない。
(101 ⇒ 102 ⇒ ・・・ ⇒ 110 の順になる)。
f:id:Infoment:20190624214241p:plain

先頭の要素を取り除いたうえで、二次元配列に戻す。
f:id:Infoment:20190624214338p:plain

一列目を抜き出し、JOIN関数でカンマ区切りの文字列を作成する。
f:id:Infoment:20190624214534p:plain

この文字列を、入力規則のリストとして設定する。
f:id:Infoment:20190624215013p:plain

出来た。
f:id:Infoment:20190624215315p:plain

前置きが長くなりましたが、そんな僕らのコードをご覧ください(「ナイツの漫才」風)。

クラスモジュール(SeaquenceClass)

' 一次元配列ソート

Public Function SortArray(ByVal arr As Variant, _
                       Optional sort_order As Excel.XlSortOrder = xlAscending) As Variant
    Dim aryList As Object
    Dim s As Variant
    Set aryList = CreateObject("System.Collections.ArrayList")

    For Each s In arr
        Call aryList.Add(s)
    Next

    Select Case sort_order
        Case xlAscending
            ' 昇順でソート。
            Call aryList.Sort
        Case xlDescending
            ' 昇順でソートののち、降順へ反転。
            Call aryList.Sort
            Call aryList.Reverse
    End Select

    SortArray = aryList.ToArray
End Function

' 二次元配列ソート(今回改修した箇所)

Public Function SortArray2D(ByVal arr As Variant, _
                         Optional sort_column As Long = 1, _
                         Optional sort_order As Excel.XlSortOrder = xlAscending) As Variant

    ' 各行要素を一次元配列として抽出し、ソートしたい列の値を付して「,」で結合。
    ' これらで構成された一次元を作成する。
    ' ※列のデータを、一番左のセルにギュッと寄せて、ソートしたい列の値を先頭に付すイメージ。
    Dim TempArray() As Variant
    ReDim TempArray(LBound(arr) To UBound(arr))
    Dim RowArray As Variant
    Dim SortKey As String
    Dim i As Long

    ' ソート列を抜き出して新たな配列を作成し、配列内の最大値を求める。
    Dim cMax As Long
        cMax = WorksheetFunction.Max(ExtractArray2(arr, sort_column))
    
    ' 最大値の桁数dを求め、10のd乗の数を求める。
    ' これをソートする列に加えることで、文字列であっても数字と
    ' 同じソート結果を得られる。
    Dim d As Long
        d = DigitNumber(cMax)
    Dim AdjustNumber As Long
        AdjustNumber = 10 ^ d
    Dim AdditionalCharacter As Variant
    
        For i = LBound(arr) To UBound(arr)
            RowArray = ExtractArray2(arr, i, i)
            AdditionalCharacter = arr(i, sort_column)
            If IsNumeric(AdditionalCharacter) Then
                AdditionalCharacter = AdjustNumber + AdditionalCharacter
            End If
            TempArray(i) = AdditionalCharacter & "," & Join(RowArray, ",")
        Next

    ' 一次元配列としてソート。
        TempArray = SortArray(TempArray, sort_order)

    ' ソートした結果を再び分割し、新たな配列に振りなおす。
    Dim DestinationArray() As Variant
    ReDim DestinationArray(LBound(arr) To UBound(arr), LBound(arr, 2) To UBound(arr, 2))
    Dim j As Long
        For i = LBound(TempArray) To UBound(TempArray)
            RowArray = Split(TempArray(i), ",")

            ' 0番目の要素はソート用であるため不要 ⇒ 1番目からループ。
            For j = 1 To UBound(RowArray)
                DestinationArray(LBound(arr) + i, LBound(arr) + j - 1) = RowArray(j)
            Next
        Next

        SortArray2D = DestinationArray

End Function

' 二次元配列内の任意の範囲を、新たな配列として返す関数

' sorce_array   抽出元の配列
' r_min         一次元要素の下限
' r_max         一次元要素の上限
' c_min         二次元要素の下限
' c_max         二次元要素の上限
' to_1d_flag    一次元または二次元の上限と下限が一致する場合、
'               抽出後の配列を一次元配列に変換するか否かのフラグ。
Function ExtractArray2(ByVal sorce_array As Variant, _
             Optional r_min As Long = -1, _
             Optional r_max As Long = -1, _
             Optional c_min As Long = -1, _
             Optional c_max As Long = -1, _
             Optional to_1d_flag As Boolean = True) As Variant

        ' 二次元配列であることの確認
        If GetArrayDimension(sorce_array) <> 2 Then
            ExtractArray2 = Array(): Exit Function
        End If

        ' 下限確認。
        ' 指定下限が無指定の場合、抽出元配列の下限を指定下限とする。
        ' ※指定下限が指定上限を上回る場合については、指定上限側で対処する。
        ' 指定下限が負の値の場合、指定下限を0とする。
        ' 指定下限が抽出元配列の下限を下回る場合、これを認める。
        If r_min = -1 Then
            r_min = LBound(sorce_array, 1)
        ElseIf r_min < 0 Then
            r_min = 0
        End If

        If c_min = -1 Then
            c_min = LBound(sorce_array, 2)
        ElseIf c_min < 0 Then
            c_min = 0
        End If

        ' 上限確認。
        ' 指定上限が無指定の場合、抽出元配列の上限を指定上限とする。
        ' 指定上限が指定下限を下回る場合、指定下限を指定上限とする。
        ' 指定上限が抽出元配列の上限を上回る場合、これを認める。
        If r_max = -1 Then
            r_max = UBound(sorce_array, 1)
        ElseIf r_max < r_min Then
            r_max = r_min
        End If

        If c_max = -1 Then
            c_max = UBound(sorce_array, 2)
        ElseIf c_max < c_min Then
            c_max = c_min
        End If

    ' 抽出用配列。
    Dim TempArray() As Variant
    ReDim TempArray(1 To r_max - r_min + 1, 1 To c_max - c_min + 1)

    Dim r As Long
    Dim C As Long

        ' 値抽出。
        For r = r_min To r_max
            For C = c_min To c_max
                On Error Resume Next
                TempArray(r - r_min + 1, C - c_min + 1) = sorce_array(r, C)
            Next
        Next

        ' 1行または1列の配列の場合、二次元配列を一次元配列に変換。
        ' ※to_1d_flag = True の場合のみ
        If to_1d_flag Then
            If UBound(TempArray, 2) = 1 Then
                TempArray = WorksheetFunction.Transpose(TempArray)
            ElseIf UBound(TempArray, 1) = 1 Then
                TempArray = WorksheetFunction.Transpose(TempArray)
                TempArray = WorksheetFunction.Transpose(TempArray)
            End If
        End If

        ExtractArray2 = TempArray
End Function

' 配列の次元数を求める関数

Private Function GetArrayDimension(arr As Variant) As Long

    If IsArray(arr) = False Then
        GetArrayDimension = -1
        Exit Function
    End If

    ' 配列の次元数を取得。

    Dim i As Long
    Dim TempNumber As Long

        On Error Resume Next
        Do While Err.Number = 0
            i = i + 1
            TempNumber = UBound(arr, i)
        Loop

        GetArrayDimension = i - 1

End Function

' 桁数を求める関数 ※有効数字のそれと異なることに注意

Function DigitNumber(num As Variant) As Long
    Dim i As Long: i = 10
    Dim j As Long: j = 1
        Do
            If num / i < 1 Then
                DigitNumber = j
                Exit Function
            Else
                i = i * 10
                j = j + 1
            End If
        Loop
End Function
標準モジュール

' リスト設定用

Sub SetList(target_range As Range, arr As Variant)
    target_range.Validation.Delete
    target_range.Validation.Add Type:=xlValidateList, _
                                AlertStyle:=xlValidAlertStop, _
                                Operator:=xlBetween, _
                                Formula1:=Join(arr, ",")
End Sub

' リスト用配列作成

Function ListArray(target_table As ListObject, _
                   item_column As Long, _
                   sort_column As Long) As Variant
    Dim arr As Variant
        arr = target_table.DataBodyRange
    Dim myMax As Long
        myMax = WorksheetFunction.Max(target_table.DataBodyRange.Columns(sort_column)) + 1
    Dim i As Long
        For i = 1 To UBound(arr)
            If arr(i, sort_column) = vbNullString Then
                arr(i, sort_column) = myMax
                myMax = myMax + 1
            End If
        Next
    
    Dim SQC As SeaquenceClass
    Set SQC = New SeaquenceClass
        arr = SQC.SortArray2D(arr, sort_column)
    ListArray = SQC.ExtractArray2(arr, , , item_column, item_column)
End Function

' テスト

Sub test()
    Dim Tb As ListObject
    Set Tb = ActiveSheet.ListObjects(1)
    Dim arr As Variant
        arr = ListArray(Tb, 1, 2)
    SetList Selection, arr
End Sub

前回テスト内で行っていた強引な辻褄合わせを、クラスモジュール内に取り込んだかたち。

なので依然として、このような手法が正しいかどうかは定かでありません。

参考まで。