文字列の先頭から文字数分抜き出したくらいで文字列全体が得られると思ったか？

# 文字列の先頭から「文字数」分抜き出しても、文字列全体が得られないことがある

[なでしこ](https://nadesi.com/)では、「[文字数](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E6%96%87%E5%AD%97%E6%95%B0&show)」命令で文字列の文字数を取得できる。
そして、「[文字抜出](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E6%96%87%E5%AD%97%E6%8A%9C%E5%87%BA&show)」命令で文字列の指定した位置から指定した長さの文字列を抜き出すことができる。

さて、抜き出す位置を文字列の先頭、抜き出す長さを文字列の文字数とすると、文字列全体が抜き出せそうに思える。
実際、[プロデル](https://produ.irelang.jp/)では、文字列全体を抜き出すことができる。

```text:プロデルで文字列を先頭から文字数分抜き出すコード
データは「💧🔥」
データの1文字目から(データの文字数)文字取り出して出力する
```

![プロデルで文字列を先頭から文字数分抜き出す](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/2b996d04-a36b-205a-66d5-a5f24898fdbe.png)

では、なでしこで同様のコードを実行してみよう。

```text:なでしこで文字列を先頭から文字数分抜き出すコード
データは「💧🔥」。
データで1から(データの文字数)文字抜き出して表示。
```

![なでしこで文字列を先頭から文字数分抜き出す](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/504fddf7-3a68-90c9-8259-85ed3c339dc6.png)

なんと、「💧」しか取り出すことができず、「🔥」の部分が欠けてしまった。

# そもそも、文字数とは？

一口に「文字数」といっても、その数え方は

* バイト数
* UTF-16のコードユニット数
* Unicodeの符号化文字の数
* (絵文字などを)結合したかたまりの数

など、多くの定義が考えられる。

* [JavaScript: 文字数を正確にカウントするには？ #JavaScript - Qiita](https://qiita.com/suin/items/3da4fb016728c024eaca)
* [JavaScriptで文字数を数えるのはそんなに簡単ではない #JavaScript - Qiita](https://qiita.com/bon127/items/491b25e90208188dafbd)

このうち前者の記事に例として挙がっていた「🏴󠁧󠁢󠁥󠁮󠁧󠁿」の「文字数」を取得すると、プロデル (1.9.1273) では `14`、なでしこ (3.6.45) では `7` となった。

[なでしこの「文字数」](https://github.com/kujirahand/nadesiko3core/blob/7c0f7233a0a706711a6feb024c219fb03fffd607/src/plugin_system.mts#L1073) は、JavaScript の [`Array.from()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/from) を用い、Unicodeの符号化文字の数を数えている。
[なぜ絵文字を含む文字を1文字ずつに分けるのにArray.fromだけで十分なのか？ #JavaScript - Qiita](https://qiita.com/alucky0707/items/697598e49ff56191c139)

一方、プロデルの「文字数」で取得できるのは、UTF-16のコードユニット数であると推測できる。

# なでしこの「文字抜出」と「何文字目」

なでしこの「文字数」は、Unicodeの符号化文字の数を取得する命令であることがわかった。
一方、[なでしこの「文字抜出」](https://github.com/kujirahand/nadesiko3core/blob/7c0f7233a0a706711a6feb024c219fb03fffd607/src/plugin_system.mts#L1210)は、JavaScriptの [`substring()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/substring) を用いて以下のように実装されている。

> ```javascript
>     fn: function (s: any, a: any, cnt: number) {
>       cnt = cnt || 1
>       return (String(s).substring(a - 1, a + cnt - 1))
>     }
> ```

この [`substring()` の定義](https://tc39.es/ecma262/multipage/text-processing.html#sec-string.prototype.substring) を見てみると、渡された引数からクランプなどを行って抽出する範囲を求めた後、

> Return the substring of S from from to to.

となっている。
この[「substring」の定義](https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#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](https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-string-type) より、「code units」とはUTF-16のコードユニットのことであり、サロゲートペアをまとめて作成する「code point」とは異なることがわかる。

すなわち、なでしこの「文字抜出」は**UTF-16のコードユニット単位**で抜き出す位置や長さを指定し、**Unicodeの符号化文字の数**を取得する「文字数」とは異なる単位を用いていることがわかる。

また、なでしこで文字列から文字列を検索する「[何文字目](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E4%BD%95%E6%96%87%E5%AD%97%E7%9B%AE&show)」命令についても、JavaScript の[`indexOf()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf) をほぼそのまま用いた[単純な実装](https://github.com/kujirahand/nadesiko3core/blob/7c0f7233a0a706711a6feb024c219fb03fffd607/src/plugin_system.mts#L1082)となっており、「文字抜出」と同様に**UTF-16のコードユニット単位**で位置を返すことがわかる。

# Unicodeの符号化文字単位で抜き出しや検索を行うには

文字列を「文字数」の処理と同様に `Array.from` で配列に変換し、[`slice()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/slice) を用いることで、Unicodeの符号化文字単位で文字列の抜き出しを行うことができる。

:::note warn
`slice()` と `substring()` では、抜き出す位置として負の値を指定した際の扱いが異なる。
:::

また、検索でヒットした位置までの文字列を抜き出し、その (欲しい数え方での) 文字数を数えることで、検索でヒットした位置を (Unicodeの符号化文字単位などの) 欲しい数え方に変換することができる。

```text:なでしこで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から(データの文字数)文字正しく抜き出し}」を表示。
「何文字目：{データで『🔥』が何文字目か}」を表示。
「正何文字目：{データで『🔥』が正しく何文字目か}」を表示。
```

以下の実行結果が得られた。

```text
データ：💧テスト🔥実験
文字数：7
文字抜出：💧テスト🔥
文字正抜出：💧テスト🔥実験
何文字目：6
正何文字目：5
```

なでしこ標準の「文字抜出」や「何文字目」では全体を抜き出せなかったり検索結果がズレたりしているが、今回作成した「文字正抜出」や「正何文字目」では見た目通りの結果が得られている。

:::note info
今回「見た目通り」の結果が得られたのは、実験に用いたデータに1個の符号化文字で表す文字しか含まれていなかったからである。
「🏴󠁧󠁢󠁥󠁮󠁧󠁿」などの複数の符号化文字からなる文字が含まれたデータを用いると、結果は「見た目」と合わなくなるだろう。
:::

# なでしこで文字列のUTF-16のコードユニット数を得るには

文字列の [`length` プロパティ](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/length)を読み取ることで、文字列のUTF-16のコードユニット数を得ることができる。

```text:なでしこで文字列のUTF-16のコードユニット数を得るコード
データは「💧🔥」。
データ["length"]を表示。
データで1から(データ["length"])文字抜き出して表示。
```

このプログラムを実行すると、以下の実行結果が得られた。

```text
4
💧🔥
```

:::note info
[プロパティ構文](https://nadesi.com/v3/doc/index.php?%E6%96%87%E6%B3%95%2F%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E6%A7%8B%E6%96%87%E3%80%8C%24%E3%80%8D&show)の `$` や `.` を用いたアクセスは、エラーになってしまった。

```text:エラーになるコード
データは「💧🔥」。
データ$lengthを表示。
```

このコードを実行すると、以下のエラーが出た。

> [実行時エラー]main.nako3(2行目): TypeError: can't assign to property "__setProp" on "\uD83D\uDCA7\uD83D\uDD25": not an object
:::

# おわりに

今回紹介した命令以外にも、なでしこでは「[文字左部分](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E6%96%87%E5%AD%97%E5%B7%A6%E9%83%A8%E5%88%86&show)」など多くの文字列処理命令がUTF-16のコードユニット単位で処理を行う。
一方、なでしこの「[文字列分解](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E6%96%87%E5%AD%97%E5%88%97%E5%88%86%E8%A7%A3&show)」命令は、UTF-16のコードユニット単位ではなく、Unicodeの符号化文字単位への分解を行うようである。

:::note info
プロデルの「[文字ずつ、区切る](https://produ.irelang.jp/docs/core/global_string.htm#:~:text=%E6%96%87%E5%AD%97%E3%81%9A%E3%81%A4%E3%80%81%E5%8C%BA%E5%88%87%E3%82%8B)」はUTF-16のコードユニット単位で処理を行い、プロデルの「文字数」との一貫性がある。
:::

文字や文字数をどのような単位で扱うかは、言語やライブラリの仕様として決めることであり、こうしなければならない、ということは無いだろう。
しかし、同じ言語やライブラリの中で単位が統一されておらず、しかも統一されていないことがパッと見わからないというのは、気持ち悪いだけでなく、不具合の原因になりうる。
(たとえば、「[文字右部分](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E6%96%87%E5%AD%97%E5%8F%B3%E9%83%A8%E5%88%86&show)」命令では文字の最後から指定した長さを取り出せるが、最後から数文字飛ばして取り出したいと思い、「文字数」をもとに位置を計算して取り出そうとすると、うまくいかない可能性があるだろう)