Twitter本文の文字数は気にしたことあっても他の文字数って気にしたこと無くない？

# はじめに

他の文字数とは、要はプロフィールの 名前・自己紹介・場所・ウェブサイト の4項目のことです。
ツイート本文とカウント方法が違ったのでそのカウント方法の検証を行う。
確証は無いため過信は禁物。

というわけで詳しい仕様に言及した Twitter 開発者ブログやドキュメント、他の検証記事、反証などがあればこの記事は無事、価値がなくなります。
情報求ム

本記事に登場するコードは特別な記載が無い限り ruby であるものとする。

# ツイート本文の文字数
とりあえず前提情報としてツイート本文の文字数計算方法をいったん見てみましょう。

twitter は多くの入力欄で文字数制限がかかっています。

[2017 年の仕様変更](https://blog.twitter.com/official/en_us/topics/product/2017/Giving-you-more-characters-to-express-yourself.html)以来、ツイート本文の本文の文字数制限は 280 文字となっています。

日本語中国語韓国語は「漢字使えるから文字数少なくても含意増やせるやろ」との理由で140文字制限
(正確には280文字制限で2文字カウントされると言った方がわかりやすいかも)

詳しく、通常の文字と絵文字の2種に分けて見てみます。

## 通常の文字
通常の文字とはかなり適当な表現ですが、
我々がよく入力する文字(英数字&日本語)のみに関して言うと、
Twitter の言語設定が日本語の場合においてアルファベットは0.5文字
ひらがな・カタカナ・漢字は1文字としてカウントされ、140文字まで入力が可能です。

正確には、Twitter が定めた4つの Unicode コードポイントの範囲の文字は1文字
それ以外は基本2文字としてカウントされる。


## 絵文字
では絵文字はどうなっているか。
文字数のことを考える時に絵文字はよく問題になる。

問題になるんですが、ツイート本文においては twemoji でサポートされている物に関しては一律で2文字(2/280文字)とカウントされる様になった。
こちらは [2018 年の仕様変更](https://twittercommunity.com/t/new-update-to-the-twitter-text-library-emoji-character-count/114607)

絵文字修飾子の有無にかかわらず2文字(ゼロ幅接合子 U+200D で肌色や性別情報を付加していても2文字)


## 基本的な文字数カウント仕様
[Counting characters](https://developer.twitter.com/en/docs/counting-characters)
色々書いたが、基本的に上記ページにまとまっている。

URL を入力したら、それより長くても短くても23文字カウントするぞ。
とかも書いてある。

大体そんな感じ

まぁ自分のプロダクトの中で Twitter に阿って文字数をカウントしたいなら [twitter-text](https://github.com/twitter/twitter-text) ライブラリを頼りにできるならこれに従うのが安定するのだろう。


# それ以外の入力欄
さてここまで前置き
ところ変わってプロフィール欄

こっちだって立派に文字数制限がかかっているが、あまり槍玉に上がらない。
開発者がAPI経由で編集を行うことが少ないからだろうか。

こちらはどうもさっきまでに挙げたルールとは異なる方法で文字数がカウントされているっぽい。

## 文字数制限
ツイート本文では `英文280文字` `和文140文字` と制限に差があったが、こちらは設定言語が日本語だろうが英語だろうが入力可能な文字数に変化はない。

ユーザー名なら50文字制限である。

まぁ、確かにツイートの制限だって日本語を2文字カウントするから140文字と謳っているだけで、設定言語が日本語でも英数字なら280文字入力できる。
こっちだってそうなのだろう？
きっと日本語を入力したら2文字使うんだ。と思っていた時期が私にもありました。



![スクリーンショット 2021-03-17 5.23.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315470/dd79a651-f6ef-53e3-841a-4f886da01a0a.png)
なんとアカウント名ではどちらでも1文字としてカウントされます。

もちろん、入力画面のリアルタイム計算が1文字と言っているだけで日本語50文字が登録できないなんてこともありません。

英数字でも50文字、日本語でも50文字まで登録できます。
できました。



念のため、HTML から `max-length` を外して無理やり入力したり、APIから値を与えることで51文字の英数字に更新しようとしましたが、文字多すぎ！と怒られました。

間違いなく英数字でも日本語でも50文字までです。


## 画面と結果が違う？
さて、次に絵文字が何文字として判定されるかを調べている時に私は混乱します。
入力画面のリアルタイム計算とバリデーションでの文字数カウントルールが食い違っているのです。

`1234567890123456789012345678901234567890123456🏊‍♀️`

上記の文字列は [POST statuses/update API](https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/post-statuses-update) から送信することで実際にアカウント名に登録可能な文字列です。おそらく50文字ぎりぎり。


ですが、登録後に画面から確認すると
![スクリーンショット 2021-03-17 5.39.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315470/5ca24474-8e3e-ed23-c4ea-09e675a3a273.png)
文字数オーバーの警告をされます。



画面のリアルタイムカウントは javascript の String.length で出しているのでしょう。
console で確認するとカウントが一致します。

![スクリーンショット 2021-03-17 11.31.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315470/ce9c9d1e-0a46-c41c-3173-353603d7d3dd.png)

JavaScript の String.length は
> length プロパティは String オブジェクトの文字列長を UTF-16 コードユニットの数で表します。

とされています。



私自身あまり詳しく無いので適当に表現しますが、UTF-16 においてサロゲートペアで表現される文字(絵文字や一部の漢字など)が絡むと画面上と内部での文字数カウントがずれる現象が発生する様です。

つまりは Twitter 内部的には UTF-16 で文字列を処理して文字列長を取得しているわけでは無いっぽいことがなんとなくわかります。


## 絵文字は何文字？
改めて、絵文字について。
プロフィール情報ならマルチバイト文字だろうがなんだろうが1文字なら1文字と判定されることがわかりました。

では先ほどは一律2文字カウントしてもらったおかげで深く考えずにいられた絵文字はどうなるでしょう。

何文字と判定されるかは絵文字によってまちまちである。
👨‍👩‍👧 ← これは登録すると5文字判定
🏊‍♀️ ← これは登録すると4文字判定

画面ではそれぞれまた違った数でカウントされるんですが、実際に登録可能な文字数には関係無いことがわかったので無視します。


ではなぜ絵文字の文字数は見た目とこうも解離するのか。

答えとしては、見た目に1文字の絵文字でも複数の文字を組み合わせて構成されている場合があるからです。
使用言語、内部で使用される文字エンコーディング方式等にも影響されますが、おそらく今回影響しているのはこれだけです。


先述の絵文字を詳細に分解してみる。

```ruby
str_1 = '👨‍👩‍👧'
p str_1.codepoints.map{|cp| sprintf("U+%04X", cp) }
# => ["U+1F468", "U+200D", "U+1F469", "U+200D", "U+1F467"]

str_2 = '🏊‍♀️'
p str_2.codepoints.map{|cp| sprintf("U+%04X", cp) }
# => ["U+1F3CA", "U+200D", "U+2640", "U+FE0F"]
```

各文字のコードポイントを16進数で表記して、頭に U+ とかつけて表示

UTF-8 としては👨‍👩‍👧は5文字、🏊‍♀️は4文字で表現されることがわかります。
それぞれの文字コードが何を表しているのか確認します。


👨‍👩‍👧
U+1F468 => 👨([Man](https://www.compart.com/en/unicode/U+1F468))
U+200D =>  ([Zero Width Joiner](https://www.compart.com/en/unicode/U+200D))
U+1F469 => 👩([Woman](https://www.compart.com/en/unicode/U+1F469))
U+200D =>  ([Zero Width Joiner](https://www.compart.com/en/unicode/U+200D))
U+1F467 => 👧([Girl](https://www.compart.com/en/unicode/U+1F467))

Zero Width Joiner(ゼロ幅接合子)という制御文字で絵文字を合成することで 夫婦 + 娘 の家族を1つの絵文字で表現してることがわかります。


U+1F3CA => 🏊([Swimmer](https://www.compart.com/en/unicode/U+1F3CA))
U+200D=>  ([Zero Width Joiner](https://www.compart.com/en/unicode/U+200D))
U+2640=> ♀([Female Sign](https://www.compart.com/en/unicode/U+2640))
U+FE0F=> ◌️([Variation Selector-16 (VS16)](https://www.compart.com/en/unicode/U+FE0F))

Swimmer の絵文字にゼロ幅接合子で女性シンボルを合成して Female Swimmer にする。
それに バリエーションセレクター と呼ばれる制御文字をさらに合成して、絵文字として表示される様に指定している。


## 結局 Twitter はどう文字列をカウントしている？

というわけで、おそらく Twitter はプロフィール項目に関しては制御文字も1文字として普通にカウントして文字数を出しているのではないかと今回私は結論づけた。

絵文字が混ざる文字列の文字列長をカウントする際にはこれらの合成絵文字が1文字にしか見えないのに複数文字としてカウントされるということが問題になるのが常なのだが、
Twitter は内部でどシンプルに文字数をカウントしている様なのでむしろあまりいろいろ気にせず、自身の使っている言語が内部的にサロゲートペアの影響を受けるエンコーディングではないか(UTF-16ではないか)だけ確認したら普通に文字数カウントしたらいいんじゃ無いかな？

という結論に個人的には達しました。

```ruby
str = '1234567890123456789012345678901234567890123456🏊‍♀️'
p str.length
# => 50
```

画面のカウントが嘘を吐くせいで混乱しただけの模様。



# 参考
1. [僕は、なぜ絵文字の長さが、直感に反するのか理解したい...!!](https://qiita.com/negiandleek/items/e2272b8da466c4e01c47)
2. [絵文字だョ! 符号化文字集合(前編)](https://mixiengineer.hatenablog.com/entry/2012/10809/)
3. [Ruby にて文字と Unicode コードポイントの相互変換を行う]
(https://vividcode.hatenadiary.org/entry/20110120/1295542000)
4. [Rubyでの文字列の「長さ」](https://diary.kitaitimakoto.net/2015/11/03.html)
5. [JavaScriptでのサロゲートペア文字列のメモ](https://qiita.com/YusukeHirao/items/2f0fb8d5bbb981101be0)
6. [Unicode 絵文字にまつわるあれこれ (絵文字の標準とプログラム上でのハンドリング)](https://qiita.com/_sobataro/items/47989ee4b573e0c2adfc)
7. [JavaScriptにおけるサロゲートペアとは](https://www.javadrive.jp/javascript/string/index11.html)
8. [サロゲートペアってなんでコワイの](https://qiita.com/wingsys/items/81d46451d8b93ab065de)