SheepDev

数値・単位・ビット

大きなIDは何ビット必要?16進数でも確認したい

使うツール

ログにある注文IDを表へ貼り付けたら、末尾だけが0になっている。別の画面では、隣り合うはずの二つのIDが同じ数字に見える。桁の長い整数は、表示できることと、一桁も変えずに扱えることが別です。小さい数で動いた処理でも、境界を越えた瞬間に問題が表へ出てくることがあります。

この記事では実在するIDを使わず、境界が分かりやすい整数を試します。整数型の範囲で収容できるかを確認し、進数変換で別表記へ移して戻します。データベースだけでなく、API、ブラウザー、表計算ソフトという受け渡しの途中で、どの表現を使うべきかを考える材料にしてください。

255の次は、同じ8bitへ入る?

整数型の範囲を開き、範囲を求めるモード、ビット幅8、符号なしを選びます。最小値0、最大値255になります。0から255までは256種類ありますが、最大値が256になるわけではありません。この『種類の数と最大値は一つ違う』という関係が、大きな整数でもそのまま続きます。

境界をまたぐ三つの値
254 = 11111110₂
255 = 11111111₂
256 = 100000000₂

必要ビット数のモードへ切り替え、符号なしで255と256を一つずつ調べると、それぞれ8bitと9bitです。増えたのは1だけでも、表すための桁は一つ増えます。『普段は最大値の近くまで使わない』という判断をするなら、現在値だけでなく、将来増える件数や予約値も別に考える必要があります。

64bitなら全部同じ、ではない

ビット幅を64へ変え、符号付きと符号なしを切り替えて比べます。符号付きは負数も表すため、正の側の最大値が小さくなります。符号付きの最大値は9,223,372,036,854,775,807、符号なしは18,446,744,073,709,551,615です。『64bitの列』という説明だけで相互に渡してよいとは判断できません。

小さい例でも、符号付き8bitの最大値は127です。正の128を表したければ、符号なしなら8bit、符号付きなら9bitが必要です。負の値を使わないIDでも、受け取り側の型が符号付きである場合は、その制約に合わせます。値の意味がIDだから自動的に符号なしになる、というわけではありません。

20桁の整数を、16進数へ移して戻す

10進数の入力:区切りカンマは入れない
18446744073709551615

進数変換で変換元10、変換先16を選びます。結果はffffffffffffffff、fが16個です。次にその結果を入力し、変換元16、変換先10へ切り替えると、元の20桁へ戻ります。別表記へ変えても整数そのものが変わらないことを、末尾まで見比べて確かめます。途中で別の電卓へ数値として貼り付けず、文字列のまま扱ってください。

16進数の一桁は4bit分を表せるので、この値は64bitがすべて1の値と対応します。ただし、文字数から必要ビット数を求めるときは、先頭のゼロや先頭桁の値に注意します。0fという二文字の表示でも、値15そのものに必要な符号なしビット数は4です。保存用の固定幅と、値を表す最小幅は別の問いです。

64bit整数に入っても、Numberで安全とは限らない

JavaScriptの通常のNumberは、64bit整数型とは異なる表現です。安全に扱える整数の上限として、9,007,199,254,740,991が使われます。その次の整数も表現できる場合はありますが、それ以降は隣り合う整数をすべて区別できる保証がありません。『この一個を表示できた』だけで、同じ桁数の全IDが安全とは考えないでください。

精度を失っていないか調べる例
9007199254740993

この値を進数変換へ直接入力し、10進から16進へ変えると20000000000001になります。逆変換で元へ戻ることも確認します。もし別の処理を通した後の値が9007199254740992へ変わっていたら、元の桁はすでに失われています。後からBigIntへ移しても、本来どの整数だったかを推測して戻すことはできません。

IDを数値にするか、文字列にするか

00123という識別番号は、数値としては123ですが、文字列としては別の表記です。進数変換は数値を扱うため、元の先頭ゼロの個数を保存する用途には向きません。足し算をする量なのか、完全一致で照合する識別子なのかを先に決めます。郵便番号や会員番号などを、数字だけだからという理由で数値へ変換しないようにしてください。

APIで文字列として渡す設計は、受け取り側の型も含めて決める必要があります。勝手に引用符を足して問題解決とせず、利用先が文字列を受け取れるか確認します。逆に、契約上は文字列なのに途中でNumberへ変換している箇所があれば、そこが桁を失う境界になり得ます。保存時だけでなく、表示・検索・CSV出力まで追うと原因を見つけやすくなります。

末尾まで一致することを、受け渡しのテストにする

確認用の値は、上限より小さい一件だけでなく、境界の直前・境界・直後をそろえます。例えば符号なし8bitなら254、255、256、Numberの安全整数なら上限付近の連続した値です。受け取り側が範囲外を明示的に拒否するのか、丸めてしまうのかを分けて記録します。拒否された値を、勝手に縮めて通すことは対策になりません。

結果を報告するときは、『64bitで大丈夫』ではなく、『この値は符号なし64bitには入るが、ブラウザーでは文字列として保持する』のように、場所と表現を一緒に書きます。必要ビット数の結果は型の選択材料ですが、実際のDB列や通信プロトコルの対応を保証しません。最後は利用する型の仕様と、同じ値を通した往復テストで確認してください。

同じビット列でも、符号の解釈で値が変わる

8bitの11111111は、符号なしなら255です。同じビット列を二の補数の符号付き整数として読むと-1になります。進数変換へ255を入れて二進数にしただけでは、符号付きの-1へ変換したことにはなりません。進数は数の表記方法、符号とビット幅は固定長の値をどう読むかの規則です。ログにffと出ていたとき、二桁の16進数だから必ず255と決める前に、そのフィールドが符号付きなのかを仕様で確認します。

同じ注意は先頭のゼロにも必要です。数としての0010と10は同じでも、商品コードの0010と10は別の識別子かもしれません。進数変換は数値を扱うので、識別子として必要な桁幅や先頭ゼロの保存を任せる場所ではありません。CSVへ出すときに文字列だったコードが数値扱いされることもあります。大きい整数だけを警戒するのではなく、小さい値でも『足し算する対象なのか、照合する名前なのか』を先に決めると、変換で失ってはいけない情報が分かります。

不具合を再現するなら、最大値一つだけでなく、その直前と直後を用意します。8bit符号なしなら254、255、256、JavaScriptの安全な整数の境界を調べるなら9007199254740991とその周辺です。値をどこで文字列から数値へ変えたのか、JSONの読み取り前後、APIの受け渡し前後などに分けて確認します。末尾が変わってから高精度な道具へ渡しても、失われた元の桁は戻りません。元データを文字列のまま保ち、最初に違いが生じる段階を探すのが近道です。

参考資料

使うツール

使い方・解説 →