滝の音

滝の音

名こそ流れてなお聞こえけれ

VBAで暗号を実装しよう その3 ポリュビオス暗号

  • はじめに

さてさて。 「暗号を実装しよう」の第3段です。
記事のストックがないので大急ぎで書いています笑

今回はポリュビオス暗号。
全く聞いたことない笑

  • さっくと歴史

紀元前2世紀に古代ギリシアポリュビオスが発明。

5*5の25マス目にアルファベットを入れて
各アルファベットを記入する代わりに
そのマス目の行列番号を記述していく
タイプのものです。

行列 1 2 3 4 5
1 a b c d e
2 f g h I,j k
3 l m n o p
4 q r s t u
5 v w x y z

この表がフォーマルタイプ。

Go home を伝達したい場合は

書き手は 22 34 23 34 32 15
のように変換して

読み手は
表を見ながら Gohome と復元します。

これももっと詳しく知りたい方はググってきてください笑

  • とりあえず暗号化

96 93 13 35 21  48 45 17 46 76 54 56 93 43 81 28 54  72 24 93 43 21 56 36 39 75 32 92 57 57 63 51 92 99  26 64 54 81 51 76 76 35 19 28  87 35 76 81 14 91 81 73 27 44 36 19 72 26  14 57 44 67 81

わー。
きーもーちーわーるーいー。
ほんとにぞわぞわする。
自分は暗号に触れる適性がないのかな笑

これがポリュビオス暗号です。
本来は全角の空行は入らないでしょうが
原文の見やすさのために取り入れました。

  • 暗号について

またまた能書き。
この手のものは換字式暗号といいます。
「かんじ」じゃなくて「かえじ」って読むらしいです。

文字を何かに置き換えるってやつですね。
今回は文字を数字に置き換えています。

文字と数字が一対一対応になっているので
換字表を知っている人同士では簡単に読み書きできます。

じゃあ換字表を知らない人が解読するにはどうすればいいのか。

これって無理ゲーっぽいですよね。。 だって対応の起こりうるパターン数が多すぎるのだもの。
スキュタレー暗号のように物量作戦はむりですね。

じゃあ最強の暗号だ!!
と思いきや

これも現代ではすでにザルの暗号になっています。
頻度分析と呼ばれる手法によって。

これは
文字の出現頻度と暗号文での換字の出現頻度を対応付けて復元していくやり方です。

9世紀にすでに解読法が記録されているみたいですね。

でもポリュビオス暗号が生まれた時期が紀元前1世紀であることを考えると
記録上では800~900年くらいは解読不能(正確には難しい?)なツールだったのかもしれませんね。

  • 暗号化のコード

今回はまず換字表を用意する必要があるので
それを作成するコードから

Function pattern_make()
 
 Dim trgt As Range
 Dim t As Integer
 Dim list() As Variant
 Dim d(0 To 1) As Integer
 Dim rp As Range
 Dim div As Integer
'----------------------
'作業用シートで並び替えて
'listに書き込む
'----------------------
 Sheets.Add
 
 For i = 1 To 83
    Cells(i, 1) = Chr(-32097 + (i - 1))
    Cells(i, 2) = Rnd()
 Next
 
 t = Cells(1, 1).End(xlDown).Row
 
 Set trgt = Range(Cells(1, 1), Cells(t, 2))
 trgt.Sort Cells(1, 2), xlAscending
 
 list = trgt
 
 Application.DisplayAlerts = False
 ActiveSheet.Delete
 Application.DisplayAlerts = True
'----------------------------
'換字表を作成する
'----------------------------
 Set rp = Cells(11, 1)
 div = 9
 For i = 1 To UBound(list, 1)
    d(0) = (i - 1) \ div + 1
    d(1) = (i - 1) Mod div + 1
    rp.Offset(d(0), d(1)) = list(i, 1)
 Next
 
End Function

コードの中のdivという変数も結構だいじです。
だたこれは根性さえあれば暗号文から読み取れますが。

次にメインの関数

Sub polybius_coding()

 Dim str As String
 Dim str2 As String
'------------------
'strを読み込む
'------------------
  str = txt_input
'------------------
'strを暗号化
'------------------
 str2 = coding(str)
'------------------
'str2を記述
'------------------
 Call txt_output(str2)
 
End Sub

形式は前回のスタキュレー暗号とほとんど一緒ですね。
というか一緒になるようにしています。

このコードの肝は関数codingなのですが
それはもちろんスタキュレーとは違う仕様です。

以下詳細。

Function coding(str) As String

 Dim pattern() As Integer
 Dim char As String
 Dim num As Integer
 Dim ans As String
 ReDim pattern(1 To 83)
'------------------------
'換字表を読み込む
'------------------------
 pattern = pattern_input
'------------------------
'strを暗号化
'------------------------
 For i = 1 To Len(str)
    char = Mid(str, i, 1)

    num = Asc(char) + 32098
    If num < 1 Or 83 < num Then
        ans = ans & char
    Else
        ans = ans & pattern(num) & " "
    End If
 Next
 
 coding = ans
End Function

この中にもさらに関数pattern_inputが入っています。
それで換字表を認識して
それに沿った変換をそれ以降で行っています。

その中でも特にいい感じのコードはこれ。

If num < 1 Or 83 < num Then
    ans = ans & char
Else
    ans = ans & pattern(num) & " "
End If

これによって換字表に入っていない文字はそのままの形で暗号文に組み込まれます。
全角のスペースとか
“―”これとか “。”これとか
がそのまま読み込まれます。
やりたい人は
上の記号たちも換字表に入れちゃえばいいのかなと思います。
多分簡単にできると思いますよー。

  • 復元のコード

換字表を持っていない状態での復元には頻度分析を使うと思いますが
頻度分析のプログラミングは難しそうなので
今回はスルーします。

今回は換字表を持っている人が復元する方法だけで。

ちなみに換字表は以下の通り。

行列 1 2 3 4 5 6 7 8 9
1
2
3
4
5
6
7
8
9
10

まずはメイン

Sub polybius_restoration()
Sheets("ポリュビオス").Activate
 Dim str As String
 Dim str2 As String
'------------------
'strを読み込む
'------------------
  str = txt_input
'------------------
'strを復元
'------------------
 str2 = restoration(str)
