読者です 読者をやめる 読者になる 読者になる

CodeIQ Blog

自分の実力を知りたいITエンジニア向けの、実務スキル評価サービス「CodeIQ(コードアイキュー)」の公式ブログです。

エンジニア夏祭り2013「はてなインターンの課題を解いてみよう」JavaScript解答編 #engineer #matsuri #hatena #javascript #夏祭り #夏の想い出

CodeIQ×はてな エンジニア夏祭り2013

CodeIQの中の人、OL元帥です。

「はてな」さんとCodeIQがタッグを組んだ『CodeIQ×はてな エンジニア夏祭り2013』
第1夜「はてなからの挑戦状」
f:id:codeiq:20130725181204j:plain

はてなサマーインターン2013事務局さんから届きました、
JavaScriptの問題の解答・解説を公開します!


f:id:codeiq:20130806163223g:plain



◇◇◇◇◇◇◇◇◇◇

全体の処理の流れ

LTSV 形式の文字列をパースして、オブジェクトの配列を返す関数の処理を大きく分割すると、次の 3 つに分けることができます。

  • 1. 各行の文字列を要素とする配列を作る
  • 2. 各行の文字列をオブジェクトに変換する処理
  • 3. 1 で作った配列の各要素に 2 の処理を適用して、配列を返す

この処理の流れに沿って、関数を実装していく方法を解説します。

parseLTSVLog 関数定義

まずは関数の定義をします。 今回の問題では 1 つの関数を定義するだけですので、次のように単に関数定義を書いてしまって良いでしょう。

function parseLTSVLog(logStr) {
    /* 処理 */
}

単に関数を定義するだけではなく、変数定義や別の関数を定義したい場合は、グローバルスコープを汚さないように (グローバルスコープ上で定義する必要のない変数や関数はグローバルスコープで定義しないように) 気を付けましょう。
次のコラムを参考にしてください。

コラム: 即時関数パターンによるスコープ生成

アップロードされた解答を見ると、次のように関数式の即時呼び出し (即時関数パターン) を使ってスコープを作っているものも多く見られました。
JavaScript (ECMAScript 5) では、変数スコープは主に関数ごとに作られるものであるため、スコープを作りたい場合にはよくこのような書き方がなされます。

下のような書き方を知らない人は一度調べてみてください。

(function () {
    // parseLTSVLog 関数の中で使用する変数や関数を parseLTSVLog 関数の
    // 外で定義したい場合は、このように即時関数パターンを使うことが多い

    // yyy や XXX は、parseLTSVLog から参照したいがグローバルスコープには
    // 定義したくない変数や関数の例
    var yyy = "...";
    function XXX() {
    }

    this.parseLTSVLog = function (logStr) {
        /* 処理 */
    };
}).call(this);
コラム: strict モード

use strict ディレクティブ ("use strict";) により strict モードを有効にしている人も何人か見られました。
use strict ディレクティブは ECMA-262 5th で導入されたもので、このディレクティブを使うことでコードが strict コードとして評価されます。
strict コードでは var で宣言されていない変数への代入でエラーになるなど、バグを発見しやすいようになっています。
use strict ディレクティブを認識しない古い JavaScript 処理系であっても、"use strict"; と書いておいてエラーになることはありませんので、基本的には use strict ディレクティブを記述しておくと良いでしょう。

1. 受け取った LTSV 形式の文字列から、各行を要素にもつ配列をつくる

ここからは parseLTSVLog 関数の中身を記述していきます。

受け取った LTSV 形式の文字列を処理するために、まずは文字列を行ごとに分けて配列にします。
LTSV 形式の文字列の各行の区切り文字は、問題文で指定した通り ("\n") ですので、次のように String.prototype.split メソッドを使うことで各行を要素とする配列を生成できます。
ちなみに、この問題における LTSV 形式の文字列の最後は "\n" で終わりますので、常に不要な空文字列が配列の最後に入ってしまいます。
Array.prototype.pop メソッドで取り除いておきましょう。

    var logLines = logStr.split("\n");
    if (logLines.length > 0) logLines.pop(); // 最後に改行があるので、最後の要素は除く

2. 各行を処理する (文字列を受け取りオブジェクトを返す) 関数

