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に対応されているので問題なく使えるみたいだ。


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

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

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

double

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

という風に格納される。

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

ヒドン(Hidden)ビットで精度を1ビットを稼ぐが面白かった。

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


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