Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyでの金額計算にFloatを使ってはいけない理由とBigDecimal・Money gemによる正確な通貨処理

お金は、どの通貨であっても一見すると浮動小数点数のように見えます。しかし、金額の計算にfloatを使うのは重大な間違いです。

floatオブジェクトは、定義上、ネイティブアーキテクチャ特有の倍精度浮動小数点表現を利用する「不正確な実数」です。不正確な数値は、会計担当者にとって大きなストレスとなります。

この記事では、RubyおよびRailsで金額データを扱うためのさまざまな選択肢を、実際のコード例を交えながらわかりやすく解説していきます。

そもそもFloatとは?

前述のとおり、floatオブジェクトは通常の整数とは異なる演算を行います。これこそがfloatが不正確な数値になってしまう主な原因です。特にRubyは(多くの言語と同様に)固定長の2進数桁でfloatを表現しています。つまり、Rubyは内部で小数を10進数から2進数へ、そしてまた10進数へと変換しているのです。

実数(いわゆる小数)を2進数表現に深く踏み込んで考えると、一部の数値は完全には表現できず、システム側で丸めるしかありません。

円周率のように無限に続く数値を思い浮かべれば理解しやすいでしょう。このような数値は、有限の桁数では完全に表現できません。floatは通常32ビットまたは64ビット程度の精度しか持たないため、限界に達した時点で数値は切り捨てられてしまいます。

ここで、古典的な例を見てみましょう。

1200 * (14.0/100)

これは数値のパーセンテージを計算するごくシンプルな方法です。1200の14%なら168になるはずですが、Rubyでこのコードを実行すると次の結果になります。

1200 * (14.0/100)
=> 168.00000000000003

一方、式のパーセンテージを14.1%に変えるだけでも、結果は変わります。

1200 * (14.1/100)
=> 169.2

もちろん、必要な小数点以下の桁数を指定してroundメソッドで最も近い値に丸めることもできます。

(my_calculation).round(2)

しかし、より複雑な計算、特に値同士の比較を行う場合には、正しい結果が保証されません。

この背後にある技術的な仕組みを深く知りたい方は、Oracleの付録文書「What Every Computer Scientist Should Know About Floating-Point Arithmetic」を読むことを強くお勧めします。floatが不正確になる理由を詳細に解説した優れた資料です。

信頼できるBigDecimal

次のコードを見てください。

require "bigdecimal"
BigDecimal("45.99")

このコードは、ECサイトのカート合計金額のような実務的なロジックをそのまま表現できます。操作される実際の値は常に45.99であり、45.9989や45.99000009のような値にはなりません。

これがBigDecimalの持つ正確性です。単純な四則演算であればfloatも同じように動作することはありますが、挙動が予測できないこと自体が最大の危険なのです。

先ほどと同じパーセント計算をBigDecimalで実行すると、結果はこうなります。

require "bigdecimal"
(BigDecimal(1200) * (BigDecimal(14)/BigDecimal(100))).to_s("F")
=> 168.0

これはirbコンソールで素早く試せる簡略版です。

BigDecimalオブジェクトをそのまま出力すると科学技術表記で表示されるため、そのままでは意図した形式になりません。to_sメソッドに引数を渡すことで書式を指定でき、BigDecimalに相当する浮動小数点形式の値を表示できます。詳細についてはRuby公式ドキュメントを参照してください。

小数点以下の桁数に上限を設けたい場合は、truncateメソッドが役立ちます。

(BigDecimal(1200) * (BigDecimal("14.12")/BigDecimal(100))).truncate(2).to_s("F")
=> 169.44

RubyMoneyプロジェクトとは

RubyMoneyは、こうした問題への取り組みから生まれたプロジェクトです。Rubyエコシステムにおいて金額データを扱いやすい優れたライブラリを提供し、開発者の負担を軽減することを目指すオープンソースコミュニティです。

プロジェクトは7つのライブラリで構成されており、中でも重要度が高いのは次の3つです。

  • Money: 金額の処理と通貨換算を扱うRubyライブラリです。Webアプリケーションに限らず、堅牢でモダンなアプリケーション内で金額を扱うためのオブジェクト指向的な選択肢を多数提供します。
  • Money-rails: RubyMoneyのRuby on Rails向け統合ライブラリです。moneyライブラリの強力な機能とRailsの柔軟性を組み合わせられます。
  • Monetize: さまざまなオブジェクトをmoneyオブジェクトへ変換するためのライブラリです。文字列のパースを大量に行うアプリケーションの補助的な存在として機能します。

さらに、興味深いライブラリが4つあります。

  • EU_central_bank: 欧州中央銀行(ECB)が公開しているレートを利用して為替レートの計算を支援するライブラリです。
  • Google_currency: Googleの通貨レートを参考に通貨換算を行うユニークなライブラリです。
  • Money-collection: moneyオブジェクトの合計・最小値・最大値を正確に計算するための補助ライブラリです。
  • Money-heuristics: money gem向けに、文字列入力のヒューリスティック分析を行うモジュールです。

「money」gemの基本

まずは最も有名なmoney gemから始めましょう。主な特徴は以下のとおりです。

  • 金額・通貨・小数点記号などの関連情報を保持するmoneyクラス。
  • 開発者が使用する通貨単位に関する情報をラップするMoney::Currencyクラス。
  • 前述の誤差を避けるため、デフォルトでは浮動小数点数ではなく整数で金額を扱います。
  • 通貨間の両替機能を標準で備えており、これが非常に強力です。

さらに、プロジェクト内の他のモデルと同じように、一貫性のあるオブジェクト指向構造によって金額データを柔軟に操作できる点も大きな魅力です。

使い方は非常にシンプルで、まずgemをインストールします。

gem install money

固定額の金額を扱う簡単な例は次のとおりです。

my_money = Money.new(1200, "USD")
my_money.cents #=> 1200
my_money.currency #=> Currency.new("USD")

ご覧のとおり、金額はセント単位で表現されます。1200セントは12ドルに相当します。

BigDecimalと同様に、これらのオブジェクトに対しても基本的な計算が可能です。例えば、

cart_amount = Money.new(10000, "USD") #=> 100 USD
discount = Money.new(1000, "USD") #=> 10 USD

cart_amount - discount == Money.new(9000, "USD") #=> 90 USD

面白いと思いませんか? これが前述したオブジェクトの本質です。コーディングしているとき、まるで無機質なただの数字ではなく、本当の金額を操作しているかのように感じられます。

通貨換算

独自の為替レート体系を持っている場合は、exchange bankオブジェクトを介して通貨換算を実行できます。次の例を見てください。

Money.add_rate("USD", "BRL", 5.23995)
Money.add_rate("BRL", "USD", 0.19111)

通貨間で値を換算したいときは、次のコードを実行します。

Money.us_dollar(100).exchange_to("BRL") #=> Money.new(523, "BRL")

四則演算や比較評価についても同様に扱えます。

iso_code(通貨の国際的な3桁コードを返す)やdecimal_mark(金額の整数部と小数部の間の文字)など、提供されている通貨属性については、ぜひ公式ドキュメントを確認してください。

そういえば忘れるところでした。money gemをインストールすると、自動的に変換を行ってくれるto_moneyというBigDecimalメソッドも使えるようになります。

「monetize」gem

RubyMoneyプロジェクトにおける各ライブラリの役割を理解しておくことが重要です。別のRubyオブジェクト(例えばString)をMoneyに変換したい場合に必要なのが、monetizeです。

まず、gemの依存関係をインストールするか、Gemfileに追加してください。

gem install monetize

もちろん、moneyもインストールされている必要があります。

異なるフォーマットで金額データを受け取る際には、parseメソッドが非常に便利です。例えば、

Money.parse("£100") == Money.new(100, "GBP") #=> true

このパースメソッドの利用場面は限られていますが、HTTPリクエストで通貨コード付きのフォーマット済み値を受け取るケースでは非常に有用です。Web上のすべてはテキストなので、文字列からmoneyへの変換は実務で重宝します。

ただし、システムが値をどう操作しているか、改ざんされる可能性はないかには注意が必要です。金融系のシステムには常に複数のセキュリティレイヤーが設けられ、受け取った値がその取引の真正な値であることを保証しなければなりません。

「money-rails」gem

これは同じ金額操作をRailsアプリケーション内で実現するためのライブラリです。

なぜRailsで動作させるためだけに別のライブラリが必要なのでしょうか? 実は、Railsプロジェクト内でも通常の四則演算であればmoney gem単体で十分使えます。しかし、Railsの構造(モデルなど)がmoneyの機能と連携する必要がある場合には、正常に動作しなくなります。

次の例を考えてみましょう。

class Cart < ActiveRecord::Base
  monetize :amount_cents
end

これは実際に動作するRailsモデルオブジェクトです。データベース(別の属性名を使いたい場合のエイリアス設定も含む)、Mongoid、REST Webサービスなどと組み合わせて利用できます。

これまで見てきたすべての機能がこのgemにも適用されます。通常は追加の設定だけで動作し、その設定はconfig/initializers/money.rbファイルに記述します。

MoneyRails.configure do |config|

  # デフォルトの通貨を設定
  config.default_currency = :usd

end

これでデフォルト通貨が指定したものに設定されます。ただし開発の過程では、為替換算が必要になったり、モデル全体で複数の通貨を扱ったりするケースが出てくるでしょう。

そのような場合、money-railsではモデルレベルで通貨を定義できます。

class Cart < ActiveRecord::Base

  # モデルレベルの通貨としてEURを使用
  register_currency :eur

  monetize :amount_cents, as: "amount"
  monetize :discount_cents, with_currency: :eur

  monetize :converted_value, with_currency: :usd

end

一度設定してしまえば、プロジェクト内でmoney型を活用するのは本当に簡単だということがわかるでしょう。

まとめ

この記事では、RubyとRailsのエコシステムで金額を扱うための選択肢を紹介しました。重要なポイントをまとめます。

  • 浮動小数点数、特に金額データを表す値の計算を行う場合は、BigDecimalMoneyのインスタンスを使いましょう。
  • 開発の一貫性を保つため、金額処理の仕組みは1つに絞り込むことをおすすめします。
  • moneyライブラリはRubyMoneyシステム全体の中核であり、非常に堅牢かつシンプルです。money-railsはRailsアプリケーション向けの同等ライブラリで、任意の値をMoneyオブジェクトへパースする必要があるときにはmonetizeが必須です。
  • Floatの使用は避けましょう。今は計算が必要なくても、将来、注意不足の開発者がfloatで計算してしまう可能性があります。そのときあなたが止めてあげられるとは限りません。

最後に、公式ドキュメントは必ず目を通すようにしてください。BigDecimalには使用例と丁寧な解説が豊富に用意されていますし、RubyMoney関連のgem群も同様です。

  1. Rubyで学ぶ挿入ソート:仕組みから計算量まで徹底解説

    ※本記事は、Rubyでさまざまなソートアルゴリズムを実装するシリーズの第4回です。第1回ではバブルソート、第2回では選択ソート、第3回ではマージソートを取り上げました。データのソート手法をさまざまな角度から探っていくシリーズもいよいよ折り返し地点。今回は挿入ソート(Insertion Sort)に焦点を当てます。挿入ソートには魅力的な特徴がたくさんあります。まず、挿入ソートは安定(stable)なアルゴリズムです。つまり、同じキーを持つ要素同士の相対的な順序が入れ替わることがありません。また、インプレース(in-place)アルゴリズムでもあるため、ソート結果を保存するための新しい配列を作成す

  2. Ruby 2.6の新機能9選|コード例でわかる注目ポイントを徹底解説

    Ruby 2.6には、開発者の生産性を高める新しい機能やパフォーマンス改善が多数盛り込まれています。 本記事では、Ruby 2.6で導入された9つの注目新機能を、実際のコード例とともにわかりやすく紹介します。最新のRuby動向をキャッチアップしたい方は、ぜひ最後までご覧ください。 1. 無限Range(Endless Range) Ruby 2.5以前でもFloat::INFINITYを使えば終端のない範囲を表現できましたが、Ruby 2.6ではさらに直感的な記法が使えるようになりました。 新しい無限Rangeは次のように書きます。 (1..) 通常のRangeが(1..10)のように終端