初めに
今回は文字列に関連するメソッドをまとめてみました。
Methods
String.prototype.charCodeAt()
charCodeAt()
は指定された文字列のインデックスに、対応したUTF-16のCode Unit
(0
-65535
)を返す。
サロゲートペアは二つのCode Unit
の組み合わせなので、charCodeAt()
は一部のCode Unit
を返すことしかできません。
// Syntax
str.charCodeAt(index)
// index in here means "code unit index"
// it will return UTF-16 "code unit"
console.log('abc'.charCodeAt(0)); // 97
console.log('abc'.charCodeAt(1)); // 98
// surrogate pair
console.log('𝒳'.length); // 2
console.log('𝒳'.charCodeAt(0)); // 55349
console.log('𝒳'.charCodeAt(1)); // 56499
String.prototype.codePointAt()
charCodeAt()
と違い、UnicodeのCode Point
を返す。(Code Unit
(0
-65535
)はcharCodeAt()
とは変わらない。)
Code Point
単位で返すのでサロゲートペアもうまく対応できる。
// Syntax
str.codePointAt(position)
console.log('abc'.codePointAt(0)); // 97
console.log('abc'.codePointAt(1)); // 98
// surrogate pair
console.log('𝒳'.length); // 2
console.log('𝒳'.codePointAt(0)); // 119987
String.prototype.charCodeAt() vs. String.prototype.codePointAt()
charCodeAt()
もcodePointAt()
もユニコード\u
から文字に表示させる前に、まず一度16進数(hex digits)に変換する必要があります。
console.log('a'.charCodeAt(0)); // 97
console.log('a'.charCodeAt(0).toString(16)); // 61
console.log('a'.codePointAt(0)); // 97
console.log('a'.codePointAt(0).toString(16)); // 61
console.log('\u0061'); // a
四桁なら\uXXXX
。一から六桁までは\u{X...XXXXXX}
で文字に変換する。
console.log('𝒳'.charCodeAt(0)); // 55349
console.log('𝒳'.charCodeAt(1)); // 56499
console.log('𝒳'.charCodeAt(0).toString(16)); // d835
console.log('𝒳'.charCodeAt(1).toString(16)); // dcb3
console.log('\ud835\udcb3'); // 𝒳
console.log('𝒳'.codePointAt(0)); // 119987
console.log('𝒳'.codePointAt(0).toString(16));
console.log('\u{1d4b3}'); // 𝒳
console.log('\u{61}'); // a
charCodeAt()
とcodePointAt()
と一番の違いは、
charCodeAt()
はUTF-16のCode Unit
単位で指定インデックスのコードを返すが、
codePointAt()
はUnicodeのCode Point
単位で、2つのCode Unit
を占めたサロゲートペアもまとめたコードを返す。
String.fromCharCode()
指定されたCode Unit
から文字に変換する。複数個も可能です。
// Syntax
String.fromCharCode(num1[, ..., numN])
console.log('abc'.charCodeAt(0, 1, 2)); // 97 // only for one index
console.log(String.fromCharCode(97)); // a
console.log(String.fromCharCode(97, 98, 99)); // abc
// decimal
console.log('𝒳'.charCodeAt(0)); // 55349
console.log('𝒳'.charCodeAt(1)); // 56499
console.log(String.fromCharCode(55349, 56499)); // 𝒳
// hexadecimal
console.log('𝒳'.charCodeAt(0).toString(16)); // d835
console.log('𝒳'.charCodeAt(1).toString(16)); // dcb3
console.log(String.fromCharCode(0xd835, 0xdcb3)); // 𝒳
console.log('\ud835\udcb3'); // 𝒳
console.log(String.fromCharCode(128970)); // // it is over 65535(0xFFFF)
上のようにString.fromCharCode()
は複数個のCode Unit
を使えるので、サロゲートペアへもうまく転換することができる。また、10進数、16進数も受け入れる。
しかしUTF-16Code Unit
の範囲(0
(0x0000)-65535
(0xFFFF))を超えた正しく表現できません。
String.fromCodePoint()
String.fromCodePoint()
はCode Point
単位で文字を変換する。
// Syntax
String.fromCodePoint(num1[, ..., numN])
String.fromCodePoint()
も10進数と16進数を受け入れる。
console.log('𝒳'.codePointAt(0)); // 119987
console.log('𝒳'.codePointAt(0).toString(16)); // 1d4b3
console.log(String.fromCodePoint(119987)); // 𝒳 // decimal
console.log(String.fromCodePoint(0x1d4b3)); // 𝒳 // hexadecimal
console.log('\u{1d4b3}'); // 𝒳
String.fromCharCode() vs. String.fromCodePoint()
String.fromCodePoint()
は0
(0x0000)-65535
(0xFFFF)を超えても正しく変換できます。
console.log(String.fromCharCode(128970)); //
console.log(String.fromCharCode(0x1F7CA)); //
console.log(String.fromCodePoint(128970)); // 🟊
console.log(String.fromCodePoint(0x1F7CA)); // 🟊
String.prototype.indexOf()
引数と同じ文字列(×正規表現)が見つかればindex
を返す。見つからない場合は-1
を返す。
// Syntax
str.indexOf(substr, position)
// position = fromIndex
二番目の引数pos
で検索の開始位置を指定することができる。
let str = 'Widget with id';
console.log(str.indexOf('Widget')); // 0 // 'Widget'
console.log(str.indexOf('widget')); // -1
console.log(str.indexOf('id')); // 1 // W'id'get
console.log(str.indexOf('id', 1)); // 1
console.log(str.indexOf('id', 2)); // 12 // 'id'
let str = 'As sly as a fox, as strong as an ox';
let target = 'as';
let pos = -1;
while ((pos = str.indexOf(target, pos + 1)) !== -1) {
console.log(pos);
}
// 7
// 17
// 27
let str = "Widget with id";
if (str.indexOf('Widget')) {
console.log('We found it');
}
// doesn't work, because indexOf return 0
if (str.indexOf('Widget') !== -1) {
console.log('We found it')
}
// We found it
String.prototype.lastIndexOf()
indexOf()
とは逆方向に検索を行う。
// Syntax
str.lastIndexOf(substr, position)
let str = 'As sly as a fox, as strong as an ox';
let target = 'as';
console.log(str.lastIndexOf(target, 20)); // 17
console.log(str.lastIndexOf(target, 10)); // 7
String.prototype.includes()
(×正規表現)
目標の文字列と一致したらtrue
、しなかったらfalse
。
// Syntax
str.includes(substr, position)
console.log('Widget'.includes('id')); // true
console.log('Widget'.includes('id', 3)); // false
String.prototype.startsWith() & String.prototype.endsWith()
(×正規表現)
目標の文字列の始まりや終わりが指定された文字列と同じ場合はtrue
、でなければfalse
。
// Syntax
str.startsWith(substr, position)
str.endsWith(substr, position)
この二つのメソッドでは\n
など改行・空白文字も文字列の一部と見なされている。
console.log('Widget'.startsWith('Wid')); // true
console.log('\nWidget'.startsWith('Wid')); // false
console.log('Wid\nget'.endsWith('get')); // true
console.log('Widget\n'.endsWith('get')); // false
String.prototype.slice()
指定したインデックスから(元の文字列を変更せず)文字列を切り取る。
// Syntax
str.slice(start[, end])
end
が指定されてない場合は最後まで切り取る、
end
が指定される場合はend
の文字列を含まれず切り取る。
let str = 'stringify';
console.log(str.slice(2)); // ringify
console.log(str.length); // 9
console.log(str.slice(2, 8)); // ringif // 'y' index is 8, the length is 9
console.log(str.slice(2, 9)); // ringify
console.log(str.slice(2, 10)); // ringify
負数の指定というのは逆方向からです。最後のインデックスは-1
と見なされる。
console.log(str.slice(-4, -1)); // gif
String.prototype.substring()
指定した区間位置の文字列を返す。
// Syntax
str.substring(start [, end])
end
の文字列は含まれない。
let str = 'stringify';
console.log(str.substring(2, 6)); // ring
console.log(str.substring(6, 2)); // ring
console.log(str.substring(-4, -8)); // '' // not supported
負数の指定は支援されていません。
String.prototype.localeCompare()
二つの文字列(頭文字)を比べるメソッドです。
// Syntax
referenceStr.localeCompare(compareStr, locales, options)
// Result
referenceStr > compareStr => positive
referenceStr < compareStr => negative
referenceStr = compareStr => 0
引数が参照文字列のコードポイントより後(大きい)は正数、
引数が参照文字列のコードポイントより前(小さい)は負数、
両者同じである場合は0
。
(環境により正数/負数の表現は変わるが、正数/負数の判定は変わりません。)
-
locales
ロケールはIETF言語タグを使います。
-
options
はIntl.Collator() constructor
の設定に従います。(デフォルトメソッドはsort
、sort
のsensitivity
はvariant
)
(Intl.Collator() constructor
ではプロパティのより詳細な設定ができる)
console.log('a' > 'A');
// true
console.log(`a: ${'a'.codePointAt(0)}, A: ${'A'.codePointAt(0)}`);
// a: 97, A: 65
console.log('a'.localeCompare('A'));
// -1
console.log('a'.localeCompare('A', 'en'));
// -1
console.log('a'.localeCompare('A', 'en', { sensitivity: 'base' }));
// 0
console.log('a'.localeCompare('A', 'en', { sensitivity: 'accent' }));
// 0
console.log('a'.localeCompare('A', 'en', { sensitivity: 'case' }));
// -1
console.log('a'.localeCompare('A', 'en', { sensitivity: 'variant' }));
// -1
/* note:
sensitivity: 'base' => a ≠ b, a = á, a = A
sensitivity: 'accent' => a ≠ b, a ≠ á, a = A
sensitivity: 'case' => a ≠ b, a = á, a ≠ A
sensitivity: 'variant' => a ≠ b, a ≠ á, a ≠ A
The default is "variant" for usage "sort"; it's locale dependent for usage "search".
*/
console.log(`á: ${'á'.codePointAt(0)}, a: ${'a'.codePointAt(0)}`);
// á: 225, a: 97
console.log('á'.localeCompare('a'));
// -1 // compare with codePoint
console.log('á'.localeCompare('a', 'en', { sensitivity: 'variant' }));
// 1 // compare with IETF tags
console.log(['á', 'a'].sort(new Intl.Collator().compare))
// [ 'á', 'a' ]
console.log(['á', 'a'].sort(new Intl.Collator('en').compare))
// [ 'a', 'á' ]
console.log(['á', 'a', 'A'].sort(new Intl.Collator('en', { caseFirst: 'upper' }).compare))
// [ 'A', 'a', 'á' ]
日本語(ja=jpn=ja-JP)ではひらがなとカタカナとの比べは区別つきません。
console.log(`あ: ${'あ'.codePointAt(0)}, ア: ${'ア'.codePointAt(0)}`);
// あ: 12354, ア: 12450
console.log('あ'.localeCompare('ア', 'en'));
// -1 // compare with wrong tag
console.log('あ'.localeCompare('ア', 'ja'));
// 0
console.log('あ'.localeCompare('ア', 'jpn'));
// 0
console.log('あ'.localeCompare('ア', 'ja-JP'));
// 0
console.log('あ'.localeCompare('ア', 'ja', { sensitivity: 'base' }));
// 0
console.log('あ'.localeCompare('ア', 'ja', { sensitivity: 'accent' }));
// 0
console.log('あ'.localeCompare('ア', 'ja', { sensitivity: 'case' }));
// 0
console.log('あ'.localeCompare('ア', 'ja', { sensitivity: 'variant' }));
// 0
漢字もコードポイントで比べる。
console.log(`太: ${'太'.codePointAt(0)}, 次: ${'次'.codePointAt(0)}`);
// 太: 22826, 次: 27425
console.log('太郎'.localeCompare('次郎', 'ja', { sensitivity: 'base' }));
// 1
console.log('太郎'.localeCompare('次郎', 'ja', { sensitivity: 'accent' }));
// 1
console.log('太郎'.localeCompare('次郎', 'ja', { sensitivity: 'case' }));
// 1
console.log('太郎'.localeCompare('次郎', 'ja', { sensitivity: 'variant' }));
// 1
String.prototype.normalize()
文字列を指定した形式で正規化した表現として返す。
// Syntax
str.normalize(form)
// note
NFC => normalize with composed canonical form
NFD => normalize with decomposed canonical form
// default is NFC
NFC
は合成正規形、複数のCode Point
で合成された文字列を取得し、正確に準ずる等価性を与える。
NFD
は分解正規形、単一のCode Point
から分解した文字列を取得し、正確に準ずる等価性を与える。
ほかにはNFKC
、NFKD
という形式があるが、ここでは省けます。
初めて見たメソッドですが、色々と試してみるとnormalize()
は特定の組み合わせだけ作用するという感じです。
console.log('S\u0307'); // Ṡ // S + dot above
console.log('S\u0307\u0323'); // Ṩ // S + dot above + dot below
console.log('S\u0323\u0307'); // Ṩ // S + dot below + dot above
console.log('\u1e68'); // Ṩ // S with two dots
console.log('S\u0307'.normalize('NFC') === 'S\u0307\u0323'.normalize('NFC')) // false
console.log('S\u0307'.normalize('NFC') === 'S\u0323\u0307'.normalize('NFC')) // false
console.log('S\u0307'.normalize('NFC') === '\u1e68'.normalize('NFC')) // false
console.log('S\u0307\u0323'.normalize('NFC') === '\u1e68'.normalize('NFC')) // true
console.log('S\u0323\u0307'.normalize('NFC') === '\u1e68'.normalize('NFC')) // true
カスタマイズではなく、ちゃんと形の合わせたCode Point
の組み合わせでないとnormalize()
はfalse
を返してくる。
最初は分解、合成という説明と例を見たら、これもサロゲートペアのようなものなのかな?って思ったんですが、実際MDNから取った例とテストの文字列を加えたら、
console.log('é'.length); // 1
console.log('é'.charCodeAt(0)); // 233
const latinSmallAcuteE = '\u00e9';
const unNormalizeE = '\u0065\u0301';
// console.log(String.fromCodePoint(0x0065)); // e
const testE1 = 'e\u0307';
const testE2 = 'e\u0303';
console.log(`${latinSmallAcuteE}, ${unNormalizeE}`);
// é, é
console.log(`${testE1}, ${testE2}`);
// ė, ẽ
console.log(latinSmallAcuteE.normalize('NFC') === unNormalizeE.normalize('NFC'));
// true
console.log(latinSmallAcuteE.normalize('NFC') === testE1.normalize('NFC'));
// false
console.log(latinSmallAcuteE.normalize('NFC') === testE2.normalize('NFC'));
// false
console.log(latinSmallAcuteE.normalize('NFC').length, unNormalizeE.normalize('NFC').length);
// 1 1
NFC
は上の例のように、(特定の)合成された組合せがすでに実在している文字に等価である正しさ(true
)を付与されました。
その逆にNFD
は分解正規形、すでにある文字が特定の組合せに等価であるようにされる。
// console.log('ñ'.length); // 1
// console.log('ñ'.charCodeAt(0)); // 241
const latinSmallTildeN = '\u00f1';
const unNormalizeN = '\u006e\u0303';
// console.log(String.fromCodePoint(0x006e)); // n
console.log(latinSmallTildeN.normalize('NFD') === unNormalizeN.normalize('NFD'));
// true
console.log(latinSmallTildeN.normalize('NFD').length, unNormalizeN.normalize('NFD').length);
// 2 2
console.log(latinSmallTildeN.normalize('NFD').charCodeAt(0).toString(16)); // 6e
console.log(latinSmallTildeN.normalize('NFD').charCodeAt(1).toString(16)); // 303
ñ
元の長さは1
だったけれど、NFD
の影響で分解されたパーツが各自の対応するCode Unit
と等価になり、長さも変えられました。