0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaScriptのStringについて part1

Last updated at Posted at 2022-09-14

初めに

今回は文字列に関連するメソッドをまとめてみました。

Methods

String.prototype.charCodeAt()

charCodeAt()は指定された文字列のインデックスに、対応したUTF-16のCode Unit0-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 Unit0-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言語タグを使います。

  • optionsIntl.Collator() constructorの設定に従います。(デフォルトメソッドはsortsortsensitivityvariant

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から分解した文字列を取得し、正確に準ずる等価性を与える。
ほかにはNFKCNFKDという形式があるが、ここでは省けます。

初めて見たメソッドですが、色々と試してみると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と等価になり、長さも変えられました。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?