ジュニアエンジニア向け コンピュータサイエンス入門シリーズ(全8回)
第2回:データの正体:0.1+0.2はなぜ0.3にならないのか

0.1+0.2はなぜ0.3にならないのか:0と1だけで文字も小数も表す仕組み【第2回】

コンソールに 0.30000000000000004 が出てきたとき、あなたは「バグだ」と思うだろうか。それとも「そういうものだ」と思うだろうか。この記事を読み終えると、コンピュータが0と1だけで文字・整数・小数を表している仕組みを「辞書(符号化)」として説明でき、金額計算やIDの取り扱いで壊れない型を自分で選べるようになります。ジュニアエンジニア向けコンピュータサイエンス入門シリーズ(全8回)の第2回です。

🎯 テーマの主役:「符号化」——0と1に意味を割り当てる辞書

今回の主役は符号化(エンコーディング)です。一言で言えば、「0と1の並びに対して、どの並びが何を意味するかを決めた辞書」のことです。

日常の例えで言うなら、モールス信号です。使えるのは「短点(トン)」と「長点(ツー)」の2種類だけ。それなのに、組み合わせ方を変えるだけでアルファベットも数字も記号も表せてしまいます。・− は A、−・・・ は B。モールス信号そのものには意味がありません。意味を与えているのは「この並びはこの文字」と決めた対応表のほうです。コンピュータがやっていることも、これと同じです。

ここで大事なのは、0と1そのものには何の意味もないという点です。1バイト(8ビット)の 01000001 という並びは、整数の 65 にも、文字の A にも、色の明るさ 65 にも、8個のオン・オフのフラグにもなります。同じ並びが、どの辞書を当てるかで別のものになる。これが符号化の正体です。

この仕組みを理解すると、次の3つができるようになります。第一に、0.1 + 0.20.3 にならない理由を説明できること。第二に、文字化けに遭遇したとき、何が食い違っているのかを特定できること。第三に、金額やIDのような「壊れてはいけない値」に、どの型を選ぶかを自分で判断できることです。

0と1の双子のキャラクターが辞書を引いて文字や数値に変わる概念イラスト 0と1の双子キャラクターが、符号表という辞書を引くことで、整数や文字や色として読めるようになる様子を描く。 0と1に意味を与えているのは、0と1ではなく「辞書」のほう 電気がオン・オフするだけ 0 1 「これは何か」を自分では決められない 符 号 表(辞書) 01000001 → 65 01000001 → A 00110001 → "1" 01000001 → 色の明るさ 00111111 → 0.1 の近似 読めるようになった結果 A 65 同じ並びでも、辞書が違えば別のもの 辞書が食い違うと「文字化け」「数値のズレ」になる

😓 動機:「動いているのに、ときどき壊れる」の正体が分からない

データの話は、地味です。地図やネットワークのように派手な障害を起こしません。そのかわり、静かに壊れます。そして静かに壊れるものは、原因を突き止めるのに時間がかかります。

よくある場面を4つ挙げます。ひとつ目は、金額の合計が1円ずれる場面です。テストでは合っていたのに、本番のデータでだけズレる。ふたつ目は、0.1 + 0.20.3 にならない場面です。「浮動小数点の誤差です」と言われて、そういうものかと納得してしまう。みっつ目は、CSVをExcelで開いたら日本語が化けた場面です。よっつ目は、APIから返ってきた大きなIDが、なぜか末尾が 0000 に変わってしまう場面です。

この4つは、実はすべて同じ原因です。計算が間違っているのではなく、辞書(符号化)と型の選び方が食い違っている。だから「動いているのに、ときどき壊れる」という不可解な症状になります。

そして厄介なのは、この種の不具合がレビューで見落とされやすいことです。型や文字コードの話は、コードの見た目を変えません。動きますし、テストも通ります。AIが生成したコードでも、float で金額を計算するコードは普通に出力されます。見えないから、指摘できない。これが、データの正体を知る価値が最も出る場面です。

🧪 仮説:データを壊すのは計算ミスではなく「辞書の食い違い」である

仮説を立てます。データのトラブルの多くは、計算アルゴリズムの問題ではなく、「同じ並びに同じ意味を割り当てる」という約束が守られていないことから生じる。

