重複のない乱数発生のアルゴリズムを考えてみた
重複のない乱数を作る
重複のない乱数を発生させるアルゴリズムを考えてみた。まあ、ちょこっとggったらスマートなやつが見つかるとは思ったけど、自分で考えるというのが大事だと思ったのですよ。
素人丸出しのやり方なので、笑ってくれたらいいと思います。
考え方
ちょこっと作戦を考えてみた。
たとえば、1~10までの乱数を作るだけなら、Rnd関数を用いて、
Int(Rnd * 10) + 1
とでもすればよかろう。
問題は、これだけだと同じ数が出てきてしまうことだ。
順番をランダムに変えたいようなとき、これでは困る。
そこで、次のような手順を考えた。
- 10個の値を格納することができる配列を準備する
- 乱数を発生させる
- (2回目以降)発生させた値を、それまでに格納した全ての値と比べる
- 同じ値にぶつかったら、乱数発生をやり直して3.に戻る
- 一度も同じ値にぶつからずに3.を終えると、新たに配列に追加する
- 2.に戻る
このやり方でできると思った。
実装
リスト1
Dim qNumbers() As Integer '……(1)' ReDim qNumbers(1 To cnt) Dim i As Integer Dim n As Integer Dim hasDone As Boolean Randomize For i = 1 To cnt Do qNumbers(i) = Int(Rnd * cnt) + 1 '……(2)' hasDone = True '……(3)' If i > 1 Then '……(4)' For n = 1 To i - 1 '……(5)' If qNumbers(i) = qNumbers(n) Then hasDone = False Exit For End If Next End If Loop While hasDone = False '……(6)' Next
まず、(1)からの2行、
Dim qNumbers() As Integer ReDim qNumbers(1 To cnt)
では、作成した乱数を格納する配列を用意し、要素数でReDimしている。cntというのは、要素数が入っている変数だと思ってください。
(2)の
qNumbers(i) = Int(Rnd * cnt) + 1
で、1~10までの数を1つ作り、ひとまず配列qNumbersに格納。
一旦(3)で
hasDone = True
としてフラグ用の変数をTrueにしておく。
すでに取得済みの数と比較する必要があるのは、ループの2回目以降なので、(4)の
If i > 1 Then
で、ループの1回目のみ(5)以下のForループを飛ばすようにした。
(5)以下の6行、
For n = 1 To i - 1 '……(5)' If qNumbers(i) = qNumbers(n) Then hasDone = False Exit For End If Next
が重複を防ぐためのロジック。
Forループの最終値を「i - 1」にしておかないと無限ループになるので注意w(←経験者)
1つ手前までのqNumbersの全ての要素と比較し、同じ値があったら即hasDoneをFalseにしてループを抜けるようにした。
従って、このループを無事抜けるということは、重複のない値がセットされていて、hasDoneがTrueになっているということ。
(6)では、Doループの繰り返し条件としてhasDone = Falseを指定しているので、重複のない値が取得できていないときはDoループの先頭に戻って値を取得しなおすことになる。
実行結果
cntを10にして、リスト1に
For i = 1 To cnt debug.Print qNumbers(i) Next
を付け加えて実行してみると、
無事に重複なく1~10の数字が格納されていることが分かる。
おわりに
重複のない乱数の取得なんていうのは、さんざん考え尽くされた類のものだと思うので、もっとエレガントかつスマートなやり方があると思う。
もう少し仕事に余裕があったら、本格的にアルゴリズムの勉強をするんだけどなあ。
マクロなし縛りでドロップダウンリストの項目を切り替える
マクロなし縛りでドロップダウンリスト項目を動的に切り替える
「マクロなし縛り」でExcelでの様式づくりをせざるを得なくなったので、久しぶりに関数であれこれやってみた。
ろくにワークシート関数も覚えていないのにマクロを覚えてしまったために、「欲しい機能は自分で作る」みたいになっていた。達人が見たら「そんなもん、ワークシート関数使ったら一発やろがぼけー」状態だったかも知れん。
せっかくの機会なので、ちょっとまじめにExcelのワークシート関数を使ってみた。
「データの入力規則」リストの切り替え
ユーザに変なデータ入力をさせないために、よくお世話になる「データの入力規則」。
特に、ドロップダウンリストで項目を選択させることのできる「リスト」機能は重宝する。
このリストの項目を条件によって切り替える、ということをやってみた。
準備
ワークシート上に、リスト用の表を2つ作る。
1つ目のリストには「フラワーライン」と名前を定義し、
2つ目のリストには「反フラワー」と名前を定義しておく。
INDIRECT関数
んで、
名前だけは知っていたけど使ったことなかったExcel関数選手権
でもやったら結構いいところまで行くんじゃないか、と個人的には思っているINDIRECT関数ですよ。
基本的には、
=INDIRECT(セル番地)
の形で、セル参照を返してくれるという関数。
たとえば、
こんなふうに、K5セルに「デコスケ」と入力されているときに、
=INDIRECT("K5")
を計算すると、
こんなふうにK5セルの値が返る。
ということは、INDIRECT関数に渡す引数を切り替えてやれば、リスト項目を切り替えることができるということになる。
「データの入力規則」の設定
「元の値」のところを、
=INDIRECT($E$1)
にして、E1セルに「フラワーライン」と入力すると、
リスト項目はこの通り。
E1セルに「反フラワー」と入力すると、
リスト項目が変わった。
おわりに
入力された値に応じて動的に何かを切り替える、となると、安易にWorksheetのイベントマクロなんかで済ませようと考えがちだけれど、たまに「マクロなし縛り」という状況に追い込んでみると
名前は知っていたけど使ったことなかったExcel関数
が使えるようになるきっかけになるかも知れない、と思いました。
ワークシート関数もしっかりマスターしないといけないなあ。
文字列の中の必要な部分にだけ書式設定をする
指定した文字の書式だけを変更する
記入見本作り
記入例を作りたかったんだが、日付の欄は、
こんな感じで、「月」と「日」と「(」、「)」をあらかじめ入れておいて、月・日・曜日だけを書いてもらうようにしている。
んで、記入者に書いてもらう部分だけを手書き風のフォントに変えて記入例を作ろうと思った。
しかし、ちょっと考えたら分かると思うが、これはめんどくさい。
たとえば、
7月23日(日)
という記入例を作ろうと思ったら、
「7」と「23」と「日」(曜日の方)だけを選択してフォントを変える
というしちめんどくさいことになる。
まあ、ちまちまと時間をかけて努力するのが好きな人ならそれほど苦痛ではないだろうが、私にとっては端的に苦痛ですw
んで、WordVBAの練習も兼ねてやってみた。
必要な部分だけ書式を変更するマクロ
Wordのオブジェクト構造がイマイチよく分かっとらんので、かなり行き当たりばったりのコードだと思うが、恥ずかしげもなく載せる。
リスト1
Option Explicit Public Const FONT_TO_USE As String = "HG正楷書体-PRO" '……(1)' Public Sub setFontNameAndItalic() Dim chr As String '……(2)' Dim i As Integer Dim numOfChar As Integer Dim flg As Boolean numOfChar = Selection.Range.Characters.Count '……(3)' For i = 1 To numOfChar chr = Selection.Range.Characters(i) '……(4)' If flg = True And chr <> ")" Then '……(5)' With Selection.Range.Characters(i).Font '……(*)' .Name = FONT_TO_USE .Italic = True End With End If If chr = "月" Or chr = "日" Or _ chr = "(" Or chr = ")" Then '……(6)' If chr = "(" Then flg = True '……(7)' If chr = ")" Then flg = False '……(8)' Else With Selection.Range.Characters(i).Font .Name = FONT_TO_USE .Italic = True End With End If Next End Sub
改めて見直すと、ブサイクなコードです。。。(´・ω・`)ショボーン
まずは、(*)のところ。実はこれがメインの処理だったりする。
With Selection.Range.Characters(i).Font '……(*)' .Name = FONT_TO_USE .Italic = True End With
まず、Withでまとめている
Selection.Range.Characters(i).Font
では、選択箇所のRangeオブジェクトの中にある文字列Charactersコレクションのi番目の文字のFontプロパティを呼び出して、Fontオブジェクトを取得している。
んでもって、次の2行、
.Name = FONT_TO_USE .Italic = True
でフォントの種類と斜体を設定する。
この処理を、それぞれの文字に対して実行するかどうかを切り替える、というやり方をしている。
ちなみに、「FONT_TO_USE」というのは定数で、リスト1の(1)で
Public Const FONT_TO_USE As String = "HG正楷書体-PRO"
と指定している。
(2)からの4行は、変数の宣言。
chr
Charactersコレクションから取得した1文字を入れる。(*)の処理を施すかどうかの条件判定に使用。別になくても良いが、コードを短くするために使っている。
i
おなじみループカウンタ。
numOfChar
選択中の文字数を格納する。Forループの上限値として使用。
flg
(*)の処理を行うかどうかを切り替えるためのフラグ。
(3)の
numOfChar = Selection.Range.Characters.Count
では、CharactersコレクションのCountプロパティを用いて文字数を取得し、変数numOfCharに格納。
ここからがForループの中身。
まず、(4)の
chr = Selection.Range.Characters(i)
で1文字を変数chrに格納。
(5)の
If flg = True And chr <> ")" Then
という条件分岐により、この段階でflgがTrueになっていたら、「)」でない限り(*)の処理を実行してしまう。
(7)のところで、「(」に出会ったらflgをTrueにするようにしているので、「(」に出会った直後のループでは、(*)を実行する。ただし、その直後はflgがTrueのままなので、放っておいたら「)」にまで(*)を実行してしまう。
それを防ぐための条件設定。うーーーむ、ブサイクすぎる。。。
(6)の
If chr = "月" Or chr = "日" Or chr = "(" Or chr = ")" Then
は、一番初歩的な条件設定。
「月」、「日」、「(」、「)」だったら何もしない、ということ。
ただし、めんどくさいのは、(*)の処理を施したい対象の中にも「月」、「日」という文字があること。言うまでもなく曜日を表す方の「月」、「日」だ。
幸い、曜日を表す方の「月」、「日」には、
「()」で括られている
という特徴があるので、
「(」に出会ったらスイッチオン、「)」に出会ったらスイッチオフ
というやり方にした。
……と書いているうちに、
カッコで括られているのは1文字って決まってるんだから、「(」に出会って次の1字に(*)の処理を施したら即flgをFalseにしたらいいんじゃね???
と思いついたというのは内緒だw
(7)、(8)の
If chr = "(" Then flg = True If chr = ")" Then flg = False
がスイッチ切り替え。
「(」に出会ったらスイッチオン、「)」に出会ったらスイッチオフ、というイメージ。
実行結果
こんなふうに、日付欄を選択して実行すると、
この通り、意図したとおりの結果となった。
おわりに
ツッコミどころ満載のコードだということは認めます。
正直、Wordのオブジェクト構造がイマイチ理解できていないので、アホみたいなコードになっていると思います。これを機に勉強しようとは思うものの、ホントにやるかどうかは分かりませんw
ただ、ちょい書きでこのぐらいできたら、しちめんどくさい作業をせずに済む(しかも、コードを書くのは楽しいので、しょうもないはずの作業が楽しくなる)ので、まあええかな、と。
Wordの表の各セルの文字列を利用しやすくする(3)
WordTableCreatorクラスの修正
id:mmYYmmdd さんからのコメント
前回の記事に、id:mmYYmmdd さんからコメントをいただいた。
全く以て仰せのとおり、というところなので、アドヴァイスに沿ってコードを修正する。
雑な配列の定義を改める
まずは、
ReDim tableArray_(1 To maxRow_, 1 To maxColumn_) とするかクラスモジュールの先頭に Option Base 1 を宣言するかしないと0番目の要素が余分になってしまいます
正直、配列を使い始めたのが割と最近だったもので、扱いが雑でした。「動きゃいい」という感覚でやっていたことは否めません。やはり、きっちりとしないといけませんな。
というわけで、二次元配列の要素数が確定した後、ReDimするところを次のように訂正する。
リスト1
ReDim tableArray_(1 To maxRow_, 1 To maxColumn_)
表のセル番地との対応を優先して1始まりにしたんですが、後々のことを考えたら0始まりの方がいいのかも……。このあたりは経験不足で何とも……。
配列をまるごと取り出すメソッドの導入
こちらについては、
テーブル丸ごと出力する関数を用意すれば一発でExcelワークシートに転記できる
とのことで、「あ、その手があったか」と。
クラスのプロパティを配列変数のように扱えないのなら、クラス内の配列変数(「今回の場合はtableArray_()」)を返り値にするメソッドを作ればいいわけだ。
そこで、次のようなメソッドを作った。
リスト2
Public Function getArray() As Variant '……(*)' If hasArray_ = True Then getArray = tableArray_ End If End Function
(*)では、返り値の型をVariantにしている。要素数がその都度異なるので、Variantで受けるのが一番楽だと思った。
ちなみに、「hasArray_」という変数は、クラスのインスタンスが無事に表のデータを取得したらTrueにするように新たに持たせた変数。
必要なのかどうかよく分からなかったけど、一応持たせてみた。
動作確認
標準モジュールに下記のコードを書いて実行してみた。
Public Sub testTableArray() Dim wtOperator As WordTableOperator Set wtOperator = New WordTableOperator wtOperator.createArrayFromTable 2, True With wtOperator ActiveSheet.Range("A1").Resize(.maxRow, .maxColumn).Value = .getArray End With End Sub
前回同様、
このWordドキュメントをアクティブにして実行。
実行結果
おおっ! ちゃんとできとる!
これで、だいぶ転記の手間が省けるなあ。
おわりに
クラスのプロパティを配列そのものにするのは無理っぽいんですが、クラスの内部で保持している配列をそのまま返すようなメソッドを書けばいい、というのは素人にはなかなか出てこない発想でした。
id:mmYYmmdd さん、ありがとうございました!!!!!!!!
Wordの表の各セルの文字列を利用しやすくする(2)
クラスのプロパティに二次元配列を持たせてみる
表の内容をそのまま配列にする
「表」ということは、二次元配列と同じ形なんである。
そこで、
クラスのプロパティを二次元配列にする
ことを試みた。
クラスの改造
まず、前回記事のリスト1のうち、フィールド・アクセサ部分を以下のように書き換える。
リスト1-1
'フィールド' Private wordApp_ As Word.Application Private wordDoc_ As Word.Document Private wordTable_() As Word.Table Private tableArray_() As String '……(1)' Private maxRow_ As Integer '……(2)' Private maxColumn_ As Integer Private isReady_ As Boolean 'アクセサ' Public Property Get wordApp() As Word.Application Set wordApp = wordApp_ End Property Public Property Get wordDoc() As Word.Document Set wordDoc = wordDoc_ End Property Public Property Get wordTable(ByVal i As Integer) As Word.Table Set wordTable = wordTable_(i) End Property Public Property Get tableArray(ByVal r As Integer, _ ByVal c As Integer) As String '……(3)' tableArray = tableArray_(r, c) End Property Public Property Get maxRow() As Integer maxRow = maxRow_ End Property Public Property Get maxColumn() As Integer maxColumn = maxColumn_ End Property Public Property Get isReady() As Boolean isReady = isReady_ End Property
(1)の
Private tableArray_() As String
では、新たにtableArray_()というString型の配列変数を準備。
さらに、(2)からの2行
Private maxRow_ As Integer Private maxColumn_ As Integer
でmaxRow_及びmaxColumn_という変数を準備した。この2つの変数には、取り扱うWordの表の行数と列数を格納する。
(3)の
Public Property Get tableArray(ByVal r As Integer, _ ByVal c As Integer) As String tableArray = tableArray_(r, c) End Property
は、tableArrayの値にアクセスするためのアクセサメソッド。
二次元の配列なので、引数が2つ必要。
そして、メソッドも1つ追加する。
リスト1-2
Public Sub createArrayFromTable(ByVal tableNum As Integer, _ ByVal hasHeader As Boolean) '……(1)' On Error GoTo ErrorCatch Dim startRow As Integer '……(2)' If hasHeader = True Then startRow = 2 If hasHeader = False Then startRow = 1 With wordTable_(tableNum) '……(3)' maxRow_ = .Rows.Count '……(4)' maxColumn_ = .Columns.Count ReDim tableArray_(maxRow_, maxColumn_) '……(5)' Dim iRow As Integer '……(6)' Dim iColumn As Integer Dim str As String Dim n As Integer n = 1 For iRow = startRow To maxRow_ '……(7)' For iColumn = 1 To maxColumn_ '……(8)' str = .Cell(iRow, iColumn).Range.Text '……(9)' tableArray_(n, iColumn) = Left(str, Len(str) - 2) '……(10)' Next n = n + 1 '……(11)' Next End With Exit Sub ErrorCatch: End Sub
まずは(1)の
Public Sub createArrayFromTable(ByVal tableNum As Integer, _ ByVal hasHeader As Boolean)
でお分かりの通り、2つの引数を受け取るようにしている。
- 第1引数:Wordドキュメント内の表の番号
- 第2引数:1行目がラベル行なのかどうか
の2つを受け取って実行する。
(2)からの3行、
Dim startRow As Integer If hasHeader = True Then startRow = 2 If hasHeader = False Then startRow = 1
では、引数hasHeaderの値によって変数startRowの値を切り替えている。
たとえば、hasHeaderがTrueであるということは、表の1行目は項目ラベルだということだから、startRowを「2」にして、表の2行目から値を取得しよう、というわけ。
(3)で
With wordTable_(tableNum)
このようにしているので、この後、End WithまではwordTable_(tableNum)に格納されたTableオブジェクト、すなわち「tableNum番目の表」が処理の対象となる。
(4)からの2行、
maxRow_ = .Rows.Count maxColumn_ = .Columns.Count
では、TableオブジェクトのRows、ColumnsコレクションのCountプロパティを用いて、変数maxRow_、maxColumn_に表の行数・列数をセットしている。
(5)では、(4)で取得した表の行数・列数を用いて
ReDim tableArray_(maxRow_, maxColumn_)
tableArray_()をReDimしている。
さて、ここからがこのメソッドの中心。
まず、(6)からの5行、
Dim iRow As Integer '……(6)' Dim iColumn As Integer Dim str As String Dim n As Integer n = 1
は、変数の準備。
iRow
Forループ(外側)のカウンタとして使用。Wordの表の行数指定を兼ねる。
iColumn
Forループ(内側)のカウンタとして使用。配列二次元目のインデックス、及びWordの表の列数指定を兼ねる。
str
表の各セルの文字列の受け取りに使用。
n
配列一次元目のインデックスの指定に使用。
Wordの表から値を取得するとき、1行目が項目ラベルなのかどうかによって、1行目から値を取得しはじめる場合と2行目から値を取得しはじめる場合の2通りがあるので、Forループのカウンタ以外に別途Wordの表の行数を指定するための変数nを準備し、「1」で初期化している。
そして、このメソッドの処理の中心が(7)からの7行。
For iRow = startRow To maxRow_ '……(7)' For iColumn = 1 To maxColumn_ '……(8)' str = .Cell(iRow, iColumn).Range.Text '……(9)' tableArray_(n, iColumn) = Left(str, Len(str) - 2) '……(10)' Next n = n + 1 '……(11)' Next
Forループがネストしているので、ちょっと見づらいかも知れないが、行方向のループと列方向のループだけなので、許容範囲だと思う。
まず、(7)、
For iRow = startRow To maxRow_
行方向のループ指定だが、開始値を変数startRowにしているのがミソ。
言うまでもなく、表の1行目から読み取る場合と2行目から読み取る場合の2種類に対応するためだ。
次の(8)、
For iColumn = 1 To maxColumn_
は、列方向のループ。各行につき、1列目から右へ右へと値を取得しては配列に格納していく、というイメージ。
(9)の
str = .Cell(iRow, iColumn).Range.Text
で、セルの文字列をstrに格納し、
(10)の
tableArray_(n, iColumn) = Left(str, Len(str) - 2)
で「ハナクソ」と改行文字を除去した上で配列にセットしている。
1行分セットし終えたら、すなわち、(8)のForループが完了したら、次の行に進むために、(11)の
n = n + 1
で変数nをインクリメントする。
このようにすれば、表内の全ての値が配列tableArray_()セットされるはずだ。
動作確認
このクラスの動作確認用に、次のコードを標準モジュールに書く。
リスト2
Public Sub testTableArray() Dim wtOperator As WordTableOperator Set wtOperator = New WordTableOperator wtOperator.createArrayFromTable 2, True '……(1)' Dim iRow As Integer Dim iColumn As Integer With wtOperator '……(2)' For iRow = 1 To .maxRow For iColumn = 1 To .maxColumn ActiveSheet.Cells(iRow, iColumn).Value = _ .tableArray(iRow, iColumn) Next Next End With Debug.Print wtOperator.tableArray(3, 3) '……(3)' End Sub
(1)の
wtOperator.createArrayFromTable 2, True
では、createArrayFromTableメソッドを、引数「2」と「True」の2つを渡して実行。
日本語訳すると、「2番目の表の値を、1行目が項目ラベルであるとみなして二次元配列としてプロパティに格納せよ」ぐらいか。
(2)からの8行(正味7行)、
With wtOperator For iRow = 1 To .maxRow For iColumn = 1 To .maxColumn ActiveSheet.Cells(iRow, iColumn).Value = _ .tableArray(iRow, iColumn) Next Next End With
は、もはや説明不要だろう。
二重のForループを用いて、tavleArrayプロパティにセットした値をアクティブシートに書き込んでいるだけだ。
あと、(3)の
Debug.Print wtOperator.tableArray(3, 3)
は、二次元のインデックスを渡して値を取得する例。
実行結果
このWordドキュメントがアクティブの状態で実行した。
Excelシート上に各値が転記されている。
イミディエイト・ウインドウには、2番目の表の3行3列目の値が表示されている。
おわりに
せっかく二次元配列として表のデータを取得しているのだから、tableArrayプロパティに表のデータを読み込ませた後、
With wtOperator ActiveSheet.Range("A1").Resize(.maxRow, .maxColumn).Value = .tableArray End With
とでも書けば、一発でExcelワークシートに転記できそうなものだが、
こんなふうにコンパイル・エラーになって、実行すらさせてくれない。
プロパティはあくまでもプロパティであって、配列変数ではない、ということなのかなあ?
Wordの表の各セルの文字列を利用しやすくする
Wordの表の中の文字列を取得するクラス
WordTableOperatorクラス
クラスモジュールを挿入して、オブジェクト名を「WordTableOperator」にした。
とりあえず、次のようなコードを書いた。
リスト1
Option Explicit 'フィールド' Private wordApp_ As Word.Application Private wordDoc_ As Word.Document Private wordTable_() As Word.Table Private isReady_ As Boolean 'アクセサ' Public Property Get wordApp() As Word.Application Set wordApp = wordApp_ End Property Public Property Get wordDoc() As Word.Document Set wordDoc = wordDoc_ End Property Public Property Get wordTable(ByVal i As Integer) As Word.Table '……(1)' Set wordTable = wordTable_(i) End Property Public Property Get isReady() As Boolean isReady = isReady_ End Property 'コンストラクタ' Private Sub Class_Initialize() On Error GoTo ErrorCatch Set wordApp_ = GetObject(, "Word.Application") '……(2)' Set wordDoc_ = wordApp_.ActiveDocument '……(3)' Dim i As Integer ReDim wordTable_(wordDoc_.Tables.Count) '……(4)' For i = 1 To UBound(wordTable_) '……(5)' Set wordTable_(i) = wordDoc_.Tables(i) Next isReady_ = True '……(6)' Exit Sub ErrorCatch: '……(7)' isReady_ = False End Sub 'メソッド' Public Function getTextFromCell(ByVal tableNum As Integer, _ ByVal rowNum As Integer, _ ByVal colNum As Integer) As String '……(8)' On Error GoTo ErrorCatch Dim str As String str = wordTable_(tableNum).Cell(rowNum, colNum).Range.Text '……(9)' getTextFromCell = str '……(*)' Exit Function ErrorCatch: getTextFromCell = "" '……(10)' End Function
少し長くなった。ちょっと説明をば。
アクセサのところの(1)、
Public Property Get wordTable(ByVal i As Integer) As Word.Table Set wordTable = wordTable_(i) End Property
については、
コチラを参照。プロパティを配列にしている。
この場合は、Wordドキュメント内にある表を、配列として保持するようにしている。
さて、今回は珍しくコンストラクタを使う。
WordTableOperatorクラスのインスタンスが生成される時点でアクティブになっているWordアプリケーション及びドキュメントをセットしてしまうことにする。
まず、(2)の
Set wordApp_ = GetObject(, "Word.Application")
では、Wordアプリケーションのインスタンスを変数にセットするのに、GetObject関数を用いている。
このときにも使った方法だが、既に開いているWordアプリケーションを取得するため、このやり方にしている。
(3)の
Set wordDoc_ = wordApp_.ActiveDocument
で、現在アクティブになっているWordドキュメントを変数にセット。
この段階で、Wordドキュメント内にいくつの表があるかは判明しているので、(4)の
ReDim wordTable_(wordDoc_.Tables.Count)
で、配列用の変数wordTable_()を表の数でReDimしている。
(5)からの3行、
For i = 1 To UBound(wordTable_) '……(5)' Set wordTable_(i) = wordDoc_.Tables(i) Next
では、Wordドキュメント上の表を、配列wordTable_()に格納している。
Wordドキュメント上の表は、DocumentオブジェクトのTablesコレクションで取得できるので、Tablesコレクションのインデックスに1から順に数字を入れていけばそれぞれのTableオブジェクトが取得できる、という仕掛けだ。
続いて(6)。ここまでたどり着いたということは、エラーが出ていないということになるので、ここで
isReady_ = True
としてisReadyプロパティをTrueにしてやる。isReadyがTrueだということは、表が取得できているということなので、メインのコードで条件分岐に使うことができる。
なお、ここまでの過程でエラーが出ていたら、(7)の
ErrorCatch: isReady_ = False
に飛んでくるので、isReadyプロパティをFalseにして終了。
あとはメソッド。とりあえず1つだけにしている。
どうでもいいけど、「getTextFromCell」って、PANTERAの"Cowboys From Hell"みたいだな、オイw
(8)の
Public Function getTextFromCell(ByVal tableNum As Integer, _ ByVal rowNum As Integer, _ ByVal colNum As Integer) As String
を見たら分かるように、
- 第1引数:表の番号
- 第2引数:表内の行番号
- 第3引数:表内の列番号
の3つを渡すと、セル内の文字列を返す、という形にしている。
(9)の
str = wordTable_(tableNum).Cell(rowNum, colNum).Range.Text
で指定したセルから文字列を取得。
なお、途中でエラーが出たら、(10)の
getTextFromCell = ""
で""を返すようにした。
動作確認
標準モジュールに次のコードを書いて、動作確認した。
リスト2
Public Sub testWordTable01() Dim wtOperator As WordTableOperator Set wtOperator = New WordTableOperator Dim str As String If wtOperator.isReady = True Then str = wtOperator.getTextFromCell(1, 2, 2) '……(1)' Debug.Print "表1の2行2列目セル内の文字列は、" & _ Len(str) & "字ですわ。" Debug.Print "右端の文字のAsciiコードは、" & _ getAsciiCodeOfChar(Right(str, 1)) & "番でんねん。" '……(2)' Debug.Print "右端から2番目の文字のAsciiコードは、" & _ getAsciiCodeOfChar(Mid(Right(str, 2), 1, 1)) & "番だすな。" '……(3)' End If Set wtOperator = Nothing End Sub Private Function getAsciiCodeOfChar(ByVal objStr As String) As Integer Dim i As Integer For i = 0 To 255 If Chr(i) = objStr Then getAsciiCodeOfChar = i Exit Function End If Next getAsciiCodeOfChar = 266 End Function
(1)の、
str = wtOperator.getTextFromCell(1, 2, 2)
では、getTextFromCellメソッドに引数(1, 2, 2)を渡しているので、
1番目の表の2行2列目のセルに入っている文字列を寄こせや!
ということになる。んで、得られた文字列を変数strにセットしている。
ちなみに、
1番目の表の2行2列目のセルに入っている文字列
ってのは、見かけ上は
「吉岡 稔真」です。
(2)の
getAsciiCodeOfChar(Right(str, 1))
では、getAsciiCodeOfCharメソッドに、(1)で得られた文字列(str)の右端の文字の文字コード番号を取得している。
これはまあ、Right関数だけだから簡単。
(3)の
getAsciiCodeOfChar(Mid(Right(str, 2), 1, 1))
がちょっとややこしい。
まず、
Right(str, 2)
で右端の2文字を抜き出して、その2文字に対してMid関数を使うことで、
右端の2文字の1文字目
すなわち、
右端から2文字目
を取得している。
まあ、
Mid(str, Len(str) - 1, 1)
でもいいですね。ハイ。
実行結果
こうなった。
指定のセル内の文字列は、見かけ上は「吉岡 稔真」の5文字(全角スペース含む)のはずだが、7文字となっている。
6文字目の文字コードが「7」、7文字目の文字コードは「13」となっている。
再びコチラによると、
ということなので、
Wordのセル内の文字列の末尾には、「ハナクソ」と「改行文字」がくっついている
ということらしい。
最後に
ということは、リスト1の(*)のところを
str = Left(str, Len(str) - 2)
とするだけで良いということになるなあ。
あとは、このクラスの使い勝手をいかに上げるか、だな。
「ゼロ文字目を返せや、コラ!」という無理難題にVBAさんはどう答えるのか
Left関数、Right関数の第2引数
Left関数、Right関数の第2引数が「0」だったらどうなるのか
やってみた。
イミディエイト・ウインドウに次のコードを入力して[Enter]を押す。
リスト1-1
?Left("吉岡 稔真",0)
リスト1-2
?Right("吉岡 稔真",0)
実行結果
どうも、第2引数に「0」を指定すると""が返されるみたいです。
結論
「左(右)から0文字目を返せや、コラ!」みたいな無理難題に対して、エラーを返すんではなくて、笑顔で(?)""を返すというのは、ありがたい仕様ですね。