'------------------
'str2を記述
'------------------
 Call txt_output(str2)

End Sub

次は関数coding

Function restoration(str) As String

 Dim pattern() As Integer
 Dim box As Variant
 Dim kaeji As Variant
 Dim d(0 To 1) As Integer
 Dim ans As String
 
 ReDim pattern(1 To 83)
'------------------------
'換字表を読み込む
'------------------------
 pattern = pattern_input
'------------------------
'strを復元
'------------------------
 box = Split(str, " ")
 kaeji = Range(Cells(12, 2), Cells(21, 10))
 
 For i = LBound(box) To UBound(box)
    If Val(box(i)) = 0 Then
        ans = ans & box(i)
    Else
        d(0) = Mid(box(i), 1, Len(box(i)) - 1)
        d(1) = Right(box(i), 1)
        ans = ans & kaeji(d(0), d(1))
    End If
 Next
 
 restoration = ans
End Function

復元した結果はこちらー。

こんかいは ぽりゅぴおすあんごうです にほんごはあるふぁべっととちがって もじすうがおおいので たいおうひょうをつくるのにも ひとくろう

またしてもやる価値のない暗号化でした笑

あ。
そういえば換字表はシート上に出力しているので
そのセル番号がこのコードで考慮されていないものになってしまうと
正しく変換されなくなってしまいます。
これはちょっとシステムとして脆弱かも。。

どうやって解決しようかなー。
たぶんメモ帳に出力することになるんだろうけど。
今回はパスで。。

  • まとめ

換字式の暗号は
換字表をどうやって共有するのかが肝ですね。
これを奪い取ることが一番楽な解読法な気がするもの笑

次回はまたちょっと「形式の修正」を行います。
関数の「txt_input」と「txt_output」に不満な部分があるので。。

具体的には
読み込み先、書き込み先のテキストファイルを選択できるようにします。

おわり。

VBAで暗号を実装しよう その2 暗号文をメモ帳で管理

  • はじめに

前回スキュタレー暗号を扱いましたが
書いたコードで不便な場所があったので
ひとつ「形式の修正」をします。

  • メモ帳を扱う

前回はシート上のセルから文字列を読み込んで
また別のセルに書き出していましたが
それだとちょっと不便かもしれないので
メモ帳から文字列を読み込んで
VBAで演算をして
結果をメモ帳に返す仕様にしましょう。

  • コード

まずはファイルから文字列を読み込んで
その値を返す関数です。

Function txt_input() As String
 Dim ans As String
 Dim file_name As String
 
 file_name = "ここにファイルのアドレスを載せます"
 Open file_name For Input As #1
    Line Input #1, ans
 Close #1
 txt_input = ans
End Function

次は演算した文字列データを
ファイルに書き出す関数です。

Function txt_output(str As String)

 Dim file_name As String
 
 file_name = "ここにファイルのアドレスを載せます"
 Open file_name For Output As #1
    Print #1, str
 Close #1

End Function
  • 組み込んでみよう

スキュタレー暗号に組み込んでみましょう。

関数をプログラムの中に組み込むことって
意外と難しいですよね。。

どこに組み込めばいいんだろう??
という風になりがち。

Sub Scytare_coding()
 Sheets("スキュタレー").Activate
 Dim str As String
 Dim str2 As String
'------------------
'strを認識
'------------------
 str = txt_input
'------------------
'strを暗号化
'------------------
 str2 = coding(str)
'------------------
'str2を記述
'------------------
 Call txt_output(str2)
End Sub

各関数は既出なので
気になる方は前回の投稿を見てください。

機能を分割しておくと
その中のどれかをちょっと書き換えるだけで済むから楽ですね~

  • まとめ

さっきも書いたのですが
機能を分割しておけば
仕様を変えたいときに変えやすいですね。
オブジェクト指向ではないのだろうけど
このやり方もまるっきり不便というわけではないかなと思っています。

VBAでパフォーマンスの良いライフゲームをつくろう その6 マシンの性能

  • はじめに

前回終わりを宣言したこのコーナーですが
ちょっと試してみたいことがあったのでもう一回だけ。

  • マシンを変えてみる

今まで使っていたのはMyノートパソコン
スペックはどうやって表現するのだろう
メモリは2Gです。

それに対して今回使ってみたのが
メモリが8Gのネカフェのパソコン。

Ver7とVer8_1を扱います。 F_sizeを100で計測。

ちなみにMyノートパソコンでの結果は前回の通り。

ノートパソコン ネカフェのパソコン
Ver7 Ver8_1 Ver7 Ver8_1
調査終了0.046875 調査終了 0.1411133 調査終了0.03125 調査終了 0.046875
更新終了0.328125 更新終了 0.21875 更新終了0.046875 更新終了 0.03125

ちょっとみにくいですかね。。
ネカフェのパソコンのほうが段違いのスピードを出しています。
Ver7もVer8_1も等しく高速になりました。

このマシンではf_sizeが100では速いってことしかわからないですね。 Ver7とVer8.1それぞれの強みがどのf_sizeまで通用するのか。
もうちょっとf_sizeを上げて測りますか。

Ver7 Ver8_1
100 0.03125 0.046875
0.078125 0.03125
140 0.03125 0.078125
0.109375 0.03125
180 0.046875 0.078125
0.125 0.054688
450 0.140625 0.390015
0.6875 0.218018
700 0.3125 0.935974
1.695313 0.218994

各f_sizeの1段目が調査時間で
2段目が更新時間です。

F_sizeを700くらいまでもっていくと
ひとつひとつのセルに書き込むことには無理があるみたいですね。
ただ1マスに700の文字列を入れることにも無理があるみたい。

んー。
これが次への布石になるだろうか。

ちなみにf_size700だと小さすぎてセルの動きがよくわからないです。
なんかざわざわしてる
くらいな見え方。

  • まとめ

今回一番理解できたことは
マシンの性能が上がるとパフォーマンスも上がるということ。

まぁ。
現状でMyノートパソコンの性能を使いこなせているかは謎ですが。
たぶん使いこなせていないんだろうなぁ。

というわけで。
このコーナーは終了。
次回のコーナーもこうご期待。

VBAでパフォーマンスの良いライフゲームをつくろう その5 Ver7と8.1

  • はじめに

