文字列の先頭から「文字数」分抜き出しても、文字列全体が得られないことがある
なでしこでは、「文字数」命令で文字列の文字数を取得できる。
そして、「文字抜出」命令で文字列の指定した位置から指定した長さの文字列を抜き出すことができる。
さて、抜き出す位置を文字列の先頭、抜き出す長さを文字列の文字数とすると、文字列全体が抜き出せそうに思える。
実際、プロデルでは、文字列全体を抜き出すことができる。
データは「💧🔥」
データの1文字目から(データの文字数)文字取り出して出力する
では、なでしこで同様のコードを実行してみよう。
データは「💧🔥」。
データで1から(データの文字数)文字抜き出して表示。
なんと、「💧」しか取り出すことができず、「🔥」の部分が欠けてしまった。
そもそも、文字数とは?
一口に「文字数」といっても、その数え方は
- バイト数
- UTF-16のコードユニット数
- Unicodeの符号化文字の数
- (絵文字などを)結合したかたまりの数
など、多くの定義が考えられる。
このうち前者の記事に例として挙がっていた「🏴」の「文字数」を取得すると、プロデル (1.9.1273) では 14
、なでしこ (3.6.45) では 7
となった。
なでしこの「文字数」 は、JavaScript の Array.from()
を用い、Unicodeの符号化文字の数を数えている。
なぜ絵文字を含む文字を1文字ずつに分けるのにArray.fromだけで十分なのか? #JavaScript - Qiita
一方、プロデルの「文字数」で取得できるのは、UTF-16のコードユニット数であると推測できる。
なでしこの「文字抜出」と「何文字目」
なでしこの「文字数」は、Unicodeの符号化文字の数を取得する命令であることがわかった。
一方、なでしこの「文字抜出」は、JavaScriptの substring()
を用いて以下のように実装されている。
fn: function (s: any, a: any, cnt: number) { cnt = cnt || 1 return (String(s).substring(a - 1, a + cnt - 1)) }
この substring()
の定義 を見てみると、渡された引数からクランプなどを行って抽出する範囲を求めた後、
Return the substring of S from from to to.
となっている。
この「substring」の定義 を見ると、
The phrase "the substring of S from inclusiveStart to exclusiveEnd" (where S is a String value or a sequence of code units and inclusiveStart and exclusiveEnd are integers) denotes the String value consisting of the consecutive code units of S beginning at index inclusiveStart and ending immediately before index exclusiveEnd (which is the empty String when inclusiveStart = exclusiveEnd).
となっており、「code units」単位で抜き出しを行うことがわかる。
6.1.4 The String Type より、「code units」とはUTF-16のコードユニットのことであり、サロゲートペアをまとめて作成する「code point」とは異なることがわかる。
すなわち、なでしこの「文字抜出」はUTF-16のコードユニット単位で抜き出す位置や長さを指定し、Unicodeの符号化文字の数を取得する「文字数」とは異なる単位を用いていることがわかる。
また、なでしこで文字列から文字列を検索する「何文字目」命令についても、JavaScript のindexOf()
をほぼそのまま用いた単純な実装となっており、「文字抜出」と同様にUTF-16のコードユニット単位で位置を返すことがわかる。
Unicodeの符号化文字単位で抜き出しや検索を行うには
文字列を「文字数」の処理と同様に Array.from
で配列に変換し、slice()
を用いることで、Unicodeの符号化文字単位で文字列の抜き出しを行うことができる。
slice()
と substring()
では、抜き出す位置として負の値を指定した際の扱いが異なる。
また、検索でヒットした位置までの文字列を抜き出し、その (欲しい数え方での) 文字数を数えることで、検索でヒットした位置を (Unicodeの符号化文字単位などの) 欲しい数え方に変換することができる。
●(SでAからCNT)文字正抜出とは
『(function(s, a, cnt) {
const arr = Array.from(String(s));
const subarr = arr.slice(a - 1, a - 1 + cnt);
return subarr.join("");
})』を[S, A, CNT]でJS関数実行して戻す。
ここまで。
●(SでAが)正何文字目とは
『(function(s, a) {
const str = String(s);
const pos = str.indexOf(a);
if (pos < 0) return 0;
return Array.from(str.substring(0, pos)).length + 1;
})』を[S, A]でJS関数実行して戻す。
ここまで。
データは「💧テスト🔥実験」。
「データ:{データ}」を表示。
「文字数:{データの文字数}」を表示。
「文字抜出:{データで1から(データの文字数)文字抜き出し}」を表示。
「文字正抜出:{データで1から(データの文字数)文字正しく抜き出し}」を表示。
「何文字目:{データで『🔥』が何文字目か}」を表示。
「正何文字目:{データで『🔥』が正しく何文字目か}」を表示。
以下の実行結果が得られた。
データ:💧テスト🔥実験
文字数:7
文字抜出:💧テスト🔥
文字正抜出:💧テスト🔥実験
何文字目:6
正何文字目:5
なでしこ標準の「文字抜出」や「何文字目」では全体を抜き出せなかったり検索結果がズレたりしているが、今回作成した「文字正抜出」や「正何文字目」では見た目通りの結果が得られている。
今回「見た目通り」の結果が得られたのは、実験に用いたデータに1個の符号化文字で表す文字しか含まれていなかったからである。
「🏴」などの複数の符号化文字からなる文字が含まれたデータを用いると、結果は「見た目」と合わなくなるだろう。
なでしこで文字列のUTF-16のコードユニット数を得るには
文字列の length
プロパティを読み取ることで、文字列のUTF-16のコードユニット数を得ることができる。
データは「💧🔥」。
データ["length"]を表示。
データで1から(データ["length"])文字抜き出して表示。
このプログラムを実行すると、以下の実行結果が得られた。
4
💧🔥
プロパティ構文の $
や .
を用いたアクセスは、エラーになってしまった。
データは「💧🔥」。
データ$lengthを表示。
このコードを実行すると、以下のエラーが出た。
[実行時エラー]main.nako3(2行目): TypeError: can't assign to property "__setProp" on "\uD83D\uDCA7\uD83D\uDD25": not an object
おわりに
今回紹介した命令以外にも、なでしこでは「文字左部分」など多くの文字列処理命令がUTF-16のコードユニット単位で処理を行う。
一方、なでしこの「文字列分解」命令は、UTF-16のコードユニット単位ではなく、Unicodeの符号化文字単位への分解を行うようである。
プロデルの「文字ずつ、区切る」はUTF-16のコードユニット単位で処理を行い、プロデルの「文字数」との一貫性がある。
文字や文字数をどのような単位で扱うかは、言語やライブラリの仕様として決めることであり、こうしなければならない、ということは無いだろう。
しかし、同じ言語やライブラリの中で単位が統一されておらず、しかも統一されていないことがパッと見わからないというのは、気持ち悪いだけでなく、不具合の原因になりうる。
(たとえば、「文字右部分」命令では文字の最後から指定した長さを取り出せるが、最後から数文字飛ばして取り出したいと思い、「文字数」をもとに位置を計算して取り出そうとすると、うまくいかない可能性があるだろう)