2011年12月25日日曜日

[ExcelVBA] 重複データの削除2


■お題
複数列内(シートをまたぐ場合込み)にあるデータで重複するものを削除せよ

■解答例
複数列にまたぐ場合は、前回おこなったようなソートしてから上下を見比べて削除という方法には無理が限界が生じますね。なぜならば、複数列が可能になった瞬間から1列に収まらないセルの数の可能性がでてきますからねぇ。。
でも、かいちゃいますか。

重複データを削除する方法で一番単純なやり方は、連想配列に放りこむ方法です。連想配列とは、キーと値を1セットで管理する変数のことで重複チェックをしたい物をキーとし、値は適当な数値でもセットしておけば、同一の値に出くわしてもキー自体が増えることがないので、自動的に重複をはじけるわけです。
ただし、ひたすらメモリーに記憶しつづけていくので大量なデータを相手にする場合効率的ではないですね。
他の言語はさておき、少なくともExcelVBAに関しては効率が悪くなるのはあきらかです。


簡単な方法だとこれくらいしか思いつかないので、連想配列を使う方法でいきます。

以下に最終的なコードを示しておきます
モジュール名:SubModule内
Option Explicit

Sub DelDuplicateRow(ColInfo As Variant, Optional StartRow As Long = 1, Optional EndRow As Long, Optional multiple As Boolean = False, Optional SN As String)
    '### 同一列の重複データのセルを削除する関数
    '### 引数
    '###   省略不可:ColInfo…列情報を数字(1~128)か文(A~IV)で指定
    '###   省略可能:StartRow…最初の行を指定。省略した場合は1行目とする
    '###   省略可能:EndRow…最後の行を指定。省略した場合は、指定列の最終行
    '###   省略可能:Multiple…複数列を重複削除対象列とする場合Trueを指定
    '###                       省略した場合は、False
    '###   省略可能:SN…シート名を指定。省略時アクティブシート名
    '###   
    '###   ColFromStrToNum関数を必要
    '######################################################################
    If SN = "" Then SN = ActiveSheet.Name
    
    Dim Col As Integer
    If IsNumeric(ColInfo) Then
        Col = ColInfo
    Else
        Col = ColFromStrToNum(CStr(ColInfo))
    End If

    If EndRow = 0 Then EndRow = Sheets(SN).Cells(65536, Col).End(xlUp).Row
    
    
    If Not (0 < StartRow And StartRow < 65536) Then
        MsgBox "スタート行として、1から65535の間で指定して下さい"
        End
    End If
    
    If Not (1 < EndRow And EndRow < 65537) Then
        MsgBox "最終行として、2から65536の間で指定して下さい"
        End
    End If
    
    If EndRow < StartRow Then
        MsgBox "スタート行と最終行ではスタート行のほうが小さくなるように設定して下さい"
        End
    End If
    
    If Not (0 < Col And Col < 257) Then
        MsgBox "列は1~256の間で指定して下さい"
        End
    End If
    
    Static hash As Object, Status As Boolean
    If multiple Then
        If Not Status Then
            Set hash = CreateObject("Scripting.Dictionary")
            Status = True
        End If
    Else
        Set hash = CreateObject("Scripting.Dictionary")
    End If
    
    
    
    Dim r As Long
    For r = StartRow To EndRow
        
        If Not hash.Exists(CStr(Sheets(SN).Cells(r, Col))) Then
            hash.Add CStr(Sheets(SN).Cells(r, Col)), 1
        Else
            Sheets(SN).Cells(r, Col) = ""
        End If
    Next
    
    Sheets(SN).Range(Sheets(SN).Cells(StartRow, Col), Sheets(SN).Cells(EndRow, Col)).sort Key1:=Sheets(SN).Cells(StartRow, Col)
End Sub
1行目:引数は、下記の通りです。
省略不可:ColInfo…列情報、数字 or 文字列どちらでも可
省略可能:StartRow…重複対象セルの開始行(省略時:1)
省略可能:EndRow…重複対象セルの終了行(省略時:指定列の最終行)
省略可能:Multiple…複数列を対象としたい時は True指定(省略時:False) 省略可能:SN…シート名を指定(省略時:アクティブシート名)

