Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rubocop_todo.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@
## Unreleased

- Fix name for ANG currency
- Change disambiguate symbol for ARS from historical to international format
- Change disambiguate symbol for ARS from historical to international format
- Replace hardcoded methods for specific currencies (CAD, EUR, GBP, USD) with generic `currency_helpers=` config option
```rb
Money.currency_helpers = {
mxn: 'MXN',
}

Money.mxn(200) #=> #<Money fractional:200 currency:MXN>

Money.add_rate("USD", "MXN", 150)
Money.new(100, "USD").as_mxn #=> #<Money fractional:15000 currency:MXN>
```

## 7.0.2

Expand Down
44 changes: 38 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,23 @@ Money.locale_backend = :i18n

# 10.00 USD
money = Money.from_cents(1000, "USD")
money.cents #=> 1000
money.currency #=> Currency.new("USD")
money.cents #=> 1000
money.currency #=> Currency.new("USD")

# Constructors
Money.new(1000, "USD") #=> #<Money fractional:1000 currency:USD>
Money.from_cents(1000, "USD") #=> #<Money fractional:1000 currency:USD>
Money.from_amount(10, "USD") #=> #<Money fractional:1000 currency:USD>
Money.usd(1000) #=> #<Money fractional:1000 currency:USD>
Money.us_dollar(500) #=> #<Money fractional:500 currency:USD>

# Comparisons
Money.from_cents(1000, "USD") == Money.from_cents(1000, "USD") #=> true
Money.from_cents(1000, "USD") == Money.from_cents(100, "USD") #=> false
Money.from_cents(1000, "USD") == Money.from_cents(1000, "EUR") #=> false
Money.from_cents(1000, "USD") != Money.from_cents(1000, "EUR") #=> true
Money.from_cents(0, "USD") == Money.from_cents(0, "EUR") #=> true
Money.from_cents(0, "USD") == 0 #=> true

# Arithmetic
Money.from_cents(1000, "USD") + Money.from_cents(500, "USD") == Money.from_cents(1500, "USD")
Expand All @@ -85,7 +94,9 @@ Money.from_amount(5, "TND") == Money.from_cents(5000, "TND") # 5 TND

# Currency conversions
some_code_to_setup_exchange_rates
Money.from_cents(1000, "USD").exchange_to("EUR") == Money.from_cents(some_value, "EUR")
Money.from_cents(1000, "EUR").exchange_to("USD") == Money.from_cents(some_value, "USD")
Money.from_cents(1000, "EUR").as_usd == Money.from_cents(some_value, "USD")
Money.from_cents(1000, "EUR").as_us_dollar == Money.from_cents(some_value, "USD")

# Swap currency
Money.from_cents(1000, "USD").with_currency("EUR") == Money.from_cents(1000, "EUR")
Expand All @@ -96,6 +107,27 @@ Money.from_cents(100, "GBP").format #=> "£1.00"
Money.from_cents(100, "EUR").format #=> "€1.00"
```

### Helpers

You can define shorthand methods for creating `Money` objects and exchanging between frequently used currencies:

```ruby
Money.currency_helpers = {
usd: 'USD',
us_dollar: 'USD',
mxn: 'MXN',
}

# Constructors
Money.mxn(200) #=> #<Money fractional:200 currency:MXN>

# Currency conversions
Money.add_rate("USD", "MXN", 150)
Money.new(100, "USD").as_mxn #=> #<Money fractional:15000 currency:MXN>
```

Some common helpers are defined by default: `usd`, `us_dollar`, `cad`, `ca_dollar`, `eur`, `euro`, `gbp`, `pound_sterling`. These defaults will be removed in a future version.

## Currency

Currencies are consistently represented as instances of `Money::Currency`.
Expand Down Expand Up @@ -231,8 +263,8 @@ an example of how it works:
Money.add_rate("USD", "CAD", 1.24515)
Money.add_rate("CAD", "USD", 0.803115)

Money.us_dollar(100).exchange_to("CAD") # => Money.from_cents(124, "CAD")
Money.ca_dollar(100).exchange_to("USD") # => Money.from_cents(80, "USD")
Money.new(100, "USD").exchange_to("CAD") # => Money.from_cents(124, "CAD")
Money.new(100, "CAD").exchange_to("USD") # => Money.from_cents(80, "USD")
```

