(070) 文字列操作・コーディングのお作法

Ippei Kishida

Last-modified:2018/03/29 22:55:17.

1 文字列操作

実用的なプログラムを作ろうとするならば、文字列を上手く処理することが必要になってくる。 たとえば カンマ区切りの数値データファイルから数値データを取り出すには、 まず行として取得される文字列に対してカンマを処理するという操作が必要になる。 今回はまず、String クラスで使える便利な機能を見ていこう。 1

1.1 準備

まず、今回の演習で使うファイルを作る。

#generateNumberData.rb
io = File.open("numberData.txt", 'w')
10000.times do |i|
  io.printf("%04d\n", i) #出力用に4桁確保し、最初の桁などが空ならば0を詰める
end
io.close

これを実行すると numberData.txt というファイルができている筈だ。

1.2 単純な比較

文字列 "1234" というデータが含まれているか見てみよう。

#stringCompare.rb
io = File.open("numberData.txt", 'r')
array = io.readlines #ファイルの全内容を1行ごとに配列に格納
io.close

array.each do |i|
  i.chomp! #ファイル読み込みなので、行末の改行文字を削除する必要あり
  if ("1234" == i)
    print "1234 is exist\n"
  end
end

1.3 パターンマッチング

前述の単純な比較では、対象の文字列全体が一致する必要がある。 しかし「文字列 "123" が含まれているもの」という判定を行いたい場合がある。 "123"という文字の並びを含むデータがどれくらいあるか見てみよう。

#stringContain.rb
io = File.open("numberData.txt", 'r')
array = io.readlines #ファイルの全内容を1行ごとに配列に格納
io.close

array.each do |i|
  i.chomp! #ファイル読み込みなので、行末の改行文字を削除する必要あり
  if (/123/ =~ i)
    print "/123/ is contained in #{i}\n"
  end
end

ここで if 文で使われている /123/ という表現に注目して欲しい。 この // で囲むデータ形式は「正規表現」と呼ばれるもので、 対象の文字列を判定するのに非常によく使われるものだ。

さて、if 文の比較演算子は少し変わっていて、「=~」になっている。 これは正規表現のみに使われる比較演算子で、 パターンマッチングにはこれを使わなければならない。 もし通常の比較演算子「==」を使った場合、 左右両辺のクラスが違うので全て偽の値を返すことになる。

1.3.1 先頭・末尾のマッチング

前節の例は文字列のどこかに 123 という部分が含まれていることを チェックしたわけだが、 その文字列がある位置の情報も含めて正規表現を作ることができる。

1.3.2 任意の文字列のマッチング

例えば「12 で始まり4 で終わる」文字列であるかを判定したいとする。 まず「/^12/」と「/4$/」を使えば良いことは分かるだろう。 そしてその間は「任意の文字列」を表す正規表現を入れてやれば良い。 具体的には、「/^12.*4/」のようにする。 この 「/./」は「任意の1文字」を表し、 「/*/」は「直前の表現の0回以上の繰り返し」を表すので、 この組み合わせである「/.*/」は「0文字以上の任意の文字列」となる。

1.3.3 マッチした部分の取り出し

先の節では任意の文字列をマッチングさせたが、 実際にマッチした部分を取り出して利用したいことがある。 その場合には以下のように、括弧 () でその部分を囲めば $1 にその括弧内でマッチした部分文字列が格納される。

#stringCompareRegExp.rb
io = File.open("numberData.txt", 'r')
array = io.readlines #ファイルの全内容を1行ごとに配列に格納
io.close

array.each do |i|
  i.chomp! #ファイル読み込みなので、行末の改行文字を削除する必要あり
  if (/^12(.*)4$/ =~ i)
    print "/^12(.*)4$/ is matched with #{i}: $1 == #{$1}\n"
  end
end

1.3.4 様々な言語で使える正規表現

正規表現は強力で多くのことができる反面、使いこなすのが難しい。 2 ここでは軽く触れる程度にとどめたが、正規表現はスルメのように噛めば噛むほど味が出る。 また正規表現は様々なプログラミング言語で使われているので、 基本を押えておくと多くの状況で対応できる。 ただし複雑で微妙なマッチングでは、言語間で微妙な差異があったりするので、 そのような場合はキチンと調べる必要がある。

1.4 文字列置換: String.sub

Ruby の文字列(String)クラスは様々な機能が用意されている。 ここで全てを紹介することはできないが、代表的なものを挙げておこう。 まず、文字列の一部を書き換える操作(String.sub 系)だ。

#stringSub.rb

str = "abcdabcd"
p str.sub("cd", "CD") #=> "abCDabcd"
p str                 #=> "abcdabcd"

str = "abcdabcd"
p str.sub!("cd", "CD") #=> "abCDabcd"
p str                  #=> "abCDabcd"

str = "abcdabcd"
p str.gsub("cd", "CD") #=> "abCDabCD"
p str                  #=> "abcdabcd"

str = "abcdabcd"
p str.gsub!("cd", "CD") #=> "abCDabCD"
p str                   #=> "abCDabCD"

sub は substitute (置換する)の略である。 上で示したコードでは sub, sub!, gsub, gsub! という4種類のメソッドが含まれている。 1つずつ見ていこう。

