PythonのStringIOをJavaScriptで実装してみた。

ご無沙汰しています。id:rokujyouhitomaです。


さてはて、PythonのStringIOモジュールをJavaScriptで実装しました。外部仕様を一緒にしたのではなく、内部仕様(プログラミング仕様)まで一緒にしました。

動機は使う予定があったから。なくても別にコーディングできるけど、抽象化したかったんで。

元コードはPyPy(Python2.7)のStringIO

コード

書いたコードはgithubにpushしました。

前職のRTM以来ClosureCompilerにハマっているので、ClosureCompiler向けのコードを書いています。

引用すると下記の通り。(注記。私独自のutili関数に依存してるので、万が一使おうとする方はgithubのコードを引用してください。)

/**
 * StringIO
 * @see Python <a href='http://docs.python.org/library/stringio.html'>StringIO</
a> modules.
 */

/** @const */ var EINVAL = 22;

/**
 * @param {boolean} closed .
 */
function _complain_ifclosed(closed) {
    if (closed) {
        throw new ValueError('I/O operation on closed file');
    }
}
/**
 * StringIO is based on Python StringIO build-in modules.
 * @param {?string=} buf .
 * @constructor
 * @extends {Object}
 */
var StringIO = function(buf) {
    base(this);

    buf = buf ? buf : '';

    this.buf = buf;
    this.len = buf.length;
    this.buflist = [];
    this.pos = 0;
    this.closed = false;
    this.softspace = 0;
};
inherits(StringIO, Object);

/**
 * return {StringIO} .
 */
StringIO.prototype.__iter__ = function() {
    return this;
};

/**
 * @return {string} read line.
 */
StringIO.prototype.next = function() {
    _complain_ifclosed(this.closed);
    var r = this.readline();
    if(!r) {
        throw new StopIteration();
        //return new StopIteration();
    }
    return r;
};
StringIO.prototype['next'] = StringIO.prototype.next;

/**
 * Free the memory buffer.
 */
StringIO.prototype.close = function() {
    if (!this.closed) {
        this.closed = true;
        delete this.buf;
        delete this.pos;
    }
};
StringIO.prototype['close'] = StringIO.prototype.close;

/**
 * Returns false because StringIO objects are not connected to a tty-like
 * device.
 */
StringIO.prototype.isatty = function() {
  _complain_ifclosed(this.closed);
  return false;
};
StringIO.prototype['isatty'] = StringIO.prototype.isatty;

/**
 * Set the file's current position.
 * 
 * The mode argument is optional and defaults to 0 (absolute file
 * positioning); other values are 1 (seek relative to the current
 * position) and 2 (seek relative to the file's end).
 * There is no return value.
 */
StringIO.prototype.seek = function(pos, mode) {
    mode = mode ? mode : 0;
    _complain_ifclosed(this.closed);
    if (this.buflist) {
        this.buf += this.buflist.join('');
        this.buflist = [];
    }
    if (mode === 1) {
        pos += this.pos;
    }
    else if (mode === 2) {
        pos += this.len;
    }
    this.pos = buildin.max(0, pos);
};
StringIO.prototype['seek'] = StringIO.prototype.seek;

/**
 * Return the file's current position.
 */
StringIO.prototype.tell = function(pos, mode) {
    mode = mode ? mode : 0;
    _complain_ifclosed(this.closed);
    return this.pos;
};
StringIO.prototype['tell'] = StringIO.prototype.tell;

/**
 * 
 */
StringIO.prototype.read = function(n) {
    n = n ? n : -1;
    _complain_ifclosed(this.closed);
    if(this.buflist) {
        this.buf += this.buflist.join('');
        this.buflist = [];
    }
    var newpos = null;
    if (n === null || n < 0) {
        newpos = this.len;
    }
    else {
        newpos = buildin.min(this.pos + n, this.len);
    }
    var r = this.buf.slice(this.pos, newpos);
    this.pos = newpos;
    return r;
};
StringIO.prototype['read'] = StringIO.prototype.read;

/**
 * @param {?number=} length .
 * @return {string} .
 */
StringIO.prototype.readline = function(length) {
    length = length ? length : null;
    _complain_ifclosed(this.closed);
    if (this.buflist) {
        this.buf += this.buflist.join('');
        this.buflist = [];
    }
    var i = string.find(this.buf, '\n', this.pos);
    var newpos = null;
    if (i < 0) {
        newpos = this.len;
    } else {
        newpos = i + 1;
    }
    if (!(length === null) && length > 0) {
        if ((this.pos + length) < newpos) {
            newpos = this.pos + length;
        }
    }
    var r = this.buf.slice(this.pos, newpos);
    this.pos = newpos;
    return r;
};
StringIO.prototype['readline'] = StringIO.prototype.readline;

/**
 * @param {number} sizehint .
 */
StringIO.prototype.readlines = function(sizehint) {
    sizehint = sizehint ? sizehint : 0;
    var total = 0;
    var lines = [];
    var line = this.readline();
    while (line) {
        lines.push(line);
        total += line.length;
        if (0 < sizehint && sizehint <= total) {
            break;
        }
        line = this.readline();
    }
    return lines;
};
StringIO.prototype['readlines'] = StringIO.prototype.readlines;