続いて各行 (上で作った配列の各要素) の処理に入ります。
関数に分ける必要は必ずしもありませんが、関数に分けると扱いやすいので、parseLTSVLog 関数の中にさらに関数を定義しようと思います。

まず、1 行を表す文字列をオブジェクトにして返す必要がありますので、関数の全体としては次のようになります。

    /**
     * LTSV 形式のログの 1 行分を処理してオブジェクトにして返す。
     * @param logLine LTSV 形式のログ文字列の 1 行分の文字列 (末尾に改行は無し)
     * @return 受け取った文字列のラベルとバリューをプロパティとしてもつオブジェクト
     */
    function parseLine(logLine) {
        var record = {};
        /* ... */
        return record;
    }

LTSV 形式では、フィールドが "\t" で区切られていますので、まずはフィールドごとに区切る必要があります。
(問題で指定した LTSV 形式の文字列の定義に誤りがあり、"\t" についての記述が抜けておりました。 申し訳ありません。)

        var fields = logLine.split("\t");

さらに、各フィールドを処理していきます。
まず、フィールドの文字列の最初にあらわれるコロンで、ラベルと値に分けます。
そして、フィールドのラベルと値をそれぞれ名前と値としてもつプロパティを record オブジェクトに追加していきます。

        fields.forEach(function (field) {
            // フィールドの最初のコロンの位置を取得
            // (コロンが存在しなければ -1 が返るが、ここでは常にコロンが存在するものと仮定して処理を進める)
            var sepIdx = field.search(/:/);
            var label = field.substring(0, sepIdx); // 文字列の先頭からコロンの直前まで
            var value = field.substring(sepIdx+1);  // コロンの直後から文字列の最後まで

            // reqtime_microsec の場合は数値にする
            if (label === "reqtime_microsec") value = +value;

            // フィールドのラベルと値をプロパティとして record に追加
            record[label] = value;
        });

ここでは、配列の各要素に対する処理に Array.prototype.forEach メソッドを使っています。
これは ECMA-262 5th で導入されたもので、最近のブラウザ (Firefox 22 や Chrome 25、IE 10 など) だと使用可能です。
今回のように配列のすべての要素に同じ処理を行う場合は、forEach メソッドを使った方が for 文を使うよりもコードの見通しがよくなると思います。
現状ではパフォーマンス的には for 文の方が良い場合が多かったり、for 文だと break などによるループの制御ができる、という風に for 文にも利点がありますので、状況に応じて使い分けてください。
forEach 以外にも、配列に対して処理を行うときに便利な mapfilter といったメソッドがあります。

今回は、ラベルと値を分けるために、まず、String.prototype.search メソッドでコロンの位置を特定し、その前後の文字列を取得する、という方法を採りました。
ラベルと値を分けるときに、field.split(":") としている人もいましたが、値にもコロンが含まれる可能性があるのでそれでは不十分であることに注意してください。
別の方法として、正規表現を使っても簡潔に書くことができます。

            var match = /^([^:]+):(.*)/.exec(field);
            if (!match) { /* マッチしなかったときに何か処理をするならここに書く */ }
            var label = match[1];
            var value = match[2];
                    // n 番目のキャプチャリングの括弧の中身を `match[n]` で取得できる

文字列を数値に変換する方法は様々あります。
単に数値が文字列で表現されていて、それを数値に変換したいという場合は次のどちらかを使うと良いでしょう。

+"100"; // 単項プラス演算子を作用させて数値にする
Number("100"); // Number 関数に渡すと数値になる

最終的に、各行を処理する関数は次のようになります。

    /**
     * LTSV 形式のログの 1 行分を処理してオブジェクトにして返す。
     * @param logLine LTSV 形式のログ文字列の 1 行分の文字列 (末尾に改行は無し)
     * @return 受け取った文字列のラベルとバリューをプロパティとしてもつオブジェクト
     */
    function parseLine(logLine) {
        var record = {};
        // タブ文字区切りでフィールドを取得
        var fields = logLine.split("\t");
        // 各フィールドを処理
        fields.forEach(function (field) {
            // フィールドの最初のコロンの位置を取得
            // (コロンが存在しなければ -1 が返るが、ここでは常にコロンが存在するものと仮定して処理を進める)
            var sepIdx = field.search(/:/);
            var label = field.substring(0, sepIdx); // 文字列の先頭からコロンの直前まで
            var value = field.substring(sepIdx+1);  // コロンの直後から文字列の最後まで

            // reqtime_microsec の場合は数値にする
            if (label === "reqtime_microsec") value = +value;

            // フィールドのラベルと値をプロパティとして record に追加
            record[label] = value;
        });
        return record;
    }
