動作確認
ここまでで一旦動かしてみましょう。コマンドプロンプトを立ち上げ、次のように入力してください。(“testfile.txt”はテスト用のテキストファイルです。別途用意してください)
c:\>ruby fe.rb testfile.txt
ESCキーを一回押してから、なにかキーを入力すると現在のカーソルの位置に文字が入力されると思います。ESCキーを再度押してから[x]キーを押し、文字を消してみてください。ただし現状では画面上の文字を変更してるだけですので、実際のデータには反映されていません。
テキストクラスの追加
データの編集を実現するためにいくつかコードを追加する必要がありますが、ソースコードが複雑化する前に、これまで単なる配列として扱っていたテキストデータをクラスとして分けておきましょう。クラスの名前はベタですがTextクラスとします。
Textクラスの機能としては
- これまでと同じように配列ぽくアクセスできる
- 指定された位置に1文字追加する
- 指定された位置から1文字削除する
ただし今回は簡略化のため2バイト文字は考慮しません。あらかじめご了承ください。
配列ぽくアクセスする
前述の通り、これまでテキストデータは配列(Arrayクラス)で保持していたのですが、TextクラスでもArrayクラスと同じような操作方法でアクセスできれば既存のコードの修正も少なくなって少し嬉しいかなと思います。
@data.push @data[index] @data.length
なにか既存のクラスのメソッドを一部だけ流用したい場合「委譲」を使います。Arrayクラスを継承してきてもよさそうですが、継承は、元となるクラスが持つ全ての操作(要はメソッド)をそのまま受け入れられる/もしくは上書きできる時のみに使う、というのが昨今のオブジェクト指向の考え方になっていますので、今回は継承はしないでおきます。
委譲
委譲とは、(継承のように)再利用したい機能を自分自身のものにするのではなく、その機能を持つオブジェクトに処理を依頼することをいいます。つまり処理の横流しです。
Rubyでは、委譲を実現するためのライブラリとして、forwardableとdelegateが標準で同梱されています。今回はわかりやすいforwardableの方を使ってTextクラスを作ってみます。
require 'Forwardable' class Text extend Forwardable def initialize # 委譲するArrayインスタンスを生成 @lines = Array.new end # Arrayのインスタンス@linesへ委譲するメソッド def_delegators :@lines, :[], :length, :push end
Forwardableの使い方として、まず委譲を利用したいクラスにForwardableモジュールをextendします。
extendとはincludeと似たものなのですが、extendを使うとクラスではなくオブジェクトに対してモジュールがMix-inされます。上のようにclass定義中に使うとClassクラスのオブジェクトであるTextクラスにForwardableモジュールがMix-inされて委譲ができるようになるという仕組みです。
うーむ、すこし分かりにくいですね…。
ものすごく平たく(&語弊を承知で)言ってしまうとクラス定義用の文法(具体的にはdef_delegators等)が、extendにより増えると理解してもよいでしょう。
そしてdef_delegatorsを使って、[],length,push等が呼ばれた場合にはそのままArrayオブジェクトの@linesに中継するよと宣言しています。これにより次のようなコーディングが可能となります。
txt = Text.new txt.push("hoge-hoge") txt.push("piyo-piyo") p txt[0] # => "hoge-hoge"
指定された位置に1文字追加する
次はテキストデータに1文字追加するメソッドをTextクラスに追加しましょう。
class Text ## 中略 ## def insert_char(pos_y,pos_x,input_ch) # データの範囲内かをチェック return unless((pos_y < @lines.size) and (pos_x < (@lines[pos_y].length+1))) # 文字列の指定した場所に一文字挿入 @lines[pos_y].insert(pos_x,input_ch.chr) end end
カーソルの現在位置と追加する文字を引数として渡すと、テキストデータ@linesにそれを追加します。Stringクラスには指定した位置に文字列を挿入するメソッドがあるので、それを呼ぶだけで処理は完了します。一応その前に範囲チェックもしています。チェックの中で
@lines[pos_y].length+1
としているのは、文字をその行に追加する際に、既存の文字列より一つ後ろにカーソルが存在するのを許すためです。
input_ch.chr
は文字コードを文字列にするためのメソッドです。
指定された位置から1文字削除する
さて次は、テキストデータから1文字削除するメソッドを書きます。
class Text ## 中略 ## def delete_char(pos_y,pos_x) # データの範囲内かをチェック return unless((pos_y < @lines.size) and (pos_x < @lines[pos_y].length)) # 文字列の指定した場所を一文字削除するために # 一旦、文字列を1バイトずつ配列に分解 arr = @lines[pos_y].split(//) # 該当箇所の要素を削除(つまり1バイト削除) arr.delete_at(pos_x) # 配列を再度文字列に変換 @lines[pos_y] = arr.to_s end end
範囲チェックのやり方などは、さっきのinsert_charとほぼ同様です。しかし困ったことにStringクラスには位置の指定で文字を消すメソッドはありません。そこで少し工夫をして次のような処理方法で指定位置の文字の削除を行います。
- まず文字列を1バイトずつ分割し配列にする
- 指定場所に位置する配列中の要素を削除
- 再び文字列に変換
すこし複雑ですが、irbを使って各メソッドの動きを確かめてみるとよく分かると思います。Rubyで何か細かな処理をする際に、Stringクラス/Arrayクラス/Hashクラスがよく登場します。ですので上記3つのクラスはリファレンスなどを一回熟読しておくことをお勧めします。(ついでに動かしてみるのも強くお勧めします)