このコーナーも第5回。
飽きたか飽きてないかでいうと
飽きてきました笑

  • Ver7は配列

Ver7はめちゃ速くなります。
このアイディアは思い浮かんではいたものの
実装するとすごいめんどくさくなりそうだなー
と思って放置していたのですが
いいやり方を思いついたのでようやく実装できます。

シートへのアクセスを減らして速度を上げることが一番簡単な手法。
今回はまずシートの情報を配列に書いて
配列の中身を書き換えて
最後に配列をシートに貼り付けます。

今まではシートの情報を取得して
計算の結果を配列に書いて
最後に配列を貼り付ける
というやり方でした。

ちょっと中途半端なやり方でしたが
シート情報を配列に入れてしまうと配列のサイズを決められてしまいます。
ふつうはそれは便利な特徴なのですが
そうなってしまうと配列の中に「隅」や「端」ができてしまいます。
それらの周囲は8マスではなくなってしまうので
計算するのに特別なやり方が必要になってしまいます。

これはまぁ書けばいいのですがちょっと大変そうだなぁ
と思って書いていませんでした。

ただ今回思いつきました。
読み込むシート情報を本来必要なものより一回り大きく読み込めばいいんです。

たとえばRange(cells(2,2),cells(9,9))の88マスを扱いたい場合は
Range(cells(1,1),cells(10,10))の10
10マスを読み込んで
実際に処理するのは8*8マス分にすれば解決します。

配列で説明すると
10*10のシート情報をStockに書き込むと
Stock(1 to 10, 1 to 10)として書き込まれます。

そしてcell_jdgでは

For i = 2 to 9
    For j = 2 to 9
        ‘処理をする
    Next
Next

みたいにすれば大丈夫です。

ただ処理の仕方も今までとは少し違うのでcell_jdgのちゃんとしたコードを載せますね。

Function cell_jdg(f_size, stock, q) As Variant()
 Dim p1 As Variant
 Dim trgt  As Range
 Dim cnt As Integer
 Dim ans() As Variant
 
 ReDim ans(1 To f_size + 2, 1 To f_size + 2)
 
'------------------------------------------------
'field内の各セルに対して
'自身と周囲の計9マスの生きているセルを数えて
'それによって生死を判定します
'生きている場合はそのセルに対応するans()に1を入れる
'------------------------------------------------
 For i = 2 To f_size + 1
    For j = 2 To f_size + 1
    
        cnt = stock(i - 1, j - 1) + stock(i - 1, j) + stock(i - 1, j + 1) + _
            stock(i, j - 1) + stock(i, j) + stock(i, j + 1) + _
            stock(i + 1, j - 1) + stock(i + 1, j) + stock(i + 1, j + 1)
            
        Select Case stock(i, j)
            Case 1
                If cnt = 3 Or cnt = 4 Then
                    ans(i, j) = 1
                End If
            Case Else
                If cnt = 3 Then
                    ans(i, j) = 1
                End If
        End Select
    
    Next j
 Next i

 cell_jdg = ans
 
End Function

これで速度がどうなるかというと。

f_size\Version Ver6_1 Ver7
30 調査終了 0.078125 調査終了0.0078125
更新終了 0.0625 更新終了0.0546875
50 調査終了 0.21875 調査終了0.0078125
更新終了 0.109375 更新終了0.109375
70 調査終了0.4208984 調査終了0.015625
更新終了0.21875 更新終了0.15625

調査時間が30倍くらい?に上がりました。
更新時間はCell_dscをいじっていないので特に変わらず。

このコーナーの初めのほうで決めた目標が100*100のセルに対して1秒間隔で画面を更新していくことでした

なんだか行けそうなので試してみますか。

調査終了0.046875
更新終了0.328125

余裕ですね笑

これにてチャレンジクリア!

。。。

というのもあっけないですね。
Ver7にケチをつけるなら
更新速度の遅さ。

7070 つまり4900マスで0.15秒
100
100 10000マスで0.32秒

これは遠からず壁が来ますね。
更新速度にこだわったVer8を作ってみましょう。

  • 更新速度を上げよう

これはシンプルなやり方がありますよね。

書き込むセル数が肝なら
それを減らせばよい。

1行の内容を1つのセルに入れてみましょう。

ただしこれを行うと欠点もあります。

セルの条件付き書式が使えなくなることと
数値以外のものを扱わねばならいことと
1つのセルに対するデータ量が増えてしまうこと。

ちなみに初期画面はこんな感じです。

“■”が生きているセル
”□”が死んでいるセル

という風に表します。

速度を測るぞ~
いまさら100*100に耐えられない仕様は論外なので
F_sizeは100で計測します。

じゃん。

調査終了 0.1411133
更新終了 0.21875

ってこれだけじゃよくわからないですね。

先ほどのVer7と比較してみると

Ver7 Ver8
調査終了0.046875 調査終了 0.1411133
更新終了0.328125 更新終了 0.21875

んー。これは。。
更新速度は速くなりましたが
調査速度が遅くなりました。
トータルではいい勝負?
ちなみにf_sizeを150にしてみると。

Ver7 Ver8_1
調査終了0.078125 調査終了0.3276367
更新終了0.722168 更新終了0.4853516

んー。

両者のいいところ悪いところがはっきり出ていますね。
これは正式にはバージョンアップできないなぁ。

  • まとめ

Ver7はVer6の意思を引き継がない形となってしまいました。
sheetの中身を配列に移すにはVariant型でないといけないらしいんですよね。

Ver8はうまいこといかなかったー
かといって悪いこともない。

Ver8_1としておきますか。

Ver7もVer8_1もいいところと悪いところがそれぞれあり
これらのいい点を統合出来たらバージョンアップということにしましょう。
ただちょっとやり方が思いつかないので
今回のライフゲームのコーナーはここまでにしましょうか。

当初の100*100はクリアしたしねっ

VBAでパフォーマンスの良いライフゲームをつくろう その4 迷走しながらもVer6まで更新

  • はじめに

さて~!!

今回はライフゲームの第4回ですね。
前回は配列をシートに模してそこにセル状態を書き込み
最後にシートに貼り付ける
という手法を取りました。

今回はVer5でできるだけforを使わない。
Ver6でデータサイズを小さくする。
を実現したいと思います。