この仮説を支持する観察が3つあります。第一に、0.1 + 0.2 の誤差は加算の実装ミスではなく、0.1 という値を2進の小数で表せないことから来ます。第二に、文字化けは文字の変換ミスではなく、同じバイト列を別の文字コードで読んだ結果です。第三に、大きなIDの末尾が変わるのは計算誤差ではなく、その値を正確に表せない型に入れた結果です。

つまり、データのトラブルを防ぐ力は「数学力」ではなく、「この値はどの辞書で、どの型で表すのが適切か」を判断する力です。そしてこれは、覚える量が少ないわりに効果が大きい知識です。整数・小数・文字列・真偽値の4つでほとんどが説明できます。

🔬 検証①:2種類の記号が、なぜ256通りになるのか

まず、0と1だけでどれだけのものを表せるのかを数えておきます。1桁(1ビット)で表せるのは 01 の2通り。2桁なら 00 01 10 11 の4通り。3桁なら8通り。桁が1つ増えるたびに、表せる組み合わせが2倍になる。これが「2のn乗」です。

8桁(1バイト)まで増やすと、2の8乗で256通りになります。文字コードの設計は、すべてここから始まります。「1バイトで256種類の文字を区別できる」という前提です。

では、その256通りの並びに、実際に数値としての意味を与えるとどうなるか。2進数の位取りを見てみます。10進数が「1の位・10の位・100の位」と10倍ずつ増えるのと同じで、2進数は「1の位・2の位・4の位・8の位」と2倍ずつ増えます。

8ビットの位の重みと、01000001が65になる計算 8ビットの各桁が128から1までの重みを持ち、1が立っている桁の重みを足すと65になることを示す図。 1が立っている桁の「重み」を足すだけ。それが2進数の読み方 位の重み(2の累乗) 128 64 32 16 8 4 2 1 ビットの並び 0 1 0 0 0 0 0 1 64 + 1 = 65 = 文字コードの「A」

この読み方ができると、整数の範囲も見えてきます。8ビットで表せるのは0〜255の256通り。負の数も扱いたい場合は、最上位ビットを符号に使う2の補数という方式で、-128〜127を表します。範囲が「0〜255」から「-128〜127」に変わるだけで、表せる個数は256のままです。範囲を広げたいならビット数を増やす。32ビットなら約42億通り、64ビットなら約1,844京通りになります。

そして、ここで実務的に重要な性質が1つ出てきます。2進数は、そのままでは人間に読みにくいということです。01000001 が65だと即答できる人は多くありません。そこで使われるのが16進数です。16進数は4ビットを1桁で表せるので、8ビットがちょうど2桁になります。

10進2進(パディングなし)16進よく出会う場所
000x00ヌル文字。C言語の文字列終端
1010100x0A改行(LF)。テキストファイルの行末
1311010x0D復帰(CR)。Windowsの改行はCR+LF
6510000010x41文字の「A」
9711000010x61文字の「a」。大文字との差は32(=0x20)
12711111110x7FASCIIの最終文字(DEL)
255111111110xFF8ビットで表せる最大の整数

ここで押さえておきたいのは、16進数は「人間のための省略記法」だということです。コンピュータは16進数という単位を知りません。1階(ハードウェア)にあるのは2進の並びだけで、16進数は2進数の読みにくさを人間が改善するために用意した表記です。だから、エラーメッセージやダンプで 0x が出てきたら、「これは生のビットを見せられている」と読み替えられます。

🔬 検証②:文字は「番号」に決まっている——文字コードという辞書

文字の話に進みます。コンピュータが文字を扱う方法は、とてもシンプルです。すべての文字に通し番号を振り、その番号を0と1で保存する。この番号表が文字コードです。

最初の標準的な文字コードが ASCII(1963年に最初の版が作られ、のちにRFC 20として1969年に文書化された規格)で、7ビットを使って128文字を定義しました。アルファベット、数字、記号、制御文字です。日本語は入っていません。

日本語を扱おうとすると、128文字では足りません。そこで各国が独自の拡張を始めます。日本ではJIS X 0208などの規格が作られ、それをマイクロソフトがWindows向けに拡張した Shift_JIS(Windows-31J/CP932)が広く使われました。結果、同じ「あ」という文字に、複数の異なる番号が割り当てられる状態になりました。これが文字化けの土壌です。

文字ASCIIShift_JIS(Windows-31J)UTF-8UTF-16(BE)
A41414100 41
(扱えない)82 A0E3 81 8230 42
(扱えない)93 FAE6 97 A565 E5
😀(扱えない)(規格上扱えない)F0 9F 98 80(4バイト)D8 3D DE 00(サロゲートペア)