Comparison and arithmetic operations work as expected:
Expand Down Expand Up @@ -601,7 +633,7 @@ Money.from_cents(10_000_00, 'EUR').format # => €10000.00
In case you're working with collections of `Money` instances, have a look at [money-collection](https://github.com/RubyMoney/money-collection)
for improved performance and accuracy.

### Troubleshooting
## Troubleshooting

If you don't have some locale and don't want to get a runtime error such as:

Expand Down
113 changes: 35 additions & 78 deletions lib/money/money.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ class << self
:default_infinite_precision,
:conversion_precision,
:strict_eql_compare
attr_reader :locale_backend
attr_reader :locale_backend,
:currency_helpers
attr_writer :default_bank,
:default_currency

Expand Down Expand Up @@ -219,6 +220,21 @@ def self.locale_backend=(value)
@locale_backend = value ? LocaleBackend.find(value) : nil
end

def self.currency_helpers=(hash)
hash.each do |name, currency|
next if singleton_class.method_defined?(name)

singleton_class.define_method(name) do |cents|
new(cents, currency)
end

define_method("as_#{name}") do
exchange_to(currency)
end
end
@currency_helpers = hash
end

def self.setup_defaults
# Set the default bank for creating new +Money+ objects.
self.default_bank = Bank::VariableExchange.instance
Expand All @@ -235,9 +251,21 @@ def self.setup_defaults
# Default the conversion of Rationals precision to 16
self.conversion_precision = 16

# Defaults to the deprecated behavior where
# Default to the deprecated behavior where
# `Money.new(0, "USD").eql?(Money.new(0, "EUR"))` is true.
self.strict_eql_compare = false

# Default to currencies previously hardcoded as methods
self.currency_helpers = {
cad: "CAD",
ca_dollar: "CAD",
eur: "EUR",
euro: "EUR",
gbp: "GBP",
pound_sterling: "GBP",
usd: "USD",
us_dollar: "USD",
}
end

def self.inherited(base)
Expand Down Expand Up @@ -298,41 +326,6 @@ def self.disallow_currency_conversion!
self.default_bank = Bank::SingleCurrency.instance
end

# Creates a new Money object of value given in the +unit+ of the given
# +currency+.
#
# @param [Numeric] amount The numerical value of the money.
# @param [Currency, String, Symbol] currency The currency format.
# @param [Hash] options Optional settings for the new Money instance
# @option [Money::Bank::*] :bank The exchange bank to use.
#
# @example
# Money.from_amount(23.45, "USD") # => #<Money fractional:2345 currency:USD>
# Money.from_amount(23.45, "JPY") # => #<Money fractional:23 currency:JPY>
#
# @return [Money]
#
# @see #initialize
def self.from_amount(amount, currency = default_currency, options = {})
raise ArgumentError, "'amount' must be numeric" unless amount.is_a?(Numeric)

currency = Currency.wrap(currency) || Money.default_currency
raise Currency::NoCurrency, "must provide a currency" if currency.nil?

value = amount.to_d * currency.subunit_to_unit
new(value, currency, options)
end

# DEPRECATED.
#
# @see Money.from_amount
def self.from_dollars(amount, currency = default_currency, options = {})
warn "[DEPRECATION] `Money.from_dollars` is deprecated in favor of " \
"`Money.from_amount`."

from_amount(amount, currency, options)
end

# Creates a new Money object of value given in the
# +fractional unit+ of the given +currency+.
#
Expand Down Expand Up @@ -427,7 +420,7 @@ def inspect
# @return [String]
#
# @example
# Money.ca_dollar(100).to_s #=> "1.00"
# Money.new(100, "CAD").to_s #=> "1.00"
def to_s
format thousands_separator: "",
no_cents_if_whole: currency.decimal_places == 0,
Expand All @@ -440,7 +433,7 @@ def to_s
# @return [BigDecimal]
#
# @example
# Money.us_dollar(1_00).to_d #=> BigDecimal("1.00")
# Money.new(1_00, "USD").to_d #=> BigDecimal("1.00")
def to_d
as_d(fractional) / as_d(currency.subunit_to_unit)
end
Expand All @@ -450,20 +443,20 @@ def to_d
# @return [Integer]
#
# @example
# Money.us_dollar(1_00).to_i #=> 1
# Money.new(1_00, "USD").to_i #=> 1
def to_i
to_d.to_i
end

