VisalBasicで一対比較アンケートを作ってみた。(解説編)

さて、「VisalBasicで一対比較アンケートを作ってみた。(紹介編)」の解説編です。とは言いましても、このプログラムの肝は

  • 外部のtxtファイルをもとに評価項目数を自動で計算し、
  • その提示順番をランダムに並び替えて提示する。

の2つだと思いますから、そこを重点的に解説しようと思います。

VBの配列について

その前に、まずVBの癖である「配列」についてします。
VBの配列はインデックスのつけ方が他のメジャーなプログラムと比べて違うので、注意してください。
とりあえずC言語系、数学系と比較した表を見せます。

C言語系(C, C++, Java等) 数学系(MatLab, R等) VB
配列の宣言例 int hoge[5]; hoge=zeros(5); Dim hoge(5) as Integer
配列の最初の値 hoge[0] hoge(1) hoge(0)
配列の最後の値 hoge[4] hoge(5) hoge(5)
配列の長さ 5 5 6

これを見てもらうとわかるのですが、VBで配列を宣言するときにカッコ内に入れる整数Nは
0からNまでの配列を作る
という意味であり、結果できるのは
素数(N+1)の配列
なわけです。正直言って「クソ仕様」以外の何者でもないと思うのですが、VBの開発者はそのお節介精神を鑑みるに
『こうすればC言語系ユーザも数学系ユーザも使えるでしょ?余計なメモリを消費するけど、これくらいいいでしょ?』
とでも考えていそうな気がしないでもないです。
でもそんな使い方をしたら、今度は配列の長さを使うときにいちいち-1しなければならなくなります。
結局、この特異的な仕様を受け入れて、適宜+1したり-1したりしなければならないのだと思います。

txtデータから評価項目およびその個数の読み込み

以上の知識を踏まえた上で、さっそくプログラムの中身を解説していきます。
まずは、外部のテキストデータから評価項目を読み込み、その個数を得るところまでです。

Dim sr As New StreamReader("items.txt", Encoding.GetEncoding("Shift_JIS"))  'StreamReaderの宣言
While (sr.Peek() >= 0)                          '読み込む行がなくなるまでEnd Whileまでを繰り返す
    Dim buf As String = sr.ReadLine             '1行読み込み、ひとまずbufに格納
    If items(items.Length - 1) = Nothing Then   'もし配列itemsの最後の要素に何も入っていなければ
        items(items.Length - 1) = buf           'そこにbufを入れて
    Else                                        'そうじゃないのならば
        ReDim Preserve items(items.Length)      '配列itemsの長さを1つ長くして空の要素を作り
        items(items.Length - 1) = buf           'そこにbufを入れます
    End If
End While
sr.Close()                                      '宣言したStreamReaderを閉じる
Num = items.Length                              'Numに配列itemsの長さを入れる

上のプログラムで出てくる変数のうち、itemsとNumはプロシージャの外でグローバス変数として宣言しています。
ついでなので、このプログラムで使っている4つのグローバル変数を以下にあげておきます。

Public items(0) As String
Public Pairs(0, 0) As String
Public Num As Integer
Public Matrix(0, 0) As Double

配列の大きさを変える時にPreserveと書いておくのは、これを書かないと配列の大きさを変えたときに
配列の中身がすべて消えてしまうからです。配列を付け足して使う場合は必ずPreserveを付記しましょう。

評価項目テーブルを作る

さて、このプログラム最大の肝である、評価項目テーブルの解説に入ります。
何がしたいのかというと、
「評価項目N個から一対比較の比較ペアN(N-1)個を作り、それをランダムで提示できるようにしたい。」
という、ただそれだけです。そのためには以下のような表があればいいと考えました。

提示順 評価項目1 評価項目2
1 りんご みかん
2 バナナ パイナップル
3 なし みかん
20 パイナップル バナナ

ですがいちいち評価項目をランダムに入れるのは大変なので、評価項目を番号で扱うことにし、
そこに対応関係をつけておくことで解決をはかっておこうとしました。こんな感じに:

提示順 評価項目1 評価項目2
1 1 2
2 3 4
3 5 2
20 4 3