この表から2つのことが読み取れます。第一に、ASCIIの範囲(0x00〜0x7F)はASCII・Shift_JIS・UTF-8の3つで完全に一致しています(UTF-16だけは 00 41 のように並びが違います)。だから英語だけのファイルは、どの文字コードで読んでも壊れません。第二に、日本語は規格ごとにバイト数が違います。Shift_JISでは2バイト、UTF-8では3バイト、UTF-16では2バイト。「日本語1文字は◯バイト」という言い方が間違いになる理由がここにあります。

では、文字コードが食い違うと何が起きるのかを見てみます。UTF-8で保存した「あ」のバイト列は E3 81 82 です。この3バイトを、Shift_JISの辞書で読むと何が起きるでしょうか。

同じバイト列を別の文字コードで読むと文字化けする仕組み UTF-8で保存された「あ」の3バイトが、UTF-8の辞書では「あ」に、別の文字コードの辞書では意味の分からない文字になる様子を描く。 保存した側と読んだ側で、辞書が違うと文字化けになる ファイルに書かれているのは、番号の並びだけ E3 81 82 3バイト(UTF-8で「あ」を保存した結果) 辞書:UTF-8(保存した側と同じ) 正しく読める 辞書:別の文字コード(読んだ側だけ違う) 意味の分からない 文字が並ぶ 壊れているのはデータではなく、読むときに使った辞書のほう

ここが重要なポイントです。文字化けはデータが壊れたのではなく、読むときに間違った辞書を使っただけです。だから文字化けしたファイルでも、正しい文字コードで読み直せば復元できることが多い。逆に言えば、変換を繰り返すと本当に壊れます。間違った辞書で読んだ結果を、さらに別の辞書で保存し直すと、元のバイト列に戻せなくなるからです。文字化けを見つけたときの原則は「まず読み直す。書き戻さない」です。

この混乱を根本から解決するために作られたのが Unicode です。世界のすべての文字に、統一された番号(コードポイント)を振る。日本語も、絵文字も、古代文字も、同じ番号表に載せます。Unicodeは現在も継続的に更新されている規格です。

Unicodeの番号を、実際のバイト列に変換する方式が UTF-8 です。UTF-8は1992年に設計され、ASCIIと完全に互換になるように作られました。ASCIIの範囲は1バイトのまま変わらないので、英語だけのファイルはUTF-8にしても壊れません。これがUTF-8が世界中で使われるようになった大きな理由です。また、1〜4バイトの可変長なので、日本語は3バイト(多くの場合)、絵文字は4バイトになります。

ただし、UTF-8にも落とし穴があります。「1文字」を数えるとき、バイト数・コードポイント数・見た目の文字数が一致しないのです。

見た目バイト数(UTF-8)コードポイント数見た目の文字数なぜ違うのか
A111ASCIIの範囲は1バイト1文字
311日本語は3バイトで1コードポイント
😀411基本多言語面の外は4バイト必要
(結合文字)621「か」+濁点で表せる。正規化で1コードポイントにもなる
👨‍👩‍👧18513つの絵文字をZWJ(幅ゼロの接合子)で繋いだ並び

ここから実務的な教訓が出ます。文字数の制限や文字列の切り詰めをバイト数でやると、日本語や絵文字が壊れます。3バイトで1文字の「あ」を、2バイトの位置で切ってしまうと、意味のない断片が残ります。だから文字数を数えるときは、UTF-8の場合はバイト数ではなくコードポイント、できれば「見た目の1文字」(Unicodeの規格では拡張書記素クラスタと呼びます)で数える必要があります。1文字の定義が3段階あると知っているだけで、この種のバグは防げます。

🔬 検証③:小数は「近似」でしか表せない——浮動小数点の割り切り

最後に小数です。ここが最も「理不尽」に感じられる部分です。結論から言えば、浮動小数点は値を近似で持つ仕組みであり、0.1 のような一見単純な数も、2進数では正確に表せません。

なぜそうなるのか。10進数の世界を思い出してください。1 ÷ 3 を10進数の小数で書くと 0.3333... と永遠に続きます。これは「10進数の位取りでは、3分の1を有限の桁で表せない」というだけの話です。同じことが、2進数でも起きます。2進数の位取りは「1/2、1/4、1/8、1/16…」という2の累乗の分数です。この組み合わせで 0.1(= 1/10)を作ろうとすると、割り切れません。

