2011年12月25日日曜日

[ExcelVBA] 重複データの削除


■お題
同一列内にあるデータで重複するものを削除せよ

■解答例
今回は、すごい簡単な問題を使って、ユーザフレンドリーなコードを書いてみます。
最終的なコードはこちら→最終的なコード

重複データを手動で削除する場合、ソートをかけて上から順に見て行き同じデータが並んでいる時削除するという手順を繰り返すことになると思います。
では、それをそのままコード化してみます。
モジュール名:SubModule内
  1. Sub DelDuplicateRow(Col As Integer)  
  2.     If Not (0 < Col And Col < 257) Then  
  3.         MsgBox "列は1~256の間で指定して下さい"  
  4.         End  
  5.     End If  
  6.       
  7.     Dim EndRow As Long  
  8.     EndRow = Cells(65536, Col).End(xlUp).Row  
  9.       
  10.     Range(Cells(1, Col), Cells(EndRow, Col)).Sort Key1:=Cells(1, Col)  
  11.       
  12.     Dim r As Long  
  13.     For r = 1 To EndRow - 1  
  14.         If Cells(r, Col) = Cells(r + 1, Col) Then Cells(r, Col) = ""  
  15.     Next  
  16.       
  17.     Range(Cells(1, Col), Cells(EndRow, Col)).Sort Key1:=Cells(1, Col)  
  18. End Sub  
1行目:列情報を数字で引数に受け取ります
2~5行目:列は1~256列までなのでそれ以外はエラーとしてメッセージを出して終了します
7行目:引数に指定された列の最終行を格納するための変数EndRowを宣言します。
セルは65536あるのでLong型ですね
8行目:最終行を取得します。
10行目:ソートしています。
12行目:For文で指定列のセルを最初から最終行に向かってループを
する時に行情報を格納する為の変数rを宣言します
13行目:For文開始です。スタートは、1行目なので1。現在のセルと一つしたのセルを比較するので     終わりは、最終行目なのでEndRow - 1を指定してます
14行目:現在のセルと一つ下のセルを比較して同じデータの時現在のセルを削除します
15行目:繰り返し
17行目:重複行は空になっているので間をつめるためにソートをかけます

モジュール名:SubModule内
  1. Sub Main()  
  2.     Call DelDuplicateRow(1)  
  3. End Sub  
A列目を指定するのであれば上記のように指定すればよいですね。



もっとも簡単なコードにして親切さのかけらもないコードが完成しました!


汎用性がなく使いづらいですね。
ここからユーザーフレンドリーなコードにかえていきましょう

Q. 重複削除したいデータは、ある列の途中にあって最初が1行目最後が最終行だと困るのですが?
A.引数にスタート行と終了行を用意しましょう。
モジュール名:SubModule内
  1. Sub DelDuplicateRow(Col As Integer, StartRow As Long, EndRow As Long)  
  2.     If Not (0 < StartRow And StartRow < 65536) Then  
  3.         MsgBox "スタート行として、1から65535の間で指定して下さい"  
  4.         End  
  5.     End If  
  6.       
  7.     If Not (1 < EndRow And EndRow < 65537) Then  
  8.         MsgBox "最終行として、2から65536の間で指定して下さい"  
  9.         End  
  10.     End If  
  11.       
  12.     If EndRow < StartRow Then  
  13.         MsgBox "スタート行と最終行ではスタート行のほうが小さくなるように設定して下さい"  
  14.         End  
  15.     End If  
  16.       
  17.     If Not (0 < Col And Col < 257) Then  
  18.         MsgBox "列は1~256の間で指定して下さい"  
  19.         End  
  20.     End If  
  21.       
  22.       
  23.     'EndRow = Cells(65536, Col).End(xlUp).Row  
  24.     Range(Cells(StartRow, Col), Cells(EndRow, Col)).Sort Key1:=Cells(StartRow, Col)  
  25.       
  26.     Dim r As Long  
  27.     For r = StartRow To EndRow - 1  
  28.         If Cells(r, Col) = Cells(r + 1, Col) Then Cells(r, Col) = ""  
  29.     Next  
  30.       
  31.     Range(Cells(StartRow, Col), Cells(EndRow, Col)).Sort Key1:=Cells(StartRow, Col)  
  32. End Sub  
