文字列のn行目のインデックスを取得

文字列のn行目の先頭が文字列先頭から何文字目かを取得する関数。 4 つ書いてみた。

function getLineIndex1(str, lineno) {
	var curline = 0;
	var len = str.length;
	for(var i = 0; i < len; i ++) {
		if(curline == lineno) return i;
		var c = str.charAt(i);
		if(c == "\r") {
			if(str.charAt(i+1) == "\n") i ++;
			curline ++;
		}
		if(c == "\n") {
			curline ++;
		}
	}
	return null;
}

function getLineIndex2(str, lineno) {
	var curline = 0;
	var len = str.length;
	for(var i = 0; i < len; i ++) {
		if(curline == lineno) return i;
		var c = str.charCodeAt(i);
		if(c == 0x0d) {
			if(str.charCodeAt(i+1) == 0x0a) i ++;
			curline ++;
		}
		if(c == 0x0a) {
			curline ++;
		}
	}
	return null;
}

function getLineIndex3(str, lineno) {
	if(str.length > 0 && lineno == 0) return 0;
	var i = 0;
	var result;
	var tag = new Object;
	try {
		str.replace(/(?:\r\n|\n|\r(?!\n))(?!$)/g, function(s,l) {
			if(++i == lineno) {
				result = l + s.length;
				throw tag;
			}
		});
		return null;
	} catch(e) {
		if(e !== tag) throw e;
		return result;
	}
}

function getLineIndex4(str, lineno) {
	var i = 0;
	var result;
	var tag = new Object;
	try {
		str.replace(/.*(?:\r\n|\n|\r|.$)/g, function(s,l) {
			if(i++ == lineno) {
				result = l;
				throw tag;
			}
		});
		return null;
	} catch(e) {
		if(e !== tag) throw e;
		return result;
	}
}

getLineIndex1 は普通に 1 文字ずつ charAt で舐めていく方法。 getLineIndex2 は getLineIndex1 の charAt を charCodeAt にかえただけ。 getLineIndex3 は String#replace を使って。 getLineIndex4 はその変形。

テストと時間計測

// http://developer.mozilla.org/ja/Core_JavaScript_1.5_Reference/Global_Objects/Array/forEach
if (!Array.prototype.forEach)
  Array.prototype.forEach = function(fun, thisp) {
    var len = this.length;
    for (var i = 0; i < len; i++)
      if (i in this)
        fun.call(thisp, this[i], i, this);
  };

var testCount;
var testNGs;
function tests(name, callback) {
	testCount = 0;
	testNGs = [];
	callback();
	if(testNGs.length == 0) {
		print(name+": all test passed ("+testCount+")");
	} else {
		print(name+": " + (testCount - testNGs.length)+" pass "+testNGs.length+" fail");
		testNGs.forEach(function(arg) {
			var i = arg[0], a = arg[1], b = arg[2];
			print(" "+i+": "+uneval(a)+" != "+uneval(b));
		});
	}
}

function is(a,b) {
	if(a != b) {
		testNGs.push([testCount, a, b]);
	}
	testCount ++;
}

var fns = [getLineIndex1, getLineIndex2, getLineIndex3, getLineIndex4];

fns.forEach(function(fn){
	tests(fn.name, function(){
		is(fn("abc",0), 0);
		is(fn("",0), null);
		is(fn("\n",0), 0);

		is(fn("abc",1), null);
		is(fn("\n",1), null);
		is(fn("\nA",1), "\n".length);
		is(fn("ABC\nDEF",1), "ABC\n".length);
		is(fn("ABC\nDEF",2), null);
		is(fn("ABC\nDEF\n",2), null);
		is(fn("ABC\nDEF\nGHI",2), "ABC\nDEF\n".length);


		is(fn("\r\n",0), 0);
		is(fn("\r\n",1), null);
		is(fn("\r\nA",1), "\r\n".length);
		is(fn("ABC\r\nDEF",1), "ABC\r\n".length);
		is(fn("ABC\r\nDEF",2), null);
		is(fn("ABC\r\nDEF\r\n",2), null);
		is(fn("ABC\r\nDEF\r\nGHI",2), "ABC\r\nDEF\r\n".length);
	});
});

var a = [];
var N = 5000;
for(var i = 0; i < N; i ++) {
	var l = (i / 50)|0;
	while(l--) {
		a[a.length] = "A";
	}
	a[a.length] = "\r\n";
}
var str = a.join("");

fns.forEach(function(fn){
	var n = N - 1;
	var start = + new Date;
	var result = fn(str, n);
	var end = + new Date;
	print(fn.name+':'+result+':'+(end - start)+' ms');
});

もっと高速な方法やエレガントな方法があったら教えてください><