Node.jsでCSVを整形する

Node.jsでstreamを使って何か書いてみたかったので。

特にライブラリにするほどのものでは無いかな…。（そもそもCSVは各々に癖がありすぎてライブラリとしてまとめるのは至難の業かもしれない）

```js:csvpretty.js
var
_ = require('underscore'),
util = require('util'),
async = require('async'),
Transform = require('stream').Transform;

_.str = require('underscore.string');
_.mixin(_.str.exports());

//exports
module.exports = function csvpretty(options) {
  return new CSVPrettyPrintStream(options);
};

//define CSVPrettyPrintStream class

//inherit Transform
util.inherits(CSVPrettyPrintStream, Transform);

function CSVPrettyPrintStream(options) {
  //call super class's constructor
  Transform.call(this, options);
  
  //private member
  this._rest = '';
  this._data = [];
}

//implements Transform
CSVPrettyPrintStream.prototype._transform = function transform(chunk, encoding, done) {
  var
  lines = chunk.toString('UTF-8').split(/\r?\n/g);
  
  if (lines.length >= 1) {
    lines[0] = this._rest + lines[0];
  }
  this._rest = lines.pop();
  
  async.map(lines, function processLine(line, next) {
    var
    csv = parseCSV(line);
    next(null, csv);
  }, function doneLines(err, results) {
    if (err) {
      done(err);
      return;
    }
    
    this._data.push.apply(this._data, results);
    done(null);
  }.bind(this));
};

//call by end of stream
CSVPrettyPrintStream.prototype._flush = function flush(done) {
  var
  csv, pretty;
  if (this._rest) {
    csv = parseCSV(this._rest);
    this._data.push(csv);
  }
  
  pretty = toPretty(this._data);
  
  this.push(pretty);
  done(null);
};

//CSV parser (one line only)
function parseCSV(line) {
  return (line.match(/("(""|[^"])*"|[^,]+),?/g) || []).map(function (x, i, xs) {
    x = _.startsWith(x, '"') ? x.slice(1, -1).replace('""', '"') : x;
    x = i === xs.length-1 ? x : x.slice(0,-1);
    return x.match(/^[0-9]+$/) ? parseInt(x, 10) : x;
  });
}

//pretty printer
function toPretty(data) {
  if (_.last(data).length === 0) data.pop();
  
  var
  maxLens = [], maxLen = 0,
  frame = '';

  data.forEach(function (line) {
    maxLen = _.max([maxLen, line.length]);
    line.forEach(function (s, i) {
      maxLens[i] = _.max([maxLens[i] || 0, size(s)]);
    });
  });
  
  frame = maxLens.map(function (n) {
    return _.repeat('-', n);
  }).join('+');
  frame = _.join('', '+', frame, '+');
  
  data = data.map(function (line) {
    if (line.length === 0) return frame;
    
    line = _.range(0, maxLen).map(function (i) {
      var
      x = line[i],
      space = _.repeat(' ', maxLens[i] - size(x || ''));
      return (typeof x === 'number' ? space + x : (x || '')  + space) + (i >= line.length-1 ? ' ' : '|');
    }).join('').slice(0,-1);
    
    return _.join('', '|', line, '|');
  }).join('\n');
  
  return _.join('', frame, '\n', data, '\n', frame);
  
  function size(s) {
    return _.map(s + '', function (c) {
      return c >= 'あ' ? 2 : 1;
    }).reduce(function (memo, x) {
      return memo + x;
    }, 0);
  }
}
```

このstreamはこう使う。

```js;csvpretty-use.js
var
csvpretty = require('./csvpretty.js');

process.stdin.pipe(csvpretty()).pipe(process.stdout);
```

`pipe`を使って処理を繋げているのがポイント。また、`process.stdin`の部分が`fs.createReadStream(filename)`だったらfilenameというファイルから読み込むことになるし、HTTPリクエストを渡せばネットワークから取得したファイルも表示できる。

で、これにGoogleトレンドから拾ってきたCSVを食わせると、

```input.csv
人気度の動向
週,javascript,node.js

2013-04-07 - 2013-04-13,28,1
2013-04-14 - 2013-04-20,27,1
2013-04-21 - 2013-04-27,28,1
2013-04-28 - 2013-05-04,25,1
2013-05-05 - 2013-05-11,26,1
2013-05-12 - 2013-05-18,27,1
2013-05-19 - 2013-05-25,26,1
2013-05-26 - 2013-06-01,26,1
2013-06-02 - 2013-06-08,27,1
2013-06-09 - 2013-06-15,26,1
2013-06-16 - 2013-06-22,27,1
2013-06-23 - 2013-06-29,26,1
2013-06-30 - 2013-07-06,25,1
2013-07-07 - 2013-07-13,27,1
2013-07-14 - 2013-07-20,26,1
2013-07-21 - 2013-07-27,26,1
2013-07-28 - 2013-08-03,26,1
2013-08-04 - 2013-08-10,26,1
2013-08-11 - 2013-08-17,14,1
```

意外と良い感じに表示される。

```
+-----------------------+----------+-------+
|人気度の動向                              |
|週                     |javascript|node.js|
+-----------------------+----------+-------+
|2013-04-07 - 2013-04-13|        28|      1|
|2013-04-14 - 2013-04-20|        27|      1|
|2013-04-21 - 2013-04-27|        28|      1|
|2013-04-28 - 2013-05-04|        25|      1|
|2013-05-05 - 2013-05-11|        26|      1|
|2013-05-12 - 2013-05-18|        27|      1|
|2013-05-19 - 2013-05-25|        26|      1|
|2013-05-26 - 2013-06-01|        26|      1|
|2013-06-02 - 2013-06-08|        27|      1|
|2013-06-09 - 2013-06-15|        26|      1|
|2013-06-16 - 2013-06-22|        27|      1|
|2013-06-23 - 2013-06-29|        26|      1|
|2013-06-30 - 2013-07-06|        25|      1|
|2013-07-07 - 2013-07-13|        27|      1|
|2013-07-14 - 2013-07-20|        26|      1|
|2013-07-21 - 2013-07-27|        26|      1|
|2013-07-28 - 2013-08-03|        26|      1|
|2013-08-04 - 2013-08-10|        26|      1|
|2013-08-11 - 2013-08-17|        14|      1|
+-----------------------+----------+-------+
```

streamはよく分からない。