Node.jsでファイルを2行ずつ処理する

昨日、1行ずつ処理する方法はわかったけど、複数行、例えば2行ずつ処理したいこともある。ググってみたら Qiita で見つけた。

 cf. ストリームを行ごとに処理~readline編~ – Qiita

真似して書いてみた。

var readline = require('readline');
var fs = require('fs');

function LineReader(stream) {
  var self = this;
  self.rl = readline.createInterface({
    "input": stream,
    "output": {}
  });
  self.list = [];
}

LineReader.prototype.forEach = function(unit, fn, callback) {
  var self = this;

  self.rl.on("line", function(line) {
    self.list.push(line);
    if (self.list.length == unit) {
      fn(self.list);
      self.list = [];
    }
  });

  self.rl.on("close", function(line) {
    if (self.list.length) {
      fn(self.list);
    }
    callback();
  });
}

var infile = process.argv[2];
var rs = fs.createReadStream(infile);
var linereader = new LineReader(rs);

linereader.forEach(2, function(list) {
  console.log(list.join(":"));
}, function() {
});
takatoh@nightschool $ cat input.txt
3
1 2 3
4
2 4 8 16
4
2 3 5 7
takatoh@nightschool $ nodejs readline_unit.js input.txt
3:1 2 3
4:2 4 8 16
4:2 3 5 7

上の実行例では2行ずつだけど、forEach の第1引数でほかの値を指定すれば任意行ずつ処理ができる。
ただ、指定した行(とその端数)ずつしか処理できないんだよな。何行ずつ処理するかを動的には決められない。Ruby の gets みたいなのがあればいいのに。

Node.jsでファイルを1行ずつ処理する

Web で見つけてきた JavaScript の動作検証をしようと、ファイルからデータ(1行に1データ)を読み込んで処理する方法を探したら見つかったのでメモ。

 cf. Node.jsでテキストを1行ずつ処理する – console.lealog();

var fs = require('fs');
var rl = require('readline');

var inputStream = fs.createReadStream('./input.txt');
var inputReadLine = rl.createInterface({'input': inputStream, 'output': {}});

inputReadLine
  .on('line', function(line){
    console.log(line * 10.0);
  })
  .on('close', function() {
  });
takatoh@nightschool $ cat input.txt
1
2
3
4
5
6
7
8
9
10
takatoh@nightschool $ nodejs readline.js
10
20
30
40
50
60
70
80
90
100

不完全文字列

昨日の make-byte-string の項で出てきた #*”aaaaa” というのは不完全文字列(incomplete string)というものらしい。#*”…” というのが不完全文字列のリテラルね。

文字列について – Gauche – A Scheme Implementation から引用すると、

不完全文字列

全てのバイト列が可変長エンコーディングとして正しく解釈できるわけではない。 定義されていないコードポイントを指している場合はまだ可変長文字列として扱えるが、 そのエンコーディングではどうにも解釈しようの無いバイトシーケンスというのが 外部から入って来る可能性がある。

Gaucheではこのような文字列をincomplete stringと呼び、そのようにマークされる。 文字列操作系の関数は、incomplete stringを可能な限りあたかも1バイト固定長文字列のように扱う。 incomplete stringとcomplete stringが接合された場合、 結果はincomplete stringになる。

要するに、文字として解釈できない可能性のある文字列(バイト列)を不完全文字列と呼ぶようだ。

文字列が不完全文字列かどうかは string-incomplete? で判断できる。