先に言っておくと
今回はだいぶ迷走の回です。

  • ちゃちゃっとVer5

Cell_jdgをいじります。
具体的にはForを一つ外します。 現状はこう。

 For Each p1 In field
    
    Set trgt = Range(p1.Offset(-1, -1), p1.Offset(1, 1))
    cnt = 0
    
    For Each p2 In trgt
        If p2 = kigou Then
            cnt = cnt + 1
        End If
    Next p2
    
    Select Case p1.Value
        Case kigou
            If cnt = 3 Or cnt = 4 Then
                stock(p1.Row - 1, p1.Column - 1) = kigou
            End If
        Case Else
            If cnt = 3 Then
                stock(p1.Row - 1, p1.Column - 1) = kigou
            End If
    End Select
 Next p1

この中のFor Each p2 in target
の部分を書き換えます。

For Each p1 In field
    Set trgt = Range(p1.Offset(-1, -1), p1.Offset(1, 1))
    cnt = WorksheetFunction.CountIf(trgt, kigou)

    Select Case p1.Value
        Case kigou
            If cnt = 3 Or cnt = 4 Then
                stock(p1.Row - 1, p1.Column - 1) = kigou
            End If
        Case Else
            If cnt = 3 Then
                stock(p1.Row - 1, p1.Column - 1) = kigou
            End If
    End Select
    
 Next p1

ForではなくConntIfを使って調べています。
こんなちょっとした変更でも。。

f_size\Version Ver4 Ver5
30 調査終了0.1557617 調査終了0.1088867
更新終了0.078125 更新終了0.078125
50 調査終了0.3911133 調査終了0.25
更新終了0.2661133 更新終了0.28125
70 調査終了0.828125 調査終了0.53125
更新終了0.5629883 更新終了0.5170898

調査フェイズが約1.5倍になっています。
Forの回数が1/9になった恩恵ですね。
できることならFieldのForも外したいのですが。。。
まだちょっと無理そうです。

  • 本質を理解せずにつくったVer6_1

最初はstock配列やkigouをString型からInteger型にすれば速くなるでしょ!
っていう軽いノリで書きました。

今は生きているセルに"■"を打ち込んでいますが
生きているセルに1、死んでいるセルに0を打ち込むタイプ。
それがVer6_1です。

f_size\Version Ver5 ver6_1
30 調査終了0.109375 調査終了0.078125
更新終了0.125 更新終了0.0625
50 調査終了0.2646484 調査終了0.21875
更新終了0.2822266 更新終了0.109375
70 調査終了0.546875 調査終了0.4208984
更新終了0.53125 更新終了0.21875

調査時間は約1.25倍
更新時間が2倍になりました。

ただ。
もうちょっと調べているうちに
Byte型というもっと小さいものある?
とか
メモリというものを使ってあれこれしている?
ということに興味を持ったので
これを機に調べてみました。

  • 記事にしようと思ったのですが

一応調べたことをまとめたのですが
ちょっとあまりにもお粗末すぎるので
概要だけ載せますね。

仕事をするとき。
メモリは作業机の大きさ。
一度に乗りきらない場合はHDDを使うが
めちゃ速度が落ちる。
なのでメモリを大きくするか
メモリが耐えられるコードを書くことが肝?

1Byteが8bitなのはただの決め事。

VBAにはVarPtrという関数があり
自分がプロシージャ中に定義したデータが格納されているメモリ番地を教えてくれる。
ただそれの活用法はよくわからず。

VBAではDimで変数の型を決めているが
それぞれの型で確保されるメモリのサイズは違う
ので必要な分だけ定義していくのがメモリにやさしそう?

んー。
なんか言葉だけ覚えたって感じです。
これをつかってVer6_2をつくる。
などの工夫はできませんでした。

んー。
何のための章だこれは?

  • 仕方ないのでVer6_1を正式にVer6に

あ。
言い忘れていました。
Ver6ではセルへのアクセスを0と1を打ち込むことにして
配列のサイズと記述の負荷を下げているのですが
それでは視覚的によろしくないですよね。

なので条件付き書式を使いました。
1と記入されたセルを黒く塗りつぶす仕様にしました。
そしてセルに記述される文字の色を白にしました。

f:id:nozomi-hayashi:20180715190656g:plain

上の表で書いた6_1の速度はたしか1,0で回しただけなので
実際はもうちょっと遅くなるかも。。

  • まとめ

んー。。。
今回は迷走の回でした。

やろうとおもったけどできなかったーということばかり。。。

でもメモリのことをちょっぴり学んだり
データ型について考えたりする機会にはなりました。

とりあえず覚えておけば何かの役に立つかな?
最後にVer6の正式バージョンのコードを載せます。

まずはlife_game_Ver6です。

Sub life_game_Ver6(f_size, return_time, q)

 Dim field As Range
 Dim gene As Integer
 Dim time_watch As Single
 Dim stock() As Integer
'---------------------------------
'fieldの設定
'---------------------------------
 Set field = Range(Cells(2, 2), Cells(2, 2).Offset(f_size - 1, f_size - 1))
'---------------------------------
'geneは世代数を記録するためのセル
'---------------------------------
 gene = Cells(1, q + 2).Value
'-------------------------------------
'調査フェイズの計測
'-------------------------------------
 time_watch = Timer
 
    stock = cell_jdg(f_size, field, q)
 
 Debug.Print "調査終了" & Timer - time_watch