なお、ここでは置換対象として文字列 "cd" を第1引数として与えたが、 これを正規表現で与えることも可能である。

str = "abcdabcd"
p str.sub(/c.*d/, "CD") #=> "abCD"
p str                 #=> "abcdabcd"

1.5 文字列から配列を生成する: String.split

キーボード入力やファイル入力によって "12 34 56" といった文字列データが得られたとしよう。 これを配列に格納するにはどうすれば良いか?

1つの文字列 "12" を数値 12 に変換するためには、 以前に見たように String.to_i を作用させれば良い。 しかし "12 34 56" は空白文字で区切られた3つの数値となるべき 部分文字列で構成されているので、直接 String.to_i を作用させることができない。 なので、まずはこの部分文字列を分解して配列に入れてやるべきだ。

#stringSplit.rb
str = "12 34 56"
p array = str.split(" ") #区切り文字。正規表現も使用可能。
array.each do |i|
  p i.to_i
end

ここで、 i.to_i で文字列から整数への変換を行っているが、 配列に入っているデータそのものは書き変わっていないことに注意が必要だ。 数値の配列を使いたければ、自前で受け入れ先の配列を作っておいて Array.push してやるなどの処理が必要だ。

2 お作法

2.1 コメント

コメントを書く必要がないほど、意図が明解で見通しの良いコードを書けるならばコメントは不要だが、そうでない多くの人 3 は適宜コメントを入れておいた方が無難だろう。

プログラマの間には「3日前のソースは他人のソース」という言葉がある (ソースとはソースコード(プログラムコード)のこと)。 人間 1週間も見なければ、何故そのようなコードを書いたのか忘れてしまうものだ。 コードを読むより日本語を読む方が得意な人は、コメントを多めに付けておいた方が良い。

2.2 変数・定数の使用

繰り返し使うデータは変数に格納して利用するのが 変更に強いプログラムを作るための第一歩だ。 一般に、2回以上使う値(文字列を含む)は変数や定数にした方が良いだろう。

変数に格納して利用するということには、 修正個所をソースファイル先頭に持って来るという効果もある。 修正箇所をソースファイルから探す手間を大幅に減らすことができる。

また、タイプミスを防ぐという意味もある。 3.14159265358979 を 3.14159265357989 と書き間違ってしまっても、 その間違いは大変発見し難い。 3.14…… が出る度に目を皿のようにしてチェックするのは、かなりやりたくない仕事だ。

2.3 変数名

変数名は内容を反映したものを付けること。

a = 60
b = 60
c = 24
d = 7
print a*b*c*d

これでは何やってるプログラムか分からない。 以下のような変数名、コメントを付すくらいの配慮は欲しい。

min  = 60 #秒
hour = 60 #分
day  = 24 #時間
week = 7  #日
print min * hour * day * week, "秒\n"

2.4 字下げと空行による整形

字下げについては既に説明した。 Ruby においては半角の空白文字や改行文字は基本的には無視されるため、 行頭の位置を調整することで一塊のブロックを視覚的に把握しやすくするということだ。

まとまりの強い個々のブロックの間に空行を挿入することも有効だ。 空行の使用方法は各自適当に工夫してみよう。 ただし使いすぎると却って見難くなってしまうので注意のこと。

2.5 除算に注意

加算・減算・乗算と違って、除算では致命的なエラーが生じることがある。 有限の数値はゼロで割ることができないので、 「divided by 0 (ZeroDivisionError)」のようなエラーメッセージを吐いて プログラムがストップしてしまう。 4

たとえばベクトルを長さで規格化しようとする時にベクトルの長さで割る、 平均を取るときに要素数で割る、といったことは頻繁にあるが、 プログラムに与えられたデータによってはそれらがゼロになることもありうる。 割り算が必要になるときには、毎回分母が0になりうるかどうかを考える習慣をつけよう。

3 課題

  1. 2010年6月2日は 日本では「2010/06/02」のように表すことが多いが、 アメリカでは「June 2nd, 2010」のように表す。 そこでアメリカ式の任意の日付を日本式に変換するプログラムを作成せよ。 ただし、入力ファイルとしてサーバの第8回課題データファイルの 自分の学籍番号のファイルを扱い、 (学籍番号)-out.txt という名前のファイルに出力するようにすること。 その際、月の名前は大小文字を区別しないようにせよ。 そのための正規表現は、たとえば /january/i のように 2つめのスラッシュの後に i を加えれば良い。

  2. 今回提示したお作法に注意し、これまで自分が作成し、 提出した全てのコードを書き直せ。 さらに書き直したポイントを箇条書にて簡潔にまとめよ。


  1. 現代的なプログラミング言語の多くは強力な文字列操作機能を備えている。 特に Ruby は日本語を含むマルチバイト文字の処理に適しているとされる。 やや古典的な言語(たとえば C言語)では、 今回紹介するような機能は基本的に全て自分で作らねばならないものが多い。

  2. 正規表現は各プログラミング言語から見れば、 自治的なルールを持つサブ言語という見方ができる。 それくらい強力で独自性を持つのだとも言えるし、 そのようにしてプログラミング言語間での交換性を保っているのだとも言える。

  3. 私もその一人。

  4. これはゼロ除算とかゼロディバイドとか呼ばれる。