From 8ad76df0e6359874257b2f09a8bfb6f48736da19 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Sat, 17 Jan 2026 16:00:42 +0100 Subject: [PATCH 1/2] Do not hardcode helper methods for specific currencies --- .rubocop_todo.yml | 2 +- README.md | 44 +++++++++-- lib/money/money.rb | 113 +++++++++------------------ lib/money/money/constructors.rb | 77 ++++++------------ lib/money/money/formatter.rb | 28 +++---- sig/lib/money/money.rbs | 64 ++------------- sig/lib/money/money/constructors.rbs | 66 ++++------------ sig/lib/money/money/formatter.rbs | 28 +++---- spec/money/constructors_spec.rb | 21 +++++ 9 files changed, 167 insertions(+), 276 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index bd6fee5e59..44522841a4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -12,7 +12,7 @@ Metrics/AbcSize: # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 231 + Max: 233 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: diff --git a/README.md b/README.md index d78be74761..61b011fa9a 100644 --- a/README.md +++ b/README.md @@ -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.from_cents(1000, "USD") #=> # +Money.from_amount(10, "USD") #=> # +Money.usd(1000) #=> # +Money.us_dollar(500) #=> # # 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") @@ -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") @@ -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) #=> # + +# Currency conversions +Money.add_rate("USD", "MXN", 150) +Money.new(100, "USD").as_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`. @@ -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: @@ -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: diff --git a/lib/money/money.rb b/lib/money/money.rb index 61a3cf8c9e..f3cba7456b 100644 --- a/lib/money/money.rb +++ b/lib/money/money.rb @@ -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 @@ -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 @@ -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) @@ -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.from_amount(23.45, "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+. # @@ -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, @@ -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 @@ -450,12 +443,12 @@ 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. @@ -463,7 +456,7 @@ def to_i # @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 @@ -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 #=> # - 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 - # 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. diff --git a/lib/money/money/constructors.rb b/lib/money/money/constructors.rb index f237cb0d22..267a6fe3e7 100644 --- a/lib/money/money/constructors.rb +++ b/lib/money/money/constructors.rb @@ -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. # @@ -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 #=> # - 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.from_amount(23.45, "JPY") # => # # # @return [Money] # - # @example - # n = Money.us_dollar(100) - # n.cents #=> 100 - # n.currency #=> # - 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 #=> # - 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 #=> # - 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 diff --git a/lib/money/money/formatter.rb b/lib/money/money/formatter.rb index c21ac93f12..41914a1281 100644 --- a/lib/money/money/formatter.rb +++ b/lib/money/money/formatter.rb @@ -19,38 +19,38 @@ class Formatter # amount of money should be formatted of "free" or as the supplied string. # # @example - # Money.us_dollar(0).format(display_free: true) #=> "free" - # Money.us_dollar(0).format(display_free: "gratis") #=> "gratis" - # Money.us_dollar(0).format #=> "$0.00" + # Money.new(0, "USD").format(display_free: true) #=> "free" + # Money.new(0, "USD").format(display_free: "gratis") #=> "gratis" + # Money.new(0, "USD").format #=> "$0.00" # # @option rules [Boolean] :with_currency (false) Whether the currency name # should be appended to the result string. # # @example - # Money.ca_dollar(100).format #=> "$1.00" - # Money.ca_dollar(100).format(with_currency: true) #=> "$1.00 CAD" - # Money.us_dollar(85).format(with_currency: true) #=> "$0.85 USD" + # Money.new(100, "CAD").format #=> "$1.00" + # Money.new(100, "CAD").format(with_currency: true) #=> "$1.00 CAD" + # Money.new(85, "USD").format(with_currency: true) #=> "$0.85 USD" # # @option rules [Boolean] :rounded_infinite_precision (false) Whether the # amount of money should be rounded when using {default_infinite_precision} # # @example - # Money.us_dollar(100.1).format #=> "$1.001" - # Money.us_dollar(100.1).format(rounded_infinite_precision: true) #=> "$1" - # Money.us_dollar(100.9).format(rounded_infinite_precision: true) #=> "$1.01" + # Money.new(100.1, "USD").format #=> "$1.001" + # Money.new(100.1, "USD").format(rounded_infinite_precision: true) #=> "$1" + # Money.new(100.9, "USD").format(rounded_infinite_precision: true) #=> "$1.01" # # @option rules [Boolean] :no_cents (false) Whether cents should be omitted. # # @example - # Money.ca_dollar(100).format(no_cents: true) #=> "$1" - # Money.ca_dollar(599).format(no_cents: true) #=> "$5" + # Money.new(100, "CAD").format(no_cents: true) #=> "$1" + # Money.new(599, "CAD").format(no_cents: true) #=> "$5" # # @option rules [Boolean] :no_cents_if_whole (false) Whether cents should be # omitted if the cent value is zero # # @example - # Money.ca_dollar(10000).format(no_cents_if_whole: true) #=> "$100" - # Money.ca_dollar(10034).format(no_cents_if_whole: true) #=> "$100.34" + # Money.new(10000, "CAD").format(no_cents_if_whole: true) #=> "$100" + # Money.new(10034, "CAD").format(no_cents_if_whole: true) #=> "$100.34" # # @option rules [Boolean, String, nil] :symbol (true) Whether a money symbol # should be prepended to the result string. The default is true. This method @@ -116,7 +116,7 @@ class Formatter # @option rules [Boolean] :html_wrap (false) Whether all currency parts should be HTML-formatted. # # @example - # Money.ca_dollar(570).format(html_wrap: true, with_currency: true) + # Money.new(570, "CAD").format(html_wrap: true, with_currency: true) # #=> "$" \ # "5" \ # "." \ diff --git a/sig/lib/money/money.rbs b/sig/lib/money/money.rbs index 0883981b10..65e7c85a0e 100644 --- a/sig/lib/money/money.rbs +++ b/sig/lib/money/money.rbs @@ -14,6 +14,8 @@ class Money extend Constructors + alias self.from_cents self.new + # Raised when smallest denomination of a currency is not defined class UndefinedSmallestDenomination < StandardError end @@ -272,30 +274,6 @@ class Money # currency exchange. Useful when apps operate in a single currency at a time. def self.disallow_currency_conversion!: () -> untyped - # 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.from_amount(23.45, "JPY") # => # - # - # @return [Money] - # - # @see #initialize - def self.from_amount: (Numeric amount, ?(Money::Currency | string | Symbol) currency, ?::Hash[untyped, untyped] options) -> Money - - # DEPRECATED. - # - # @see Money.from_amount - def self.from_amount: (Numeric amount, ?(Money::Currency | string | Symbol) currency, ?::Hash[untyped, untyped] options) -> Money - - alias self.from_cents self.new - # Creates a new Money object of value given in the # +fractional unit+ of the given +currency+. # @@ -362,7 +340,7 @@ class Money # @return [String] # # @example - # Money.ca_dollar(100).to_s #=> "1.00" + # Money.new(100, "CAD").to_s #=> "1.00" def to_s: () -> string # Return the amount of money as a BigDecimal. @@ -370,7 +348,7 @@ class Money # @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: () -> BigDecimal # Return the amount of money as a Integer. @@ -378,7 +356,7 @@ class Money # @return [Integer] # # @example - # Money.us_dollar(1_00).to_i #=> 1 + # Money.new(1_00, "USD").to_i #=> 1 def to_i: () -> int # Return the amount of money as a float. Floating points cannot guarantee @@ -389,7 +367,7 @@ class Money # @return [Float] # # @example - # Money.us_dollar(100).to_f #=> 1.0 + # Money.new(100, "USD").to_f #=> 1.0 def to_f: () -> Float # Returns a new Money instance in a given currency leaving the amount intact @@ -423,36 +401,6 @@ class Money # Money.new(2000, "USD").exchange_to(Currency.new("EUR")) def exchange_to: ((Money::Currency | string | Symbol) other_currency) { () -> int } -> Money - # 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 #=> # - def as_us_dollar: () -> Money - - # 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: () -> Money - - # 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: () -> Money - # 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. diff --git a/sig/lib/money/money/constructors.rbs b/sig/lib/money/money/constructors.rbs index 2641b248af..0feb3ff1f2 100644 --- a/sig/lib/money/money/constructors.rbs +++ b/sig/lib/money/money/constructors.rbs @@ -1,6 +1,6 @@ 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. # @@ -12,62 +12,26 @@ class Money 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 #=> # - def ca_dollar: (int cents) -> Money - - 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.from_amount(23.45, "JPY") # => # # # @return [Money] # - # @example - # n = Money.us_dollar(100) - # n.cents #=> 100 - # n.currency #=> # - def us_dollar: (int cents) -> Money - - alias usd us_dollar + # @see #initialize + def from_amount: (Numeric amount, ?(Money::Currency | string | Symbol) currency, ?::Hash[untyped, untyped] options) -> Money - # Creates a new Money object of the given value, using the Euro currency. - # - # @param [Integer] cents The cents value. - # - # @return [Money] + # DEPRECATED. # - # @example - # n = Money.euro(100) - # n.cents #=> 100 - # n.currency #=> # - def euro: (int cents) -> Money - - alias eur euro - - # Creates a new Money object of the given value, in British pounds. - # - # @param [Integer] pence The pence value. - # - # @return [Money] - # - # @example - # n = Money.pound_sterling(100) - # n.fractional #=> 100 - # n.currency #=> # - def pound_sterling: (int pence) -> Money - - alias gbp pound_sterling + # @see Money.from_amount + def from_dollars: (Numeric amount, ?(Money::Currency | string | Symbol) currency, ?::Hash[untyped, untyped] options) -> Money end -end \ No newline at end of file +end diff --git a/sig/lib/money/money/formatter.rbs b/sig/lib/money/money/formatter.rbs index f0badffaef..4942aa5c88 100644 --- a/sig/lib/money/money/formatter.rbs +++ b/sig/lib/money/money/formatter.rbs @@ -12,38 +12,38 @@ class Money # amount of money should be formatted of "free" or as the supplied string. # # @example - # Money.us_dollar(0).format(display_free: true) #=> "free" - # Money.us_dollar(0).format(display_free: "gratis") #=> "gratis" - # Money.us_dollar(0).format #=> "$0.00" + # Money.new(0, "USD").format(display_free: true) #=> "free" + # Money.new(0, "USD").format(display_free: "gratis") #=> "gratis" + # Money.new(0, "USD").format #=> "$0.00" # # @option rules [Boolean] :with_currency (false) Whether the currency name # should be appended to the result string. # # @example - # Money.ca_dollar(100).format #=> "$1.00" - # Money.ca_dollar(100).format(with_currency: true) #=> "$1.00 CAD" - # Money.us_dollar(85).format(with_currency: true) #=> "$0.85 USD" + # Money.new(100, "CAD").format #=> "$1.00" + # Money.new(100, "CAD").format(with_currency: true) #=> "$1.00 CAD" + # Money.new(85, "USD").format(with_currency: true) #=> "$0.85 USD" # # @option rules [Boolean] :rounded_infinite_precision (false) Whether the # amount of money should be rounded when using {infinite_precision} # # @example - # Money.us_dollar(100.1).format #=> "$1.001" - # Money.us_dollar(100.1).format(rounded_infinite_precision: true) #=> "$1" - # Money.us_dollar(100.9).format(rounded_infinite_precision: true) #=> "$1.01" + # Money.new(100.1, "USD").format #=> "$1.001" + # Money.new(100.1, "USD").format(rounded_infinite_precision: true) #=> "$1" + # Money.new(100.9, "USD").format(rounded_infinite_precision: true) #=> "$1.01" # # @option rules [Boolean] :no_cents (false) Whether cents should be omitted. # # @example - # Money.ca_dollar(100).format(no_cents: true) #=> "$1" - # Money.ca_dollar(599).format(no_cents: true) #=> "$5" + # Money.new(100, "CAD").format(no_cents: true) #=> "$1" + # Money.new(599, "CAD").format(no_cents: true) #=> "$5" # # @option rules [Boolean] :no_cents_if_whole (false) Whether cents should be # omitted if the cent value is zero # # @example - # Money.ca_dollar(10000).format(no_cents_if_whole: true) #=> "$100" - # Money.ca_dollar(10034).format(no_cents_if_whole: true) #=> "$100.34" + # Money.new(10000, "CAD").format(no_cents_if_whole: true) #=> "$100" + # Money.new(10034, "CAD").format(no_cents_if_whole: true) #=> "$100.34" # # @option rules [Boolean, String, nil] :symbol (true) Whether a money symbol # should be prepended to the result string. The default is true. This method @@ -109,7 +109,7 @@ class Money # @option rules [Boolean] :html_wrap (false) Whether all currency parts should be HTML-formatted. # # @example - # Money.ca_dollar(570).format(html_wrap: true, with_currency: true) + # Money.new(570, "CAD").format(html_wrap: true, with_currency: true) # #=> "$5.70 CAD" # # @option rules [Boolean] :sign_positive (false) Whether positive numbers should be diff --git a/spec/money/constructors_spec.rb b/spec/money/constructors_spec.rb index 7feca11bb5..70206034d5 100644 --- a/spec/money/constructors_spec.rb +++ b/spec/money/constructors_spec.rb @@ -82,4 +82,25 @@ expect(special_money_class.pound_sterling(0)).to be_a special_money_class end end + + describe ".currency_helpers=" do + it "allows adding new currency helper methods" do + money_class = Class.new(Money) + money_class.currency_helpers = { jpy: "JPY", yen: "JPY" } + + expect(money_class.jpy(1000)).to eq Money.new(1000, "JPY") + expect(money_class.yen(500)).to eq Money.new(500, "JPY") + end + + it "creates as_ instance methods for currency exchange" do + money_class = Class.new(Money) + money_class.currency_helpers = { jpy: "JPY" } + + Money.add_rate("USD", "JPY", 150) + money = money_class.new(100, "USD") + + expect(money).to respond_to(:as_jpy) + expect(money.as_jpy).to eq money.exchange_to("JPY") + end + end end From 773d5e586029015a17e3647292ce62e1a2991ac9 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Sat, 17 Jan 2026 21:28:58 +0100 Subject: [PATCH 2/2] Update CHANGELOG.md --- CHANGELOG.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c6f474ca8..b1b14ce8da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.add_rate("USD", "MXN", 150) + Money.new(100, "USD").as_mxn #=> # + ``` ## 7.0.2