/**
 * @param {?number=} size .
 */
StringIO.prototype.truncate = function(size) {
    size = size ? size : null;
    _complain_ifclosed(this.closed);
    if (size === null) {
        size = this.pos;
    }
    else if (size < 0) {
        throw new IOError(EINVAL, 'Negative size not allowed');
    }
    else if (size < this.pos) {
        this.pos = size;
    }
    this.buf = this.getvalue().slice(0, size);
    this.len = size;
};
StringIO.prototype['truncate'] = StringIO.prototype.truncate;

/**
 * @param {string} s .
 */
StringIO.prototype.write = function(s) {
    //Write a string to the file.
    //There is no return value.
    _complain_ifclosed(this.closed);
    if (s === '') {
        return;
    }
    var spos = this.pos;
    var slen = this.len;
    if (spos === slen) {
        this.buflist.push(s);
        this.len = this.pos = spos + s.length;
        return;
    }
    if (spos > slen) {
        var str;
        var i;
        var len = spos - slen;
        for (i = 0; i < len; i++) {
            str += "\0";
        }
        this.buflist.push(str);
        slen = spos;
    }
    var newpos = spos + s.length;
    if (spos < slen) {
        if (this.buflist) {
            this.buf += this.buflist.join('');
        }
        this.buflist = [this.buf.substring(0, spos), s,
                        this.buf.substring(newpos, s.length)];
        this.buf = '';
        if (newpos > slen) {
            slen = newpos;
        }
    } else {
        this.buflist.push(s);
        slen = newpos;
    }
    this.len = slen;
    this.pos = newpos;
};
StringIO.prototype['write'] = StringIO.prototype.write;

/**
 * @param {Array|Object} iterable .
 */
StringIO.prototype.writelines = function(iterable) {
    var key;
    var line;
    for (key in iterable) {
        line = iterable[key];
        this.write(line);
    }
};
StringIO.prototype['writelines'] = StringIO.prototype.writelines;

/**
 * 
 */
StringIO.prototype.flush = function() {
    _complain_ifclosed(this.closed);
};
StringIO.prototype['flush'] = StringIO.prototype.flush;

/**
 * @return {string} .
 */
StringIO.prototype.getvalue = function() {
    if (this.buflist) {
        this.buf += this.buflist.join('');
        this.buflist = [];
    }
    return this.buf;
};
StringIO.prototype['getvalue'] = StringIO.prototype.getvalue;

テストコード

当然テストコードもPyPyプロジェクトのリポジトリにあったテストコードをJasmineのテストで書き直しました。

/*
 * @see pypy-1.7/lib-python/2.7/test/test_StringIO.py
 */
var StopIteration = require('../src/template/template').StopIteration;
var StringIO = require('../src/template/template').StringIO;

describe('TestGenericStringIO', function() {
  var _line = '';
  var _lines = '';
  var _fp;

  beforeEach(function(){
    _line = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!';
    for (var i = 0; i < 5; ++i) {
      _lines += _line + '\n';
    }
    _fp = new StringIO(_lines);
  });

  afterEach(function(){
    _line = '';
    _lines = '';
    _fp = null;
  });

  it('test_reads', function(){
    //expect(f.seek()).toThrow();
    expect(_fp.read(10)).toEqual(_line.slice(0, 10));
    expect(_fp.readline()).toEqual(_line.slice(10) + '\n');
    expect(_fp.readlines(60).length).toEqual(2);
    _fp.seek(0);
    expect(_fp.readline(-1)).toEqual(_line + '\n');
  });

  it('test_writes', function(){
    var f = new StringIO();
    //expect(f.seek()).toThrow();
    f.write(_line.slice(0, 6));
    f.seek(3);
    f.write(_line.slice(20, 26));
    f.write(_line[52]);
    expect(f.getvalue()).toEqual('abcuvwxyz!');
  });

  it('test_writelines', function(){
    var f = new StringIO();
    f.writelines([_line[0], _line[1], _line[2]]);
    f.seek(0);
    expect(f.getvalue()).toEqual('abc');
  });

  it('test_truncate', function(){
    var f = new StringIO();
    expect(f.closed).toBeFalsy();
    f.close();
    expect(f.closed).toBeTruthy();
    f = new StringIO('abc');
    expect(f.closed).toBeFalsy();
    f.close();
    expect(f.closed).toBeTruthy();
  });

  if('test_isatty', function(){
    var f = new StringIO();
    expect(f.isatty()).toBeFalsy();
    f.close();
  });

  it('test_iterator', function(){
    expect('__iter__' in _fp).toBeTruthy();
    expect('next' in _fp).toBeTruthy();
    var i = 0;
    var line;
    while (true) {
        try {
            line = _fp.next();
        } catch (e) {
            if (e instanceof StopIteration){
                break;
            }
        }
        i += 1;
    }
    expect(i).toEqual(5);
    _fp.close();
  });

});

現状、このテストコードはクリアしてます。

まとめ

JSはPythonに比べて組み込み関数がしょぼいのが残念です。