小さなクラスを作る(7)~セルの列符号が簡単に取得できるラッパークラス

列符号取得が可能なRangeオブジェクトのラッパークラス

きっかけ

「列番号はColumプロパティで簡単に取得できるのに、列符号って取り出しにくいよなー」と思って、コチラを参考にXDF列までに対応した自作関数を作ったのがそもそもの始まり。

他のよく使う機能を一つのモジュールにまとめて使い回していたんだが、モジュール内のプロシージャが増えるにつれ、管理がいい加減になってしまって、放置プレーになってしまっていた。

ブックAに仕込んだ○○プロシージャは修正したけど、ブックBの○○プロシージャは以前のまま、みたいな。

ちょうどクラスモジュールの練習中でもあるので、

一つのセルを包み込んで、列符号を尋ねたら応えてくれて、なおかつRangeオブジェクトの諸機能もそのまま使える

クラスを作ってみようと思い立った。

これを「ラッパークラス」と呼んでよいものかどうかは自信がないので、達人のみなさん、ツッコミよろしく!

準備

クラスモジュールを挿入して、オブジェクト名を「WrappedCell」にする。

リスト1-1 宣言セクション
Option Explicit
'自作エラー情報のための構造体    '……(1)'
Private Type errorType
  Number As Long
  Description As String
End Type
'拡張できるように配列で宣言    '……(2)'
Private myError() As errorType
'エラーインデックス用の番号を列挙体で準備    '……(3)'
Private Enum errIndex
  CELL_NOT_SINGLE = 0 'セルが複数渡された
  CELL_NOT_GOT  'セル未取得
End Enum
リスト1-1の説明

まずは下ごしらえ。普通はフィールドの宣言から始めるけど、今回はちょっと違う。

    • (1)では、自作エラーを表示するために、ErrオブジェクトのRaiseメソッドの引数用の構造体を準備した。まあ、たった2つのパラメータごときを構造体にまとめるというのは鶏を割くに牛刀を用いるの観なきにしもあらずだが、使わないと忘れるので。
    • (2)では、(1)で準備したerrorType型の変数myErrorを配列で準備している。あとで自作エラーの種類を増やすときに楽なようにこうした。他にもエラーを吐かせるべきことがあるかも知れんので。いわゆるYAGNIの原則には反しますがw
    • (3)では、 id:imihitoさんのこのとき
    • のアドヴァイス
処理のブロックに番号をつけるときに列挙型(Enum)を使うと、識別子を付けられるのでただの数字より見やすくなってオススメ

を生かして配列myErrorのインデックス番号を列挙体で準備した。今後、エラーの種類を増やすときは、ここで番号に意味のある文字列を割り振ったら良い。

リスト1-2 フィールド
'フィールド
Private oneSelf_ As Range
Private columnLetter_ As String
リスト1-2の説明

とりあえず、包み込む対象のセルそのものと、そのセルの列符号をフィールドとして持たせることにした。他になんかあるだろうか???

リスト1-3 アクセサ(1)
'アクセサ
Public Property Set oneSelf(ByRef newCell As Range)
  If newCell.Count <> 1 Then    '……(1)'
    Call raiseError(errIndex.CELL_NOT_SINGLE)    '……(2)'
    Exit Property
  End If
  Set oneSelf_ = newCell    '……(3)'
End Property
Public Property Get oneSelf() As Range
  If oneSelf_ Is Nothing Then    '……(4)'
    Call raiseError(errIndex.CELL_NOT_GOT)    '……(5)'
  End If
  Set oneSelf = oneSelf_
End Property
リスト1-3の説明

珍しくProperty Set(Let)を書いた。

Javaファンの私からすると、Propertyプロシージャってなんだかヘンテコリンな感じがするので、別途Setterメソッドを作るという手もあったんですけどね。

  • (1)では、このクラスに渡されるRangeオブジェクトをチェック。
  • このクラスは、あくまでも単一のセルを包み込むのが目的なので、複数セルが渡されたら(2)でエラーを吐かせる。プログラマに知らせることが目的なのであえてエラーを吐かせるのだ。
    エラー表示用のraiseErrorメソッドは後で実装する。
  • (1)のIf節を何事もなく通過したら(3)で仮変数oneSelf_に渡されたセルをセットしている。
    ちなみに、(1)のIfに対応したElse節内に書くこともできるが、あえてこうしている。
    というのも、「If~Else」を多用すると可読性が落ちる(Elseの条件は、If、ElseIfと見比べないと分からない)ため。「ガード節」というらしい。たしかに「If~Else」って読みにくいときがあるんだよな。
  • (4)も(1)と同じ。oneSelfプロパティにセルがセットされていないのに呼び出そうとしたら(5)でエラーを吐く。まあ、なくても普通にエラーが出るだろうけど、こうした方が原因が特定しやすいと思うので。
