"誕生 (born)" または "死亡 (died)" で始まらない段落は無視することができます。文字列がある単語で始まるかどうかをどうやってテストしたらよいでしょう? charAt メソッドを使って文字列から特定の文字を引き出すことができます。x.charAt(0) で最初の文字を、1 で 2 番目の文字を、という具合で文字を引き出していきます。次の方法で文字列が "born (誕生)" で始まるかをチェックします:
var paragraph = "born 15-11-2003 (mother Spot): White Fang";
show(paragraph.charAt(0) == "b" && paragraph.charAt(1) == "o" &&
paragraph.charAt(2) == "r" && paragraph.charAt(3) == "n");
しかしこれでは手際が良くありません ― 1 つの単語をチェックするのに 10 文字も調べなければなりません。ここで知っておくべきは、1 行が異常に長いときは複数行に分けることが可能だということです。行頭を元の行の最初のエレメントで並べると読み易くなります。
文字列には slice と呼ばれるメソッドもあります。最初の引数で与える場所の文字で始まり、次の引数で与える場所の前の場所の文字で終わる文字列の一部がコピーされます。これによってチェックは次のように短く書くことができます。
show(paragraph.slice(0, 4) == "born");
Ex. 4.4
共に文字列である 2 つの引数を取る関数 startsWith を書いてください。最初の引数が次の引数の文字で始まる場合は true を返し、そうでない場合は false を返します。
function startsWith(string, pattern) {
return string.slice(0, pattern.length) == pattern;
}
show(startsWith("rotation", "rot"));
charAt または slice を使って存在しない文字列の一部を取り出すとどうなりますか? パターンが一致させる文字列より長い場合も上記の startsWith は有効ですか?
show("Pip".charAt(250));
show("Nop".slice(1, 10));
charAt は与えられた場所に文字が無い時は "" を返し、slice は存在しない文字列の部分を無視するに過ぎません。
したがって startsWith は有効です。startsWith("Idiots", "Most honoured colleagues") が呼び出されると、string には十分な文字が無いため、slice 呼び出しでは常に pattern よりも短い文字列が返されます。そのため、== と比較すると false が返されます。そしてこれは間違っていません。
異常な (しかし正しい) プログラムへの入力を考慮することは無駄ではありません。窮地の問題 (corner cases) と呼ばれるもので、'正常な' 入力で完璧に処理をこなすプログラムでも窮地の問題に出合って大失敗するというのはよくあることです。
猫の問題で未解決なのは段落から名前を抽出することだけになりました。アルゴリズムは次のとおりでした:
1. 段落中のコロンを探します。
2. コロンの後の部分を抜き出します。
3. コンマを探し、抜き出した部分を名前に分けます。
これは "died" (死亡) で始まる段落にも "born" (誕生) で始まる段落にも適用する必要があります。これを関数にし、これら異なる段落を扱う 2 つのコードが共にこの関数を使えるようにすると良いでしょう。
Ex. 4.5
段落を引数とし、名前配列を返す catNames 関数を書くことができますか?
文字列の indexOf メソッドを使って文字列中の文字または従属文字列の (最初の) 場所を探すことができます。また、slice に引数を 1 つだけ与えると、その場所から最後までの部分の文字列が返されます。
コンソールを使って関数を '検討する' ことができます。例えば、"foo: bar".indexOf(":") と入力すると次のようになります。
function catNames(paragraph) {
var colon = paragraph.indexOf(":");
return paragraph.slice(colon + 2).split(", ");
}
show(catNames("born 20/09/2004 (mother Yellow Bess): " +
"Doctor Hobbles the 2nd, Noog"));
アルゴリズムの記述には、コロンとコンマの後にスペースがあることが省かれていました。文字列をスライスするのに使う + 2 は、コロンとその後のスペースを除外するのに必要です。名前がコンマとスペースによって区切られるため、split への引数にはコンマとスペースが含まれます。
この関数はチェックを行いません。ここでは入力が常に正しいこととします。
断片を繋ぎ合わせます。次のようにします:
var mailArchive = retrieveMails();
var livingCats = {"Spot": true};
for (var mail = 0; mail < mailArchive.length; mail++) {
var paragraphs = mailArchive[mail].split("\n");
for (var paragraph = 0;
paragraph < paragraphs.length;
paragraph++) {
if (startsWith(paragraphs[paragraph], "born")) {
var names = catNames(paragraphs[paragraph]);
for (var name = 0; name < names.length; name++)
livingCats[names[name]] = true;
}
else if (startsWith(paragraphs[paragraph], "died")) {
var names = catNames(paragraphs[paragraph]);
for (var name = 0; name < names.length; name++)
delete livingCats[names[name]];
}
}
}
show(livingCats);
ぎっしり詰まって難解なコードなので、これをもっと軽くします。しかしその前に結果を見てみましょう。特定の猫の生存をチェックする方法は知っています:
if ("Spot" in livingCats)
print("Spot lives!");
else
print("Good old Spot, may she rest in peace.");
しかし生きている猫を全てリストするにはどうしたらよいでしょう? in キーワードは for と一緒に使うと別の意味を持ちます:
for (var cat in livingCats)
print(cat);
オブジェクト内の属性の名前を見直すループです。これにより、セット内の全ての名前を一覧表にすることができます。
コードには足を踏み込めないジャングルのような部分が幾つかあります。猫の問題の回答例でもこれが悩みの種です。このジャングルに光を当てるには、意図的に空の行を追加する方法もあります。これで見易くなりますが、真の解決にはなりません。
コードをバラバラにする必要があります。我々は既に助っ人となる関数 startsWith と catNames を書きました。これらは共に問題の小さく分かりやすい部分の対処策になります。これで行きましょう。
function addToSet(set, values) {
for (var i = 0; i < values.length; i++)
set[values[i]] = true;
}
function removeFromSet(set, values) {
for (var i = 0; i < values.length; i++)
delete set[values[i]];
}
これらの関数はセット中の名前の追加と削除を処理します。これで内部ループのほとんどが解決します:
var livingCats = {Spot: true};
for (var mail = 0; mail < mailArchive.length; mail++) {
var paragraphs = mailArchive[mail].split("\n");
for (var paragraph = 0;
paragraph < paragraphs.length;
paragraph++) {
if (startsWith(paragraphs[paragraph], "born"))
addToSet(livingCats, catNames(paragraphs[paragraph]));
else if (startsWith(paragraphs[paragraph], "died"))
removeFromSet(livingCats, catNames(paragraphs[paragraph]));
}
}
我ながらかなり改良されました。
addToSet と removeFromSet は、変数 livingCats を直接使うこともできるのに、何故セットを引数とするのでしょう? 理由は、こうすることによって addToSet と removeFromSet が 我々の現在の問題に完全には束縛されなくて済むからです。もし addToSet が直接 livingCats を変更したら、addCatsToCatSet 等から呼び出されることになるでしょう。これでより一般的な使い易いツールになりました。
差し当たってこれらの関数を他の目的で使うことはありませんが、このように書くことは有益です。何故なら、'自足的' で、livingCats と呼ばれる外部変数を知らなくても、それら自身読み易く理解可能だからです。
これらは純関数ではありません: set 引数として渡されるオブジェクトを変更するからです。本当の純関数と比べると少々トリッキーですが、見境なく変数の値を変えてしまう関数よりはずっと複雑さが軽減されます。