10進の3分の1と2進の0.1がどちらも割り切れないことを示す図 10進数では3分の1が割り切れず、2進数では0.1が割り切れないことを並べて示し、コンピュータが途中で打ち切って近似値として保存する様子を描く。 どちらも「その世界の表し方では割り切れない」だけのこと 10進数の世界(人間が使う) 1 ÷ 3 = 0.3333333333333333… 3が永遠に続く。有限の桁では書けない 2進数の世界(コンピュータが使う) 0.1 = 0.00011001100110011… 0011が永遠に続く。有限の桁では書けない コンピュータは桁数が有限なので、必ずどこかで打ち切る → ここで誤差が生まれる 0.1(64ビット) = 0.1000000000000000055511151231257827…(ごく僅かに大きい) 誤差は「バグ」ではなく、有限の桁で表すことの必然的な結果

「誤差が出る」と聞くと、欠陥のように感じます。しかし実際は違います。もし2進数で 0.1 を正確に表そうとしたら、無限の桁が必要になり、無限のメモリが要ります。有限の資源で扱うための合理的な割り切りが浮動小数点です。だから問題は「誤差があること」ではなく、誤差があっても壊れない設計を選ぶことです。

現在の浮動小数点の標準が IEEE 754(1985年に最初の版、現在は2019年版)です。よく使うのは2種類です。32ビットの単精度(係数に23ビット)と、64ビットの倍精度(係数に52ビット)です。

ビット数正確に表せる整数の上限小数の桁数目安主な用途
単精度(32ビット)32約1,677万約7桁グラフィックス、機械学習の重み、通信量を減らしたい場面
倍精度(64ビット)642^53−1 = 9,007,199,254,740,991約15〜16桁JavaScriptのNumber、Pythonのfloat、科学計算の既定
整数(64ビット)64約922京なし(誤差ゼロ)金額、ID、カウンタ。誤差を許さない値
十進浮動小数点可変言語・ライブラリ依存設定した桁数どおり会計、税計算。decimal型など

この表の2行目「倍精度で正確に表せる整数の上限」が、実務で最も効いてきます。2^53 − 1(2の53乗 − 1)を超える整数は、倍精度では近い値に丸められる可能性があります。JavaScriptの Number.MAX_SAFE_INTEGER がこの値なのは、そういう理由です。そして、この制限はJSONにも影響します。JSONの仕様(RFC 8259)は、この範囲までの整数なら実装間で一致すると明記しています。つまり、それより大きな整数をJSONでやり取りすると、受け取った側の言語によって値が変わることがあるのです。実際、SNSなどが発行する巨大なIDを扱うとき、文字列として受け取るのが定石になっているのはこのためです。

📊 結果:型の選び方で、起きるバグの種類が決まる

ここまでの内容を「何を選べばいいか」の形にまとめます。型は「数値か文字列か」の2択ではなく、その値に何を許すかの宣言です。