リスト1-4 アクセサ(2)
Public Property Get columnLetter() As String
  If oneSelf_ Is Nothing Then
    Call errorRaiser(errIndex.CELL_NOT_GOT)
    Exit Property
  End If
  Dim iAlpha As Integer       '列符号最上位けた'
  Dim iBeta As Integer        '列符号中位けた'
  Dim iRemainder As Integer   '列符号最下位けた'
  With oneSelf_
    If .Column > 702 Then '列番号が702を超えるとき、列符号は3けた    '……(1)'
      iAlpha = Int((.Column - 27) / 676)    '……(2)'
      columnLetter_ = Chr(iAlpha + 64)    '……(3)'
      iBeta = Int(((.Column - (iAlpha * 676)) - 1) / 26)    '……(4)'
      columnLetter_ = columnLetter_ & Chr(iBeta + 64)
      iRemainder = (.Column - (iAlpha * 676) - (iBeta * 26))
      columnLetter_ = columnLetter_ & Chr(iRemainder + 64)
    ElseIf .Column > 26 Then  '列番号が26を超えるとき、列符号は2けた    '……(5)'
      iBeta = Int((.Column - 1) / 26)
      columnLetter_ = Chr(iBeta + 64)
      iRemainder = .Column - (iBeta * 26)
      columnLetter_ = columnLetter_ & Chr(iRemainder + 64)
    Else  '列番号26までは列符号1けた    '……(6)'
      iRemainder = .Column
      columnLetter_ = Chr(iRemainder + 64)
    End If
  End With
  columnLetter = columnLetter_
End Property
リスト1-4の説明

ココが今回のメイン。列番号の取得はめっちゃ簡単なのに、列符号の取得のなんとメンドウなことよ。高校1年程度の数学ができるんならこのコードは理解できると思う。(1)~(3)がどうしても理解できないんならあきらめてください。

  • (1)。まず、1列目(A)~26列目(Z)は列符号1けた。これは楽勝ですな。んで、27列目(AA)から702行目(ZZ)が列符号2けた。
    アルファベット2字の組み合わせは26×26=676通り。AAから数えて676番目のZZは、そこまでの26を足して702番目になるでしょ?
  • (2)は、列符号の左端の文字を求めるための計算。ZZ(702列目)の次がAAA(703列目)ってのがミソ。
    703列目以降は、BAA列が(27+676)+676=1379列目、CAA列が(27+676)+676+676=2055列……というように、左端のアルファベットが進んでいくので、左端のアルファベットは[列数-27を676で割った商]番目のアルファベット、ということになる。
  • 何番目のアルファベットかさえわかれば、あとは、その数字を文字に変換してやればよい。アルファベットの大文字「A」は文字コードが65番目なので、Chr関数の引数に[(2)で求めた数+64]を渡してやれば、めでたくアルファベットが得られる。
  • (1)~(3)が理解できるなら、(4)はもはや説明不要だろう。
    列番号から676のかたまりを除去すると、後は右の2けたの問題ですからね。
    AAA列を例にとると、703-676-1=26、26÷26=1なので、真ん中のアルファベットは1番目、すなわちAということ。
  • (5)は2けたになるとき、(6)は1けたになるとき、それぞれ同じような理屈で文字に変換している。
リスト1-5 コンストラクタとメソッド
'コンストラクタ
Private Sub Class_Initialize()
  ReDim myError(2) As errorType    '……(1)'
  With myError(0)
    .Number = 10000
    .Description = "WrappedCellのoneSelfプロパティに複数のセルを渡すことはできません。"
  End With
  With myError(1)
    .Number = 10001
    .Description = "WrappedCellのoneSelfプロパティにセルがセットされていません。"
  End With
End Sub
'メソッド
Private Sub raiseError(ByVal errIndex As Integer)    '……(2)'
  Err.Raise myError(errIndex).Number, myError(errIndex).Description
End Sub
リスト1-5の説明

珍しくコンストラクタの出番。このクラス独自のエラーをセットしている。

  • (1)では、今のところエラーの種類は2種類だけなので、RedimしてそれぞれNumberとDescripitionをセットしている。
  • (2)は、自作エラーを吐かせるメソッド。クラスの内部でしか使わないのでPrivateにしている。こんなもん、あっちこっちで使われたらかなわんしw
    引数として配列のインデックス番号を受け取って、それに応じたNumberとDescriptionをセットしているだけ。

実行

標準モジュールのコード
Sub test05()
  Dim target As WrappedCell
  Set target = New WrappedCell
  Set target.oneSelf = Selection
  Debug.Print target.columnLetter
  Set target = Nothing
End Sub

実行結果

f:id:akashi_keirin:20170330121456j:plain

セルを選んで、実行すると、

f:id:akashi_keirin:20170330121504j:plain

ほれ、この通り列符号が取得できた。

f:id:akashi_keirin:20170330121510j:plain

結合されたセルでも

f:id:akashi_keirin:20170330121516j:plain

大丈夫。

f:id:akashi_keirin:20170330121533j:plain

ただし、複数セルを選択して実行すると

f:id:akashi_keirin:20170330121543j:plain

エラーを吐く。

「[WrappedCellクラスのインスタンス].oneSelf.」まで入力すると、

f:id:akashi_keirin:20170330121526j:plain

このようにインテリセンスが働くので、Rangeオブジェクトのプロパティ・メソッドが普通に使える。

おわりに

とりあえず、何の役に立つのか分からないけど、今後拡張できたらしてみよう。

……とここまで書いてきてアレなんだが、

セルの列符号ぐらい、もっと簡単に取得できるんじゃね?

と思ってしまったのだった。

リスト2
Sub test06()
  Dim tgtCell As Range
  Set tgtCell = ActiveCell
  Dim str As String
  Dim chr As String
  Dim i As Integer
  Dim tmp As String
  str = Replace(tgtCell.Address, "$", "")
  For i = 1 To Len(str)
    chr = Mid(str, i, 1)
    If chr Like "[0-9]" Then
      chr = ""
    End If
    tmp = tmp & chr
  Next
  str = tmp
  Debug.Print str
End Sub
リスト2の実行結果

f:id:akashi_keirin:20170330121551j:plain

AZN5セルを選択して実行すると、

f:id:akashi_keirin:20170330121559j:plain

でけとる……。

む、むなしい……orz

@akashi_keirin on Twitter