はじめに
今回はMongoDBのお話です。
使っている中で色々と癖があったり無かったりな所がありますが、意外と日本語の情報が無い(特に最近)ということもあるので少しずつ小ネタでも書いていこうかなと思います。
MongoDBで四捨五入するには
ということで早速本題です。
例えばこんなtest_result
コレクションがあったとします。(_id
は以下省略)
[
{ "no": 1, "name": "John", "score": 56.4 },
{ "no": 2, "name": "Jennifer", "score": 68.5 },
{ "no": 3, "name": "Robert", "score": 72.62 },
{ "no": 4, "name": "Mary", "score": 56.51 },
{ "no": 5, "name": "Charlotte", "score": 79.5 },
]
何のデータなのやらという感じですが、このそれぞれのドキュメントのscore
フィールドを整数で四捨五入した結果も追加した状態で取得したいなぁ~と考えたとします。
その場合は下記のようなクエリになります。当然Aggregationの出番ですね。
db.test_result.aggregate([
{
$addFields: {
score_round: { $round: ['$score', 0] }
}
}
])
.toArray()
$round
が四捨五入を行うオペレータで、1つ目の引数に対象の数値(今回はscore
フィールド)、2つ目の引数に対象の桁数を設定できます。
今回は整数なので0指定ですが、2を設定すれば小数第二位まで、-2を設定すれば百の位までと色々指定できます。(余談ですが、省略も可能でその場合は0になります)
簡単ですね。ということで結果はこの通りです。
[
{ "no": 1, "name": "John", "score": 56.4, "score_round": 56 },
{ "no": 2, "name": "Jennifer", "score": 68.5, "score_round": 68 },
{ "no": 3, "name": "Robert", "score": 72.62, "score_round": 73 },
{ "no": 4, "name": "Mary", "score": 56.51, "score_round": 57 },
{ "no": 5, "name": "Charlotte", "score": 79.5, "score_round": 80 },
]
...特に言うことはないと思ったら、Jenniferのスコアが変です。68.5を四捨五入したら68になっています。
と思ったらCharlotteの79.5は予想通り80になってるし…なぜ???
なぜこうなるのか
初めに言っておきますが、これは仕様です。
$round
は一般的に考える四捨五入ではなく、偶数丸め(銀行丸め)を行っているというだけの話です。
偶数丸め(銀行丸め)
端数(今回で言えば小数第1位)が5以外の場合は通常の四捨五入と同じですが、5の場合は結果が偶数になる方へ丸めていきます。
表にするとこんな感じ。
元の数値 | 四捨五入 | 偶数丸め(銀行丸め) |
---|---|---|
0.5 | 1 | 0 |
1.5 | 2 | 2 |
2.5 | 3 | 2 |
3.5 | 4 | 4 |
4.5 | 5 | 4 |
5.5 | 6 | 6 |
わざわざこんなことをするメリットとしては、端数処理の後の合計値と元の数値の合計値を比べた際に誤差が少なくなることです。
ここら辺は詳しい方が色々なサイトで記述しているのでこれ以上は割愛しますが、まあ四捨五入ではないので大抵の場合は意図していない結果になってしまいます。
ちゃんと公式サイトにも下記の記述があります。
When rounding on a value of 5, $round rounds to the nearest even value.
気づかずに使うと大惨事ですね…。
対処方法
じゃあ意図した四捨五入にするにはどうするのか?という話ですが、残念ながらMongoDBには四捨五入を行うオペレータは用意されていません。
じゃあ無理なのか?という話なのですが、残念ながら力業で四捨五入する方法があります。
db.test_result.aggregate([
{
$addFields: {
score_round: {
$trunc: [{ $add: ['$score', 0.5] }, 0]
}
}
}
])
.toArray()
要は$add
で四捨五入の対象となる端数の桁(今回は小数第1位)に5を足し、その後$trunc
で端数を切り捨てるという処理を行っています。
これにより端数が5以上であれば切り上げ後に切り捨てが行われ、5未満であれば変わらずそのまま切り捨てられるということですね。
結果は下記になります。
[
{ "no": 1, "name": "John", "score": 56.4, "score_round": 56 },
{ "no": 2, "name": "Jennifer", "score": 68.5, "score_round": 69 },
{ "no": 3, "name": "Robert", "score": 72.62, "score_round": 73 },
{ "no": 4, "name": "Mary", "score": 56.51, "score_round": 57 },
{ "no": 5, "name": "Charlotte", "score": 79.5, "score_round": 80 },
]
無事Jenniferの68.5が69になり、意図した結果になりました。
ちなみに四捨五入する桁を変えたい場合は、$add
や$trunc
の桁数を調整すれば対応可能です。よかったよかった。
終わりに
様々なDBやら言語やらでround系のメソッドはいっぱいあり、大半が普通?の四捨五入だそうですがちゃんと調べておかないとこれは偶数丸めだった!となるので注意しなければいけないようです。
逆に仕様上偶数丸めをしなければいけない仕様かもしれないので、そこは要件定義でしっかり詰める必要がありますね。
そういった諸々を考えると、やや本末転倒ですがMongoDBで四捨五入しなければいけない状況でも無ければデータを取得した後に四捨五入を処理するほうが柔軟性を考えても良いのではと考えてしまいます。(もちろん知識として知っておくのは重要ですが)
とにかく数値を扱うシステム(大半がそうだろうけども)だとこういう細かい誤差が命取りなので、怖い話ですね。気を付けていきましょう。
余談:整数以外で$round
を使うとどうなる?
さて、気になってしまったので余談です。
$round
は丸める桁数を指定することもできるのは上述の通りですが、じゃあ小数点以下で丸める場合だとどうなるのか?という疑問が湧きます。
端数が5なら最も近い偶数に丸めるという話ですが、そもそも小数点以下で丸めるなら最も近い偶数とは??となりますね。ということでやってみました。
とりあえず小数二桁目を四捨五入してみます。($round: [任意の数値, 1]
)
1.05 → 1.1
1.15 → 1.1
1.25 → 1.2
1.35 → 1.4
1.45 → 1.4
1.55 → 1.6
1.65 → 1.6
1.75 → 1.8
1.85 → 1.9
1.95 → 1.9
2.05 → 2
2.15 → 2.1
2.25 → 2.2
2.35 → 2.4
2.45 → 2.5
2.55 → 2.5
2.65 → 2.6
2.75 → 2.8
2.85 → 2.9
2.95 → 3
3.05 → 3
3.15 → 3.1
3.25 → 3.2
3.35 → 3.4
3.45 → 3.5
3.55 → 3.5
3.65 → 3.6
3.75 → 3.8
3.85 → 3.9
3.95 → 4
4.05 → 4
4.15 → 4.2
4.25 → 4.2
4.35 → 4.3
4.45 → 4.5
4.55 → 4.5
4.65 → 4.7
4.75 → 4.8
4.85 → 4.8
4.95 → 5
……法則が分からなさすぎる!!
小数第一位の数字が偶数かどうかで見てくれるのかと思いきや、全く法則もなく.65が.7になったり.6になったりしてます。
しかも整数部分が偶数と奇数で法則があることもなく、何と言ったらいいのかわからない。何だこれ…
誰かご存じの方居たら、是非ご教授いただきたいです。
何かしらの計算を行ったうえでこうなっているのでしょうが、自分では調べても出てこず理解を諦めました。
この結果で正しいと言われることも無いでしょうし、ひとまず$round
は整数以外では使わないほうがいいのかもしれません。何だこのオペレータは…