'--------------------------------------
'更新フェイズの計測
'--------------------------------------
 time_watch = Timer
    Call continue_jdg(stock, q)
    Call cell_dsc(field, stock)
 Debug.Print "更新終了" & Timer - time_watch
 '-------------------------
 '世代数を更新する
 '-------------------------
 Cells(1, q + 2).Value = gene + 1
 '---------------------------------
 'このプロシージャを頭から繰り返す
 '---------------------------------
 Application.OnTime Now() + TimeValue(return_time), _
  "'life_game_Ver6 " & f_size & ", """ & return_time & """," & q & "'"

End Sub

次にそれぞれの関数。
cell_jdg

Function cell_jdg(f_size, field, q) As Integer()
 Dim p1 As Variant
 Dim trgt  As Range
 Dim cnt As Integer
 Dim stock() As Integer
 
 ReDim stock(1 To f_size, 1 To f_size)
 
'------------------------------------------------
'field内の各セルに対して
'自身と周囲の計9マスの生きているセルを数えて
'それによって生死を判定します
'生きている場合はそのセルに対応するstockに1を入れる
'------------------------------------------------
 For Each p1 In field
    Set trgt = Range(p1.Offset(-1, -1), p1.Offset(1, 1))
    cnt = WorksheetFunction.Sum(trgt)

    Select Case p1.Value
        Case 1
            If cnt = 3 Or cnt = 4 Then
                stock(p1.Row - 1, p1.Column - 1) = 1
            End If
        Case Else
            If cnt = 3 Then
                stock(p1.Row - 1, p1.Column - 1) = 1
            End If
    End Select
    
 Next p1
 
 cell_jdg = stock
End Function

つぎはcontinue_jdg

Function continue_jdg(stock, q)
'-------------------------------
'stockがない場合
'全滅なのでstop
'-------------------------------
 'ここは今までとだいぶ違うアルゴリズムを考えているので
 '次のバージョンアップで書きます
'-------------------------------
'ループを終わらせたい場合
'cells(1,q)に何かを書き込めばstop
'-------------------------------
 If Cells(1, q) <> "" Then
    Stop
 End If
End Function

ここの処理の一部が放置しっぱなしになっていますね。。

最後がcell_dsc

Function cell_dsc(field, stock)
'-------------------------
'生存セルに1を書き込む
'-------------------------
 field = stock
End Function

基本的にバージョンアップは上位互換だと思っているので番号の低いバージョンのコードは全部消しています。
唯一Ver4_1だけは毛並みが違ったので残していますが。
でもこのブログのように低いバージョンのものも記事として残しておくと
また更新するたびにVerNの数字を上げていくと
自分の成長がはっきりと感じられていいですね。

コードを書くぐらいで成長していく今だけの楽しみかもしれませんが笑

VBAでパフォーマンスの良いライフゲームをつくろう その3 測定にてVer4を決めよう

  • はじめにの前に

見たまま編集で書いたものをMarkdownに移植させたので
もしかしたら一部変なところがあるかもです

  • はじめに

さて~!!!!
今回からようやく機能面をいじります!!

今回は特に。
次世代に生き残るセルの記録とそれを記述する箇所をいじります。
現状がVer3なのですが今回はVer4まで更新します。

  • バージョンアップについて

今までは根幹的なところをいじらなかったため一本道の更新でしたが
ここからは「考え方」が入ってくるためなかなかまっすぐは進まないです。
いくつか候補を出してそれぞれの性能を測ってそれで正式な更新版を決めていきます。
その過程は省いてもいいのかもしれませんが
せっかくなのでその試行錯誤ものせてみようと思います。

  • RangeとUnionでVer4_1

次世代に生き残るセルの記録とそれの記述。
Ver3ではどうやっているのかというと。
生き残るセルのアドレスをいったん保存して
Fieldをいったんまっさらにして
保存したアドレスのセルにひとつずつ■を書き込む。
というやり方です。

とりあえず見た目的には「ひとつずつ」というところを何とかしたいですね。
処理速度的にもおそらくですが「一気に」できたほうが速い気がしますよね。

ということでVer4_1を考えました。
生き残るセルのアドレスを保存するのではなく
生き残るセルを直接まとめて
それらに一気に書き込むというやり方。

具体的にはstockをRangeとして定義して
生き残るセルをUnionを使ってまとめていきます。
コード全体を書くのは重いのでcell_jdgとcell_dscを載せますね。
まずはcell_jdg

Function cell_jdg2(field, kigou, q) As Range
 Dim trgt As Range
 Dim p1 As Variant
 Dim p2 As Variant
 Dim cnt As Integer
 Dim stock As Range
 Dim num As Integer
 Set stock = Cells(1, q + 4)
'------------------------------------------------
'field内の各セルに対して
'自身と周囲の計9マスの生きているセルを数えて
'それによって生死を判定します
'生きている場合はそのセルをstockにunion
'------------------------------------------------
  For Each p1 In field
    Set trgt = Range(p1.Offset(-1, -1), p1.Offset(1, 1))
    cnt = 0
    For Each p2 In trgt
        If p2 = kigou Then
            cnt = cnt + 1
        End If
    Next p2
    Select Case p1.Value
        Case kigou
            If cnt = 3 Or cnt = 4 Then
                Set stock = Union(Range(p1.Address), stock)
            End If
        Case Else
            If cnt = 3 Then
                Set stock = Union(Range(p1.Address), stock)
            End If
    End Select
 Next p1
 Set cell_jdg2 = stock
End Function

つづいてcell_dsc

Function cell_dsc2(stock, kigou)
'------------------------------
'いったんシートをまっさらに
'------------------------------
 Cells.ClearContents
'-------------------------
'生存セルに■を書き込む
'-------------------------
 stock = kigou
End Function

さて。ここからがパフォーマンスの肝。
測定です。

F_size=50で計測を。

ver3 50 ver4_1 50
調査終了0.390625 調査終了0.703125
更新終了0.6259766 更新終了0.1416016
調査終了0.390625 調査終了0.6562
更新終了0.53125 更新終了0.140625

おお!
更新速度がだいぶ上がりましたね!
4倍くらい?
しかし調査速度がやけに遅い??

ver3 70 ver4_1 70
調査終了0.703125 調査終了2.391602
更新終了1.016602 更新終了0.5458984
調査終了0.75 調査終了2.25
更新終了1.052734 更新終了0.546875
Ver3 100 Ver4_1 100
調査終了1.484375 調査終了16.25
更新終了2.867188 更新終了0.34375
調査終了1.5 調査終了14.625
更新終了2.84375 更新終了0.8125

んー。F_sizeを上げてみると調査にかかる時間がネックになりますね。
ただ更新速度は比較的すばらしいのですが。。

おそらくUnionがおいしくないみたいなのでちょっとした実験を。
F_sizeを50にして
調査フェイズでUnionをする箇所をコメント化してみました。
これによってUnionの部分のみが無効になります。

その時の調査にかかった時間が0.4060059です。
これはVer3の調査時間と同程度です。

つまりUnionは時間がかかることがわかりますね。

ついでに。
配列の操作(Ver3では生存セルのアドレスを配列に記録しています)は速いこともわかります。
Ver4_1の調査フェイズでUnion部分をコメント化したものは
つまりはForの処理を回しただけのものです。
Ver3はForの処理を回してさらに配列の処理も行っています。
それにもかかわらずそれらふたつの処理速度がほとんど同じということは
配列の処理は速い。ということですよね。

あとは現在の調査フェイズのForの処理をf_size=50で回すと0.4秒はかかってしまうこともわかります。

  • 配列を貼り付けるVer4_2

Unionは遅い。
配列は速い。
その二つがわかりました。
今回できればクリアしたいことは。
生存セルの記述を「一気に」行うこと。

ということは。
配列で処理しつつ記述は一気に行う。

ができればいいのですよね。
ということでVer4_2です。
Ver3とだいたい同じなのですが。
生存セルのアドレスを保存するのではなく
Fieldとおなじサイズの配列を定義して
生存セルのアドレスと対応する箇所の配列に■を入れることにします。

。。。
説明が難しい。
Fieldの(1,1)のセルが生存する場合は
Stock(1,1)=”■”
とするということです!!

そして生存セルを記述する際にはfield = stock と書くだけでstockの状態を貼り付けることができます。

すると。。。

ver3 30 ver4_1 30 ver4_2 30
調査終了0.171875 調査終了0.15625 調査終了0.15625
更新終了0.1083984 更新終了0.046875 更新終了0.21875
調査終了0.15625 調査終了0.171875 調査終了0.1572266
更新終了0.1083984 更新終了0.0625 更新終了0.2333984
ver3 50 ver4_1 50 ver4_2 50
調査終了0.390625 調査終了0.703125 調査終了0.3916016
更新終了0.6259766 更新終了0.1416016 更新終了0.2958984
調査終了0.390625 調査終了0.6562 調査終了0.375
更新終了0.53125 更新終了0.140625 更新終了0.296875
ver3 70 ver4_1 70 ver4_2 70
調査終了0.703125 調査終了2.391602 調査終了0.7041016
更新終了1.016602 更新終了0.5458984 更新終了0.609375
調査終了0.75 調査終了2.25 調査終了0.703125
更新終了1.052734 更新終了0.546875 更新終了0.53125
Ver3 100 Ver4_1 100 Ver4_2 100
調査終了1.484375 調査終了16.25 調査終了1.469
更新終了2.867188 更新終了0.34375 更新終了0.8920002
調査終了1.5 調査終了14.625 調査終了1.452999
更新終了2.84375 更新終了0.8125 更新終了0.8610001

あらまぁびっくり!
Ver3と調査速度はほとんど変わらず更新速度を3倍にひきあげました!

それにf_sizeが70以上ではVer4_1と同程度の更新時間!
これはめちゃ強いですよ!

ただf_sizeが50以下の場合ではVer4_1の処理は優秀ですね。
これは捨てがたい魅力です。

  • まとめ

とりあえずVer4_2を正式のバージョンアップVer4としますが。
Ver4_1も捨てずに残しておきます。
何かを改善すれば最強のアルゴリズムになるかもですので。。

あ。そういえばしれっと更新した部分があります。
「initialization」のコードもVer4_2式に変更しました。
あとは「mother」にqという変数を増やしました。
世代を表示するセルなどを固定しないための変数です。
これをつけないと大事なセルがfieldに飲み込まれてしまう場合があるのです。

最後に「mother」「initialization」「life_game_ver4」のコードを載せますね。

Private Sub CmdB1_Click()
 Dim f_size As Integer
 Dim kigou As String
 Dim return_time As String
 Dim field As Range
 Dim q As Integer
'-----------------------------
'フォームの数値の読み込み
'-----------------------------
 f_size = Val(TB1)
 return_time = TB2
 kigou = TB3
'----------------------------
'fieldの設定
'----------------------------
 Set field = Range(Cells(2, 2), Cells(2, 2).Offset(f_size - 1, f_size - 1))
'---------------------------------
'qは大事なセルを指定するための数値
'---------------------------------
 q = q_jdg(f_size)
'----------------------------
'実行コードの決定
'----------------------------
 Select Case True
    Case OB1
        Call initialization(f_size, kigou, field)
    Case OB2
        Debug.Print "Ver4_1 " & f_size
        Call life_game_Ver4_1(f_size, return_time, kigou, q)
 End Select
 Unload Me
End Sub
Function q_jdg(f_size) As Integer
 Dim ans As Integer
 Do While ans < f_size
    ans = ans + 26
 Loop
 q_jdg = ans
End Function
Sub initialization(f_size, kigou, field)
 Dim p1 As Variant
 Dim stock() As String
 Cells.ClearContents
 ReDim stock(1 To f_size, 1 To f_size)
 '-----------------------------------------------------
 'fieldのセルひとつひとつに
 '一定の確率で■を入れます
 '-----------------------------------------------------
 For Each p1 In field
    If Rnd() < 0.5 Then
        stock(p1.Row - 1, p1.Column - 1) = kigou
    End If
 Next p1
 field = stock
End Sub
Sub life_game_Ver4(f_size, return_time, kigou, q)
 Dim field As Range
 Dim gene As Integer
 Dim time_watch As Single
 Dim stock() As String
'---------------------------------
'fieldの設定
'---------------------------------
 Set field = Range(Cells(2, 2), Cells(2, 2).Offset(f_size - 1, f_size - 1))
'---------------------------------
'geneは世代数を記録するためのセル
'---------------------------------
 gene = Cells(1, q + 2).Value
'-------------------------------------
'調査フェイズの計測
'-------------------------------------
 time_watch = Timer
    stock = cell_jdg(f_size, field, kigou, q)
 Debug.Print "調査終了" & Timer - time_watch
'--------------------------------------
'更新フェイズの計測
'--------------------------------------
 time_watch = Timer
    Call continue_jdg(stock, q)
    Call cell_dsc(field, stock)
 Debug.Print "更新終了" & Timer - time_watch
 '-------------------------
 '世代数を更新する
 '-------------------------
 Cells(1, q + 2).Value = gene + 1
 '---------------------------------
 'このプロシージャを頭から繰り返す
 '---------------------------------
 Application.OnTime Now() + TimeValue(return_time), _
  "'life_game_Ver4 " & f_size & ", """ & return_time & """ , """ & kigou & """," & q & "'"
End Sub
Function cell_jdg(f_size, field, kigou, q) As String()
 Dim p1 As Variant
 Dim p2 As Variant
 Dim trgt As Range
 Dim cnt As Integer
 Dim stock() As String
 ReDim stock(1 To f_size, 1 To f_size)
'------------------------------------------------
'field内の各セルに対して
'自身と周囲の計9マスの生きているセルを数えて
'それによって生死を判定します
'生きている場合はそのセルに対応するstockに■を入れる
'------------------------------------------------
 For Each p1 In field
    Set trgt = Range(p1.Offset(-1, -1), p1.Offset(1, 1))
    cnt = 0
    For Each p2 In trgt
        If p2 = kigou Then
            cnt = cnt + 1
        End If
    Next p2
    Select Case p1.Value
        Case kigou
            If cnt = 3 Or cnt = 4 Then
                stock(p1.Row - 1, p1.Column - 1) = kigou
            End If
        Case Else
            If cnt = 3 Then
                stock(p1.Row - 1, p1.Column - 1) = kigou
            End If
    End Select
 Next p1
 cell_jdg = stock
End Function
Function continue_jdg(stock, q)
'-------------------------------
'stockがない場合
'全滅なのでstop
'-------------------------------
 'ここは今までとだいぶ違うアルゴリズムを考えているので
 '次のバージョンアップで書きます
'-------------------------------
'ループを終わらせたい場合
'cells(1,q)に何かを書き込めばstop
'-------------------------------
 If Cells(1, q) <> "" Then
    Stop
 End If
End Function
 
Function cell_dsc(field, stock)
'-------------------------
'生存セルに■を書き込む
'-------------------------
 field = stock
End Function

今回は以上です。
ブログにあげるために整理している段階で気づくこともやはりいくつかありますね。

一番感じたのは速度の結果がめちゃわかりにくこと。
これは次回までに改善したいですが。。
この書式でVerまで書いてしまっていることがつらい。。。

こういう意味でも根幹の機能面ではない
いうなれば「デザイン面」は初めに整えておかないとひたすら面倒なことになります。。

ただ元の素養がないのでこうして面倒な思いをしてあげていくしかないのか。。。
よく言えば「不器用」なやりかた。
不器用に「」がつくと良いイメージがあります笑

今回はここでおわり。

VBAでパフォーマンスの良いライフゲームをつくろう その2 関数への分割とユーザーフォーム

  • はじめにの前に

この記事は以前に「見たまま」編集であげたものを「Markdown」に変更したものです。
載せているコードの見易さのために移しました。

  • はじめに  

この記事はとっくにあげていたつもりが  

あげわすれていました。  

 

あんまり本稿とは関係のない、リアルタイムとのずれの話を。  

 

ノートパソコンを買ってからは記事をワードに書いておいて時期を見計らって投稿しているのですが  

記事をストックしておくと当然ながら「今の自分」とのずれが生じますよね。  

特に今回のようなバージョンを更新していくコーナーではめちゃくちゃ違和感があります。  

明らかな「過去の自分」を発表しているような変な感覚。  

 

現在の自分はこの投稿のコードを書いた時よりは進化しているのでほんとに変な感じです。  

この投稿ではバージョンを1から3にあげるのですが  

リアルタイムではもっと更新されています。  

フリーザじゃないけどこのコードはあと5段階強くなります。。。  

 

違和感。  

 

でもそれって何かを発信するときには少なからず感じることなのかもしれませんね。  

起きてから発信するまでには時差があるので。  

 

今まではブログをその場で書いたらその場で投稿していたのであまりその違和感は感じませんでしたが  

今後はそういった感覚にも慣れないとなぁ。  

 

例えば集団で何かをしようとか、人に何かをさせようと思ったらこの違和感をより感じるでしょうしね。  

(実はさいきん、プログラミング初心者にjavaを教えて、自分の書いてほしいコードを書いてくれるマンを育てようとしているのですが、それはまた別のコーナーで。たぶん次のコーナーで書きます)  

 

  • 関数への分割  

可読性をあげるためにlife_game_Ver2の各フェイズを関数に書き換えました。  

些細なことですが、何度もコードを書き換えていく場合は細かく関数に分けておくことが吉だと思います。  

Sub life_game_Ver3()

 Dim kigou As String

 Dim f_size As Integer

 Dim field As Range

 Dim gene As Integer

 Dim time_watch As Single

 Dim stock() As String

'---------------------------------

'geneは世代数を記録するためのセル

'---------------------------------

 gene = Cells(1, 26 * 3 + 2).Value

'------------------------------

'生きているセルには■を入れます

'------------------------------

 kigou = "■"

'-----------------------------------------------------

'fieldはセルの生死判定をする領域

'f_size * f_size のセルを扱います

'-----------------------------------------------------

 f_size = 70

 Set field = Range(Cells(2, 2), Cells(2, 2).Offset(f_size - 1, f_size - 1))

'-------------------------------------

'調査フェイズの計測

'-------------------------------------

 time_watch = Timer

    stock() = cell_jdg(field, kigou)

 Debug.Print "調査終了" & Timer - time_watch

'--------------------------------------

'更新フェイズの計測

'--------------------------------------

 time_watch = Timer

    Call continue_jdg(stock)

    Call cell_dsc(stock, kigou)

 Debug.Print "更新終了" & Timer - time_watch

'-------------------------

'世代数を更新する

'-------------------------

 Cells(1, 26 * 3 + 2).Value = gene + 1

'---------------------------------

'このプロシージャを頭から繰り返す

'---------------------------------

 Application.OnTime Now() + TimeValue("0:00:02"), "life_game_Ver3"

End Sub

続いてそれぞれの関数です。  

まずはcell_jdg。  

これは各セルの次世代の状態を判定する関数です。  

内容はVer2と同じです。  

Function cell_jdg(field, kigou) As String()

 Dim trgt As Range

 Dim p1 As Variant

 Dim p2 As Variant

 Dim cnt As Integer

 Dim stock() As String

 Dim num As Integer

 num = 0

 ReDim stock(num)

'------------------------------------------------

'field内の各セルに対して

'自身と周囲の計9マスの生きているセルを数えて

'それによって生死を判定します

'生きている場合はそのセルのアドレスをstockに保存

'------------------------------------------------

 For Each p1 In field

    Set trgt = Range(p1.Offset(-1, -1), p1.Offset(1, 1))

    cnt = 0

    For Each p2 In trgt

        If p2 = kigou Then

            cnt = cnt + 1

        End If

    Next p2

    Select Case p1.Value

        Case kigou

            If cnt = 3 Or cnt = 4 Then

                ReDim Preserve stock(num)

                stock(num) = p1.Address

                num = num + 1

            End If

        Case Else

            If cnt = 3 Then

                ReDim Preserve stock(num)

                stock(num) = p1.Address

                num = num + 1

            End If

    End Select

 Next p1

 cell_jdg = stock

End Function

次はcontinue_jdg。  

繰り返し処理を行うかを決めます。  

 

Function continue_jdg(stock)

'-------------------------------

'stockがない場合

'全滅なのでstop

'-------------------------------

 If stock(0) = "" Then

    Stop

 End If

'-------------------------------

'ループを終わらせたい場合

'cells(1,52)に何かを書き込めばstop

'-------------------------------

 If Cells(1, 26 * 3) <> "" Then

    Stop

 End If

End Function

最後がcell_dsc。  

次世代に生存するセルを記述します。  

Function cell_dsc(stock, kigou)

 Dim p1 As Variant

'------------------------------

'いったんシートをまっさらに

'------------------------------

 Cells.ClearContents

'-------------------------

'生存セルに■を書き込む

'-------------------------

 For Each p1 In stock

    Range(p1) = kigou

 Next p1

End Function

はい。  

これで関数への分割が終わりました。  

ただこれだけでVer3に更新というのも味気がないので  

もう一つ追加というか整理をします。  

  • ユーザーフォームによる変数の指定  

今使っているコードは「initialization」と「life_game_verN」のふたつです。  

現状ではf_sizeを変更するためにはそれぞれのコード内のf_sizeを書き換えないといけないので面倒くさいです。  

ほかにも何かの変数の中身を変えたい場合に二度手間になってしまうのは情けない。  

なのでそれらをまとめて管理するコードを書きます。  

見やすさを重視したいのでユーザーフォームを作りましょう。  

ユーザーフォームは「中学数学」のコーナーでだいぶ慣れた気がします。  

 

ユーザーフォームの名前は「mother」にします。  

自分なりのコードネーム?をいろいろ使うのにあこがれています笑  

ユーザーフォームの見た目はこんな感じ。  

 

f:id:nozomi-hayashi:20180711200613g:plain

 

今回は水色系にしてみました。  

 

せっかくなのでf_sizeだけでなく  

kigouとreturn_timeもここで指定します。  

return_timeはOnTimeに入れる値です。  

ここで何秒おきに繰り返すかも指定します。  

 

続いてユーザーフォームのコードを。  

Private Sub CmdB1_Click()

 Dim f_size As Integer

 Dim kigou As String

 Dim return_time As String

 Dim field As Range

'-----------------------------

'フォームの数値の読み込み

'-----------------------------

 f_size = Val(TB1)

 return_time = TB2

 kigou = TB3

'----------------------------

'fieldの設定

'----------------------------

 Set field = Range(Cells(2, 2), Cells(2, 2).Offset(f_size - 1, f_size - 1))

'----------------------------

'実行コードの決定

'----------------------------

 Select Case True

    Case OB1

        Call initialization(f_size, kigou, field)

    Case OB2

        Call life_game_Ver3(f_size, return_time, kigou)

 End Select

End Sub

当然ですがこれによって「initialization」と「life_gae_VerN」の中身も少し変わります。  

 

Sub initialization(f_size, kigou, field)

 Dim p1 As Variant

 Cells.ClearContents

 '-----------------------------------------------------

 'fieldのセルひとつひとつに

 '一定の確率で■を入れます

 '-----------------------------------------------------

 For Each p1 In field

    If Rnd() < 0.5 Then

        Range(p1.Address) = kigou

    End If

 Next p1

End Sub

life_game_VerNのほうは。。。  

Sub life_game_Ver3(f_size, return_time, kigou)

 Dim field As Range

 Dim gene As Integer

 Dim time_watch As Single

 Dim stock() As String

'---------------------------------

'fieldの設定

'---------------------------------

 Set field = Range(Cells(2, 2), Cells(2, 2).Offset(f_size - 1, f_size - 1))

'---------------------------------

'geneは世代数を記録するためのセル

'---------------------------------

 gene = Cells(1, 26 * 3 + 2).Value

'-------------------------------------

'調査フェイズの計測

'-------------------------------------

 time_watch = Timer

    stock() = cell_jdg(field, kigou)

 Debug.Print "調査終了" & Timer - time_watch

'--------------------------------------

'更新フェイズの計測

'--------------------------------------

 time_watch = Timer

    Call continue_jdg(stock)

    Call cell_dsc(stock, kigou)

 Debug.Print "更新終了" & Timer - time_watch

'-------------------------

'世代数を更新する

'-------------------------

 Cells(1, 26 * 3 + 2).Value = gene + 1

'---------------------------------

'このプロシージャを頭から繰り返す

'---------------------------------

 Application.OnTime Now() + TimeValue(return_time), _

  "'life_game_Ver3 """ & f_size & """, """ & return_time & """ , """ & kigou & """'"

End Sub

こんな感じです。  

 

これでユーザーフォームでいくつかの変数を扱えます。  

各プロシージャで定義する必要がなくなったのでコードの見た目もすっきりしました。  

Ver3はここまで。  

 

本当はVer1でこのくらいのレベルまで行ってもいいはずなのですが。  

二度もバージョンアップしたのにまだ体裁の面しかいじれていない。。  

まだまだ修行不足ですね。  

 

  • まとめ  

実はこの記事を書く前にVer2の機能面を変更したVer3とVer4を書いたのですが  

速度を測る際にあちこちのコード内の数字をいじって走らせることにストレスを感じたのでこのVer3をはさむことにしました。  

 

なので実はVer4とVer5もすでにできているのですが  

記事に書くのは次回になりそうです。。  

もうVer6とVer7での変更点も決めているのに。。  

 

これはプログラミングうんぬんよりは計画性の問題な気がします。  

もう少しまともになろう。