プログラム悪戦苦闘日記

はてなダイアリーからの移行(遺物)

Excel VBAからパスワードつきAccessにADOで接続する

    con As ADODB.Connection
    Set con = New ADODB.Connection
    
    With con
         .Provider = "Microsoft.Jet.OLEDB.4.0"
         .Properties("Data Source").Value = ThisWorkbook.Path & "\db1.mdb"
         .Properties("Jet OLEDB:Database Password").Value = "xxx"
         .Open
     End With

 3分でできるExcel VBAとか、これだけ出来れば完璧Excel VBA5つの技、とかいう類のものには書いてないから。

ベルウィックサーガ買っちまった

 一週間更新が止まってしまったが、これは仕事が忙しく慢性的な残業で休出で…、というはずもなく、単にベルウィックサーガをやっていたからだったりする。で、参考URL

 ネタとして自分流攻略でも書いてみようかと思った。まぁネタなので真に受けないように…。まだ6章までしかクリアしてないし。でもプレイ時間は25時間超え、リセット含めれば30時間は軽く越えてると思う。やっぱり自分はヘタレプレイヤーだ。初回はプレイ以前のはなし。

命中率の算出式

命中率 = 武器の精度×10 + スキルレベル

ひたすら空振りまくるこのゲーム。命中率はこうらしい。これから命中率を上げるために必要なことは、

  • 武器の精度に注意する
  • スキルレベルを上げる

で、スキルレベルを上げるには武器を振ればよいわけで(空振りもOK)、つまり、ダメージが低く精度も低い武器 --- たとえば槍ならショートスピア --- を装備してひたすら空振り空振り……しまくればいいことになる。もちろんターン制限のないマップで。

意外と便利なショートカット

  • 街の建物の中や店の中で×ボタン

出口に行く

  • 街のマップで×ボタン

執務室にカーソルが行く

  • 街で□ボタン

編成画面とかのメニューがでる。便利なのは一番下にある「自室へ戻る」と「外へ出る」(執務室にいる場合)

  • Select + Start

強制リセット --- じゃなくて今はソフトリセットと呼ぶのか。戦闘マップで使うと「最初からやり直す」があり、これを使えばいちいちキャラの初期位置を設定しなおさなくて済む。地味だけど便利。
 他にもいろいろあるが、よく使うのはこれかな。

食堂のメニュー

前章のセーブ回数によって決まるらしい。だから、食堂へ行ってメニューを見る→気にいらなかったら前章の最後のデータをロード→1回セーブ→次章へ行く、を繰り返せばよいらしい。

食堂のメンバー

食堂のメンバーは、その章で初めて食堂へ入ったときに、そのときの出撃メンバーに固定されてしまう(食堂の人数が不足している場合は、まだ追加できる)。これは後から出撃メンバーから除いてもだめなようだ。人数はよくわからないが、上位8人は固定らしい。だから傭兵にメシ食わせたいときは、メシ食わせないメンバーをいったん出撃から外して、メシ食わせたいメンバーを上位8人になるようにする。全員にメシ食わせたいときはダメだけど。

バグ?

中断セーブがあるシステムデータにあるセーブデータを、普通にロードすると(中断から再開しない)設定がデフォルトに戻る。いちいち再設定するのが面倒くさい。かならず中断から開始→ロードするほうがよい。
 
 次回は1章の予定。

Zip圧縮 -part3 日本語文字化け-

PIP2005-05-26

 エントリー名を日本語にすると文字化けする。エンコードを変えれば可能かと思ったが、UTF8、ISO8859-1、ISO2202-JPを試したがだめだった。どうすりゃいいんだ?

import java.util.*;
import java.util.zip.*;
import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        byte[] buf = new byte[1024*1024];
        
        ZipOutputStream zip = new ZipOutputStream(new FileOutputStream("hoge.zip"));
        zip.putNextEntry(new ZipEntry(new String("あいうえお.txt".getBytes(), "Shift_JIS")));
        
        FileInputStream in = new FileInputStream("あいうえお.txt");
        int size = in.read(buf);
        in.close();
        
        zip.write(buf, 0, size);
        zip.closeEntry();
        
        zip.close();
        
        
        ZipFile zfile = new ZipFile("hoge.zip");
        Enumeration e = zfile.entries();
        while(e.hasMoreElements()) {
            ZipEntry entry = (ZipEntry) e.nextElement();
            System.out.println(entry.getName());
            for(byte b: entry.getName().getBytes())
                System.out.println(b);
        }
        zfile.close();
    }
}

