# encoding: utf-8 require 'money/bank/variable_exchange' require 'money/money/arithmetic' require 'money/money/parsing' require 'money/money/formatting' # Represents an amount of money in a given currency. class Money include Comparable include Arithmetic include Formatting include Parsing # The BigDecimal amount with as many decimal-places as the currency # # @return [BigDecimal] attr_reader :amount # The currency the money is in. # # @return [Currency] attr_reader :currency # The +Money::Bank+ based object used to perform currency exchanges with. # # @return [Money::Bank::*] attr_reader :bank # Class Methods class << self # Each Money object is associated to a bank object, which is responsible # for currency exchange. This property allows you to specify the default # bank object. The default value for this property is an instance of # +Bank::VariableExchange.+ It allows one to specify custom exchange rates. # # @return [Money::Bank::*] attr_accessor :default_bank # The default currency, which is used when +Money.new+ is called without an # explicit currency argument. The default value is Currency.new("USD"). The # value must be a valid +Money::Currency+ instance. # # @return [Money::Currency] attr_accessor :default_currency # Use this to disable i18n even if it's used by other objects in your app. # # @return [true,false] attr_accessor :use_i18n # The default BigDecimal::ROUND_MODE (BigDecimal::ROUND_HALF_UP) # # @return [Integer] attr_accessor :default_rounding_mode # Use this to enable the ability to assume the currency from a passed symbol # # @return [true,false] attr_accessor :assume_from_symbol end # Set the default bank for creating new +Money+ objects. self.default_bank = Bank::VariableExchange.instance # Set the default currency for creating new +Money+ object. self.default_currency = Currency.new("USD") # Set the default rounding-mode self.default_rounding_mode = BigDecimal::ROUND_HALF_UP # Default to using i18n self.use_i18n = true # Default to not using currency symbol assumptions when parsing self.assume_from_symbol = false # Create a new money object with value 0. # # @param [Currency, String, Symbol] currency The currency to use. # # @return [Money] # # @example # Money.zero #=> # def self.zero(currency = default_currency) Money.new(0, currency) end # Alias for Money.zero # # @param [Currency, String, Symbol] currency The currency to use. # # @return [Money] # # @example # Money.empty #=> # def self.empty(currency = default_currency) zero(currency) end # Creates a new Money object of the given value, using the Canadian # dollar currency. # # @param [Numeric] amount The amount. # # @return [Money] # # @example # n = Money.ca_dollar(1.00) # n.amount #=> 1.000 # n.currency #=> # def self.ca_dollar(amount) Money.new(amount, "CAD") end # Creates a new Money object of the given value, using Indian Rupee currency. # # @param [Numeric] amount The amount. # # @return [Money] # # @example # n = Money.inr(100) # n.amount #=> 100 # n.currency #=> # def self.inr(amount) Money.new(amount, "INR") end # Creates a new Money object of the given value, using the American dollar # currency. # # @param [Numeric] amount The amount. # # @return [Money] # # @example # n = Money.us_dollar(100) # n.amount #=> 100 # n.currency #=> # def self.us_dollar(amount) Money.new(amount, "USD") end # Creates a new Money object of the given value, using the Euro currency. # # @param [Numeric] amount The amount. # # @return [Money] # # @example # n = Money.euro(100) # n.amount #=> 100 # n.currency #=> # def self.euro(amount) Money.new(amount, "EUR") end # Adds a new exchange rate to the default bank and return the rate. # # @param [Currency, String, Symbol] from_currency Currency to exchange from. # @param [Currency, String, Symbol] to_currency Currency to exchange to. # @param [Numeric] rate Rate to exchange with. # # @return [Numeric] # # @example # Money.add_rate("USD", "CAD", 1.25) #=> 1.25 def self.add_rate(from_currency, to_currency, rate) Money.default_bank.add_rate(from_currency, to_currency, rate) end # Creates a new Money object of +amount+ value, with given +currency+. # The amount is rounded to number of decimal-places in the currency # # Alternatively you can use the convenience # methods like {Money.ca_dollar} and {Money.us_dollar}. # # @param [Numeric, String] amount The money amount. # @param [Currency, String, Symbol] currency The currency format. # @param [Money::Bank::*] bank The exchange bank to use. # # @return [Money] # # @example # Money.new(100.1235) # #=> # # # Money.new("100.123") # #=> # # def initialize(amount, currency = Money.default_currency, bank = Money.default_bank) ensure_numeric(amount) amount = BigDecimal.new(amount.to_s) unless amount.is_a?(BigDecimal) @currency = Currency.wrap(currency, Currency.default_subunit_to_unit) @amount = amount.round(@currency.decimal_places, Money.default_rounding_mode) @bank = bank end # Return string representation of currency object # # @return [String] # # @example # Money.new(100, :USD).currency_as_string #=> "USD" def currency_as_string currency.to_s end # Set currency object using a string # # @param [String] val The currency string. # # @return [Money::Currency] # # @example # Money.new(100).currency_as_string("CAD") #=> # def currency_as_string=(val) @currency = Currency.wrap(val) end # Returns a Fixnum hash value based on the +cents+ and +currency+ attributes # in order to use functions like & (intersection), group_by, etc. # # @return [Fixnum] # # @example # Money.new(100).hash #=> 908351 def hash [amount.hash, currency.hash].hash end # Uses +Currency#symbol+. If +nil+ is returned, defaults to "¤". # # @return [String] # # @example # Money.new(100, "USD").symbol #=> "$" def symbol currency.symbol || "¤" end # Common inspect function # # @return [String] def inspect "#" end # Returns the amount of money as a string. # # @return [String] # # @example # Money.ca_dollar(100).to_s #=> "100.00" def to_s s = sprintf("%0.#{@currency.decimal_places}f", amount) s.gsub(".", decimal_mark) end # Return the amount. # # @return [BigDecimal] # # @example # Money.us_dollar(100).to_d => BigDecimal.new("100") def to_d amount end # Return the amount of money as a float. Floating points cannot guarantee # precision. Therefore, this function should only be used when you no longer # need to represent currency or working with another system that requires # decimals. # # @return [Float] # # @example # Money.us_dollar(100).to_f => 1.0 def to_f @amount.to_f end # Conversation to +self+. # # @return [self] def to_money(given_currency = nil) given_currency = Currency.wrap(given_currency) if given_currency if given_currency.nil? || self.currency == given_currency self else exchange_to(given_currency) end end # Receive the amount of this money object in another Currency. # # @param [Currency, String, Symbol] other_currency Currency to exchange to. # # @return [Money] # # @example # Money.new(2000, "USD").exchange_to("EUR") # Money.new(2000, "USD").exchange_to(Currency.new("EUR")) def exchange_to(other_currency) other_currency = Currency.wrap(other_currency) @bank.exchange_with(self, other_currency) end # Receive a money object with the same amount as the current Money object # in american dollars. # # @return [Money] # # @example # n = Money.new(100, "CAD").as_us_dollar # n.currency #=> # def as_us_dollar exchange_to("USD") end # Receive a money object with the same amount as the current Money object # in canadian dollar. # # @return [Money] # # @example # n = Money.new(100, "USD").as_ca_dollar # n.currency #=> # def as_ca_dollar exchange_to("CAD") end # Receive a money object with the same amount as the current Money object # in euro. # # @return [Money] # # @example # n = Money.new(100, "USD").as_euro # n.currency #=> # def as_euro exchange_to("EUR") end # Allocates money between different parties without loosing pennies. # After the mathematically split has been performed, left over pennies will # be distributed round-robin amongst the parties. This means that parties # listed first will likely receive more pennies then ones that are listed later # # @param [0.50, 0.25, 0.25] to give 50% of the cash to party1, 25% ot party2, and 25% to party3. # # @return [Array] # # @example # Money.new(0.005, "USD").allocate([0.3,0.7)) #=> [Money.new(2), Money.new(3)] # Money.new(1.00, "USD").allocate([0.33,0.33,0.33]) #=> [Money.new(0.334), Money.new(0.333), Money.new(0.333)] def allocate(splits) allocations = splits.inject(0.0) {|sum, i| sum += i } raise ArgumentError, "splits add to more then 100%" if (allocations - 1.0) > Float::EPSILON amount_in_subunits = self.to_subunits left_over = amount_in_subunits amounts = splits.collect do |ratio| fraction = (amount_in_subunits * ratio / allocations).floor left_over -= fraction fraction end left_over.times { |i| amounts[i % amounts.length] += 1 } amounts.collect { |amount| Money.from_subunits(amount, currency) } end # Split money amongst parties evenly without loosing pennies. # # @param [2] number of parties. # # @return [Array] # # @example # Money.new(100, "USD").split(3) #=> [Money.new(34), Money.new(33), Money.new(33)] def split(num) raise ArgumentError, "need at least one party" if num < 1 amount_in_subunits = to_subunits low_subunits = amount_in_subunits / num low = Money.from_subunits(low_subunits, currency) high = Money.from_subunits(low_subunits + 1, currency) remainder = amount_in_subunits % num (0...num).collect do |index| index < remainder ? high : low end end def ensure_numeric(amount) raise ArgumentError, "Invalid amount: #{amount}" unless numeric?(amount) end def numeric?(amount) Kernel.Float(amount) rescue false end def to_subunits (amount * currency.subunit_to_unit).to_i end def self.from_subunits(amount, currency = Money.default_currency) adjusted_amount = BigDecimal.new(amount.to_s) / currency.subunit_to_unit Money.new(adjusted_amount, currency) end end