CodeIQの「はてなインターンの課題を解いてみよう」を解いたよ

http://codeiq.hatenablog.com/entry/2013/08/08/115705を解きました。CodeIQの「複数書式が混在するログをやっつける」を解いたよ - 四角革命前夜の前に書くはずだったのだけど、すっかり忘れてた。
リポジトリGitHub - hatena/Hatena-Intern-Exercise2013 at CodeIQ-JSです。


まあ、内容はLTSVなのでまたアレなのだけど……
node.jsだけじゃなくてブラウザでも使えるようにしたら良いのかな。

// 課題 : 関数 `parseLTSVLog` を記述してください
(function(global) {

  'use strict';

  var parser = new Parser(new Splitter, new Validator);

  /**
   * LTSVログをパースし、配列を返します
   *
   * @param {String} text LTSV形式の文字列
   * @return {String[]} パースした配列
   */
  global.parseLTSVLog = function parseLTSVLog(text) {
    // 空文字列の場合は空の配列を返す
    //
    // 渡されたLTSV文字列が今回の課題の書式に沿っていない場合は
    // 例外を返すようにしている一方で、空文字列だけ特別扱いするのは
    // 微妙な気がしますが… テストを通らないので仕方なくこうします
    return (text === '') ? [] : parser.parseLtsv(text);
  };

  /**
   * LTSVをパースするクラスです
   *
   * @constructor
   * @param {Splitter} splitter Splitterのインスタンス
   * @param {Validator} validator Validatorのインスタンス
   */
  function Parser(splitter, validator) {
    if (!(splitter instanceof Splitter)) {
      throw new TypeError('splitter must be a Splitter instance');
    }

    if (!(validator instanceof Validator)) {
      throw new TypeError('validator must be a Validator instance');
    }

    this.splitter_ = splitter;
    this.validator_ = validator;
  }

  /**
   * LTSVログをパースし、配列を返します
   *
   * @param {String} ltsv LTSV形式の文字列
   * @return {String[]} パースした後の配列
   */
  Parser.prototype.parseLtsv = function(ltsv) {
    var result = [];

    this.splitter_.splitLtsv(ltsv).forEach(function(record) {
      result.push(this.parseRecord(record));
    }.bind(this));

    return result;
  };

  /**
   * レコードをパースし、オブジェクトを返します
   *
   * @param {String} record LTSV形式のレコード文字列
   * @return {Object} パースした後のオブジェクト
   */
  Parser.prototype.parseRecord = function(record) {
    var f = {};

    this.splitter_.splitRecord(record).forEach(function(field) {
      var fieldData = this.parseField(field);

      f[fieldData.label] = fieldData.value;
    }.bind(this));

    return f;
  };

  /**
   * フィールドをパースし、ラベルと値が入ったオブジェクトを返します
   *
   * @param {String} field LTSV形式のフィールド文字列
   * @return {Object} パースした後のオブジェクト
   */
  Parser.prototype.parseField = function(field) {
    var fieldData = this.splitter_.splitField(field),
        label = fieldData.label,
        value = fieldData.value;

    if (!this.validator_.isValidLabel(label)) {
      throw new SyntaxError('"' + label + '" is invalid label');
    }

    if (!this.validator_.isValidValue(value)) {
      throw new SyntaxError('"' + value + '" is invalid value');
    }

    // ラベル名がreqtime_microsecで値が数値と思われる場合は数値化する
    // FIXME: 対数表記(1e10など)や小数を含む場合、数値に変換されない
    // 整数値しか渡されないとのことなので今回は対応しない
    if (label === 'reqtime_microsec' && /^-?\d+$/.test(value)) {
      value = parseInt(value, 10);
    }

    return {
      label: label,
      value: value
    };
  };

  /**
   * LTSVを分割するクラスです
   *
   * @constructor
   */
  function Splitter() {
    // 今回の課題では'\n'固定なので/\r?\n/にはしない
    this.ltsvSeparator_ = '\n';
    this.recordSeparator_ = '\t';
    this.fieldSeparator_ = ':';
  }

  /**
   * LTSVログをレコードの配列に分割します
   *
   * @param {String} ltsv LTSV形式の文字列
   * @throws {TypeError} ltsvが文字列でない場合
   * @return {String[]} 分割後の配列
   */
  Splitter.prototype.splitLtsv = function(ltsv) {
    if (typeof ltsv !== 'string') {
      throw new TypeError('ltsv must be a string');
    }

    // 後方の改行(LF, CRLF)をすべて削除してから分割する
    return ltsv.replace(/(?:\r?\n)+$/, '').split(this.ltsvSeparator_);
  };

  /**
   * レコードをフィールドの配列に分割します
   *
   * @param {String} record LTSV形式のレコードの文字列
   * @throws {TypeError} recordが文字列でない場合
   * @return {String[]} 分割後の配列
   */
  Splitter.prototype.splitRecord = function(record) {
    if (typeof record !== 'string') {
      throw new TypeError('record must be a string');
    }

    // 後方の改行(LF, CRLF)をすべて削除してから分割する
    return record.replace(/(?:\r?\n)+$/, '').split(this.recordSeparator_);
  };

  /**
   * フィールドをラベルと値に分割します
   *
   * @param {String} field LTSV形式のフィールドの文字列
   * @throws {TypeError} fieldが文字列でない場合
   * @throws {SyntaxError} ラベルと値の区切り文字(":")が存在しない場合
   * @return {Object} 分割後のオブジェクト
   */
  Splitter.prototype.splitField = function(field) {
    var fieldStr, separatorPos;

    if (typeof field !== 'string') {
      throw new TypeError('field must be a string');
    }

    fieldStr = String(field);
    separatorPos = fieldStr.indexOf(this.fieldSeparator_);

    // ':'が見つからなかった場合エラーを投げる
    // MEMO: Validatorの役割のような気もする
    if (separatorPos === -1) {
      throw new SyntaxError('field separator not found');
    }

    return {
      label: fieldStr.slice(0, separatorPos),
      value: fieldStr.slice(separatorPos + 1)
    };
  };

  /**
   * LTSVのバリデーションをするクラスです
   *
   * @constructor
   */
  function Validator() {
    this.labelSyntax_ = /^[^\n\r\t:]+$/;
    this.fieldSyntax_ = /^[^\n\r\t]*$/;
  }

  /**
   *
   * @param {String} label
   * @return {Boolean} 課題のLTSV仕様に沿っている場合はtrueを、それ以外はfalse
   */
  Validator.prototype.isValidLabel = function(label) {
    return this.labelSyntax_.test(label);
  };

  /**
   *
   * @param {String} value
   * @return {Boolean} 課題のLTSV仕様に沿っている場合はtrueを、それ以外はfalse
   */
  Validator.prototype.isValidValue = function(value) {
    return this.fieldSyntax_.test(value);
  };

}(this));

同じもの何回も書いててもレベルアップしないのでなんとかしたいかも。