乱数を格納した配列を作るFunction

文字をランダムに並べ替える

乱数を作るのはめんどくさい

ランダムに並べ替えるという作業をするときには、乱数を発生させて使えば良いのだが、毎度毎度乱数を発生させる処理を書くのは正直メンドクサイ。

最大数を与えたら、1~最大数をランダムに並べて配列にぶち込んでくれるような関数でもあれば、その配列を0~最大数マイナス1の順で呼び出してコレクションのインデックスにすることによって、コレクションをランダムに並べ替えて出力することが可能になると考えた。

ランダムに並べ替えて配列にぶち込むFunction

リスト1 標準モジュール
Public Function createRandomArray( _
                  ByVal maxNum As Integer, _
                  ByVal allowDuplicate As Boolean) _
                    As Variant    '……(1)'
  Dim flg() As Boolean
  ReDim flg(maxNum - 1)    '……(2)'
  Dim i As Integer
  Dim retArray() As Integer    '……(3)'
  ReDim retArray(maxNum - 1)
  Randomize
  Dim tmp As Integer
  For i = 0 To maxNum - 1
    Do
      tmp = Int(maxNum * Rnd + 1)    '……(4)'
    '///乱数:Int((最大値 - 最小値 + 1) * Rnd + 最小値)'
    Loop Until flg(tmp - 1) = False    '……(5)'
    retArray(i) = tmp    '……(6)'
    If Not allowDuplicate Then flg(tmp - 1) = True    '……(7)'
  Next
  createRandomArray = retArray    '……(8)'
End Function

ちょっとめんどくさいけれど、自分の備忘のためにも説明を書いておく。

(1)の

Public Function createRandomArray( _
                  ByVal maxNum As Integer, _
                  ByVal allowDuplicate As Boolean) _
                    As Variant

引数maxNumは最大数。たとえば、こいつを10にしたら、1~10までをランダムに取り出して要素数10の配列にぶち込んでいくということ。

引数allowDuplicateは、番号の重複を許可するかどうか。Trueにすると重複無しで配列を作成。Falseにすると重複ありで配列を作成することになる。Falseを指定する場面があるのかどうかは不明。

返り値の型はVariantにした。最初Integerにしていたんだけれど、「型が一致しません」エラーが出て対応策が分からなかったので。

(2)の

ReDim flg(maxNum - 1)

では、引数で渡されたmaxNumを用いて配列変数flgをRedimしている。はじめから

Dim flg(maxNum - 1) As Boolean

でうまく行きそうなもんだが、こうすると「定数式が必要です」エラーが出る。

(3)の

Dim retArray() As Integer

は、返り値用の配列変数。createRandomArrayを配列みたいにして直接値をぶち込んで行くことができないので、一旦配列を作っておいて、完成後この配列を返す、という形を取る。

(4)の

tmp = Int(maxNum * Rnd + 1)

で、一旦1~maxNumの範囲の整数をランダムに生み出して変数tmpに格納する。

(5)の

Loop Until flg(tmp - 1) = False

でDo~Loopの終了条件を指定している。

たとえば、tmpに10が入っているとすると、flg(10-1)、すなわち配列変数flg()の10番目の要素がfalseだったらループを抜けるということ。

ループを抜けると、(6)の

retArray(i) = tmp

で配列変数retArrayにtmpの値をぶち込み、(7)の

If Not allowDuplicate Then flg(tmp - 1) = True

で、重複を許可しない場合に限ってflg(tmp - 1)、すなわちtmpが10の場合は配列変数flg()の10番目をTrueに変える。

こうすることで、この後仮に(4)で10がtmpに代入されたとしても、(5)のループ終了条件を満たさなくなる。すなわち、この後10が配列変数retArrayの要素になることはないということ。

こうして、Forループが終了すると配列変数retArrayには1~maxNumまでの整数がランダムにぶち込まれていることになるので、後は(8)の

createRandomArray = retArray

で配列retArrayを返しておしまい。

実行

標準モジュールに下記のコードを書いて実行してみる。

スト2 標準モジュール
Public Sub test()
  Dim a As Variant
  a = createRandomArray(10, False)
  Dim i As Integer
  For i = 0 To 9
    Debug.Print a(i)
  Next
End Sub

f:id:akashi_keirin:20171021230753j:plain

この通り、無事に1~10が重複無しのランダムに並んでいる。

選択範囲の単語をランダムに並べ替える

自作Function「createRandomArray」を利用して、選択範囲の単語をランダムに並べ替えるマクロを作ってみる。

