データ通信量(ギガ)の目安

動画・SNS・音楽などの使い方と時間から、1か月のデータ通信量(ギガ)と目安のプランを出す。

2進小数の誤差0.5 の丸め方

言語ごとの結果

正解表 11件のうち、正解と一致した件数です。

PHPPHP 8.4.26

よくある書き方9 / 11

直した書き方11 / 11

PythonPython 3.14.7

よくある書き方8 / 11

直した書き方11 / 11

GoGo 1.27.2

よくある書き方8 / 11

直した書き方11 / 11

JavaScriptNode 24.15.0

よくある書き方9 / 11

直した書き方11 / 11

なぜ割れたのか

よくある書き方が落ちたケースを、原因ごとにまとめた解説です(1本)。書いてある「落ちた言語」が今回の検証結果と一致することを確かめてから載せています。

2進小数の誤差

ちょうど7GB なのに「10GB プラン」、22.5GB なのに「22 GB」——境目の反対側に落ちた

対象のケースc002c003c005 落ちた言語PHPPythonGoJavaScript

音楽ストリーミング(0.08GB/時)を1日3.5時間・月25日聴くと、0.08 × 3.5 × 25 = ちょうど7GB です。目安のプランは「7GB 以下」なので 7GB プラン が正解ですが、4言語とも 10GB プラン と答えました。

SNS(0.3GB/時)を1日3時間・月25日だと 22.5GB で、整数に四捨五入して 23 GB と表示するのが正解ですが、4言語とも 22 GB と表示しました。

なぜ境目を越えたのか

計算正確な値2進数の小数での結果何が起きたか
0.08 × 3.5 × 2577.000000000000001「7 以下」を満たさず、次の 10GB プランへ
0.3 × 3 × 2522.522.4999999999999960.5 に届かず、四捨五入で切り捨て

0.08 や 0.3 は2進数の小数で正確に表せないので、掛け算の結果がわずかに上か下にずれます。表示を切り替える境目(7GB 以下か、0.5 以上か)にちょうど乗る値では、そのわずかなずれで答えが変わります。

偶数丸めの言語は、12.5GB も外した

ビデオ通話(0.5GB/時)を1日5時間・月5日だと 12.5GB です。12.5 は2進数でも正確なので、PHP と JavaScript は正しく 13 GB にしました。Python(round)と Go(strconv.FormatFloat)は 0.5 ちょうどを偶数の方へ丸めるので 12 GB になりました。

どう直したか

1時間あたりの通信量・時間・日数を分数のまま掛け、境目との比較と四捨五入を整数で行いました。PHP は BcMath\Number、Python は fractions.Fraction、Go は math/big.Rat、JavaScript は BigInt を使っています。

正解表と、よくある書き方の答え

正解は実装より先に決めて固定しています。言語の列の ✓ は正解と一致、赤い値はその言語が実際に返した間違った答えです。

ケース入力正解 PHPPythonGoJavaScript
c001動画HD・1日2時間・30日
per_hour
1.0
hours
2
days
30
monthly
60 GB
per_day
2.0 GB
hours_3gb
3 時間
hours_20gb
20 時間
plan
100GB プランが目安
✓✓✓✓
c002SNS・月22.5GB(整数への四捨五入が .5) 0.3 × 3 × 25 = 22.5 → 23 GB。浮動小数点では 22.499… になり 22 GB なぜ? 解説を読む
per_hour
0.3
hours
3
days
25
monthly
23 GB
per_day
922 MB
hours_3gb
10 時間
hours_20gb
66 時間 40 分
plan
30GB プランが目安
monthly"22 GB"monthly"22 GB"monthly"22 GB"monthly"22 GB"
c003音楽・月ちょうど7GB(プランの境目) 0.08 × 3.5 × 25 = 7 → 7GB プラン。浮動小数点では 7.000000000000001 になり 10GB プランに上がる なぜ? 解説を読む
per_hour
0.08
hours
3.5
days
25
monthly
7.0 GB
per_day
287 MB
hours_3gb
37 時間 30 分
hours_20gb
250 時間
plan
7GB プランが目安
plan"10GB プランが目安"plan"10GB プランが目安"plan"10GB プランが目安"plan"10GB プランが目安"
c004音楽高音質・3GB でちょうど20時間 3 ÷ 0.15 = 20 時間。浮動小数点では 19.999… になり、分が 60 になる(繰り上げないと「19 時間 60 分」)
per_hour
0.15
hours
1
days
30
monthly
4.5 GB
per_day
154 MB
hours_3gb
20 時間
hours_20gb
133 時間
plan
5GB プランが目安
✓✓✓✓
c005ビデオ通話・月12.5GB(偶数丸めで割れる) 12.5 → 13 GB。偶数丸めだと 12 GB なぜ? 解説を読む
per_hour
0.5
hours
5
days
5
monthly
13 GB
per_day
2.5 GB
hours_3gb
6 時間
hours_20gb
40 時間
plan
20GB プランが目安
✓monthly"12 GB"monthly"12 GB"✓
c006Web閲覧・少しだけ(MB 表示) 0.03 GB = 30.72 MB → 30.7 MB
per_hour
0.06
hours
0.5
days
1
monthly
30.7 MB
per_day
30.7 MB
hours_3gb
50 時間
hours_20gb
333 時間
plan
3GB プランが目安
✓✓✓✓
c0074K動画・長時間(分だけ・時と分) 3 ÷ 7 = 0.428… 時間 → 26 分、20 ÷ 7 = 2.857… 時間 → 2 時間 51 分
per_hour
7.0
hours
12
days
31
monthly
2604 GB
per_day
84 GB
hours_3gb
26 分
hours_20gb
2 時間 51 分
plan
大容量・無制限プランが目安
✓✓✓✓
c008利用時間0 境界値。0.0 MB・3GB プラン
per_hour
1.0
hours
0
days
30
monthly
0.0 MB
per_day
0.0 MB
hours_3gb
3 時間
hours_20gb
20 時間
plan
3GB プランが目安
✓✓✓✓
c009ちょうど1GB 0.04 × 25 = 1 → 1.0 GB
per_hour
0.04
hours
1
days
25
monthly
1.0 GB
per_day
41.0 MB
hours_3gb
75 時間
hours_20gb
500 時間
plan
3GB プランが目安
✓✓✓✓
c010100時間以上 20 ÷ 0.06 = 333.3… → 333 時間
per_hour
0.06
hours
2
days
30
monthly
3.6 GB
per_day
123 MB
hours_3gb
50 時間
hours_20gb
333 時間
plan
5GB プランが目安
✓✓✓✓
c011大容量プラン 360 GB → 大容量・無制限
per_hour
3.0
hours
4
days
30
monthly
360 GB
per_day
12 GB
hours_3gb
1 時間
hours_20gb
6 時間 40 分
plan
大容量・無制限プランが目安
✓✓✓✓

コード

「違い」では、よくある書き方から直した書き方へ、どの行をどう変えたかを示しています(赤が消した行、緑が足した行、濃い色が行の中で変わった部分)。計算の部分だけを載せ、入出力を受け渡す部分は除いています。

Usage.php+26 −13よくある書き方(182168f)→ 直した書き方

@@ 変更前 1行目〜 / 変更後 1行目〜 @@
11 <?php
22 declare(strict_types=1);
33
4+use BcMath\Number;
5+
46 /**
57 * データ通信量(ギガ)の目安。
6− * v1: よくある書き方(float で計算し、round と number_format で表示)。
8+ * v2: 十進(BcMath\Number)で計算し、四捨五入は RoundingMode::HalfAwayFromZero で明示する。
9+ * float だと 0.08 × 3.5 × 25 が 7.000000000000001 になり、7GB プランの境目を越えてしまう。
10+ * 割り算(3 ÷ 0.07 など)は割り切れないことがあるので、四捨五入に影響しない桁数(30桁)まで出す。
711 */
8−function fmtData(float $gb): string
12+function fmtData(Number $gb): string
913 {
14+ $r = fn (Number $x, int $p): string => $x->round($p, RoundingMode::HalfAwayFromZero)->value;
1015 if ($gb >= 10) {
11− return number_format($gb, 0, '.', '') . ' GB';
16+ return $r($gb, 0) . ' GB';
1217 }
1318 if ($gb >= 1) {
14− return number_format($gb, 1, '.', '') . ' GB';
19+ return oneDecimal($gb) . ' GB';
1520 }
1621 $mb = $gb * 1024;
17− return $mb >= 100 ? (int) round($mb) . ' MB' : number_format($mb, 1, '.', '') . ' MB';
22+ return $mb >= 100 ? $r($mb, 0) . ' MB' : oneDecimal($mb) . ' MB';
1823 }
1924
20−function fmtHours(float $h): string
25+/** 小数第1位に四捨五入し、必ず小数1桁で表す("7.0") */
26+function oneDecimal(Number $x): string
27+{
28+ $t = (int) ($x * 10)->round(0, RoundingMode::HalfAwayFromZero)->value;
29+ return intdiv($t, 10) . '.' . $t % 10;
30+}
31+
32+function fmtHours(Number $h): string
2133 {
2234 if ($h >= 100) {
23− return (int) round($h) . ' 時間';
35+ return $h->round(0, RoundingMode::HalfAwayFromZero)->value . ' 時間';
2436 }
25− $whole = (int) floor($h);
26− $mins = (int) round(($h - $whole) * 60);
37+ $whole = (int) $h->floor()->value;
38+ $mins = (int) (($h - $whole) * 60)->round(0, RoundingMode::HalfAwayFromZero)->value;
2739 if ($mins === 60) {
2840 $whole++;
2941 $mins = 0;
@@ 変更前 37行目〜 / 変更後 49行目〜 @@
3749 /** @return array<string, string> */
3850 function calcUsage(string $perHour, string $hours, string $days): array
3951 {
40− $p = (float) $perHour;
41− $h = (float) $hours;
42− $monthly = $p * $h * (float) $days;
52+ $p = new Number($perHour);
53+ $perDay = $p * new Number($hours);
54+ $monthly = $perDay * new Number($days);
4355 $plan = '大容量・無制限プランが目安';
4456 foreach ([3, 5, 7, 10, 20, 30, 50, 100] as $t) {
4557 if ($monthly <= $t) {
@@ 変更前 47行目〜 / 変更後 59行目〜 @@
4759 break;
4860 }
4961 }
50− return ['monthly' => fmtData($monthly), 'per_day' => fmtData($p * $h), 'hours_3gb' => fmtHours(3 / $p), 'hours_20gb' => fmtHours(20 / $p), 'plan' => $plan];
62+ return ['monthly' => fmtData($monthly), 'per_day' => fmtData($perDay),
63+ 'hours_3gb' => fmtHours((new Number(3))->div($p, 30)), 'hours_20gb' => fmtHours((new Number(20))->div($p, 30)), 'plan' => $plan];
5164 }

仕様

入力

項目型説明
per_hour十進文字列用途の1時間あたりの通信量(GB/時)。元ツールの選択肢(例: 動画HD "1.0"・SNS "0.3"・音楽 "0.08")
hours十進文字列1日の利用時間。0 以上
days十進文字列1か月の利用日数。1 以上

出力(すべて元ツールの表示の形の文字列)

項目定義
monthly月間通信量 = per_hour × hours × days を下の「通信量の表示」で
per_day1日の通信量 = per_hour × hours を同じく
hours_3gb3GB で使える時間 = 3 ÷ per_hour を下の「時間の表示」で
hours_20gb20GB で使える時間 = 20 ÷ per_hour を同じく
plan月間通信量が 3・5・7・10・20・30・50・100 GB 以下になる最小のプラン → "7GB プランが目安"。100 を超えたら "大容量・無制限プランが目安"

通信量の表示(GB → 文字列)

値表示
10 GB 以上整数に四捨五入して "23 GB"
1 GB 以上小数第1位に四捨五入して "7.0 GB"
1 GB 未満MB(× 1024)に直し、100 MB 以上なら整数 "922 MB"、未満なら小数第1位 "30.7 MB"

どの表示を使うかは、丸める前の正確な値で決める(9.96 GB は 1GB 以上の欄で "10.0 GB")。

時間の表示(時間 → 文字列)

  • 100 時間以上: 整数に四捨五入して "250 時間"
  • それ未満: 整数部を時、残りを分(× 60 を四捨五入)。分が 60 になったら時に繰り上げる。時が 0 なら "26 分"、分が 0 なら "20 時間"、それ以外は "2 時間 51 分"

計算の約束

  • 計算は正確な十進(または有理数)で行う。2進浮動小数点の誤差で、境目(10GB・7GB など)の反対側に落ちてはいけない
  • 四捨五入は 0.5 ちょうどを繰り上げる(偶数丸めではない)

期待値の作り方

expect は oracle.py で、有理数による定義の直訳から生成した(2026-10-11)。実装ではない。runner は使わない。