変更点は、
・引数にStartRow、EndRowを追加
・最終行を取得するのを廃止
・ソートの始まりのセル、For文のはじめの値をそれぞれStartRowに変更



これで決め細やかな設定ができるようになりました!
と、思って満足したらだめです。

Q. 最初が1行目最後が最終行の決め打ちは困ると言ったけども毎回指定するなんてありえないです><
A.変数のオプション化をします。

こういう時に役立つのが変数のOptionalキーワードです。
引数に値がなければ、プログラムの方でデフォルトの値を決めるという方法です。
書き方は
Optional (受け渡し方法[ByVal]) 変数名 (as 変数型[variant]) (=デフォルト値) (括弧)表示は、省略可能を現しており[四角括弧]は、省略時のデフォルト値です 一番最後のデフォルト値は定数でなければいけません。

モジュール名:SubModule内
  1. Sub DelDuplicateRow(Col As IntegerOptional StartRow As Long = 1, Optional EndRow As Long)  
  2.     If EndRow = 0 Then EndRow = Cells(65536, Col).End(xlUp).Row  
  3.       
  4.     If Not (0 < StartRow And StartRow < 65536) Then  
  5.         MsgBox "スタート行として、1から65535の間で指定して下さい"  
  6.         End  
  7.     End If  
  8.       
  9.     If Not (1 < EndRow And EndRow < 65537) Then  
  10.         MsgBox "最終行として、2から65536の間で指定して下さい"  
  11.         End  
  12.     End If  
  13.       
  14.     If EndRow < StartRow Then  
  15.         MsgBox "スタート行と最終行ではスタート行のほうが小さくなるように設定して下さい"  
  16.         End  
  17.     End If  
  18.       
  19.     If Not (0 < Col And Col < 257) Then  
  20.         MsgBox "列は1~256の間で指定して下さい"  
  21.         End  
  22.     End If  
  23.       
  24.       
  25.       
  26.     Range(Cells(StartRow, Col), Cells(EndRow, Col)).Sort Key1:=Cells(StartRow, Col)  
  27.       
  28.     Dim r As Long  
  29.     For r = StartRow To EndRow - 1  
  30.         If Cells(r, Col) = Cells(r + 1, Col) Then Cells(r, Col) = ""  
  31.     Next  
  32.       
  33.     Range(Cells(StartRow, Col), Cells(EndRow, Col)).Sort Key1:=Cells(StartRow, Col)  
  34. End Sub  
変更点は、
・引数にのうちStarRowとEndRowをオプション化
・StartRowに値がない時は1を設定(Optionalキーワード内)
・EndRowは、設定がない時にEndメソッドで最終行を取得


利用する時は、こんな風になる。
モジュール名:SubModule内
  1. Sub Main()  
  2.     'A列 最初~最終行を対象  
  3.     Call DelDuplicateRow(1)  
  4.       
  5.     'B列 6行目~11行目を対象  
  6.     Call DelDuplicateRow(2, 6, 11)  
  7.       
  8.     'C列 6行目~最終行を対象  
  9.     Call DelDuplicateRow(3, 6)  
  10.       
  11.     'D列 最初~11行目を対象  
  12.     Call DelDuplicateRow(4, EndRow:=11)  
  13.   
  14. End Sub  


実行結果はこうなる
↑背景色が緑の部分が重複削除セル指定範囲


これで、必要な時に引数を追加すればよくなったので利用しやすさがぐんと上がりました。 これでおしまい!
なんて思いませんよね?

Q.列名を数字に治すのがめんどいです。CI列って数字でいくつでしたっけ?><
A.ですよねぇ・・・。列名は文字列で指定しましょう。

やることは、一つ。
列名はA→Zまでいくとケタが繰り上がりAAとなるので26進数といえますね。
26進数を10進数に変換するコードをかくだけですね。


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

モジュール名:SandModule内
  1. Sub t1()  
  2.     Debug.Print ColFromStrToNum("a")  
  3.     Debug.Print ColFromStrToNum("z")  
  4.     Debug.Print ColFromStrToNum("IV")  
  5.     Debug.Print ColFromStrToNum("iv")  
  6.     Debug.Print "-----"  
  7. End Sub  
  8.   
  9. Sub t2()  
  10.     Debug.Print ColFromStrToNum("1")  
  11.     Debug.Print "-----"  
  12. End Sub  