ヘタレプログラムをやっつけろ

 ※2005/05/24 書きなおしました
 
 仕事柄、と言うわけではないはずだが、どういう訳か自分のところにはプログラムのメンテナンスという仕事がよく来る。そのため他人が作ったプログラムを解析する、ということが多い。
 メンテナンスといっても、単にプログラムの可読性を上げればよいということはなく、「パフォーマンスアップ」であることが多い。つまり速くしろ、と。そのため、どこにボトルネックがあるのかを調べるため、プログラムを読む必要がでてくる。本当は、時間測定だけして、時間が掛かっているところだけ直して終わらせる、というのが理想なのだが、現実はそれだけで済むことはない。プログラムが、それをさせてくれない構造になっている。どういうことか? つまり、プログラムが汚すぎて、一部を修正すれば済む、と言うレベルではないのだ。
 ひどいといっても、あまりピンとこない人が多いようだ。おまえがプログラムをちゃんと読めてないのだろうと。しかし、それを言う前に現物を見てほしい。守秘義務とやらがあるので、具体的なプログラムは公開できないが、これに近いものが Cプログラミング診断室 (http://www.pro.or.jp/~fuji/mybooks/cdiag/) の第2部にある。信じられないかもしれないが、というか自分もこの業界にはいるまで信じていなかったが、このテのプログラムが普通に存在しているのである。

 汚いプログラムを紹介しているサイト/本はいくつかあるが、そのプログラムに対処する方法はあまり述べられていないように思う。今回は、そのようなヘタレプログラムに対処する、自分なりの方法を紹介する。しかし、これはまだ研究段階であり、まだまだ発展途上である。以下に紹介する方法は『万能薬』ではないし、『完全』でもない。しかし、手をつけるためのヒントにはなるのではないだろうか。

ヘタレプログラムを見る前に

 まずは事前準備。用意するものは、

  • 腐ったチューニングするプログラムを印刷する

 もしこれを読んでいる人の中には、プログラムを紙に出すことに抵抗を感じる人がいると思う。そんなもの、お気に入りのテキストエディタ1つあれば十分だ、と。しかし、そんなプライドは捨てたほうがいい。対象は、再起不能な糞プログラムである。あなたの常識の範囲にはないプログラムである。とても常識では太刀打ち出来ない代物なのだ。悪いことは言わない、印刷しとけ。でないと後悔します。

  • 赤ボールペンとマーカー

マーカーは最低でも3色欲しい。5色あれば理想だ。これはもちろん、印刷したプログラムに色を塗るためだ。

紙に出して置きながら、テキストエディタも必要なのである。これは検索機能が充実しているものがいい。正規表現が使えるとベストだ。対象は想像をしえない記述をしてくる。例えば変数名が a とか xxx とかだ。変数名 a なんて、普通の検索機能で抽出するのはつらい。しかし、正規表現はなくても使いやすいエディタであればなんとかなる。自分がいつも使っているエディタはTera Padだ。
 
 さて、準備ができただろうか。これからが本番である。

ヘタレプログラムの罠を回避する

 次は実際にプログラムの解析方法だ。

  • コメントは無視する

 記念すべき一等賞は「コメント」だ。通常、重要なヒントになるはずのコメントが、なぜ、無視しなければならないのか? それは、コメントにうそがあるからである。信じられないことだが、納品済みのプログラムのコメントにうそがあるのだ。
 なぜコメントにうそが入るのか。これは私の推測になるが、最初はちゃんと書いてあったのだろうと思う。しかし、テストをしたり仕様変更が起こったりで、プログラムを修正しまくったが、時間がないとかの理由でコメントは放置されていたのではないかと思う。
 ほかにも、コメントを見ても有益な情報がないから、と言う理由もある。プログラムを見れば一目瞭然のものから、「ドキュメントの機能xxの実装」とかだ。

  • else に対応する ifを探す

 「Cプログラミング診断室」にもあるが、if〜elseがなんと遠いことか。しかもネストが深い。ifの6重ネストなんて、ありえないと思ってました。インデントが半角4マスなのに、横スクロールが必要なんて。とにかくelse に対応するifをすべて結び付けて置くこと。対応関係を、印刷した紙に書き込んでおくこと。またC言語系だと中括弧 { } のインデントがあっていない場合が多々あるので、十分に気を付けるけること。

  • エラー処理を消す

 先の操作でif - else の対応付けができたら、エラー処理と思えるif文は消してしまおう。ペンで塗りつぶすのである、2度と目につかないように。とにかくヘタレプログラムの共通な特徴として、正常処理とエラー処理がシマシマになっている、ということだ。これを塗りつぶすことで、ようやく正常処理が何かが見えてくるだろう。

 これは言語によるが、もしグローバル変数、あるいはそれに匹敵するものがある場合は、グローバル変数が使われているところに色を塗ろう。しかし、グローバル変数がたくさんある場合はカラフルになってしまうので、変数を絞る。テキストエディタの検索機能で、たくさん使われている変数が何かを調べてから、色を付けよう。グローバル変数はどこで値が設定/変更されているかが分からない、一番神経を使うところである。また、C言語系のようにポインタがある言語で、ポインタがグローバル変数になっていたときは要注意である。そのポインタに何がセットされているかが不明である場合が多いからだ。関数によって意味が違っている場合が多い。新手の多態性か、と思ってしまう。グローバル変数を使いまわしているのだ。このようなグローバルなポインタは、MS Excelなどで、関数とそのときポインタが指している変数のテーブルを作ることを勧める。もちろん、関数ごとではなく状態ごと、つまり、今どういう処理をしているかで、ポインタが指している変数が変わっている場合がある(つまり、同じ関数であっても、処理タイミングによって指している変数が違う)。この場合は、関数ではなく状態でテーブルを作ろう。柔軟に対処することが重要である。

  • 状態テーブルをつくる

 先のグローバル変数〜のときにも述べたが、状態テーブルをつくろう。グローバル変数を解析したら、そのなかにきっと状態をを表す変数があるはずだ(ない場合は、ここは飛ばして良い)。この状態を表す変数の値によって、関数の動作やグローバル変数の役割が変わる。新種の有限状態オートマトンか、と思ってしまう。この手のプログラムは次のようにな構造になっている場合が多い(C言語の場合)。

int n; // これがグローバル変数

void func1()
{
    switch(n)
    {
    case 1: ...なんか処理...; n = 2; break;
    case 2: ...なんか処理...; n = 3; break;
    case 3: ...なんか処理...; n = 4; break;
    …以下、状態の数だけcaseがある
    }
    
    func2();
}

void func2()
{
    switch(n)
    {
    case 1: ...なんか処理...; n = 2; break;
    case 2: ...なんか処理...; n = 3; break;
    case 3: ...なんか処理...; n = 4; break;
    …以下、状態の数だけcaseがある
    }
    
    func3();
}

void func3() { ... 以下同様 ... }

 こういう構造になっているために、条件分岐が増えるため if - then - else チェーンが膨れ上がり、状態によって関数の動作がかわるため、何をしている関数かがわからない(=ドキュメントが書けない)、となってしまうのだ。
 
 ここまででは、まだまだ完全系には程遠いが、取っ掛かりはできたのではないかと思う。この先は、そのプログラムのクセによってケースバイケースの対応になる。今後はこのクセの見つけ方や、その対処法方を調べることになるが、残念ながらまだそこまで研究が進んでいない。うまく法則性を見つけられればいいのだが。

リファクタリングが適用できない訳

 もしかしたら、リファクタリングを使えばいい、と思った人もいるのではないだろうか。自分もリファクタリングを知ったとき、これでうまく機械的に処理できる、と思った。しかし、適用できないという事実にぶち当たった。なぜならば、リファクタリングが適用できる条件として、

  • そのプログラムにバグがない
  • メソッドや関数の事前条件や事後条件が明確である
  • (xUnitなどの)テストケースがある。またはドキュメントがある

が必要だからだ。しかし、ヘタレプログラムは以上の3つのうちのどれか、というよりすべて満たしていないのが普通である。理由をひとつずつみてみよう。
1.納品されているプログラムにバグがある
 信じられないことだがバグがあるのである。それも特殊ケースではなく、単にプログラムを追っていっただけで発見できるようなものである。例えば、明らかにif文の条件(ifの数)が足りないものである。これは、本当にテストしたのか、なぜ稼動しているプログラムなのにバグが露呈しないか、と疑問に思うが、単にこの関数が呼び出されるときの引数が、つねに1パターン(固定)であるからであった。
2.メソッドや関数の事前条件や事後条件が不明
 ドキュメントを見れば、とかコメントを見れば分かると思う人は、まだ本当の恐怖をしらない人だ。先にも行った通りコメントはダメだ、うそがある。そのプログラムをよく見てみよう。コメントに書かれている関数の引数の数と、実際の関数の引数の数がちがうでしょ。コメントは役に立たない。ではドキュメントはどうか。こんなヘタレプログラムを作るくらいだから、当然まともな日本語を書くことも出来ず、ドキュメントと称するそのコンピューター上のメモリは、プログラムを単に日本語にコンバートしたものである。プログラムをみて分からなければ、ドキュメントをみてもわからないのである。例↓

プログラム
if( price > 1000 )
    total_price = price - price * 0.1;
else
    total_price = price;

ドキュメント
もし、価格が1000円より多ければ、
  全価格は、価格から価格の10%を引いた値を設定する。
そうでなければ、
  全価格は、価格を設定する

(「価格が1000円を超えたら一割引にする」ではだめなのか?)

3.テストケースがない。テストしたドキュメントがない。
 もちろんテストしたのか、とプログラムを納品した会社を問い詰めれば、したと答えるだろう。しかし、きっとドキュメントは無いというだろう。そして必要なら作りますがどうしますか?と聞いてくるだろう。ここで間違えても「作ってくれ」と答えてはいけない。彼らも何をテストしたか覚えていないのだ。そんな彼らが作るドキュメントは見えている。先と同じプログラムからドキュメントを抽出したものだ(手作業でね)。つまり、こちらが把握している以上の情報は得られないということだ。期待してはいけない。時間を無駄にするだけだ。
 そんなわけで、テストケースが書けない。テストケースが書けないからリファクタリングができない(リファクタリングをした結果が、前と動作が同じであることに自信が持てない)。もう、とにかく、だめなのだ。
 

最後に

 ヘタレプログラムと付き合っていると、常人では思いつかないような大技を多く見かける。しかし、このようなところに注意が行ってしまうと、そこに引っかかって先に進めなくなってしまう。プログラムを読む、ということは、あくまで何の処理をしているかを把握することであるので、あまり些細なところにこだわらないほうがいいだろう。そうしないと、時計が12時を過ぎてしまう。適当に対処して人間らしい生活を確保しよう!

参考URL

定番だが、やっぱりここが有名。初心者がみると嫌な気分になるが、この業界にいるとうなずいてしまうことが多い。メンテナンスを繰り返しパッチを当てまくった結果こうなったから仕方ない、と言う人がいるが、それは違うのである。最初から汚いのだ。

ZIP圧縮でもしてみる -part2 ZIPファイルの読み込み -

 今回は読み込み。読みは書き込みの反対なので、ZipInputStreamとZipEntryをつかって読み込むことができる。しかし読み込みは、ZipOutputStreamと同様、ZInputStream#read(bute[], int, int)のみなので、Zipアーカイブ内にあるテキストファイルを読みたいとき不便である。そのため、InputStreamを取得できる、ZipFileとZipEntryを使った方法を紹介しよう。

import java.io.*;
import java.util.*;
import java.util.zip.*;

public class Main {
    public static void main(String[] args) throws Exception {
        byte[] buf = new byte[1024];
        ZipFile zf = new ZipFile("hoge.zip"); // (1)
        
        Enumeration e = zf.entries();
        while( e.hasMoreElements() ) { // (2)
            ZipEntry entry = (ZipEntry) e.nextElement();
            String entryname = entry.getName();
            String filename  = entryname.split("/")[entryname.split("/").length - 1];
            InputStream in   = zf.getInputStream(entry); // (3)
            
            int size = -1;
            FileOutputStream out = new FileOutputStream(filename);
            while( (size = in.read(buf)) != -1 )
                out.write(buf, 0, size);
            out.flush();
            out.close();
            in.close();
        }
        
        zf.close();
    }
}

 まず、ZipFileのコンストラクタで開くファイルを指定する (1)。ZipFile#entries()でZipEntryのEnumerationを取得できるので、いつものようにhasMoreElements()とnextElement()で次々に要素を取得していく (2)。取得したZipEntryをZipFile#getInputStream()に渡すことによって、InputStreamを取得できる (3)。このInputStreamをInputStreamReaderのコンストラクタに渡せば、直接テキストファイルを読むことができる。今回は、単純に解凍したデータをファイルにはいているだけなので、InputStreamを取得する利点がないが、テキストファイルを読むときはこちらのほうがいいだろう。

ZIP圧縮をしてみる - part1 zip書き込み -

 JavaでZIPファイルを扱うには java.util.zip パッケージを使う。このパッケージはJDK1.1からあるにもかかわらず、ZIP圧縮を説明しているサイトは非常に少ないと思う。まぁ、javadoc読めば分かる、ということなのか。とにかく今回はzipの書き込みをやってみたいと思う。プログラムは下記の通りで、結果は画像*1のようになる。

import java.util.zip.*;
import java.io.*;

public class Main {
    public static void main(String[] args) 
    throws Exception {
        String str = init(); // ZIP保存したい文字列
        
        ZipOutputStream zip = new ZipOutputStream(new FileOutputStream("hoge.zip")); // (1)
        zip.setLevel(9); // 最高の圧縮レベル
        
        zip.putNextEntry(new ZipEntry("piyo1.txt")); // (2)
        zip.write(str.getBytes(), 0, str.length()*2); // (3)
        zip.closeEntry(); // (3)
        
        zip.putNextEntry(new ZipEntry("piyo2.txt"));
        zip.write(str.getBytes(), 0, str.length()*2);
        zip.closeEntry();
        
        zip.putNextEntry(new ZipEntry("hoge/piyo3.txt"));
        zip.write(str.getBytes(), 0, str.length()*2);
        zip.closeEntry();
        
        zip.close(); // (5)
    }
    
    static String init() {
        StringBuilder buf = new StringBuilder(2048);
        for(int i=1; i<=1024; ++i)
            buf.append("ほげ");
        return buf.toString();
    }
}

 ZIP圧縮ファイルを作るのに必要なクラスは、ZipOutputStreamとZipEntryの2つだけだ。ZipOutputStreamクラスが、ZIPアーカイブを全体を出力するクラスで、これが任意のOutputStreamに結び付けられる。ファイルとして出力するときはFileOutputStreamに結びつければ良い (1)。
 ZipEntryが、ZIPアーカイブに入れる「ファイル」や「ディレクトリ」を表す。ZipEntryをnewして、このインスタンスをZipOutputStream#putNextEntry()すれば加えられる (2)。実際にデータを書きこむのは、ZipEntryにではなくZipOutputStrea.write(byte[], int, int)である (3)。このメソッドで現在のZipEntryに書き込む。基本的にZipEntryは読み込みに使うようなので、いくらこのクラスのメソッドを調べてもデータは書き込めない(エントリー情報を設定するsetterメソッドはいくつかある)。書き込むメソッドは、先のwriteしかないから、byte配列しか書き込めない。
 ZipEntryに必要なデータをすべて書き込んだら、ZipOutputStream#closeEntry()でZipEntryを閉じる (3)。続けて別のエントリーを書き込みたいときは、(1)〜(3)を繰り返せばよい。
 また、ディレクトリを書き込みたいときは、(4)にある通り hoge/piyo.txt のように、エントリー名を /(スラッシュ)で区切って行けばよい。
 最後にZipOutputStream#close()で、Zipストリーム自体を閉じる (5)。
 
 やっぱり、あまり難しくなかった。これとjava.io系のディレクトリ捜査やファイル属性取得を組み合わせれば、ディレクトリを丸ごとZIP圧縮ができるだろう。

*1:画像がぼやけて見にくいので撤去しました