リスト3 標準モジュール
Public Sub randomSortByWord()
  Dim num As Long
  Dim wordsArray() As String
  With Selection
    num = .Words.Count    '……(1)'
    ReDim wordsArray(num - 1)    '……(2)'
    Dim i As Integer
    For i = 0 To num - 1    '……(3)'
      wordsArray(i) = .Words(i + 1)
    Next
  End With
  Dim wordsOrder As Variant
  wordsOrder = createRandomArray(num, False)    '……(4)'
  Dim str As String
  For i = 0 To num - 1    '……(5)'
    str = str & wordsArray(wordsOrder(i) - 1)
  Next
  Selection.TypeText Text:=str    '……(6)'
End Sub

(1)の

num = .Words.Count

では、変数numにSelectionオブジェクト(この場合は選択範囲)のWordsコレクションのCountプロパティを参照することで選択範囲の「単語数」を取得し、変数numにぶち込んでいる。

(2)では、(1)で得られた単語数をもとに配列変数wordsArray()をRedim。

(3)からの3行

For i = 0 To num - 1    '……(3)'
  wordsArray(i) = .Words(i + 1)
Next

で、一旦選択範囲の各単語を配列に格納。配列のインデックスは0から始まるけれど、Wordsコレクションのインデックスは1から始まるので、Wordsコレクションのインデックスのところは「i + 1」になる。

(4)の

wordsOrder = createRandomArray(num, False)

では、単語を取り出す順番を格納する配列変数wordsOrderにcreateRandomArray関数の返り値を格納。

これで配列変数wordsOrderには1~単語数のそれぞれの数字がランダムな順番で格納されることになる。

(5)からの3行

For i = 0 To num - 1    '……(5)'
  str = str & wordsArray(wordsOrder(i) - 1)
Next

では、変数strに配列変数wordsArrayに格納されている単語を1つづつ配列変数wordsOrderの要素で指定して取り出し、連結していく。

Forループが終了した時点で、strには、単語をランダムに並べ替えた文字列が完成していることになる。

んで最後に(6)の

Selection.TypeText Text:=str

で、SelectionオブジェクトのTypeTextメソッドを用いてstrに格納された文字列を書き込んでおしまい。

SelectionオブジェクトのTypeTextメソッドは、文字列が選択された状態で実行すると、選択範囲を引数Textで指定された文字列で上書きする。

実行結果

f:id:akashi_keirin:20171021230805j:plain

こんなふうに文字列を選択状態にして実行すると、

f:id:akashi_keirin:20171021230815j:plain

このようになる。

悲しいかな、「単語」と言っても、「Wordが認識する単語」に過ぎず、結果はめちゃくちゃである。

おわりに

今回はFunctionでやってみたが、Functionの返り値がVariant型になってしまったり、その結果、呼び出す側でも変数をVariantにしないといけないというのは何ともブサイクなので、イマイチだなあと思ってしまう。

クラスにした方がキレイに書けるかも知れない。

相変わらず使い道があるのかどうかはよく分からないw

@akashi_keirin on Twitter

追記

例によってid:imihito さんからアドヴァイスをいただいた。

返り値の型を
`As Integer()`
のように後ろに丸括弧を付けた形にすることで指定した型の配列を返すことができます。

とのこと。

早速、上掲のリスト1リスト2を、次のように書き換えてみた。

リスト1改 標準モジュール
Public Function createRandomArray( _
                  ByVal maxNum As Integer, _
                  ByVal allowDuplicate As Boolean) _
                    As Integer()    '……(*)'
  Dim flg() As Boolean
  ReDim flg(maxNum - 1)
  Dim i As Integer
  Dim retArray() As Integer
  ReDim retArray(maxNum - 1)
  Randomize
  Dim tmp As Integer
  For i = 0 To maxNum - 1
    Do
      tmp = Int(maxNum * Rnd + 1)
    '///乱数:Int((最大値 - 最小値 + 1) * Rnd + 最小値)'
    Loop Until flg(tmp - 1) = False
    retArray(i) = tmp
    If Not allowDuplicate Then flg(tmp - 1) = True
  Next
  createRandomArray = retArray
End Function

(*)のところで、返り値の型指定を

As Integer()

にしました。

スト2改 標準モジュール
Public Sub randomSortByWord()
  Dim num As Long
  Dim wordsArray() As String
  With Selection
    num = .Words.Count
    ReDim wordsArray(num - 1)
    Dim i As Integer
    For i = 0 To num - 1
      wordsArray(i) = .Words(i + 1)
    Next
  End With
  Dim wordsOrder() As Integer    '……(*)'
  wordsOrder = createRandomArray(num, False)
  Dim str As String
  For i = 0 To num - 1
    str = str & wordsArray(wordsOrder(i) - 1)
  Next
  Selection.TypeText Text:=str
End Sub

こちらも(*)の部分、createRandomArrayの返り値を受け取る変数wordsOrderの宣言を

Dim wordsOrder() As Integer

このようにIntegerの配列型にしました。

結果

無事、実行できました。

毎度毎度のことながら、id:imihito さん、

ありがとうございました!!!!!!!!(*´∀`)