評価項目とその番号の関係はちなみにこんな風:

評価項目 番号
りんご 1
みかん 2
バナナ 3
パイナップル 4
なし 5

さて、後は評価項目の組み合わせですが、どうすれば簡単なコードで書けるかを考え、以下のようにしました:

  • SortedListを使い、1列目に乱数を入れる。(SortedListについては後で説明します)
  • 評価項目の組み合わせはN進数を用いて2ケタの数字を作ることで対処する。

以上を踏まえて、実際のコードを見てみましょう。

Dim sl As New System.Collections.SortedList                          'SortedListを宣言
Randomize()                                                          '乱数を使用するにあたり乱数種を生成
For i = 0 To Num * Num - 1                                           '評価項目ペアの数だけNextまで繰り返す
    If i Mod Num <> System.Math.Floor(i / Num) Then                  '※欄外コメント参照
        sl.Add(Rnd(), (i Mod Num) + System.Math.Floor(i / Num) * 10) '※欄外コメント参照
    End If
Next

ReDim Pairs(Num * (Num - 1) - 1, 2 - 1)                              '評価項目ペアを入れる配列を宣言
For i = 0 To sl.Count - 1                                            '評価項目ペアの数だけNextまで繰り返す
    Pairs(i, 0) = items(System.Math.Floor(sl.GetByIndex(i) / 10))    '1列目に一の位の番号に対応する項目を入れる
    Pairs(i, 1) = items(sl.GetByIndex(i) Mod 10)                     '2列目に十の位の番号に対応する項目を入れる
Next

欄外コメント参照という箇所を2つ作りましたが、ここではN進数的な考え方を使って2ケタの数字を作っています。
2行あるうちの2行目が、重要な部分です。

sl.Add(Rnd(), (i Mod Num) + System.Math.Floor(i / Num) * 10)

SortedListに1行追加し、その1列目には乱数を、2列目には2ケタの整数を入れています。
この2ケタの整数、C言語系がわかる人向けにCで書き直すと

i % Num + (i / Num) * 10

ということです。エクセルの関数を用いて書くならば

Mod(i,Num)+Rounddown(i/Num,0)*10

てなところでしょうか。つまり、
iをNumで割ったときの商を十の位に、余りを一の位にした2ケタの整数を生成
しているわけです*1。先程まで見せていた表を用いるなら、こういうものを作っているわけです:

提示順 評価項目1+評価項目2
1 01
2 02
3 03
4 04
5 10
6 12
19 42
20 43

この表を見ていただくとわかるとおり、生成している2ケタの整数にはゾロ目(00や11)はありません。
それは、数字を生成する前に、ゾロ目はIf文ではじいているからです。それが前半の

If i Mod Num <> System.Math.Floor(i / Num) Then

です。ゾロ目にならないときに、数字を生成するようになっているわけですね。

SortedListのお話

最後にSortedListの話をして、この記事を終えたいと思います。
上の表を見ると、評価項目の番号が小さい順に並んでいます。
2ケタの数字を作る際に使ったiはFor文で0からNum*Num-1まで回しているのだから当然です。
ですから、これを最後にランダムな順序に変えなければなりません。ハッシュテーブルにしなければならないわけです。
そこでSortedListの出番です。これは、アイテムが追加されるたびに、SortedListの1列目の基準にソートを自動でしてくれる、
そのような機能を持つ特殊な配列と考えるとよいでしょう。
今回のプログラムでは1列目に乱数を入れていますから、結果として2列目をランダムに並び替えていることになります。
こうして、重複することなくランダムな順序で評価項目番号を並べることができました。
あとは、ランダムに並び替えた2ケタの評価項目番号を基に、配列Pairsに2つの評価項目を格納すれば目的達成です。

*1:このような仕組みのため、実は仕様上評価項目数は10個が限界となっています。
それ以上評価したいって?
でしたらこのプログラム上で10と書いてある部分を100にしていただければ、100個まで評価可能になっています。
しかし評価項目数が10個の段階で、比較回数は10×9=90回ですけど、そんな大変な実験大丈夫ですか?