テストしてみる。 t1関数の出力は
  1. 1   
  2.  26   
  3.  256   
  4.  256   
  5. -----  
期待した値がでてますね。

t2関数の出力は「アルファベットを引数に渡して下さい」とでますね。
あとは、SubModule内のDelDuplicateRowの引数を文字列で受け取り変換するコードを追加すればおしまいですね。

以下に最終的なコードを示しておきます
モジュール名:SubModule
  1. Option Explicit  
  2.   
  3. Sub DelDuplicateRow(ColStr As StringOptional StartRow As Long = 1, Optional EndRow As Long)  
  4.     '### 同一列の重複データのセルを削除する関数  
  5.     '### 引数  
  6.     '###   省略不可:ColStr…列情報を文字列で指定 A~IV  
  7.     '###   省略可能:StartRow…最初の行を指定。省略した場合は1行目とする  
  8.     '###   省略可能:EndRow…最後の行を指定。省略した場合は、指定列の最終行  
  9.     '###  
  10.     '### ColFromStrToNum関数を必要  
  11.     '######################################################################  
  12.   
  13.     Dim Col As Integer  
  14.     Col = ColFromStrToNum(ColStr)  
  15.     If EndRow = 0 Then EndRow = Cells(65536, Col).End(xlUp).Row  
  16.       
  17.       
  18.     If Not (0 < StartRow And StartRow < 65536) Then  
  19.         MsgBox "スタート行として、1から65535の間で指定して下さい"  
  20.         End  
  21.     End If  
  22.       
  23.     If Not (1 < EndRow And EndRow < 65537) Then  
  24.         MsgBox "最終行として、2から65536の間で指定して下さい"  
  25.         End  
  26.     End If  
  27.       
  28.     If EndRow < StartRow Then  
  29.         MsgBox "スタート行と最終行ではスタート行のほうが小さくなるように設定して下さい"  
  30.         End  
  31.     End If  
  32.       
  33.     If Not (0 < Col And Col < 257) Then  
  34.         MsgBox "列は1~256の間で指定して下さい"  
  35.         End  
  36.     End If  
  37.       
  38.       
  39.       
  40.     Range(Cells(StartRow, Col), Cells(EndRow, Col)).Sort Key1:=Cells(StartRow, Col)  
  41.       
  42.     Dim r As Long  
  43.     For r = StartRow To EndRow - 1  
  44.         If Cells(r, Col) = Cells(r + 1, Col) Then Cells(r, Col) = ""  
  45.     Next  
  46.       
  47.     Range(Cells(StartRow, Col), Cells(EndRow, Col)).Sort Key1:=Cells(StartRow, Col)  
  48. End Sub  

モジュール名:CommonModule
  1. Option Explicit  
  2.   
  3. Function ColFromStrToNum(ColStr As String)  
  4.     '### 列情報 文字列→数字に変換する関数  
  5.     '### アルファベット以外を指定するとエラーとなる  
  6.     '##############################################  
  7.       
  8.     Dim List() As Integer, i As Integer, position As Integer  
  9.     ReDim List(Len(ColStr) - 1)  
  10.       
  11.     Dim Code As Integer  
  12.     position = 1  
  13.     For i = Len(ColStr) - 1 To 0 Step -1  
  14.         Code = Asc(UCase(Mid(ColStr, position, 1)))  
  15.         If Not (64 < Code And Code < 91) Then  
  16.             MsgBox "アルファベットを引数に渡し手下さい"  
  17.             End  
  18.         Else  
  19.             List(i) = Code  
  20.         End If  
  21.         position = position + 1  
  22.     Next  
  23.       
  24.     ColFromStrToNum = 0  
  25.     For i = 0 To UBound(List)  
  26.         ColFromStrToNum = ColFromStrToNum + (List(i) - 64) * 26 ^ i  
  27.     Next  
  28. End Function  
本当は、まだ手を加えたかったのですが別の機会とします。

0 件のコメント: