いろんな言語で四捨五入(Ruby, Java, VBA)
開発チームのハッタです。
家賃保証事業のシステムを担当しています。
業務で複数の言語で四捨五入を使う処理を書く機会があり、その際にハマったポイントなどを備忘録を兼ねてまとめます。
複数言語で四捨五入とかを書くことになった経緯
業務的な背景
家賃保証事業では、契約時に発生する「初回保証料」、保証期間が満了し更新する時に発生する「更新料」が主な売り上げとなります。
そして、「初回保証料」「更新料」は保証期間で按分して(金額を月数で割って)売上高を計上するルールとなっています。
たとえば、
初回保証料: 50,000円
保証期間: 24ヶ月
の場合、1月当たりの売上高は
50000 / 24 = 2083.33333...
となり、小数点以下を切り捨て・切り上げ・四捨五入のいずれかで処理しなければいけませんが、弊社では四捨五入を採用しています。
なお、そのままだと月数 * 1月当たりの売上高と初回保証料が一致しないので、差額を初月の売上に寄せます。
なぜ複数の言語で?
最初はVBA
家賃保証事業をやっているラクーンレントは、グループ内で一番新しい会社ということもあり、バックオフィスシステムの整備がまだ十分ではありません。
とはいえ日々業務は動き続けるので、時には急ぎで業務効率化の必要に迫られることもあり、そんな時にはVBAでサクッとマクロを作ることもあります。
今回の按分処理もそんな経緯で当初はVBAで実装しました。
その後Java & Ruby
しかし、しばらく運用されてデータが蓄積されていくとマクロでは運用が厳しくなり、本格的なシステム化を検討することになりました。
この時点でのラクーンレントのバックオフィス構成は、
- FileMaker
- 内製したRuby on Railsシステム
- FileMakerとRailsシステムをつなぐデータ連携用Javaアプリ
となっていました。
データ連携アプリがJavaで書かれているのは、FileMakerのデータを参照するドライバがJDBCとWin/Mac用ODBCしか提供されていないためです。
そして諸々の業務要件から、データ連携アプリとRailsシステムの両方で按分処理が必要となり、Java・Rubyでの実装が必要となりました。
各言語の具体的な書き方
※今回の要件は浮動小数点数の誤差が問題になるようなものではないので下記のような書き方になっていますが、要件次第ではJava,RubyではBigDecimalを使うなど、別の考慮が必要になります。
VBA
Dim price As Long: price = 50000
Dim months As Integer: months = 24
Dim monthlyPrice As Long
monthlyPrice = Application.WorksheetFunction.Round(price / months, 0)
浮動小数点数型を使わずに書けます。
Application.WorksheetFunction.Round
はExcelの数式のROUND
と同じ処理です。
Math.Round
を使ったり、関数を使わずに整数型にそのまま代入したりしても、一見、四捨五入できたように見えることもあります。
しかし、Math.Round
や整数型へのキャストは「銀行型丸め」と呼ばれる端数処理をするので、想定と異なる結果になってしまいます。
銀行型丸めとは、丸める対象の桁が5の場合、その一つ上の桁が偶数になるように切り捨てor切り上げを行う処理です。
Dim i As Integer: i = 1.5 ' = 2
Dim j As Integer: j = 2.5 ' = 2
Dim k As Double: k = Math.Round(1.15, 1) ' = 1.2
Dim l As Double: l = Math.Round(1.25, 1) ' = 1.2
このような結果になります。
Roundというそのものズバリな関数で上記のような動きをするのは、なかなかのハマりポイントだと思います。
Java
long price = 50000L;
int months = 24;
long monthlyPrice = Math.round((double) price / months);
どちらか一方でもdoubleなら商もdoubleになるので、それをMath.round
に渡してあげます。
doubleにキャストしなくても動きますが、price / months
が整数同士の除算であるため小数点以下が切り捨てられ、正しい結果が得られません。
JavadocにMath.roundの引数がdoubleであることが明記されているので、ちゃんと気にして書けばなんてことはないのですが、
long monthlyPrice = Math.round(price / months);
と書いてもコンパイル時も実行時もエラーにならないので、うっかりハマるかもしれません。
Ruby
price = 50000
months = 24
monthlyPrice = (price.to_f / months).round
Javaと同じようにどちらか一方でもFloatにキャストすればOKです。
こちらも同じくキャストしない場合にエラーにはならず、小数点以下が常に切り捨てられるので、テスト時に期待結果が得られるか注意が必要です。
他にも、
monthlyPrice = (price.quo(months)).round
monthlyPrice = (price.fdiv(months)).round
と、Numericクラスのメソッドを使って書くこともできます。
SQL
今回題材にした業務ではSQLで四捨五入を書く機会はなかったのですが、後学のために調べてみました。
Oracle, MySQLを対象にしましたが、両方同じ動きだったのでまとめて記載します。(サンプルコードはMySQL)
Select文
mysql> select 1/2, round(1/2);
+--------+------------+
| 1/2 | round(1/2) |
+--------+------------+
| 0.5000 | 1 |
+--------+------------+
round
を使えば四捨五入、使わなければそのまま抽出、というシンプルな結果です。
Insert文
create table round_test (price int, roundPrice int, floatPrice float);
というテーブルにInsertすると、
mysql> insert into round_test values (1/2, round(1/2), 1/2), (49/100, round(49/100), 49/100);
+-------+------------+------------+
| price | roundPrice | floatPrice |
+-------+------------+------------+
| 1 | 1 | 0.5 |
| 0 | 0 | 0.49 |
+-------+------------+------------+
という結果になります。
round
を使わずに整数型のカラムにInsertすると、四捨五入された値が登録されます。
まとめ
今回の事態は、やっつけ気味のシステム増改築が一番の原因だと思うので「ちゃんと設計しようよ」で片付く話ではありますが、、
受託開発や、自社サービスでも担当サービスを移った場合などは、異なる言語で同じような処理を実装することはよくあることだと思います。
ニッチな内容だったとは思いますが、どこかの誰かの助けになれば幸いです。