15行目:SNが渡されなかった時は、アクティブシートの名前を指定
17行目:Col…対象列を格納する変数
18行目:引数のColInfoに入っている情報が数字かそうじゃないか判別。
19行目:数字ならばColに代入
21行目:数字でなければ、ColFromStrToNum関数に渡し何列目かに変換する
24行目:引数のEndRowに値が渡されなければ最終行を取得し代入する

27行目~45行目までエラーチェック
47行目:hash…重複判定に使う連想配列を宣言。
Staticステートメントで宣言することで、関数を抜けても値を保持しておく。
Statusオブジェクト宣言後に、Scripting.DictionaryをSetしたかどうかを判定する用の変数
Staticステートメントで宣言することで、関数を抜けても値を保持しておく 48行目:MultipleがTrue(=重複削除対象列が複数列)かどうか判別
49行目:Scripting.DictionaryをセットしてあるかどうかStatus変数で判別
50行目:Scripting.Dictionaryをセット(連想配列が使えるようになる)
51行目:Statusをtrueにし、次回呼び出しの場合
54行目:Scripting.Dictionaryをセットし、連想配列を使えるようにする
60行目:For文で指定された列の最初のセルから最後のセルまでループしていきます
62行目:連想配列に登録済みかどうかを判別します
63行目:登録されてないときは、キーに追加します
65行目:セル内容を削除します
69行目:重複対象範囲をソートします ColFromStrToNum関数は、前回と一緒のままです。

モジュール名:CommonModule
Option Explicit

Function ColFromStrToNum(ColStr As String)
    '### 列情報 文字列→数字に変換する関数
    '### アルファベット以外を指定するとエラーとなる
    '##############################################
    
    Dim List() As Integer, i As Integer, position As Integer
    ReDim List(Len(ColStr) - 1)
    
    Dim Code As Integer
    position = 1
    For i = Len(ColStr) - 1 To 0 Step -1
        Code = Asc(UCase(Mid(ColStr, position, 1)))
        If Not (64 < Code And Code < 91) Then
            MsgBox "アルファベットを引数に渡し手下さい"
            End
        Else
            List(i) = Code
        End If
        position = position + 1
    Next
    
    ColFromStrToNum = 0
    For i = 0 To UBound(List)
        ColFromStrToNum = ColFromStrToNum + (List(i) - 64) * 26 ^ i
    Next
End Function
3行目:列情報を受けとる引数を用意します。
8行目:List配列…列情報を1ケタずつ分解しキャラコードに変換した値を格納
i変数…For文でList配列のインデックスとして使用
position…列情報を1文字ずつに分解する際に使用
9行目:List配列の要素数を定義。文字列-1
11行目:Code…列情報を分解した文字をキャラコードに変換した値を格納
12行目:列情報を分解する際の文字位置を初期化する
13行目:列情報のキャラコードを1文字目を配列の最後から順に格納するため For文の最初の数字を配列の最後の要素、最後を配列の最初の要素 Stepで-1を指定。26進数から10進数に計算する際に都合がよいため
14行目:列情報のうち一文字取り出し、大文字に変換し、キャラコードに変換する
15~17行目:アルファベット以外はエラーにする
19行目:配列にキャラコードを代入
21行目:列情報を1文字に切り出すための文字位置情報を1つ増やす
22行目:繰り返し
24行目:26進数から10進数へ変換する際の値を代入するまえに初期化する
25~27行目:26進数から10進数へ変換する

以下使用例を示します。
Sub t1()
    Dim c As Integer
    For c = 1 To 5
        Call DelDuplicateRow3(c)
    Next
End Sub
まずは、複数列を対象範囲にしなかった場合
それぞれの列で重複がはじかれます。 複数列のオプションを有効にすると…
Sub t2()
    Dim c As Integer
    For c = 1 To 5
        Call DelDuplicateRow3(c, multiple:=True)
    Next
End Sub
このように全体で重複セルを削除します。

>別シートも同じ容量でオプションの引数であるSNにシート名をセットすれば同様のことができます

ところで…Staticステートメントのスコープってプログラムが終了するまで値を保存ってなっているが終了っていつなんだろう??少なくともマクロのリセットボタンを押すと値を捨ててくれるようだけどただプログラムの実行が完了しただけじゃ、値を捨てないみたい。。。 扱いが要注意だな。

0 件のコメント: