Rustを最近練習していて、小数点の値を任意の桁数で四捨五入してまとめる処理を行おうとした際にPythonより少し複雑だったので、その理由を調べたらPythonのround()の問題点を知ったので、まとめました。
小数点を丸める場面
RustでWEB API構築の練習をしていて、その中でBMIを計算する機能を作成しました。
BMIの計算式は下記の通りです。
\text{BMI} = \frac{体重(kg)}{(\text{身長(m)})^2}
BMIの計算結果に小数点が含まれる場合も多いので、一定の桁で丸めてから表示したいと思いました。
Rustで任意の桁数で小数点をまとめる
実際にRustでは下記のように作成しました。
#[post("/bmi")]
pub async fn bmi_endpoint(req_body: web::Json<models::BmiRequest>) -> impl Responder {
let height = req_body.height/100.0; //身長は一般的にcmで受け取るので、mに変換
let weight = req_body.weight;
let bmi_row = weight / height.powi(2); //BMIを計算する。powiは引数乗を計算
let bmi = (bmi_row * 100.0).round() / 100.0; // 小数点第三位を四捨五入して、小数点2桁にする
let category = match bmi {
n if n < 18.5 => "Underweight",
n if n < 24.9 => "Normal weight",
n if n < 29.9 => "Overweight",
_ => "Obesity",
};
// 構造体を生成
let response = BmiResponse {
bmi,
category: category.to_string(),
};
// JSONとして返す
HttpResponse::Ok().json(response)
}
小数点以下を丸める部分はここです。
let bmi = (bmi_row * 100.0).round() / 100.0;
処理の流れは次のようになっています(例:3.14159)
- BMIの計算結果に100を掛ける(例:3.14159*100 = 314.159)
- 少数第一位を四捨五入して整数にする(例:314.159 → 314)
- 計算結果を100で割り、少数に戻す(例:314/100 = 3.14)
とても冗長に感じますよね。
Pythonだとroundを使って小数点以下を直接指定できます。
bmi = round(bmi_row, 2)
Rustの標準ライブラリにroundがない!
Rustでももっと簡単にやりたいのになぁ〜と思ったので、ChatGPTを使って調べました。
ここで明確に説明できないのが申し訳ないですが、2進法では小数点以下の値で正確に保持できない場合がある ということが原因です。
Pythonで実際にやってみると下記のようになります。
数学的には0.3が正しいですが、Pythonで出力される値が大きくなってしまします。
>>>print(0.1 + 0.2)
0.30000000000000004
そのため、研究など厳密な値が求められるシーンではDecimal型などが使われるそうです。
Pythonのroundはこの誤差を許容(無視)していました。
value = 1.005
rounded = round(value, 2)
print(rounded)
期待値は1.1ですが、実際の結果は下記のようになります。
1.0
1.005が実際にコンピュータ内でどのように認識されているかは下記のようになっています。
>>>'{:.20f}'.format(1.005)
1.00499999999999989342
このような場合の誤差をPythonのroundは許容しています。
Pythonを長く使っていましたが、あまり意識してきませんでした。
Rustは低レイヤーで採用されることが多いので、このような誤差が隠されてしまうことで誤解を招くことを避けたり、精度が求められる場面では専用の型を使用することが推奨されているため、標準ライブラリとしてPythonのroundに当たる機能が搭載されていないようです。
私自身、誤差のことを考えずにPythonで使用してきてしまったので、反省です。
そして、前出のRustの処理も浮動小数点の処理が適切にできていません。
誤差が出るケースをサンプルで作成しようと思ったのですが、うまく作成できなかったのでこれからまた挑戦してみます!!
追記
コメントでご指摘頂きましたが、Pythonのround()がそもそも四捨五入ではなく、偶数丸めでした。
ご指摘ありがとうございました。