TwitterUIDの挙動とJavaScriptのBigIntについて

TwitterのUIDについて調べてたら、自分の浮動小数点の挙動についての理解度が低かったので調べたことについてまとめておく。


ことの発端は、spreadsheet上の人力で管理されているTwitterのユーザー情報をデータベースに入れる作業をしていた時だった。

以前同じような作業をしたとき結構漏れがあったので、Twitter UIDの妥当性やscreen nameが本当に存在するかどうかを確認する必要があった。

TwitterのIDチェッカーなどのWebサイトを利用してもよかったが、100件を超える量のデータを手動で確認取るのは面倒だったため、以下のような検証scriptを雑に書いた。

#!/bin/bash
while read row; do
    TWITTER_ID=`echo ${row} | cut -d , -f 1`
    TWITTER_UID=`echo ${row} | cut -d , -f 2`
    TWITTER_REQUEST_UID=`curl -X GET -H "Authorization: Bearer <TWITTER_TOKEN>" -s "https://api.twitter.com/1.1/users/show.json?screen_name=${TWITTER_ID}" | jq ".id"`
    if [ ${TWITTER_REQUEST_UID} -ne ${TWITTER_UID} ]; then
        echo "${TWITTER_ID}: ${TWITTER_UID}${TWITTER_REQUEST_UID}"
    fi
done < ~/Desktop/twitter.csv

そうしたら半分くらいのTwitter UIDがずれてしまった。明らかにおかしいと思ったので、きちんと調査することにした。


Twitter Developer Documentに Twitter IDs という記事がある。

https://developer.twitter.com/en/docs/twitter-ids

以下のようなことが書かれていた。

  • ユーザーの増加によりTwitterのUIDは64bit
  • unsignedでuniqueな値として管理されている
  • JavaScriptの整数のサイズは53bitに制限されている
  • api responseでは整数(id)と文字列(id_str)の両方を返すような実装になっている

ここから分かるのは、自分は↑のshell scriptで id を見ていたから正しい値をとれていなかった、~id_str~ を使うべきだったことが分かる。

たしかに、以下のように toString() をしたらずれることについて確認がとれたがどうしてだろうか。 また、今回はbash scriptを書いたのにJavaScriptと同じ挙動をするのはどうしてなのか調べる必要がある。

~ 。+゚(∩´﹏'∩)゚+。 < node
Welcome to Node.js v15.0.1.
Type ".help" for more information.
> (10765432100123456789).toString()
'10765432100123458000'

JavaScriptの数値については JavaScriptの数値型完全理解が一番良くまとまっていた。

これによると、JavaScriptの数値型はすべて IEEE 754 倍精度の浮動小数点(double型)で表現されている。たしかに、MDNのNumberの記事にも同じような記述がある。

double型で安全に表現できる最大値は Number.MAX_SAFE_INTEGER で取ることができ、 Number.isSafeInteger() などでもチェックできる。

> Number.isSafeInteger(10765432100123456789)
false
> Number.MAX_SAFE_INTEGER
9007199254740991

JavaScriptには bigint も用意されている。

MDNには以下のように書かれているので日常使いするのは辞めておくべきだろう。

Number と BigInt との間の型変換は精度が落ちる可能性があるため、 BigInt は値が論理的に253以上になる場合にのみ使用し、この2つの型の間で型変換を行わないこと推奨します。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/BigInt

さきほどの巨大な値でも正確に出力できる。

> (10765432100123456789n).toString()
'10765432100123456789'

caniuseを見ているとほとんどのブラウザがBigIntに対応されているので問題なく使えるみたいだ。


浮動小数点にの挙動についてもう少し見てみる。

ググったらいっぱい出てくるので計算方法は割愛するが、浮動小数点数型と誤差に分かり易くまとまっている。

double の表す値 = (-1)^符号部 × 2^(指数部-1023) × 1.仮数部
Figure 1: double

Figure 1: double

  • 符号は、0なら正、1なら負
  • 指数部は、「2^指数」の指数の部分に1023を引いたものが11bit符号なしの整数の形で格納されている
  • 仮数部は、実際の仮数部の先頭の「1」を取り除いた残りが格納されている

という風に格納される。

仮数部が52bitだが、double型の精度が53bitなのは 1.仮数部1 部分もカウントされるからみたいだ。

ヒドン(Hidden)ビットで精度を1ビットを稼ぐがおもしろかった。

今回の問題はJavaScriptというよりは浮動小数点の問題なのでbashでも同じ。


昔CSの授業で習った気もするけどすっかり忘れていたので今一度勉強できてよかった。