gosh> (string-incomplete? "abc")
#f
gosh> (string-incomplete? #*"abc")
#t

フツウの文字列 “abc” では #f が、不完全文字列 #*”abc” では #t が返ってきた。なるほど。

「外部から入って来る」というのは、例えばコマンドライン引数なんかだろうか。

(define main
  (lambda (args)
    (print (string-incomplete? (cadr args)))))
takatoh@nightschool $ gosh string-incomplete.scm foo
#f

あれ、#f が返ってきた。てことは不完全文字列じゃないってことだ。
ファイルから入力した場合はどうだろう?

(define main
  (lambda (args)
    (print (string-incomplete? (call-with-input-file (cadr args) port->string)))))
takatoh@nightschool $ gosh string-incomplete2.scm input.txt
#f

これも #f だ。
どういう時に不完全文字列になるんだろう?

文字列を長さ1の文字列のリストに分割したい(その2)

コメントで string という手続きを教えてもらった。

 cf. 6.12 文字列 – Gauche ユーザリファレンス

gosh> (string #\a)
"a"

これを使えば、昨日の string->string-list はこう書ける。

gosh> (define string->string-list
  (lambda (s)
    (map string (string->list s))))
string->string-list
gosh> (string->string-list "abc")
("a" "b" "c")

うーん、上のページは目を通したはずなんだけど見落としてたよ。文字列を「変換する」方法ばかり探してたからかもしれない。文字列の構築子の項にちゃんと載ってた。

せっかくなので、文字列を作る方法について少しまとめておこう。

string char …

上でも書いたように、文字から文字列を作る。引数の文字は1つじゃなくてもいい。

gosh> (string #\a #\b #\c)
"abc"

make-string k :optional char

長さ k の文字列を返す。char が与えられればそれで内容を満たすってあるけど、なかったらどうなるんだ?

gosh> (make-string 5 #\x)
"xxxxx"
gosh> (make-string 5)
"     "

なるほど、スペースが使われるのね。

make-byte-string k :optional byte

make-string に似てるけど、オプショナルな引数 byte は文字じゃなくて整数。

gosh> (make-byte-string 5)
#*"\0\0\0\0\0"
gosh> (make-byte-string 5 97)
#*"aaaaa"

んむ、なんか見たことのない表現が出てきた。

x->stirng obj

文字列への強制型変換手続き。簡単に言うと obj を表示するときの文字列に変換される、ってことでいいのかな。

gosh> (x->string "abc")
"abc"
gosh> (x->string 123)
"123"
gosh> (x->string 'foo)
"foo"
gosh> (x->string '(1 2 3))
"(1 2 3)"

[追記]

コメントでは gauche.collection モジュールの map を使う方法も教えてもらったので、それも書いておこう。標準ライブラリの map はリストしか受け付けないけど、gauche.collection モジュールの map は文字列を含むコレクションにを受け付ける。なので、string->list を省略できる。

gosh> (use gauche.collection)
#<undef>
gosh> (define string->string-list
  (lambda (s)
    (map string s)))
string->string-list
gosh> (string->string-list "abc")
("a" "b" "c")

文字列を長さ1の文字列のリストに分割したい

文字のリストにするのには string->list が使える。

gosh> (string->list "abc")
(#\a #\b #\c)

でも今日欲しいのは文字のリストじゃなくて、文字列(長さ1)のリストだ。例えば Ruby ではこう書ける。

2.1.1 :001 > "abc".split(//)
 => ["a", "b", "c"]

空(?)の正規表現で分割すると1文字ずつの配列になる。Ruby じゃ定番だね。

で、Scheme。string-split が正規表現も受け付けてくれるというので、上と同じように試したらダメだった。

gosh> (string-split "abc" #//)
*** ERROR: string-split: splitter must not match a null string: #//
Stack Trace:
_______________________________________
  0  (error "string-split: splitter must not match a null string:" spli ...
        At line 58 of "/usr/share/gauche-0.9/0.9.3.3/lib/gauche/stringutil.scm"
  1  scanner

結局こういうのふうに書いてみたけど、なんかスマートじゃない。

gosh> (define string->string-list
  (lambda (s)
    (map (lambda (c) (list->string (list c))) (string->list s))))
string->string-list
gosh> (string->string-list "abc")
("a" "b" "c")

文字から長さ1の文字列へ変換する手続きが見当たらないんだよな。需要がないのかな。

Max Range Sum

昨日に引き続き CodeEval から。

 cf. Max Range Sum – CodeEval

(define slices
  (lambda (lis n)
    (let loop ((l1 lis) (l2 '()))
      (if (< (length l1) n)
          (reverse l2)
          (loop (cdr l1)
            (cons (take l1 n) l2))))))

(define sum
  (lambda (lis)
    (apply + lis)))

(define solv
  (lambda (str)
    (let* ((s (string-split str ";"))
           (n (string->number (car s)))
           (lis (map string->number (string-split (cadr s) " "))))
      (apply max (cons 0 (map sum (slices lis n)))))))

(define print-solv
  (lambda (str)
    (print (solv str))))

(define main
  (lambda (args)
    (with-input-from-file (cadr args)
      (lambda ()
        (port-for-each print-solv read-line)))))
takatoh@nightschool $ cat input.txt
5;7 -3 -10 4 2 8 -2 4 -5 -2
6;-4 3 -10 5 3 -7 -3 7 -6 3
3;-7 0 -45 34 -24 7
takatoh@nightschool $ gosh max_range_sum.scm input.txt
16
0
17

Details

Details って意味がわかんないんだけど、CodeEval のこれ。

 cf. Details – CodeEval

Scheme でやってみた。

(define dots
  (lambda (lis)
    (let loop ((l lis) (d 0))
      (cond
        ((char=? (car l) #\Y) d)
        ((char=? (car l) #\.) (loop (cdr l) (+ d 1)))
        (else (loop (cdr l) 0))))))

(define solv
  (lambda (str)
    (apply min (map dots (map string->list (string-split str ","))))))

(define print-solv
  (lambda (str)
    (print (solv str))))

(define main
  (lambda (args)
    (with-input-from-file (cadr args)
      (lambda ()
        (port-for-each print-solv read-line)))))

基本的には CodeEval に提出した Python 版と同じ。

takatoh@nightschool $ cat input.txt
XX.YY,XXX.Y,X..YY,XX..Y
XXX.YYYY,X...Y..Y,XX..YYYY,X.....YY,XX....YY
XX...YY,X....YY,XX..YYY,X..YYYY
XXYY,X..Y,XX.Y
takatoh@nightschool $ gosh details.scm input.txt
1
1
2
0

たぶんあってる。

多値

昨日、継続渡しの練習で partition という手続きを書いた。
同じ名前の手続きが SRFI-1 ライブラリに定義されている。けど、ちょっと動作が違う。

gosh> (use srfi-1)
#<undef>
gosh> (partition odd? '(1 2 3 4 5))
(1 3 5)
(2 4)

(1 3 5) と (2 4) がひとつのリストに入っているんじゃなくて、2行にわたって表示されている。実は、SRFI-1 の partition は2つの値を返している。partition のように2つ(あるいはそれ以上)の値を返すことを「多値」というらしい。

 cf. Scheme:多値

多値を返すには values を使う。

gosh> (values 'foo 'bar 'baz)
foo
bar
baz

一方、多値を受け取るには receive が使える。

gosh> (receive (odds evens) (partition odd? '(1 2 3 4 5))
  (print odds))
(1 3 5)
#<undef>

odds と evens が返される多値に束縛される変数だ。必要がないからといって、数を合わせないとダメらしい。

gosh> (receive (odds) (partition odd? '(1 2 3 4 5))
  (print odds))
*** ERROR: received more values than expected
Stack Trace:
_______________________________________

ふーん、なんとなくわかった(ような気がする)けど、ちょっとピンとこないな。

継続渡しの練習

リストを、述語 p を適用した結果が真になるものと偽になるものに分ける partition を継続渡しスタイルで書いてみた。

(define partition
  (lambda (p lis co)
    (cond
      ((null? lis)
        (co '() '()))
      ((p (car lis))
        (partition p (cdr lis) (lambda (l r) (co (cons (car lis) l) r))))
      (else
        (partition p (cdr lis) (lambda (l r) (co l (cons (car lis) r))))))))

(print (partition odd? '(1 2 3 4 5) list))
takatoh@nightschool $ gosh partition.scm
((1 3 5) (2 4))