# Return the amount of money as a float. Floating points cannot guarantee
# Returns 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
# floats.
#
# @return [Float]
#
# @example
# Money.us_dollar(100).to_f #=> 1.0
# Money.new(100, "USD").to_f #=> 1.0
def to_f
to_d.to_f
end
Expand Down Expand Up @@ -520,42 +513,6 @@ def exchange_to(other_currency, &)
end
end

# Receive a money object with the same amount as the current Money object
# in United States dollar.
#
# @return [Money]
#
# @example
# n = Money.new(100, "CAD").as_us_dollar
# n.currency #=> #<Money::Currency id: usd>
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 #=> #<Money::Currency id: cad>
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 #=> #<Money::Currency id: eur>
def as_euro
exchange_to("EUR")
end

# Splits a given amount in parts without losing pennies. The left-over pennies will be
# distributed round-robin amongst the parties. This means that parts listed first will likely
# receive more pennies than ones listed later.
Expand Down
77 changes: 23 additions & 54 deletions lib/money/money/constructors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

class Money
module Constructors
# Create a new money object with value 0.
# Creates a new Money object with value 0.
#
# @param [Currency, String, Symbol] currency The currency to use.
#
Expand All @@ -16,70 +16,39 @@ def empty(currency = default_currency)

alias zero empty

# Creates a new Money object of the given value, using the Canadian
# dollar currency.
# Creates a new Money object of value given in the +unit+ of the given
# +currency+.
#
# @param [Integer] cents The cents value.
#
# @return [Money]
# @param [Numeric] amount The numerical value of the money.
# @param [Currency, String, Symbol] currency The currency format.
# @param [Hash] options Optional settings for the new Money instance
# @option [Money::Bank::*] :bank The exchange bank to use.
#
# @example
# n = Money.ca_dollar(100)
# n.cents #=> 100
# n.currency #=> #<Money::Currency id: cad>
def ca_dollar(cents)
new(cents, "CAD")
end

alias cad ca_dollar

# Creates a new Money object of the given value, using the American dollar
# currency.
#
# @param [Integer] cents The cents value.
# Money.from_amount(23.45, "USD") # => #<Money fractional:2345 currency:USD>
# Money.from_amount(23.45, "JPY") # => #<Money fractional:23 currency:JPY>
#
# @return [Money]
#
# @example
# n = Money.us_dollar(100)
# n.cents #=> 100
# n.currency #=> #<Money::Currency id: usd>
def us_dollar(cents)
new(cents, "USD")
end
# @see #initialize
def from_amount(amount, currency = default_currency, options = {})
raise ArgumentError, "'amount' must be numeric" unless amount.is_a?(Numeric)

alias usd us_dollar
currency = Currency.wrap(currency) || Money.default_currency
raise Currency::NoCurrency, "must provide a currency" if currency.nil?

# Creates a new Money object of the given value, using the Euro currency.
#
# @param [Integer] cents The cents value.
#
# @return [Money]
#
# @example
# n = Money.euro(100)
# n.cents #=> 100
# n.currency #=> #<Money::Currency id: eur>
def euro(cents)
new(cents, "EUR")
value = amount.to_d * currency.subunit_to_unit
new(value, currency, options)
end

alias eur euro

# Creates a new Money object of the given value, in British pounds.
#
# @param [Integer] pence The pence value.
# DEPRECATED.
#
# @return [Money]
#
# @example
# n = Money.pound_sterling(100)
# n.fractional #=> 100
# n.currency #=> #<Money::Currency id: gbp>
def pound_sterling(pence)
new(pence, "GBP")
end
# @see Money.from_amount
def from_dollars(amount, currency = default_currency, options = {})
warn "[DEPRECATION] `Money.from_dollars` is deprecated in favor of " \
"`Money.from_amount`."

alias gbp pound_sterling
from_amount(amount, currency, options)
end
end
end
Loading