扱いたいものやってはいけない選択適切な選択起きるバグ
金額浮動小数点で計算する最小通貨単位の整数、または十進型で計算する合計が1円ずれる。桁数が増えるほどズレが累積する
巨大なID(SNS・決済など)数値型で受け取る文字列として受け渡し、比較も文字列で行う末尾の桁が丸められ、別人のデータを参照する
比率・割合等号で比較する(a == b許容誤差をつけて比較する(差がある値より小さいか)「同じ値のはずなのに一致しない」判定ミス
日本語を含む文字列の長さ制限バイト数で切るコードポイント、または見た目の1文字で切る文字の途中で切れて、意味のない記号が残る
真偽値文字列の "false" で保存する真偽値型で保存する"false" が真と判定される。廃止予定の挙動に依存する
日時ローカル時刻の文字列で保存するUTCで保存し、表示時に変換するサマータイムや海外利用で時刻がずれる

特に最後の2行は、データの「型」というより「約束」の問題です。真偽値を文字列で持つと、"false" という文字列が真として扱われる危険があります。日時をローカル時刻の文字列で保存すると、その地域のルールが変わった瞬間に壊れます。型を選ぶとは、将来の壊れ方を選ぶことだと言っても過言ではありません。

💭 考察:データ型は、いちばん安いドキュメントである

ここまでの内容を抽象化すると、こうなります。データ型とは「この値はこういう意味で、この範囲を超えません」という宣言です。

考えてみれば、これはドキュメントの役割と同じです。ところが、型はコメントと決定的に違う点があります。型は機械が検証してくれるのです。コメントは古くなっても誰も教えてくれませんが、型は合わなければコンパイルエラーや実行時エラーになります。だから「型は最強のドキュメント」とよく言われます。

動的型付けの言語を使っている人にも、この話は関係があります。PythonやJavaScriptには型注釈が必須ではありませんが、書き手の頭の中には必ず「この変数は何か」という型があるはずです。そして、その頭の中の型が曖昧なまま書かれたコードは、遅かれ早かれ壊れます。user_id という変数に、文字列が入るときと数値が入るときが混ざっていたら、それは型の設計が失敗しているサインです。

もう1つ、実務で効く見方があります。型を狭くすると、テストが減るということです。たとえば「金額は必ず最小通貨単位の整数」と決めれば、小数の丸めを検証するテストは不要になります。逆に「なんでも数値型で受ける」と決めると、丸め・オーバーフロー・精度のテストが必要になります。型の選択はテストの量を決めています

📌 注目ポイント

この記事の核心を4点に絞ります。

第一に、0と1には意味がありません。意味を与えているのは辞書(符号化)です。 同じ E3 81 82 が、UTF-8では「あ」になり、別の文字コードでは意味の分からない記号になります。だから文字化けの原因は「データの破損」ではなく「辞書の取り違え」です。

第二に、文字コードを揃えれば、多くの文字化けは事前に防げます。 新規に作るものはUTF-8に統一する。外部から来るデータは、どの文字コードかを確認してから読む。文字化けを見つけたら、まず正しい辞書で読み直す(書き戻さない)。この3つで大半は解決します。

第三に、浮動小数点の誤差は欠陥ではなく、有限の資源で扱うための割り切りです。 そして、誤差があること自体は問題ではありません。誤差を許してはいけない値に浮動小数点を使うことが問題です。金額と巨大なIDが代表例です。

第四に、型は将来の壊れ方を決めます。 狭い型(整数、文字列、真偽値)を選ぶと、テストも減り、壊れ方も予測しやすくなります。逆に「とりあえず数値」と決めると、精度・範囲・丸めのテストが必要になります。

💡 活用事例:3億7,000万ドルを消した、たった1行の型変換

データの型が「静かに壊れる」ものだという話をしてきましたが、静かでは済まなかった例があります。1996年6月4日、ヨーロッパのロケット「アリアン5」の初号機が打ち上げの約37秒後に破壊されました。ロケット本体と搭載していた衛星を合わせて、約3億7,000万ドル相当の損失が発生したとされています。

事故調査委員会の報告書によれば、原因はソフトウェアの型変換のオーバーフローでした。ロケットの慣性基準装置(姿勢や速度を測る計器)の中で、64ビットの浮動小数点で表現された値を、16ビットの符号付き整数に変換する処理がありました。この処理はアリアン4では問題なく動いていました。ところが、アリアン5はアリアン4より加速が速く、変換前の値が16ビット整数で表せる範囲(-32768〜32767)を超えてしまったのです。

アリアン5で起きた型変換オーバーフローの連鎖 64ビット浮動小数点の大きな値を16ビット整数の小さな箱に入れようとしてオーバーフローが起き、計器が停止し、ロケットが破壊されるまでの連鎖を描く。 アリアン4と同じコードが、アリアン5では「大きすぎる値」を受け取った ① 64ビット浮動小数点 姿勢のずれを表す値 アリアン5では範囲を超過 とても広い箱 変換 ② 16ビット整数 -32768 〜 32767 とても狭い箱 入りきらない ③ 例外で停止 予備系も同じ設計で 同時に停止 ④ 機体破壊 打ち上げ37秒後 調査報告書が指摘した2つの設計上の問題 ① 変換元の値が範囲に入っているかの検査が、この変換には無かった(同じ演算の別経路には存在していた) ② 打ち上げ後は不要になった処理を止めずに動かし続けていた(アリアン4では不要でも問題にならなかった) 「動いていたコード」は、環境が変わると意味が変わる

この事例の怖さは、コードが間違っていなかったことです。アリアン4では正しく動いていました。変わったのは環境(加速の速さ)であり、その環境でコードの前提(値は必ず16ビットに収まる)が崩れました。報告書は、変換元の値が範囲内かを検査していなかったことと、打ち上げ後は不要になった処理を止めていなかったことの2点を問題として指摘しています。

ここからジュニアエンジニアが持ち帰れる教訓は3つです。第一に、「動いているコード」は、環境が変われば意味が変わります。第二に、型変換は失敗しうる操作であり、検査を省いてはいけません。第三に、予備系を同じ設計にすると、同じ原因で同時に落ちます。3つ目は第1回の話にも通じます。同じ階に同じ設計の予備を置いても、階そのものが壊れたときは両方やられます。

ちなみに、日本語の現場でよく出会う小さな事故にも、同じ構造があります。MySQLには以前 utf8 という名前の文字コードがありましたが、これは最大3バイトまでしか扱えませんでした。絵文字は4バイト必要なので、保存しようとすると ? に置き換わります。この問題への対処として、4バイト対応の utf8mb4 が導入されました(MySQL 5.5.3 以降)。「utf8」という名前なのに全部のUnicodeを扱えないという、名前と実態の食い違いが混乱を招いた例です。

✅ 要点まとめ

読み終えたあなたが持ち帰るべきエッセンスを、6つに圧縮します。

  • 0と1には意味がない。意味を与えるのは辞書(符号化)。同じバイト列が複数の意味を持ちうる
  • 1バイトは256通り。ビット数を増やせば範囲が広がるが、有限であることは変わらない
  • 文字コードが食い違うと文字化けする。ただし壊れるのはデータではなく読み方。まず読み直す
  • Unicodeは世界共通の番号表、UTF-8はその変換方式。ASCIIと互換で、1〜4バイトの可変長
  • 浮動小数点は近似。0.1 は2進で割り切れない。誤差そのものは欠陥ではなく割り切り
  • 型は将来の壊れ方を決める。金額と巨大なIDに浮動小数点を使わない。文字数はバイト数で数えない

🚀 取り込み方

「明日から使うには何をすればいいか」を、期間ごとに分けて示します。

今日(5分でできること)

自分のPCのターミナルで、次の1行を実行してみてください(Pythonが入っている場合)。

print(0.1 + 0.2)

0.30000000000000004 と表示されます。これが誤差の実物です。次に print(0.1 + 0.2 == 0.3) を実行すると False になります。「小数の等号比較は信用できない」という感覚を、1回の実行で体に入れておくのが目的です。

今週(小さく試す)

自分の担当コードから、次の3つを探してください。(1) 金額を浮動小数点で扱っている箇所、(2) 文字数をバイト数で数えている箇所、(3) IDを数値型で受け取っている箇所。見つかったら、すぐ直さなくてかまいません。リストにしておくだけで、レビューのときに指摘できるようになります。

文字コードの確認も一度やっておくと安心です。LinuxやmacOSでは file -i ファイル名 で文字コードを推測できます。Pythonなら open(path, encoding="utf-8") のように文字コードを明示する癖を付けてください。省略した場合の既定値は実行環境によって変わるため、明示するのが安全です。

今月(業務に組み込む)

チームのコーディング規約に、次の3行を追加できないか提案してみてください。「新規ファイルの文字コードはUTF-8とする」「金額は最小通貨単位の整数で扱う」「巨大なIDは文字列として扱う」。規約に書くのが難しければ、自分のレビューコメントで毎回指摘するだけでも効果があります。型の話は合意コストが低いわりに効果が長く続くので、最初の1つとして向いています。

🔥 ハマりポイント

つまずきやすい5つの落とし穴を、「〜と思いがちだが、実は〜」の形で整理します。

その1:誤差は大きな計算でしか出ないと思いがちだが、実は足し算1回で出る

症状は、単純な計算なのに結果が合わないこと。原因は、0.1 のような値が2進で正確に表せないこと。対処法は、許容誤差を使った比較に切り替えることです。たとえば「差の絶対値が 0.000000001(10の-9乗)より小さければ等しいとみなす」という形にします。ただし金額だけは誤差を許してはいけないので、整数か十進型に切り替えてください。

その2:文字コードを指定しなくても動くと思いがちだが、実は環境で変わる

症状は、自分のPCでは読めたファイルが、サーバーでは化けること。原因は、文字コードの既定値がOSや言語のバージョンで変わること。対処法は、読み書きのたびに文字コードを明示することです。特にWindowsとLinuxの間でファイルをやり取りするときは必ず明示してください。

その3:文字数をバイト数で数えても大丈夫と思いがちだが、実は日本語と絵文字で壊れる

症状は、保存した名前やメッセージの末尾が意味不明な記号になること。原因は、マルチバイト文字の途中で切ったこと。対処法は、コードポイント数か見た目の1文字で数えることです。データベースのカラム長を決めるときも同じで、「VARCHAR(255)」の255がバイトなのか文字なのかを必ず確認してください。

その4:大きな整数は数値型で受けたほうが自然と思いがちだが、実は丸められる

症状は、APIから受け取ったIDの末尾が 000 に変わっていること。原因は、倍精度で正確に表せる整数の上限(2の53乗 − 1 = 9,007,199,254,740,991)を超えていること。対処法は、最初から文字列として受け取ることです。整数として扱いたくなりますが、IDに算術は不要なので文字列で十分です。

その5:「1文字」は1つの番号だと思いがちだが、実は複数の番号で1文字になることがある

症状は、絵文字や濁点付き文字を削除したら、一部だけ残ったり別の文字になったりすること。原因は、見た目の1文字が複数のコードポイントで構成されていること(濁点の結合、絵文字の連結など)。対処法は、見た目の1文字単位で扱うAPIを使うことです。文字数のカウント・切り詰め・逆順処理は、この問題が起きやすい処理です。

🔄 比較:どんなときに何を選ぶか

最後に、値の種類ごとに「どの表現を選ぶか」を比較します。正解は1つではなく、何を優先するかで変わります。正直に、向き不向きを書いておきます。

表現強み弱み向いているケース
整数型誤差ゼロ。比較も合計も安全。速い小数を扱えない。範囲の上限がある金額(最小通貨単位)、ID、件数、カウンタ
倍精度浮動小数点広い範囲と十分な精度。ハードウェアで高速誤差がある。大きな整数は丸められる科学計算、グラフィックス、統計処理
十進浮動小数点10進で指定桁の精度を保証。会計に強い倍精度より遅い。ライブラリ依存税計算、請求、金融系の集計
文字列(数値の代替)丸めが起きない。桁数制限がない算術ができない。比較に注意が必要巨大なID、電話番号、郵便番号、口座番号
UTCのタイムスタンプ時差とサマータイムの影響を受けない表示のたびに変換が必要保存・比較・並び替え。表示は変換して行う

この表で一番見落とされやすいのが4行目の「文字列」です。電話番号の先頭の 0、郵便番号の 0、口座番号の桁数。数値に見えるが数値ではないものは、文字列として扱うのが正解です。判断基準はシンプルで、「その値で足し算をしたくなるか」を自問してください。したくならなければ、それは文字列です。

📅 今後の展望

文字コードの統一は、ほぼ決着しました。新規のシステムではUTF-8が既定になり、Webの仕様(WHATWG Encoding Standard)でも扱いが整理されています。残っているのは過去の資産です。Shift_JISで作られた古いシステムとの連携は、今後もしばらく続きます。新しく作る側がUTF-8に統一し、境界で変換するという方針が現実的です。

浮動小数点の側は、変化が起きています。AIの計算では、精度を落としてでも速くする方向が主流になりました。32ビットよりさらに小さい16ビットの浮動小数点(半精度)や、8ビットの整数で重みを表現する手法が広く使われています。誤差を許容して資源を節約するという、この記事で扱った考え方の極端な応用です。つまりデータの正体を知る重要性は、AI時代にむしろ上がっています。

もう1つ、見逃せない変化があります。文字数の数え方が、これからも揺れ続けるということです。絵文字は毎年のように新しいものが追加され、肌の色や家族構成の組み合わせでコードポイント数が変わります。結合文字や異体字セレクタ(同じ文字の字形違いを選ぶ仕組み)も増えました。「1文字」という概念は、人間にとっては自明でも、コンピュータにとっては規約なのだと考えるのが安全です。文字数制限を設計するときは、この前提に立ってください。

🗺️ 次回予告:なぜディスクはメモリより1000倍遅いのか

第2回では「データをどう表すか」を扱いました。第3回は、そのデータをどこに置き、どう運ぶかの話です。

コンピュータのメモリには、速度の階段があります。CPUの中のレジスタ、CPUの近くのキャッシュ、主記憶のメモリ、SSD、ネットワーク越しのストレージ。上に行くほど速くて小さく、下に行くほど遅くて大きい。この階段の段差を知ると、「なぜこのコードは遅いのか」が説明できるようになります。同時に、スタックとヒープという2つのメモリ領域の違いも扱います。第2回で扱った「型」と「大きさ」の知識が、そのまま効いてくる回です。

まとめ

この記事を読んだあなたは、0.30000000000000004 を見ても慌てなくなります。そして、その値がどの辞書で、どの型で表されているかを最初に考えるようになります。金額に浮動小数点が使われていれば指摘でき、文字数をバイト数で数えているコードを見れば止まれます。

データの正体は、地味です。派手な障害を起こさず、静かに壊れます。だからこそ、静かに壊れる前に気づける人が、チームの中で効いてきます。0と1しかない世界で、私たちは「これは文字です」「これは金額です」と約束を積み重ねて意味を作っています。その約束がすべてだと知っていれば、約束が破れた場所も見つけられます。

参考文献

  1. IEEE, “IEEE Standard for Floating-Point Arithmetic (IEEE 754-2019)” — https://standards.ieee.org/
  2. David Goldberg, “What Every Computer Scientist Should Know About Floating-Point Arithmetic”, ACM Computing Surveys, 1991 — https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
  3. Unicode Consortium, “The Unicode Standard” — https://www.unicode.org/versions/latest/
  4. F. Yergeau, “RFC 3629: UTF-8, a transformation format of ISO 10646”, IETF, 2003 — https://www.rfc-editor.org/rfc/rfc3629
  5. Unicode Consortium, “UAX #29: Unicode Text Segmentation”(拡張書記素クラスタの定義) — https://www.unicode.org/reports/tr29/
  6. Unicode Consortium, “UAX #15: Unicode Normalization Forms” — https://www.unicode.org/reports/tr15/
  7. Unicode Consortium, “UTR #36: Unicode Security Considerations”(見た目が似た文字によるなりすまし) — https://www.unicode.org/reports/tr36/
  8. T. Bray (ed.), “RFC 8259: The JavaScript Object Notation (Data Interchange) Format”, IETF, 2017 — https://www.rfc-editor.org/rfc/rfc8259
  9. V. Cerf, “RFC 20: ASCII format for Network Interchange”, IETF, 1969 — https://www.rfc-editor.org/rfc/rfc20
  10. WHATWG, “Encoding Standard” — https://encoding.spec.whatwg.org/
  11. J. L. Lions (Chairman), “ARIANE 5 Flight 501 Failure: Report by the Inquiry Board”, European Space Agency, 1996 — https://www.esa.int/
  12. Python Software Foundation, “Floating Point Arithmetic: Issues and Limitations”(Python チュートリアル) — https://docs.python.org/3/tutorial/floatingpoint.html
  13. Oracle, “MySQL 8.0 Reference Manual: Character Sets and Collations”(utf8mb4utf8 の扱い) — https://dev.mysql.com/doc/refman/8.0/en/charset.html
  14. IEEE Computer Society, “IEEE 754-2019 — IEEE Standard for Floating-Point Arithmetic(解説記事: floating-point-gui.de)” — https://floating-point-gui.de/
  15. 独立行政法人情報処理推進機構(IPA), 「安全なウェブサイトの作り方」 — https://www.ipa.go.jp/security/
  16. Digital Aggregates / 0.30000000000000004.com — https://0.30000000000000004.com/
ジュニアエンジニア向け コンピュータサイエンス入門 ── 全8回の一覧
いま読んでいるのは 第2回 です。読みたい回から始めても構いません。
  1. 第1回:全体地図:その1行は「5階建てのビル」で動いている
  2. ▶ 第2回:データの正体:0.1+0.2はなぜ0.3にならないのか(この記事)
  3. 第3回:CPUとメモリ:1回のメモリアクセスは100回の計算より高くつく
  4. 第4回:OSの仕事:「ファイルを開く」は何をしているのか
  5. 第5回:アルゴリズムとデータ構造:100万件から1人を探すのに20回で足りる
  6. 第6回:ネットワーク:クリックした荷物はどうやって海を渡るのか
  7. 第7回:データベース:100人が同時に書き換えても壊れないのはなぜか
  8. 第8回:並行と並列:awaitを1つ忘れただけで、なぜ本番だけ壊れるのか

© Copyright 2005-2026| Rui Software | All Rights Reserved