六帖webアプリではmiraiserverという無料のホスティングサービスを使っている。LanguageTrainerで単語をひたすら登録していたら、30くらい連続で登録したところで動作しなくなった。真っ白なページが表示される。miraiserverの過負荷防止のための制限に引っかかったらしい。そういえばTrainNaviでも突然時刻表のデータを得られなくなることがあった。これじゃ日常ログに記録しまくる処理ができない。30問くらい解いたところで動作不能になってしまう。困った。
たぶん、処理ごとにデータベースをオープン・クローズしていることが原因なのではないかと思う。PHPの勉強不足で、データベースをオープンしっぱなしにする方法がわからない。もしこれができれば、あとはSQL文を実行するだけだから制限には引っかからないはず。ああ、まともな稼働はまだまだ先だなぁ。仕方がないからローカルにデータを移して勉強しよう。
ローカルで使ってみて、致命的なバグがいくつもあったので修正した。
プログラミング
LanguageTrainer ある程度完成
六帖Webアプリ更新
2014-08-03の続き。一日かけてほぼ形になった。本日作業したのは
・出題画面作成、問題選択論理・ログ機能実装
・若干の仕様変更、デバッグ(一番時間がかかった)
で、ひとまず動作するようになった。問題は英語のみ22問登録した。明日から随時問題を登録してまともに使用してみよう。
まだ作業できてないのは
・問題集の編集機能の充実。現在新規追加しかできない。登録済み問題の閲覧、編集、削除機能を付ける。ただし削除はログとの兼ね合いで面倒なので後回し。
・統計機能。覚えた問題の数、解いていない問題の数、最後にアプリを使用した日など。
・問題集にパスワードをかけるべき。
・語学以外の形式に対応する。lttable.TableTyleによって動作を変える。
・ドキュメントの作成。まともに開発・保守するプログラム第一号になりそうだ。
・(バグ)空の問題集を選択するとエラーになる
これらはまた来週。
Eclipseの効果はすさまじく作業効率が2倍くらいになった。PHPでまともにプログラム組んだのはほぼ初めてだったのに、いつもの仕事のように作業できた。好きで組むプログラムの何と楽しいことよ。仕事だとすぐ眠くなってしまうのに自分で作るとちっとも眠くならない。
今回学んだ、SQLで一方のテーブルに存在するが他方のテーブルに存在しないレコードの抽出方法。一度も解いていない問題の一覧を選択するときに使用した。テーブルBに存在しないレコード一覧をテーブルAからゲットするなら、WHERE句に
NOT EXISTS(SELECT 1 FROM B WHERE リレーション条件)
を入れてやればいい。今回、ltelement(問題集)の中から一度も解いてない、つまりltlogテーブルにレコードがない者の一覧を作成したかったので、
SELECT ltelement.ElementID FROM ltelement WHERE ltelement.TableID=?? AND NOT EXISTS(SELECT 1 from ltlog WHERE ltlog.UserID=?? AND ltelement.TableID=ltlog.TableID AND ltelement.ElementID=ltlog.ElementID)
とした。
Head First Design Patterns
Head First Design Patterns
デザインパターン入門書。引っ越しで時間が取れず読み終わるのに3か月もかかった。2004年の本なのでJavaの仕様が古いと思われるけれど、精神は汲み取れた。全体を通して言っていることは、クラス間の依存性をなくすということ。あるオブジェクトが他のオブジェクトの仕様を知らなくても良いようにする。継承よりインターフェースでプログラミングする。これがこの本の一番のポイントだったように思う。10年間行き当たりばったりでプログラミングしてきたことを大いに反省した。これをどのように実際の仕事につなげていくか、は各言語仕様の詳しい解説書が必要になるだろう。
次はさしあたりすぐに必要なPHPの本を読む予定。
やはり平日は時間がない
完成はまだまだ先にお預けか。
Eloquent Javascriptを読み終わった。正直、作ってるプログラムの抽象度が高くて何をやっているのか感覚でしか分からないところが多かった。foreachに相当する関数やらメッセージハンドラの登録やら、全部自前の関数でやってしまう野性味あふれる本だった。おそらく現代なら巷に溢れているライブラリで間に合わせてしまうだろうに。JavaScriptは組み込みオブジェクトもすぐ改造できるくらい自由度が高い上に少ないコードで手っ取り早くブラウザの数多い機能を操れることが魅力だが、リスクも高く、自分でプログラムを把握していないとたちまちバグだらけになる恐れがある。俺ルールをがっちり固めておかないと辛い。
ところで、オンライン版がなんと読み終わった日に2nd editionにアップグレードしてしまった。。!チャプターの数が1.5倍に増えていて、前回見当たらなかったnode.jsやcanvasへの言及やらチャプター名が完全に変わっているところもあり、おそらく全面改訂だろう。3か月くらいかかったというのに。ああ、この苦労は何だったんだ。
進捗
作り始めてみたら、PHPだけですべて実装できそうなので、JavaScriptを使うのをやめてPHPだけで実装することにした。引っ越しが入ったとはいえ4か月間かけて読んだ本はデザインパターンとJavaScriptの本だったので、何の役にも立たなかった。。PHPはまだ少ししか触っていないので、ちょうど良い経験になりそうだ。
ログイン画面、問題集作成画面、問題登録画面のみ作成完了。肝心の出題画面ができていないので、まだ非公開。
ログイン画面についてはここを参照した。
Eclipseを使用して開発中。オートコンプリートがそこそこ使えること、テストが即座にできることから、メモ帳を使っていた時代より作業時間は大幅に短縮できている。デフォルトでステップ実行できないことが残念。Xdebugというツール?を導入すればできるらしいので、次回試してみよう。
1点、PHPは画面遷移を伴うアプリケーションしか作れないのではないか、という危惧があったが、formタグのactionを空にする、という手法を使えば、ボタンを押してPOSTしても自分のページに遷移するだけでしかもクロスサイトスクリプティングも防げる、と一石二鳥であるとわかったので、これを使う。
<form action="" method="post"> <input name="password" type="text" value="pass" /> <input name="action" type="submit" value="login" /> </form>
のようにすれば、$_POST[“pass”] で入力したパスワードをゲットして、PHPだけでもパスワード入力画面に「パスワードが違います」などと表示することができる。ただし自ページ遷移なので、同じPHPのコードを2回実行することになる。上の例ならloginボタンを押したことを検知するために
// ログインボタンが押された場合 if (isset($_POST["login"])) { // 入力チェック if (empty($_POST["password"])) { $errorMessage = "パスワードが未入力です。"; } }
のようにせにゃいかん。これなら1回目にページを表示したときはloginボタンが押されてないのでif文の中は実行されず、次にloginボタンを押せばif文の中が実行される。
LanguageTrainer続き
※自分用のメモなので、適宜編集します
今日は日曜なので、実装の時間が若干は取れそうだ。
2014-04-10の仕様を変更する。
必要なテーブル
[LTUser] ユーザー
UserID
UserName
Password[LTTable] テーブル名
TableID
TableName
TableType 出題フォーマット番号[LTElement]問題
TableID
ElementID
Question 問題 XMLで書く
Answer 意味 XMLで書く[LTLog]学習履歴
UserID
TableID
ElementID
Memory 記憶した程度を数値で記録 0:忘れた 1:なんとなく覚えている 2:覚えている
Flag 復習のためのフラグ 0:一度も解いていない 1:1回解いた 2:一週間後の復習をした
4:ニ週間後の復習をした 5:一か月後の復習をした
Count 解いた回数
Time 最後に解いた日付(年月日で十分)
[テーブル仕様について]
・仕様を作成しながら、これって別に言語の学習以外にも使えるよね、と思ったので、LTTable.TableTypeを追加し、この数字に対応してQuestionの解釈を変更できるようにする予定。例えば、いずれ囲碁も学習したいので、詰碁を登録してQuestionとAnswerでは画像を表示したい。このとき、表示を切り替える必要がある。汎用性を持たせるため、XMLで書く予定。TableTypeでCSSや表示方法を切り替えてはどうか。
・LTLogはUserID-TableID-ElementIDでユニークにして、更新していく形式をとる。問題と使用歴が多くなるにしたがってログが異様に多くなってしまうことを防ぐ。
[動作仕様案]
1)ログインして、UserIDを決定
2)テーブルを選択して、TableIDを決定
3)一定の法則に従い(後述)ElementIDを決定し、問題を出題 LTElement.Questionを表示する
4)「答えを見る」ボタンを押したらLTElement.Meaningを表示、記憶の度合いによって「覚えていない」「なんとなく覚えている」「覚えている」を押させる、押したらLTLogに記録する
5)問題登録画面では問題と解答をセットで入力させる
3a)「一定の法則」はサーバー側で記述する。これが一番肝の論理となる。
・2014-04-01の記事で書いたように、忘却曲線に対応した復習のタイミングに該当した問題を優先して出題する。
すなわち、
学習した翌日に一回目
その一週間後に二回目
二回目の復習から二週間後に三回目
三回目の復習から一ヵ月後に四回目
LTLog.Flag=1 かつ 現在時刻 – LtLog.Time>=1日 の問題
LTLog.Flag=2 かつ 現在時刻 – LtLog.Time>=7日 の問題
LTLog.Flag=3 かつ 現在時刻 – LtLog.Time>=14日 の問題
LTLog.Flag=4 かつ 現在時刻 – LtLog.Time>=1か月 の問題
の順で優先して出題する。
復習時に「覚えている」を押したらFlagをインクリメントする。「なんとなく覚えている」なら要復習なのでFlagはそのまま。「忘れた」なら復習のやり直し、Flagはデクリメントする。Flagは0にはしない。
・これらを使い切ったら、未出題の問題、すなわちLtLogテーブルにレコードが存在しないものを出題する。未出題の問題がなくなったら、ランダムに出題する。ランダムに出題した場合はFlagとTimeを更新しない。ただし、「忘れた」場合のみ、Flagをデクリメントする。
・Memoryはランダム出題の際の優先度として使用する予定。数字が少ない(忘れてる)ほど出題されやすくする。
デザインパターン
FareMap4
簡単な保存・読み込み機能を追加(ファイル1つのみ)
読み込んだ時の駅名の位置がおかしい。
FareMap3 運賃一覧表示
FareMap更新。
・同一名の乗換駅はひとまとめにして表示するようにした。
・運賃一覧表示に対応した。駅をダブルクリックすると他の全駅への運賃が表示される。計算はほぼ正しいようだ。
・駅、駅名をドラッグできるようにした。保存はまだできない。
残り作業
・路線図の保存、読み込み、駅の大きさなどの変更
・同一駅間に複数の路線がある場合は線をずらして表示する
・計算の進捗を表示したい
・路線追加
・速度増加
.NETアプリケーションで遅延バインディングを使ってExcelのオートシェイプをコピー
Excelで画面上の文字にマルを付ける依頼があった。
それも、VBAからではなく、C#の.NETアプリケーションから。
しかも、バージョン違いを吸収するために、遅延バインディングを使え、と。
いろいろ試したところ、シートにマルのオートシェイプを作っておいて、それをコピペする方法でないと不可能と分かった。ところが参照したネット上のTipsは間違っていて、時間を食った。結局、素直にコピペ(Shape.Duplicate + Shape.Cut + WorkSheet.Paste)するのが一番良いとわかった。恐ろしく面倒なコードになった。
// Excelアプリケーションオブジェクト
private object xlsApplication = null;
// Workbooksオブジェクト
private object xlsBooks = null;// Excelアプリケーションオブジェクト
protected object XlsApplication
{
get
{
// 存在しない場合は作成する
if (xlsApplication == null)
{
Type classType = Type.GetTypeFromProgID(“Excel.Application”);
xlsApplication = Activator.CreateInstance(classType);
}
return xlsApplication;
}
}// Workbooksオブジェクト
protected object Workbooks
{
get
{
if (xlsBooks == null)
{
xlsBooks = XlsApplication.GetType().InvokeMember(“Workbooks”,
BindingFlags.GetProperty, null, XlsApplication, null);
}
return xlsBooks;
}
}//あらかじめshapeSheetNameシートにitemNameというオートシェイプが作成されているものとして、
//sheetNameシートにnewItemNameという名前でオートシェイプを座標(x,y)にコピーする
public void CopyShape(int bookIndex, string sheetName, string shapeSheetName,
string itemName, string newItemName, float x, float y)
{
object book = null;
object sheets = null;
object sheet = null;
object shapeSheet = null;
object shapesFrom = null;
object shapesTo = null;
object shape = null;
object newShape = null;
object newShape2 = null;try
{
// WorkBooksは
// WorkbooksオブジェクトからBookオブジェクトを取得
book = Workbooks.GetType().InvokeMember(“Item”, BindingFlags.GetProperty,
null, Workbooks, new object[]{ bookIndex });// BookオブジェクトからSheetsオブジェクトを取得
sheets = book.GetType().InvokeMember(“Worksheets”, BindingFlags.GetProperty,
null, book, null);// SheetsオブジェクトからSheetオブジェクトを取得
sheet = sheets.GetType().InvokeMember(“Item”, BindingFlags.GetProperty,
null, sheets, new object[]{ sheetName} );// Shapeのあるシートをゲット
shapeSheet = sheets.GetType().InvokeMember(“Item”, BindingFlags.GetProperty,
null, sheets, new object[]{ shapeSheetName} );//Shapeをゲッツ
shapesFrom = shapeSheet.GetType().InvokeMember(“Shapes”, BindingFlags.GetProperty,
null, shapeSheet, null);
shape = shapesFrom.GetType().InvokeMember(“Item”, BindingFlags.InvokeMethod,
null, shapesFrom, new object[] { itemName });//コピペする
newShape = shape.GetType().InvokeMember(“Duplicate”, BindingFlags.InvokeMethod,
null, shape, null);
newShape.GetType().InvokeMember(“Name”, BindingFlags.SetProperty,
null, newShape, new object[] { newItemName });
newShape.GetType().InvokeMember(“Cut”, BindingFlags.InvokeMethod, null, newShape, null);
sheet.GetType().InvokeMember(“Paste”, BindingFlags.InvokeMethod,
null, sheet, null);//配置
shapesTo = sheet.GetType().InvokeMember(“Shapes”, BindingFlags.GetProperty,
null, sheet, null);
newShape2 = shapesTo.GetType().InvokeMember(“Item”, BindingFlags.InvokeMethod,
null, shapesTo, new object[] { newItemName });
newShape2.GetType().InvokeMember(“Left”, BindingFlags.SetProperty,
null, newShape2, new object[] { x });
newShape2.GetType().InvokeMember(“Top”, BindingFlags.SetProperty,
null, newShape2, new object[] { y });
}
finally
{
//COMオブジェクトの掃除
ReleaseComObject(newShape2);
ReleaseComObject(newShape);
ReleaseComObject(shape);
ReleaseComObject(shapesTo);
ReleaseComObject(shapesFrom);
ReleaseComObject(shapeSheet);
ReleaseComObject(sheet);
ReleaseComObject(sheets);
ReleaseComObject(book);
}
}