コラム: JavaScript における String.prototype.split の limit 指定について

Perl や Ruby では my ($label, $value) = split /:/, field, 2label, value = *field.split(':', 2) という風に split の上限を与えることで最初のコロンの位置で 2 分割するということができます。
JavaScript でも、String.prototype.split の第 2 引数として limit を渡すことができるのですが、この limit 指定は Ruby や Perl のlimit 指定は挙動が違っていて、「すべてのコロンで分割したうえで、先頭から個数 limit の分だけ返す」 という挙動を示します。

var str = "aaa:bbb:ccc:ddd";
str.split(":", 2); // => ["aaa", "bbb"]

Ruby や Perl と挙動が違うのは結構びっくりしますし、いちいちこういう部分まで仕様を覚えておくなんてことは普通はしませんので、ちゃんと 「フィールド中に複数のコロンが含まれている場合に期待通りの処理がなされるか」 というテストを書いておくということは大事ですね。

3. ログ文字列の各行に parseLine 関数を適用する

最後に、logLines の各要素を parseLine 関数に渡して、戻り値からなる配列を作る、ということをします。
ここでは、上で紹介した Array.prototype.map メソッドを使用します。

    var records = logLines.map(parseLine);

配列の各要素に関数の処理を適用し、返り値からなる新たな配列を生成してくれます。 便利ですね。

完成

最終的な parseLTSVLog 関数の全体像は次のようになります。

/**
 * LTSV 形式の文字列を受け取り、各行を表すオブジェクトを要素としてもつ配列を生成して返す。
 * @param logStr LTSV 形式のログ文字列
 * @return LTSV 文字列のラベルとバリューをプロパティとしてもつオブジェクトからなる配列
 */
function parseLTSVLog(logStr) {
    function parseLine(logLine) {
        var record = {};
        // タブ文字区切りでフィールドを取得
        var fields = logLine.split("\t");
        // 各フィールドを処理
        fields.forEach(function (field) {
            // フィールドの最初のコロンの位置を取得
            // (コロンが存在しなければ -1 が返るが、ここでは常にコロンが存在するものと仮定して処理を進める)
            var sepIdx = field.search(/:/);
            var label = field.substring(0, sepIdx); // 文字列の先頭からコロンの直前まで
            var value = field.substring(sepIdx+1);  // コロンの直後から文字列の最後まで

            // reqtime_microsec の場合は数値にする
            if (label === "reqtime_microsec") value = +value;

            // フィールドのラベルと値をプロパティとして record に追加
            record[label] = value;
        });
        return record;
    }

    var logLines = logStr.split("\n");
    logLines.pop(); // 最後に改行があるので、最後の要素は除く
    var records = logLines.map(function (logLine) {
        return parseLine(logLine);
    });
    return records;
}

渡された文字列が期待どおりの形式でなかった場合の例外処理などは記述していませんが、必要に応じて (期待どおりの形式の文字列でないものが渡されない可能性がある場合など) そのような処理も入れると良いでしょう。



◇◇◇◇◇◇◇◇◇◇



いかがでしたか?
丁寧で読み応えのある解説でしたね~。

明日は「はてなサマーインターン2013事務局」さんのPerlの問題の解答・解説を公開します!お楽しみに♪



CodeIQ × はてな エンジニア夏祭り19113 第2夜 ブログでわっしょい開催中!
f:id:codeiq:20130806104304g:plain
「納涼!ほんとにあった怖いコード」「CodeIQの問題・パズルを考えよう!」のいずれかのお題でブログを書くと、豪華審査員が選評してくれます。
詳しくはキャンペーンページヘ!



エンジニアのための新しい転職活動!CodeIQのウチに来ない?の特集ページを見る