minting 1.9.6 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +244 -121
- data/Rakefile +17 -26
- data/bin/bench_check +2 -2
- data/doc/agents/api_review-2026-06-15.md +1 -1
- data/doc/agents/copilot-instructions.md +5 -5
- data/doc/agents/expired/copilot-instructions.md +2 -2
- data/doc/agents/expired/gemini_gem_evaluation.md +2 -2
- data/lib/minting/aliases.rb +22 -0
- data/lib/minting/currency/currency.rb +87 -12
- data/lib/minting/data/crypto-currencies.yaml +126 -0
- data/lib/minting/data/world-currencies.yaml +75 -23
- data/lib/minting/mint/i18n.rb +79 -29
- data/lib/minting/mint/mint.rb +1 -26
- data/lib/minting/mint/registry/crypto.rb +59 -0
- data/lib/minting/mint/registry/registration.rb +1 -2
- data/lib/minting/mint/registry/symbols.rb +37 -30
- data/lib/minting/mint/rounding.rb +10 -7
- data/lib/minting/mint.rb +1 -2
- data/lib/minting/money/allocation/allocation.rb +2 -2
- data/lib/minting/money/allocation/split.rb +1 -1
- data/lib/minting/money/arithmetics/operators.rb +10 -13
- data/lib/minting/money/clamp.rb +6 -6
- data/lib/minting/money/coercion.rb +1 -1
- data/lib/minting/money/comparable.rb +3 -3
- data/lib/minting/money/constructors.rb +3 -42
- data/lib/minting/money/conversion.rb +23 -18
- data/lib/minting/money/format/format.rb +100 -0
- data/lib/minting/money/format/formatter.rb +102 -0
- data/lib/minting/money/format/to_s.rb +34 -99
- data/lib/minting/money/format/validator.rb +34 -0
- data/lib/minting/money/money.rb +25 -13
- data/lib/minting/money/parse.rb +127 -0
- data/lib/minting/money/rounding.rb +27 -0
- data/lib/minting/version.rb +1 -1
- data/lib/minting.rb +17 -8
- metadata +9 -28
- data/doc/Mint/Currency.html +0 -1943
- data/doc/Mint/CurrencyRegistry.html +0 -511
- data/doc/Mint/Money.html +0 -4607
- data/doc/Mint/RangeStepPatch.html +0 -277
- data/doc/Mint/Registry.html +0 -863
- data/doc/Mint/Rounding.html +0 -506
- data/doc/Mint/UnknownCurrency.html +0 -136
- data/doc/Mint.html +0 -1004
- data/doc/Minting.html +0 -142
- data/doc/_index.html +0 -180
- data/doc/class_list.html +0 -54
- data/doc/css/common.css +0 -1
- data/doc/css/full_list.css +0 -206
- data/doc/css/style.css +0 -1089
- data/doc/file.README.html +0 -277
- data/doc/file_list.html +0 -59
- data/doc/frames.html +0 -22
- data/doc/index.html +0 -277
- data/doc/js/app.js +0 -801
- data/doc/js/full_list.js +0 -334
- data/doc/js/jquery.js +0 -4
- data/doc/method_list.html +0 -686
- data/doc/top-level-namespace.html +0 -151
- data/lib/minting/mint/aliases.rb +0 -16
- data/lib/minting/mint/parser/parser.rb +0 -97
- data/lib/minting/mint/parser/separators.rb +0 -41
- data/lib/minting/money/format/formatting.rb +0 -97
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Crypto currency definitions (opt-in registration)
|
|
4
|
+
module Mint
|
|
5
|
+
# Internal registry for currencies, symbols, and zero-money cache.
|
|
6
|
+
module Registry
|
|
7
|
+
@crypto_currencies = nil
|
|
8
|
+
CRYPTO_MUTEX = Monitor.new
|
|
9
|
+
|
|
10
|
+
private_constant :CRYPTO_MUTEX
|
|
11
|
+
|
|
12
|
+
# Returns the list of built-in crypto currency definitions.
|
|
13
|
+
#
|
|
14
|
+
# These are not registered by default — call {.register_crypto} to opt in.
|
|
15
|
+
#
|
|
16
|
+
# @return [Array<Currency>] frozen array of crypto currency definitions
|
|
17
|
+
def self.crypto_currencies
|
|
18
|
+
@crypto_currencies || CRYPTO_MUTEX.synchronize do
|
|
19
|
+
@crypto_currencies ||= begin
|
|
20
|
+
path = File.join(File.expand_path('../../data', __dir__), 'crypto-currencies.yaml')
|
|
21
|
+
YAML.load_file(path).map { |entry| Currency.new(**entry.transform_keys(&:to_sym)) }.freeze
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Registers one or more crypto currencies into the shared currency registry.
|
|
27
|
+
#
|
|
28
|
+
# Raises on duplicate registration or unknown code — use +rescue+ if
|
|
29
|
+
# idempotent bulk registration is needed.
|
|
30
|
+
#
|
|
31
|
+
# @param codes [Array<String>] one or more crypto currency codes
|
|
32
|
+
# @raise [ArgumentError] if a code is not a known crypto currency
|
|
33
|
+
# @raise [KeyError] if the currency code is already registered
|
|
34
|
+
# @return [Array<Currency>] the newly registered Currency objects
|
|
35
|
+
def self.register_crypto(*codes)
|
|
36
|
+
entries = crypto_currencies
|
|
37
|
+
index = entries.each_with_index.to_h { |currency, index| [currency.code, index] }
|
|
38
|
+
missing = codes.reject { |code| index.key?(code) }
|
|
39
|
+
raise ArgumentError, "Unknown crypto code(s): #{missing.join(', ')}" unless missing.empty?
|
|
40
|
+
|
|
41
|
+
codes.map do |code|
|
|
42
|
+
c = entries[index[code]]
|
|
43
|
+
Currency.register(code:, subunit: c.subunit, symbol: c.symbol, priority: c.priority)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Registers all built-in crypto currencies at once.
|
|
48
|
+
#
|
|
49
|
+
# Raises on the first duplicate — call +rescue+ if idempotency is needed.
|
|
50
|
+
#
|
|
51
|
+
# @raise [KeyError] if any currency code is already registered
|
|
52
|
+
# @return [Array<Currency>] the newly registered Currency objects
|
|
53
|
+
def self.register_all_crypto
|
|
54
|
+
crypto_currencies.map do |c|
|
|
55
|
+
Currency.register(code: c.code, subunit: c.subunit, symbol: c.symbol, priority: c.priority)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -5,44 +5,51 @@ module Mint
|
|
|
5
5
|
module Registry
|
|
6
6
|
extend self
|
|
7
7
|
|
|
8
|
-
# Looks up a currency by its display symbol.
|
|
9
|
-
#
|
|
10
|
-
# @param symbol [String] the display symbol (e.g. "$", "R$")
|
|
11
|
-
# @return [Currency, nil] the highest-priority currency for the symbol
|
|
12
|
-
# @api private
|
|
13
8
|
def currency_for_symbol(symbol)
|
|
14
|
-
|
|
15
|
-
@
|
|
9
|
+
sync_symbols
|
|
10
|
+
@symbols_map[symbol]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def symbol_shared?(symbol)
|
|
14
|
+
sync_symbols
|
|
15
|
+
@shared_symbols.include?(symbol)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
# Scans +input+ for registered currency symbols and returns the first match.
|
|
19
|
-
#
|
|
20
|
-
# @param input [String] the string to scan
|
|
21
|
-
# @return [Currency, nil]
|
|
22
|
-
# @api private
|
|
23
18
|
def detect_currency(input)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
27
|
-
nil
|
|
19
|
+
sync_symbols
|
|
20
|
+
input.match(@symbols_regex) { |m| @symbols_map[m[0]] }
|
|
28
21
|
end
|
|
29
22
|
|
|
30
23
|
private
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
25
|
+
def build_symbol_regex(symbols)
|
|
26
|
+
/(?<![a-zA-Z])#{Regexp.union(symbols)}(?![a-zA-Z])/
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sync_symbols
|
|
30
|
+
MUTEX.synchronize do
|
|
31
|
+
return if @symbols_list
|
|
32
|
+
|
|
33
|
+
symbols_list = []
|
|
34
|
+
currencies.each_value do |currency|
|
|
35
|
+
next unless currency.symbol
|
|
36
|
+
|
|
37
|
+
symbols_list << [currency.symbol, currency]
|
|
38
|
+
symbols_list << [currency.disambiguate_symbol, currency] if currency.disambiguate_symbol
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@shared_symbols = symbols_list.map(&:first).tally
|
|
42
|
+
.select { |_, count| count > 1 }
|
|
43
|
+
.keys
|
|
44
|
+
.to_set
|
|
45
|
+
.freeze
|
|
46
|
+
|
|
47
|
+
symbols_list.sort_by! { |sym, cur| [-sym.length, -cur.priority] }
|
|
48
|
+
symbols_list.uniq! { |sym, _| sym }
|
|
49
|
+
|
|
50
|
+
@symbols_list = symbols_list.freeze
|
|
51
|
+
@symbols_map = symbols_list.to_h.freeze
|
|
52
|
+
@symbols_regex = build_symbol_regex(@symbols_map.keys)
|
|
46
53
|
end
|
|
47
54
|
end
|
|
48
55
|
end
|
|
@@ -10,17 +10,20 @@ module Mint
|
|
|
10
10
|
MODES = {
|
|
11
11
|
half_up: ->(amount, ndigits) { amount.round(ndigits, half: :up) },
|
|
12
12
|
half_down: ->(amount, ndigits) { amount.round(ndigits, half: :down) },
|
|
13
|
+
half_even: ->(amount, ndigits) { amount.round(ndigits, half: :even) },
|
|
13
14
|
floor: ->(amount, ndigits) { amount.floor(ndigits) },
|
|
14
15
|
ceil: ->(amount, ndigits) { amount.ceil(ndigits) },
|
|
15
16
|
truncate: ->(amount, ndigits) { amount.truncate(ndigits) },
|
|
16
17
|
down: ->(amount, ndigits) { amount.truncate(ndigits) }
|
|
17
18
|
}.freeze
|
|
18
19
|
|
|
20
|
+
THREAD_KEY = :minting_rounding_mode
|
|
21
|
+
|
|
19
22
|
# Returns the currently active rounding mode, falling back to +:half_up+.
|
|
20
23
|
# @api private
|
|
21
24
|
# @return [Symbol]
|
|
22
25
|
def self.current_mode
|
|
23
|
-
Thread.current[
|
|
26
|
+
Thread.current[THREAD_KEY] || :half_up
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
# Rounds +amount+ to +ndigits+ using the currently scoped rounding mode.
|
|
@@ -30,7 +33,7 @@ module Mint
|
|
|
30
33
|
# @param ndigits [Integer]
|
|
31
34
|
# @return [Rational]
|
|
32
35
|
def self.apply(amount, ndigits)
|
|
33
|
-
mode = Thread.current[
|
|
36
|
+
mode = Thread.current[THREAD_KEY]
|
|
34
37
|
if mode
|
|
35
38
|
MODES.fetch(mode).call(amount.to_r, ndigits)
|
|
36
39
|
else
|
|
@@ -47,18 +50,18 @@ module Mint
|
|
|
47
50
|
def self.with_mode(mode)
|
|
48
51
|
raise ArgumentError, "Unknown rounding mode: #{mode}" unless MODES.key?(mode)
|
|
49
52
|
|
|
50
|
-
prev = Thread.current[
|
|
51
|
-
Thread.current[
|
|
53
|
+
prev = Thread.current[THREAD_KEY]
|
|
54
|
+
Thread.current[THREAD_KEY] = mode
|
|
52
55
|
yield
|
|
53
56
|
ensure
|
|
54
|
-
Thread.current[
|
|
57
|
+
Thread.current[THREAD_KEY] = prev
|
|
55
58
|
end
|
|
56
59
|
end
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
# When loaded, patch Currency to delegate rounding through this module.
|
|
60
63
|
# Remove the default fast-path method first to avoid redefinition warnings.
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
Money::Currency.remove_method(:normalize_amount)
|
|
65
|
+
Money::Currency.define_method(:normalize_amount) do |amount|
|
|
63
66
|
Mint::Rounding.apply(amount, subunit)
|
|
64
67
|
end
|
data/lib/minting/mint.rb
CHANGED
|
@@ -7,8 +7,7 @@ require_relative 'mint/dsl/range'
|
|
|
7
7
|
require_relative 'mint/dsl/string'
|
|
8
8
|
require_relative 'mint/i18n'
|
|
9
9
|
require_relative 'mint/mint'
|
|
10
|
-
require_relative 'mint/parser/parser'
|
|
11
|
-
require_relative 'mint/parser/separators'
|
|
12
10
|
require_relative 'mint/registry/registry'
|
|
11
|
+
require_relative 'mint/registry/crypto'
|
|
13
12
|
|
|
14
13
|
require_relative 'money/money'
|
|
@@ -10,14 +10,14 @@ module Mint
|
|
|
10
10
|
# @raise [ArgumentError] if the proportions list is empty or sums to zero
|
|
11
11
|
#
|
|
12
12
|
# @example Proportional allocation
|
|
13
|
-
# money =
|
|
13
|
+
# money = Money.from(10.00, 'USD')
|
|
14
14
|
# money.allocate([1, 2, 3]) #=> [[USD 1.67], [USD 3.33], [USD 5.00]]
|
|
15
15
|
def allocate(proportions)
|
|
16
16
|
whole = proportions.sum.to_r
|
|
17
17
|
raise ArgumentError, 'Need at least 1 proportion element' if proportions.empty?
|
|
18
18
|
raise ArgumentError, 'Proportions total must not be zero' if whole.zero?
|
|
19
19
|
|
|
20
|
-
amounts = proportions.map { |rate| currency.normalize_amount(
|
|
20
|
+
amounts = proportions.map { |rate| currency.normalize_amount(amount * rate / whole) }
|
|
21
21
|
allocate_left_over(amounts: amounts, left_over: amount - amounts.sum)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
@@ -12,7 +12,7 @@ module Mint
|
|
|
12
12
|
# @raise [ArgumentError] if quantity is not a positive integer
|
|
13
13
|
#
|
|
14
14
|
# @example Even split
|
|
15
|
-
# money =
|
|
15
|
+
# money = Money.from(10.00, 'USD')
|
|
16
16
|
# money.split(3) #=> [[USD 3.34], [USD 3.33], [USD 3.33]]
|
|
17
17
|
def split(slices)
|
|
18
18
|
raise ArgumentError, 'Slices quantity must be an poitive integer' unless slices.positive? && slices.integer?
|
|
@@ -9,11 +9,10 @@ module Mint
|
|
|
9
9
|
# @return [Money] the sum of the addition
|
|
10
10
|
# @raise [TypeError] if addition involves a different currency or incompatible types
|
|
11
11
|
def +(addend)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
end
|
|
12
|
+
return self if addend == 0
|
|
13
|
+
return copy_with(amount: amount + addend.amount) if addend.is_a?(Money) && currency == addend.currency
|
|
14
|
+
|
|
15
|
+
raise TypeError, "#{addend} can't be added to #{self}"
|
|
17
16
|
end
|
|
18
17
|
|
|
19
18
|
# Performs subtraction with another {Money} instance or standard zero Numeric.
|
|
@@ -22,10 +21,9 @@ module Mint
|
|
|
22
21
|
# @return [Money] the difference of the subtraction
|
|
23
22
|
# @raise [TypeError] if subtraction involves a different currency or incompatible types
|
|
24
23
|
def -(subtrahend)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
end
|
|
24
|
+
return self if subtrahend == 0
|
|
25
|
+
return copy_with(amount: amount - subtrahend.amount) if subtrahend.is_a?(Money) && currency == subtrahend.currency
|
|
26
|
+
|
|
29
27
|
raise TypeError, "#{subtrahend} can't be subtracted from #{self}"
|
|
30
28
|
end
|
|
31
29
|
|
|
@@ -52,10 +50,9 @@ module Mint
|
|
|
52
50
|
# @raise [TypeError] if divisor is of incompatible type or different currency
|
|
53
51
|
# @raise [ZeroDivisionError] if division by zero is attempted
|
|
54
52
|
def /(divisor)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
end
|
|
53
|
+
return copy_with(amount: amount / divisor) if divisor.is_a? Numeric
|
|
54
|
+
return amount / divisor.amount if divisor.is_a?(Money) && currency == divisor.currency
|
|
55
|
+
|
|
59
56
|
raise TypeError, "#{self} can't be divided by #{divisor}"
|
|
60
57
|
end
|
|
61
58
|
|
data/lib/minting/money/clamp.rb
CHANGED
|
@@ -26,19 +26,19 @@ module Mint
|
|
|
26
26
|
# if min is a Range, and max is not nil
|
|
27
27
|
#
|
|
28
28
|
# @example In range
|
|
29
|
-
#
|
|
29
|
+
# Money.from(5, 'USD').clamp(0, 10) #=> [USD 5.00] (returns self)
|
|
30
30
|
#
|
|
31
31
|
# @example Out of range, with Numeric bounds
|
|
32
|
-
#
|
|
32
|
+
# Money.from(50, 'USD').clamp(0, 10) #=> [USD 10.00]
|
|
33
33
|
#
|
|
34
34
|
# @example Out of range, with Money bounds
|
|
35
|
-
# loss =
|
|
36
|
-
# floor =
|
|
37
|
-
# ceil =
|
|
35
|
+
# loss = Money.from(-5, 'USD')
|
|
36
|
+
# floor = Money.from(0, 'USD')
|
|
37
|
+
# ceil = Money.from(10, 'USD')
|
|
38
38
|
# loss.clamp(floor, ceil) #=> [USD 0.00]
|
|
39
39
|
#
|
|
40
40
|
# @example Subunit-0 currency (JPY)
|
|
41
|
-
#
|
|
41
|
+
# Money.from(500, 'JPY').clamp(0, 100) #=> [JPY 100]
|
|
42
42
|
def clamp(min_or_range, max = nil)
|
|
43
43
|
if min_or_range.is_a?(Range)
|
|
44
44
|
raise(ArgumentError, "Either amount range alone or two amounts accepted: #{max}") if max
|
|
@@ -9,7 +9,7 @@ module Mint
|
|
|
9
9
|
# @param other [Numeric] the left-hand operand to coerce
|
|
10
10
|
# @return [Array(CoercedNumber, Money)] coerced operand array
|
|
11
11
|
# @example
|
|
12
|
-
# price =
|
|
12
|
+
# price = Money.from(10, 'USD')
|
|
13
13
|
# 5 * price #=> [USD 50.00] (via coercion)
|
|
14
14
|
def coerce(other)
|
|
15
15
|
[CoercedNumber.new(other), self]
|
|
@@ -25,12 +25,12 @@ module Mint
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# @example
|
|
28
|
-
# two_usd ==
|
|
28
|
+
# two_usd == Money.from(2r, 'USD') #=> [$ 2.00]
|
|
29
29
|
# two_usd > 0 #=> true
|
|
30
|
-
# two_usd >
|
|
30
|
+
# two_usd > Money.from(2, 'USD') #=> false
|
|
31
31
|
# two_usd > 1
|
|
32
32
|
# => TypeError: [$ 2.00] can't be compared to 1
|
|
33
|
-
# two_usd >
|
|
33
|
+
# two_usd > Money.from(2, 'BRL')
|
|
34
34
|
# => TypeError: [$ 2.00] can't be compared to [R$ 2.00]
|
|
35
35
|
#
|
|
36
36
|
def <=>(other)
|
|
@@ -29,34 +29,7 @@ module Mint
|
|
|
29
29
|
# Money.no_currency(100) #=> [XXX 100]
|
|
30
30
|
def self.no_currency(amount) = from(amount, 'XXX')
|
|
31
31
|
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# Returns +nil+ when the input is invalid or currency cannot be determined.
|
|
35
|
-
#
|
|
36
|
-
# @param input [String] Amount input, optionally including a currency symbol or code
|
|
37
|
-
# @param currency [String, Symbol, Currency, nil] ISO code when not present in +input+
|
|
38
|
-
# @return [Money, nil]
|
|
39
|
-
#
|
|
40
|
-
# @example With explicit currency
|
|
41
|
-
# Money.parse('19.99', 'USD') #=> [USD 19.99]
|
|
42
|
-
# Money.parse('garbage', 'USD') #=> nil
|
|
43
|
-
#
|
|
44
|
-
# @example With symbol or code in the string
|
|
45
|
-
# Money.parse('$19.99') #=> [USD 19.99]
|
|
46
|
-
# Money.parse('USD 1,234.56') #=> [USD 1234.56]
|
|
47
|
-
def self.parse(input, currency = nil) = Mint.parse(input, currency)
|
|
48
|
-
|
|
49
|
-
# Like {.parse} but raises on failure.
|
|
50
|
-
#
|
|
51
|
-
# @param input [String] Amount input, optionally including a currency symbol or code
|
|
52
|
-
# @param currency [String, Symbol, Currency, nil] ISO code when not present in +input+
|
|
53
|
-
# @return [Money]
|
|
54
|
-
# @raise [ArgumentError] when +input+ is invalid or currency cannot be determined
|
|
55
|
-
#
|
|
56
|
-
# @example
|
|
57
|
-
# Money.parse!('19.99', 'USD') #=> [USD 19.99]
|
|
58
|
-
# Money.parse!('garbage', 'USD') #=> ArgumentError
|
|
59
|
-
def self.parse!(input, currency = nil) = Mint.parse!(input, currency)
|
|
32
|
+
# Parsing is defined in +parse+. See {Money.parse} and {Money.parse!}.
|
|
60
33
|
|
|
61
34
|
# Returns a frozen zero Money in the given currency.
|
|
62
35
|
#
|
|
@@ -81,7 +54,7 @@ module Mint
|
|
|
81
54
|
# @example JPY (subunit 0)
|
|
82
55
|
# Money.from_subunits(1234, 'JPY') #=> [JPY 1234]
|
|
83
56
|
# @example Round trip
|
|
84
|
-
# m =
|
|
57
|
+
# m = Money.from(9.99, 'USD')
|
|
85
58
|
# Money.from_subunits(m.subunits, 'USD') == m #=> true
|
|
86
59
|
def self.from_subunits(subunits, currency)
|
|
87
60
|
raise ArgumentError, 'subunits must be an Integer' unless subunits.is_a?(Integer)
|
|
@@ -98,7 +71,7 @@ module Mint
|
|
|
98
71
|
# @param amount [Numeric] The new monetary amount
|
|
99
72
|
# @return [Money] A new Money object with the new amount, or self if the amount is unchanged
|
|
100
73
|
# @example
|
|
101
|
-
# price =
|
|
74
|
+
# price = Money.from(10.00, 'USD')
|
|
102
75
|
# price.copy_with(amount: 15.00) #=> [USD 15.00]
|
|
103
76
|
# price.copy_with(amount: 10.00) #=> [USD 10.00] (returns self)
|
|
104
77
|
def copy_with(amount:)
|
|
@@ -113,18 +86,6 @@ module Mint
|
|
|
113
86
|
end
|
|
114
87
|
end
|
|
115
88
|
|
|
116
|
-
# Returns a new Money with the given amount in the same currency.
|
|
117
|
-
#
|
|
118
|
-
# @deprecated Use {#copy_with} instead. Will be removed in v2.
|
|
119
|
-
# @param new_amount [Numeric] the new monetary amount
|
|
120
|
-
# @return [Money] a new Money instance, or self if unchanged
|
|
121
|
-
# @example
|
|
122
|
-
# Mint.money(10, 'USD').mint(15) #=> [USD 15.00]
|
|
123
|
-
def mint(new_amount)
|
|
124
|
-
warn 'Money#mint is now deprecated and will be removed in v2'
|
|
125
|
-
copy_with(amount: new_amount)
|
|
126
|
-
end
|
|
127
|
-
|
|
128
89
|
private
|
|
129
90
|
|
|
130
91
|
# Initializes a new Money object with the given amount and currency.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'bigdecimal'
|
|
4
|
+
require 'bigdecimal/util'
|
|
4
5
|
require 'erb'
|
|
5
6
|
|
|
6
7
|
module Mint
|
|
@@ -10,7 +11,7 @@ module Mint
|
|
|
10
11
|
#
|
|
11
12
|
# @return [BigDecimal] the decimal representation of the money amount
|
|
12
13
|
# @example
|
|
13
|
-
#
|
|
14
|
+
# Money.from(9.99, 'USD').to_d #=> 0.999e1
|
|
14
15
|
def to_d = amount.to_d 0
|
|
15
16
|
|
|
16
17
|
# Converts the monetary amount to a standard float.
|
|
@@ -26,36 +27,40 @@ module Mint
|
|
|
26
27
|
# @return [String] HTML5 `<data>` representation
|
|
27
28
|
def to_html(format = DEFAULT_FORMAT)
|
|
28
29
|
title = Kernel.format("#{currency_code} %0.#{currency.subunit}f", amount)
|
|
29
|
-
body = format(format
|
|
30
|
+
body = format(format)
|
|
30
31
|
%(<data class='money' title='#{title}'>#{ERB::Util.html_escape(body)}</data>)
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
# Truncates and converts the monetary amount to an Integer.
|
|
34
|
-
#
|
|
35
|
-
# @return [Integer] the integer representation of the money amount
|
|
36
|
-
# @example
|
|
37
|
-
# Mint.money(9.99, 'USD').to_i #=> 9
|
|
38
|
-
# Mint.money(-9.99, 'USD').to_i #=> -9
|
|
39
|
-
def to_i = amount.to_i
|
|
40
|
-
|
|
41
34
|
# Returns a Hash representation of the money instance.
|
|
42
35
|
#
|
|
43
36
|
# @return [Hash] hash with :currency (String) and :amount (String) keys
|
|
44
37
|
# @example
|
|
45
|
-
#
|
|
38
|
+
# Money.from(134120, 'BRL').to_hash
|
|
46
39
|
# #=> { currency: "BRL", amount: "134120.00" }
|
|
47
40
|
def to_hash
|
|
48
41
|
{ currency: currency_code, amount: Kernel.format("%0.#{currency.subunit}f", amount) }
|
|
49
42
|
end
|
|
50
43
|
|
|
51
|
-
#
|
|
52
|
-
# Highly optimized to run without external dependencies.
|
|
44
|
+
# Deserializes a Hash into a Money instance.
|
|
53
45
|
#
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
46
|
+
# Accepts both symbol and string keys, matching the output of {#to_hash}.
|
|
47
|
+
#
|
|
48
|
+
# @param hash [Hash] a hash with +:currency+ (or +"currency"+) and
|
|
49
|
+
# +:amount+ (or +"amount"+) keys
|
|
50
|
+
# @return [Money] the deserialized Money instance
|
|
51
|
+
# @raise [Mint::UnknownCurrency] if the currency can't be resolved
|
|
52
|
+
# @raise [ArgumentError] if amount is not parseable as a Rational
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# Money.from_hash(currency: "USD", amount: "9.99")
|
|
56
|
+
# #=> [USD 9.99]
|
|
57
|
+
# @example Round-trip
|
|
58
|
+
# m = Money.from(134120, "BRL")
|
|
59
|
+
# Money.from_hash(m.to_hash) == m #=> true
|
|
60
|
+
def self.from_hash(hash)
|
|
61
|
+
currency = Currency.resolve!(hash[:currency] || hash['currency'])
|
|
62
|
+
amount = currency.normalize_amount(Rational(hash[:amount] || hash['amount']))
|
|
63
|
+
amount.zero? ? currency.zero : new(amount, currency)
|
|
59
64
|
end
|
|
60
65
|
|
|
61
66
|
# Returns the exact internal Rational representation of the monetary amount.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mint
|
|
4
|
+
# :nodoc:
|
|
5
|
+
class Money
|
|
6
|
+
# Formats money as a string with a customizable template, thousand delimiter,
|
|
7
|
+
# and decimal separator.
|
|
8
|
+
#
|
|
9
|
+
# @param template [String, Hash, nil] Either a format string with placeholders
|
|
10
|
+
# (%<symbol>s, %<amount>f, %<currency>s, %<integral>d, %<fractional>d, %<dsymbol>s),
|
|
11
|
+
# or a Hash with per-sign keys (:positive, :negative, :zero) each
|
|
12
|
+
# holding a format string. A Hash is convenient for sign-aware formats
|
|
13
|
+
# such as accounting parentheses:
|
|
14
|
+
#
|
|
15
|
+
# money.format({ negative: '(%<symbol>s%<amount>f)' })
|
|
16
|
+
#
|
|
17
|
+
# Missing keys fall back to the module default, so a Hash with only
|
|
18
|
+
# :negative will still format positives sensibly. The valid keys are
|
|
19
|
+
# :positive, :negative, :zero; anything else raises ArgumentError.
|
|
20
|
+
# When +nil+, falls back to +Mint.locale_backend+ if set, otherwise
|
|
21
|
+
# +"%<symbol>s%<amount>f"+.
|
|
22
|
+
# @param thousand [String, false, nil] Thousands delimiter (e.g., ',' for 1,000).
|
|
23
|
+
# When +nil+, falls back to +Mint.locale_backend+ if set, otherwise +","+.
|
|
24
|
+
# @param decimal [String, nil] Decimal separator (e.g., '.' or ',').
|
|
25
|
+
# When +nil+, falls back to +Mint.locale_backend+ if set, otherwise +"."+.
|
|
26
|
+
# @param locale [Symbol, String, nil] Locale key passed to the +Mint.locale_backend+
|
|
27
|
+
# callable when resolving locale-aware separators and format. Ignored when
|
|
28
|
+
# +Mint.locale_backend+ is a Hash or nil. Accepts symbols (+:en+, +:'pt-BR'+)
|
|
29
|
+
# and strings (+"pt-BR"+), passed through as-is (matching Rails +I18n.locale+
|
|
30
|
+
# convention).
|
|
31
|
+
# @return [String] Formatted money string
|
|
32
|
+
#
|
|
33
|
+
# @raise [ArgumentError] if +template+ is not a String or Hash, the Hash is
|
|
34
|
+
# empty, or the Hash contains an unrecognised key.
|
|
35
|
+
#
|
|
36
|
+
# @example Basic formatting
|
|
37
|
+
# money = Money.from(1234.56, 'USD')
|
|
38
|
+
# money.format #=> "$1,234.56"
|
|
39
|
+
# money.format(thousand: '.', decimal: ',') #=> "$1.234,56"
|
|
40
|
+
# money.format(decimal: ',', thousand: '') #=> "$1234,56"
|
|
41
|
+
#
|
|
42
|
+
# @example Custom templates
|
|
43
|
+
# money.format('%<amount>f') #=> "1234.56"
|
|
44
|
+
# money.format('%<currency>s %<amount>f') #=> "USD 1234.56"
|
|
45
|
+
# money.format('%<amount>f %<symbol>s') #=> "1234.56 $"
|
|
46
|
+
# money.format('%<symbol>s%<amount>+f') #=> "$+1234.56"
|
|
47
|
+
#
|
|
48
|
+
# @example Integral & fractional parts
|
|
49
|
+
# money.format('%<integral>d.%<fractional>02d') #=> "1234.56"
|
|
50
|
+
# price = Money.from(0.99, 'USD')
|
|
51
|
+
# price.format('%<integral>d dollars and %<fractional>02d cents')
|
|
52
|
+
# #=> "0 dollars and 99 cents"
|
|
53
|
+
#
|
|
54
|
+
# @example Per-sign Hash format (accounting parentheses)
|
|
55
|
+
# loss = Money.from(-1234.56, 'USD')
|
|
56
|
+
# loss.format({ negative: '(%<symbol>s%<amount>f)' }) #=> "($1,234.56)"
|
|
57
|
+
# Money.from(0, 'BRL').format({ zero: '--' }) #=> "--"
|
|
58
|
+
#
|
|
59
|
+
# @example Padding and alignment
|
|
60
|
+
# money.format('%<amount>10.2f') #=> " 1234.56"
|
|
61
|
+
# money.format('%<symbol>s%<amount>010.2f') #=> "$0001234.56"
|
|
62
|
+
#
|
|
63
|
+
# @example Locale-aware formatting (with Mint.locale_backend set)
|
|
64
|
+
# money.format # decimal and thousand come from locale_backend
|
|
65
|
+
# money.format(locale: :en) # locale passed to backend callable
|
|
66
|
+
# money.format(locale: 'pt-BR') # strings work too
|
|
67
|
+
#
|
|
68
|
+
def format(template = nil, decimal: nil, thousand: nil, width: nil, locale: nil)
|
|
69
|
+
template, decimal, thousand = resolve_format_options(template, decimal:, thousand:, locale:)
|
|
70
|
+
|
|
71
|
+
case template
|
|
72
|
+
when Hash # :noop - validated in Formatter.for
|
|
73
|
+
when String then template = { positive: template }
|
|
74
|
+
else raise ArgumentError, 'Invalid template. Only String or Hash are accepted'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
formatted = Formatter.for(template, decimal, thousand).format(self)
|
|
78
|
+
|
|
79
|
+
width ? formatted.rjust(width) : formatted
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Alias for {#format}. Takes the same arguments.
|
|
83
|
+
alias to_fs :format
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def resolve_format_options(template, decimal:, thousand:, locale:)
|
|
88
|
+
backend_opts = Mint.resolve_locale_for(locale:)
|
|
89
|
+
|
|
90
|
+
template ||= backend_opts[:format] || DEFAULT_FORMAT
|
|
91
|
+
decimal ||= backend_opts[:decimal] || '.'
|
|
92
|
+
if thousand.nil?
|
|
93
|
+
thousand = backend_opts[:thousand] || ','
|
|
94
|
+
thousand = false if thousand == decimal
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
[template, decimal, thousand]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|