EcmaScript 6 で String.prototype.codePointAt()
が導入され、サロゲートペアを考慮した文字列関数を定義しやすくなった。es6-shim が配布されているので、古いブラウザーでも利用可能である。io.js では codePointAt
だけでなく for..of
を使うことができる。使い方はこちらの記事を参照。
2015年9月4月追記
文字列の長さ、部分文字列を求めるメソッドを利用できる npm のパッケージを公開しました。チュートリアルとはメソッドの名前は異なります。
文字数を求める
文字数に関しては、正規表現を使ったほうがコードがコンパクトになる。文字列の length
プロパティからサロゲートペアの組を引けばよい。名前は ICU の UnicodeString#countChar32 から借りた。
if (!String.prototype.countChar32) {
String.prototype.countChar32 = function() {
return this.length - (this.match(/[\uD800-\uDBFF][\uDC00-\uDFFFF]/g)||[]).length;
};
}
var str = '\uD842\uDFB7野家';
console.log(3 === str.countChar32());
codePointAt
から定義すると次のようになる。
if (!String.prototype.countChar32) {
String.prototype.countChar32 = function() {
var pos = 0;
var length = this.length;
var cp = 0;
var count = 0;
while (pos < length) {
cp = this.codePointAt(pos);
pos += cp < 0x10000 ? 1 : 2;
++count;
}
return count;
};
}
var str = '\uD842\uDFB7野家';
console.log(3 === str.countChar32());
指定したインデックスに対応する1文字取り出す
ICU の UnicodeString#char32At
から名前を借りた。
if (!String.prototype.char32At) {
String.prototype.char32At = function(offset) {
var pos = 0;
var length = this.length;
var cp = 0;
var size = 0;
var index = 0;
while (pos < length) {
cp = this.codePointAt(pos);
size = cp < 0x10000 ? 1 : 2;
if (index === offset) {
return this.substr(pos, size);
}
pos += size;
++index;
}
return '';
};
}
var str = '\uD842\uDFB7野家';
console.log('\uD842\uDFB7' === str.char32At(0));
文字列を逆順に並べ替える
if (!String.prototype.reverse) {
String.prototype.reverse = function() {
var pos = 0;
var length = this.length;
var cp = 0;
var size = 0;
var ret = '';
while (pos < length) {
cp = this.codePointAt(pos);
size = cp < 0x10000 ? 1 : 2;
ret = this.substr(pos, size) + ret;
pos += size;
}
return ret;
};
}
var str = '\uD842\uDFB7野家';
console.log('家野\uD842\uDFB7' === str.reverse());
部分文字列を取り出す
先ほど定義した countChar32
と char32At
を組み合わせる。
if (!String.prototype.substr32) {
String.prototype.substr32 = function(pos, length) {
var count = this.countChar32();
var ret = '';
var last = pos + length - 1;
for (var i = 0; i < count; ++i) {
if (i >= pos && last >= i) {
ret += this.char32At(i);
}
if (i === last) {
return ret;
}
}
return ret;
};
}
var str = '\uD842\uDFB7野家';
console.log('野' === str.substr32(1, 1));
codePointAt
から定義するのであれば、少しコードが長くなる。
if (!String.prototype.substr32) {
String.prototype.substr32 = function(pos, length) {
var offset = 0;
var count = this.length;
var cp = 0;
var size = 0;
var start = 0;
var index = 0;
var last = pos + length - 1;
var retSize = 0;
while (offset < count) {
cp = this.codePointAt(offset);
size = cp < 0x10000 ? 1 : 2;
if (start === 0 && index === pos) {
start = offset;
}
if (index >= pos && last >= index) {
retSize += size;
}
if (index === last) {
return this.substr(start, retSize);
}
offset += size;
++index;
}
return String(this);
};
}
var str = '\uD842\uDFB7野家';
console.log(str.substr32(1, 1));
1文字ずつコールバックを適用する
名前は Ruby の String#eachChar から借りた。
if (!String.prototype.eachChar) {
String.prototype.eachChar = function(callback) {
var pos = 0;
var length = this.length;
var cp = 0;
var size = 0;
while (pos < length) {
cp = this.codePointAt(pos);
size = cp < 0x10000 ? 1 : 2;
callback(this.substr(pos, size));
pos += size;
}
};
}
var str = '\uD842\uDFB7野家';
str.eachChar(function(c) {
console.log(c);
});
1つずつコードポイントを取り出してコールバックを適用する
上述のメソッドを少し修正すればよい。名前は Ruby の String#each_codepoint
から借りた。
if (!String.prototype.eachCodePoint) {
String.prototype.eachCodePoint = function(callback) {
var pos = 0;
var length = this.length;
var cp = 0;
var size = 0;
while (pos < length) {
cp = this.codePointAt(pos);
size = cp < 0x10000 ? 1 : 2;
callback(cp);
pos += size;
}
};
}
var str = '\uD842\uDFB7野家';
str.eachCodePoint(function(cp) {
console.log('U+' + cp.toString(16).toUpperCase());
});