minting 1.3.0 → 1.5.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 +31 -3
- data/Rakefile +4 -4
- data/doc/agents/AGENTS.md +25 -0
- data/doc/agents/copilot-instructions.md +75 -0
- data/doc/agents/gemini_gem_evaluation.md +245 -0
- data/doc/agents/recommendations.md +335 -0
- data/doc/agents/rubocop-issues.md +332 -0
- data/lib/minting/mint/currency.rb +9 -8
- data/lib/minting/mint/currency_store.rb +68 -0
- data/lib/minting/mint/refinements.rb +3 -1
- data/lib/minting/mint/registry.rb +21 -61
- data/lib/minting/mint.rb +4 -1
- data/lib/minting/money/allocation.rb +4 -1
- data/lib/minting/money/arithmetics.rb +25 -10
- data/lib/minting/money/coercion.rb +3 -1
- data/lib/minting/money/comparable.rb +5 -6
- data/lib/minting/money/constructors.rb +66 -0
- data/lib/minting/money/conversion.rb +9 -17
- data/lib/minting/money/formatting.rb +42 -8
- data/lib/minting/money/money.rb +60 -34
- data/lib/minting/money/parse.rb +16 -19
- data/lib/minting/money.rb +3 -0
- data/lib/minting/version.rb +3 -1
- data/lib/minting.rb +2 -0
- data/minting.gemspec +1 -0
- metadata +23 -2
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Mint
|
|
2
4
|
# Represents a specific currency unit, identified by ISO 4217 alphabetic code
|
|
3
5
|
#
|
|
@@ -8,25 +10,24 @@ module Mint
|
|
|
8
10
|
:fractional_multiplier, :minimum_amount,
|
|
9
11
|
:name, :priority
|
|
10
12
|
|
|
11
|
-
def inspect
|
|
12
|
-
"<Currency:(#{code} #{symbol} #{subunit})>"
|
|
13
|
-
end
|
|
13
|
+
def inspect = "<Currency:(#{code} #{symbol} #{subunit} #{name})>"
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
# Normalizes numeric amounts for this currency
|
|
16
|
+
# 1. Converts to Rational
|
|
17
|
+
# 2. Rounds to respect currency subunit
|
|
18
|
+
def normalize_amount(amount) = amount.to_r.round(subunit)
|
|
18
19
|
|
|
19
20
|
private
|
|
20
21
|
|
|
21
22
|
def initialize(code:, symbol:, subunit: 0, priority: 0, country: nil, name: nil)
|
|
22
|
-
@code = code
|
|
23
|
+
@code = code
|
|
23
24
|
@subunit = subunit.to_i
|
|
24
25
|
@symbol = symbol
|
|
25
26
|
@priority = priority.to_i
|
|
26
27
|
@country = country
|
|
27
28
|
@name = name
|
|
28
29
|
@fractional_multiplier = 10**@subunit
|
|
29
|
-
@minimum_amount =
|
|
30
|
+
@minimum_amount = Rational(1, fractional_multiplier)
|
|
30
31
|
freeze
|
|
31
32
|
end
|
|
32
33
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
# Mint currency store (internal)
|
|
6
|
+
module Mint
|
|
7
|
+
# Internal currency storage and loading.
|
|
8
|
+
# Manages the registry cache and currency symbol lookups.
|
|
9
|
+
module CurrencyStore
|
|
10
|
+
# Returns the hash of all registered currencies.
|
|
11
|
+
#
|
|
12
|
+
# @return [Hash{String => Currency}] registered currencies mapped by code
|
|
13
|
+
# @api private
|
|
14
|
+
def self.currencies
|
|
15
|
+
@currencies ||= begin
|
|
16
|
+
registry = { 'XXX' => Currency.new(code: 'XXX', name: 'No currency', symbol: '¤') }
|
|
17
|
+
load_currencies(registry)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Registered symbols sorted for detection: longest match wins, then parser priority.
|
|
22
|
+
#
|
|
23
|
+
# @return [Array<Array<String, Currency>>] sorted symbol-to-currency mappings
|
|
24
|
+
# @api private
|
|
25
|
+
def self.currency_symbols
|
|
26
|
+
@currency_symbols ||= begin
|
|
27
|
+
currencies.values
|
|
28
|
+
.reject { |currency| currency.symbol.empty? }
|
|
29
|
+
.map { |currency| [currency.symbol, currency] }
|
|
30
|
+
.sort_by { |symbol, currency| [-symbol.length, -currency.priority] }
|
|
31
|
+
end.freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Clears and refreshes the currency symbol cache.
|
|
35
|
+
# Called when currencies are registered.
|
|
36
|
+
#
|
|
37
|
+
# @api private
|
|
38
|
+
def self.invalidate_symbols_cache
|
|
39
|
+
@currency_symbols = nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Loads currencies from YAML file into the registry.
|
|
43
|
+
#
|
|
44
|
+
# @param registry [Hash] the registry hash to populate
|
|
45
|
+
# @return [Hash] the populated registry
|
|
46
|
+
# @api private
|
|
47
|
+
def self.load_currencies(registry)
|
|
48
|
+
base = File.expand_path('../data', __dir__)
|
|
49
|
+
path = File.join(base, 'currencies.yaml')
|
|
50
|
+
|
|
51
|
+
data = YAML.load_file(path)
|
|
52
|
+
data.each do |entry|
|
|
53
|
+
code = entry['code']
|
|
54
|
+
registry[code] = Currency.new(
|
|
55
|
+
code: code,
|
|
56
|
+
subunit: entry['subunit'],
|
|
57
|
+
symbol: entry['symbol'],
|
|
58
|
+
priority: entry['priority'],
|
|
59
|
+
country: entry['country'],
|
|
60
|
+
name: entry['name']
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
registry
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private_class_method :load_currencies
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Mint currency registration and factory (public API)
|
|
3
4
|
module Mint
|
|
4
5
|
# Creates a new {Money} instance with the given amount and currency code.
|
|
5
6
|
#
|
|
6
7
|
# @param amount [Numeric] the financial value
|
|
7
|
-
# @param currency_code [String
|
|
8
|
+
# @param currency_code [String] the ISO currency code
|
|
8
9
|
# @return [Money] the instantiated Money object
|
|
9
10
|
# @raise [ArgumentError] if the currency code is not registered
|
|
10
11
|
def self.money(amount, currency_code)
|
|
11
12
|
currency = currency(currency_code)
|
|
12
13
|
return Money.create(amount, currency) if currency
|
|
13
14
|
|
|
14
|
-
raise ArgumentError, "[#{currency.inspect}] is not a registered currency.
|
|
15
|
+
raise ArgumentError, "[#{currency.inspect}] is not a registered currency."
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
# Returns default zero, no currency money
|
|
@@ -20,35 +21,27 @@ module Mint
|
|
|
20
21
|
# Finds a registered currency by its code, symbol,
|
|
21
22
|
# or retrieves it directly if already a Currency object.
|
|
22
23
|
#
|
|
23
|
-
# @param currency [String,
|
|
24
|
+
# @param currency [String, Currency] the currency identifier or object
|
|
24
25
|
# @return [Currency, nil] the registered Currency instance or nil if not found
|
|
25
26
|
def self.currency(currency)
|
|
26
|
-
|
|
27
|
-
when Currency
|
|
28
|
-
currency
|
|
29
|
-
when Symbol
|
|
30
|
-
currencies[currency.to_s]
|
|
31
|
-
else
|
|
32
|
-
currencies[currency]
|
|
33
|
-
end
|
|
27
|
+
currency.is_a?(Currency) ? currency : CurrencyStore.currencies[currency]
|
|
34
28
|
end
|
|
35
29
|
|
|
36
30
|
# Registers a new currency if not already registered.
|
|
37
31
|
#
|
|
38
|
-
# @param code [String
|
|
32
|
+
# @param code [String] the unique currency code (e.g. 'USD', 'EUR')
|
|
39
33
|
# @param subunit [Integer] the decimal subunit precision (defaults to 2)
|
|
40
34
|
# @param symbol [String] the display symbol (defaults to '')
|
|
41
35
|
# @param priority [Integer] parser precedence priority (defaults to 0)
|
|
42
36
|
# @return [Currency] the registered or existing Currency instance
|
|
43
37
|
# @raise [ArgumentError] if the code layout is invalid or register throws an error
|
|
44
|
-
def self.register_currency(code:, subunit:
|
|
45
|
-
code
|
|
46
|
-
currencies[code] || register_currency!(code:, subunit:, symbol:, priority:)
|
|
38
|
+
def self.register_currency(code:, subunit: 0, symbol: '', priority: 0)
|
|
39
|
+
CurrencyStore.currencies[code] || register_currency!(code:, subunit:, symbol:, priority:)
|
|
47
40
|
end
|
|
48
41
|
|
|
49
42
|
# Strictly registers a new currency, raising a KeyError if already registered.
|
|
50
43
|
#
|
|
51
|
-
# @param code [String
|
|
44
|
+
# @param code [String] the unique currency code
|
|
52
45
|
# @param subunit [Integer] the decimal subunit precision
|
|
53
46
|
# @param symbol [String] the display symbol
|
|
54
47
|
# @param priority [Integer] parser precedence priority
|
|
@@ -56,59 +49,26 @@ module Mint
|
|
|
56
49
|
# @raise [ArgumentError] if the code contains invalid characters
|
|
57
50
|
# @raise [KeyError] if the currency code is already registered
|
|
58
51
|
def self.register_currency!(code:, subunit:, symbol: '', priority: 0)
|
|
59
|
-
code
|
|
52
|
+
raise ArgumentError, 'Currency code must be String' unless code.is_a? String
|
|
60
53
|
unless code.match?(/^[A-Z_]+$/)
|
|
61
54
|
raise ArgumentError,
|
|
62
|
-
"Currency code must
|
|
63
|
-
end
|
|
64
|
-
if currencies[code]
|
|
65
|
-
raise KeyError,
|
|
66
|
-
"Currency: #{code} already registered"
|
|
55
|
+
"Currency code must only letters or '_' ('USD',, 'MY_COIN')"
|
|
67
56
|
end
|
|
68
57
|
|
|
58
|
+
currencies = CurrencyStore.currencies
|
|
59
|
+
raise KeyError, "Currency: #{code} already registered" if currencies[code]
|
|
60
|
+
|
|
69
61
|
currency = currencies[code] = Currency.new(code:, subunit:, symbol:, priority:)
|
|
70
|
-
|
|
62
|
+
CurrencyStore.invalidate_symbols_cache
|
|
71
63
|
currency
|
|
72
64
|
end
|
|
73
65
|
|
|
74
|
-
# Returns the hash of all registered currencies.
|
|
75
|
-
#
|
|
76
|
-
# @return [Hash{String => Currency}] registered currencies mapped by code
|
|
77
|
-
def self.currencies
|
|
78
|
-
@currencies ||= begin
|
|
79
|
-
registry = { 'XXX' => Currency.new(code: 'XXX', name: 'No currency', symbol: '¤') }
|
|
80
|
-
load_currencies(registry)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
66
|
# Registered symbols sorted for detection: longest match wins, then parser priority.
|
|
67
|
+
# Internal API - used by Money parser.
|
|
68
|
+
#
|
|
69
|
+
# @return [Array<Array<String, Currency>>] sorted symbol-to-currency mappings
|
|
70
|
+
# @api private
|
|
85
71
|
def self.currency_symbols
|
|
86
|
-
|
|
87
|
-
currencies.values
|
|
88
|
-
.map { |currency| [currency.symbol, currency] }
|
|
89
|
-
.reject { |symbol, _| symbol.empty? }
|
|
90
|
-
.sort_by { |symbol, currency| [-symbol.length, -currency.priority] }
|
|
91
|
-
end.freeze
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def self.load_currencies(registry)
|
|
95
|
-
base = File.expand_path('../data', __dir__)
|
|
96
|
-
path = File.join(base, 'currencies.yaml')
|
|
97
|
-
|
|
98
|
-
data = YAML.load_file(path)
|
|
99
|
-
data.each do |entry|
|
|
100
|
-
code = entry['code']
|
|
101
|
-
registry[code] = Currency.new(
|
|
102
|
-
code: code,
|
|
103
|
-
subunit: entry['subunit'],
|
|
104
|
-
symbol: entry['symbol'],
|
|
105
|
-
priority: entry['priority'],
|
|
106
|
-
country: entry['country'],
|
|
107
|
-
name: entry['name']
|
|
108
|
-
)
|
|
109
|
-
end
|
|
110
|
-
registry
|
|
72
|
+
CurrencyStore.currency_symbols
|
|
111
73
|
end
|
|
112
|
-
|
|
113
|
-
private_class_method :load_currencies
|
|
114
74
|
end
|
data/lib/minting/mint.rb
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Mint
|
|
4
|
+
# Allocation and splitting
|
|
2
5
|
class Money
|
|
3
6
|
# Proportionally allocates the monetary amount among a list of ratios.
|
|
4
7
|
# Disperses any subunit rounding amounts across the initial slots
|
|
@@ -50,7 +53,7 @@ module Mint
|
|
|
50
53
|
last_slot = (left_over / minimum).to_i - 1
|
|
51
54
|
(0..last_slot).each { |slot| amounts[slot] += minimum }
|
|
52
55
|
end
|
|
53
|
-
amounts.map { Money.new(
|
|
56
|
+
amounts.map { |amount| Money.new(amount, currency) }
|
|
54
57
|
end
|
|
55
58
|
end
|
|
56
59
|
end
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Mint
|
|
4
|
+
# Money Arithmetics
|
|
2
5
|
class Money
|
|
3
6
|
# Returns the absolute value of the monetary amount as a new {Money} instance.
|
|
4
7
|
#
|
|
@@ -27,9 +30,10 @@ module Mint
|
|
|
27
30
|
# @return [Money] the sum of the addition
|
|
28
31
|
# @raise [TypeError] if addition involves a different currency or incompatible types
|
|
29
32
|
def +(addend)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
case addend
|
|
34
|
+
when 0 then return self
|
|
35
|
+
when Money then return mint(amount + addend.amount) if same_currency?(addend)
|
|
36
|
+
end
|
|
33
37
|
raise TypeError, "#{addend} can't be added to #{self}"
|
|
34
38
|
end
|
|
35
39
|
|
|
@@ -39,11 +43,10 @@ module Mint
|
|
|
39
43
|
# @return [Money] the difference of the subtraction
|
|
40
44
|
# @raise [TypeError] if subtraction involves a different currency or incompatible types
|
|
41
45
|
def -(subtrahend)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
case subtrahend
|
|
47
|
+
when 0 then return self
|
|
48
|
+
when Money then return mint(amount - subtrahend.amount) if same_currency?(subtrahend)
|
|
45
49
|
end
|
|
46
|
-
|
|
47
50
|
raise TypeError, "#{subtrahend} can't be subtracted from #{self}"
|
|
48
51
|
end
|
|
49
52
|
|
|
@@ -72,10 +75,22 @@ module Mint
|
|
|
72
75
|
# @raise [TypeError] if divisor is of incompatible type or different currency
|
|
73
76
|
# @raise [ZeroDivisionError] if division by zero is attempted
|
|
74
77
|
def /(divisor)
|
|
75
|
-
|
|
76
|
-
return amount / divisor
|
|
77
|
-
|
|
78
|
+
case divisor
|
|
79
|
+
when Numeric then return mint(amount / divisor)
|
|
80
|
+
when Money then return amount / divisor.amount if same_currency? divisor
|
|
81
|
+
end
|
|
78
82
|
raise TypeError, "#{self} can't be divided by #{divisor}"
|
|
79
83
|
end
|
|
84
|
+
|
|
85
|
+
# Performs exponentiation of the monetary value by a standard scalar Numeric.
|
|
86
|
+
#
|
|
87
|
+
# @param exponent [Numeric]
|
|
88
|
+
# @return [Money] reult of amount ** exponent
|
|
89
|
+
# @raise [TypeError] if exponent is not Numeric
|
|
90
|
+
def **(exponent)
|
|
91
|
+
return mint(amount**exponent) if exponent.is_a?(Numeric)
|
|
92
|
+
|
|
93
|
+
raise TypeError, "#{self} can't be powered by #{exponent}"
|
|
94
|
+
end
|
|
80
95
|
end
|
|
81
96
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Mint
|
|
4
|
+
# Implements the standard Ruby coercion protocol.
|
|
2
5
|
class Money
|
|
3
|
-
# Implements the standard Ruby coercion protocol.
|
|
4
6
|
# Allows {Money} to interact seamlessly as the right-hand operand in Numeric arithmetic.
|
|
5
7
|
#
|
|
6
8
|
# @param other [Numeric] the left-hand operand to coerce
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Mint
|
|
2
|
-
# :nodoc
|
|
3
4
|
# Comparison methods
|
|
4
5
|
class Money
|
|
5
6
|
include Comparable
|
|
@@ -28,12 +29,10 @@ module Mint
|
|
|
28
29
|
#
|
|
29
30
|
def <=>(other)
|
|
30
31
|
case other
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return amount <=> other.amount if currency == other.currency
|
|
32
|
+
in 0 then amount <=> other
|
|
33
|
+
in Mint::Money if same_currency?(other) then amount <=> other.amount
|
|
34
|
+
else raise TypeError, "#{inspect} can't be compared to #{other.inspect}"
|
|
35
35
|
end
|
|
36
|
-
raise TypeError, "#{inspect} can't be compared to #{other.inspect}"
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
def nonzero? = amount.nonzero?
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mint
|
|
4
|
+
# Money constructors
|
|
5
|
+
class Money
|
|
6
|
+
# Creates a new Money immutable object with the specified amount and currency
|
|
7
|
+
# @param amount [Numeric] The monetary amount
|
|
8
|
+
# @param currency [Currency] The currency object
|
|
9
|
+
# @raise [ArgumentError] If amount is not numeric or currency is invalid
|
|
10
|
+
def self.create(amount, currency)
|
|
11
|
+
raise ArgumentError, 'amount must be Numeric' unless amount.is_a?(Numeric)
|
|
12
|
+
|
|
13
|
+
checked_currency = Mint.currency(currency)
|
|
14
|
+
raise ArgumentError, "Currency not found (#{currency})" unless checked_currency
|
|
15
|
+
|
|
16
|
+
new(checked_currency.normalize_amount(amount), checked_currency)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Builds a Money from a fractional (smallest-unit) Integer amount.
|
|
20
|
+
# This is the inverse of {#fractional}: for USD, the fractional unit is
|
|
21
|
+
# 1 cent; for JPY it is 1 yen; for IQD it is 1 dinar (subunit 3).
|
|
22
|
+
#
|
|
23
|
+
# @param fractional [Integer] the amount expressed in the currency's
|
|
24
|
+
# smallest unit (e.g. cents). Must be an Integer to preserve exactness.
|
|
25
|
+
# @param currency [String, Symbol, Currency] the currency identifier
|
|
26
|
+
# @return [Money] the resulting Money instance
|
|
27
|
+
# @raise [ArgumentError] if +fractional+ is not an Integer or +currency+
|
|
28
|
+
# is not registered
|
|
29
|
+
#
|
|
30
|
+
# @example USD cents
|
|
31
|
+
# Money.from_fractional(123_456, 'USD') #=> [USD 1234.56]
|
|
32
|
+
# @example JPY (subunit 0)
|
|
33
|
+
# Money.from_fractional(1234, 'JPY') #=> [JPY 1234]
|
|
34
|
+
# @example Round trip
|
|
35
|
+
# m = Mint.money(9.99, 'USD')
|
|
36
|
+
# Money.from_fractional(m.fractional, 'USD') == m #=> true
|
|
37
|
+
def self.from_fractional(fractional, currency)
|
|
38
|
+
raise ArgumentError, 'fractional must be an Integer' unless fractional.is_a?(Integer)
|
|
39
|
+
|
|
40
|
+
checked_currency = Mint.currency(currency)
|
|
41
|
+
raise ArgumentError, "Currency not found (#{currency})" unless checked_currency
|
|
42
|
+
|
|
43
|
+
amount = Rational(fractional, checked_currency.fractional_multiplier)
|
|
44
|
+
new(amount, checked_currency)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns a new Money object with the specified amount, or self if unchanged
|
|
48
|
+
# @param new_amount [Numeric] The new amount
|
|
49
|
+
# @return [Money] A new Money object or self
|
|
50
|
+
def mint(new_amount)
|
|
51
|
+
new_amount = currency.normalize_amount(new_amount)
|
|
52
|
+
new_amount == amount ? self : Money.new(new_amount, currency)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Initializes a new Money object with the given amount and currency.
|
|
58
|
+
# @param amount [Numeric] The monetary amount
|
|
59
|
+
# @param currency [Currency] The currency object
|
|
60
|
+
def initialize(amount, currency)
|
|
61
|
+
@amount = amount
|
|
62
|
+
@currency = currency
|
|
63
|
+
freeze
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -1,25 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bigdecimal'
|
|
1
4
|
require 'erb'
|
|
2
5
|
|
|
3
6
|
module Mint
|
|
4
7
|
# Conversion and serialization logic for {Money} instances.
|
|
5
8
|
class Money
|
|
6
|
-
# Converts the monetary amount to a
|
|
9
|
+
# Converts the monetary amount to a BigDecimal object.
|
|
7
10
|
#
|
|
8
11
|
# @return [BigDecimal] the decimal representation of the money amount
|
|
9
|
-
|
|
10
|
-
def to_d
|
|
11
|
-
raise NoMethodError, 'decimal gem required' unless defined?(BigDecimal)
|
|
12
|
-
|
|
13
|
-
amount.to_d 0
|
|
14
|
-
end
|
|
12
|
+
def to_d = amount.to_d 0
|
|
15
13
|
|
|
16
14
|
# Converts the monetary amount to a standard float.
|
|
17
15
|
# Note: Using float conversion loses precision guarantees.
|
|
18
16
|
#
|
|
19
17
|
# @return [Float] the floating-point representation of the money amount
|
|
20
|
-
def to_f
|
|
21
|
-
amount.to_f
|
|
22
|
-
end
|
|
18
|
+
def to_f = amount.to_f
|
|
23
19
|
|
|
24
20
|
# Renders a safe HTML5 `<data>` element containing the formatted currency.
|
|
25
21
|
# Embeds the ISO currency description and raw value as the metadata `title` attribute.
|
|
@@ -29,15 +25,13 @@ module Mint
|
|
|
29
25
|
def to_html(format = DEFAULT_FORMAT)
|
|
30
26
|
title = Kernel.format("#{currency_code} %0.#{currency.subunit}f", amount)
|
|
31
27
|
body = to_s(format: format)
|
|
32
|
-
%(<data class='money' title='#{
|
|
28
|
+
%(<data class='money' title='#{title}'>#{ERB::Util.html_escape(body)}</data>)
|
|
33
29
|
end
|
|
34
30
|
|
|
35
31
|
# Truncates and converts the monetary amount to an Integer.
|
|
36
32
|
#
|
|
37
33
|
# @return [Integer] the integer representation of the money amount
|
|
38
|
-
def to_i
|
|
39
|
-
amount.to_i
|
|
40
|
-
end
|
|
34
|
+
def to_i = amount.to_i
|
|
41
35
|
|
|
42
36
|
def to_hash
|
|
43
37
|
{ currency: currency_code, amount: Kernel.format("%0.#{currency.subunit}f", amount) }
|
|
@@ -56,8 +50,6 @@ module Mint
|
|
|
56
50
|
# Returns the exact internal Rational representation of the monetary amount.
|
|
57
51
|
#
|
|
58
52
|
# @return [Rational] the rational representation of the money amount
|
|
59
|
-
def to_r
|
|
60
|
-
amount
|
|
61
|
-
end
|
|
53
|
+
def to_r = amount
|
|
62
54
|
end
|
|
63
55
|
end
|
|
@@ -1,13 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Mint
|
|
2
4
|
# Formatting functionality for Money objects
|
|
3
5
|
class Money
|
|
4
6
|
# Formats money as a string with customizable format, thousand delimiter, and decimal
|
|
5
7
|
#
|
|
6
|
-
# @param format [String] Format string with placeholders
|
|
8
|
+
# @param format [String, Hash] Either a Format string with placeholders
|
|
9
|
+
# (%<symbol>s, %<amount>f, %<currency>s), or a Hash with per-sign keys
|
|
10
|
+
# (:positive, :negative, :zero) each holding a format string. A Hash
|
|
11
|
+
# is convenient for sign-aware formats such as accounting parentheses:
|
|
12
|
+
#
|
|
13
|
+
# money.to_s(format: { negative: '(%<symbol>s%<amount>f)' })
|
|
14
|
+
#
|
|
15
|
+
# Missing keys fall back to the module default, so a Hash with only
|
|
16
|
+
# :negative will still format positives sensibly. The valid keys are
|
|
17
|
+
# :positive, :negative, :zero; anything else raises ArgumentError.
|
|
7
18
|
# @param thousand [String, false] Thousands delimiter (e.g., ',' for 1,000)
|
|
8
19
|
# @param decimal [String] Decimal separator (e.g., '.' or ',')
|
|
9
20
|
# @return [String] Formatted money string
|
|
10
21
|
#
|
|
22
|
+
# @raise [ArgumentError] if +format+ is not a String or Hash, the Hash
|
|
23
|
+
# is empty, or the Hash contains an unrecognised key.
|
|
24
|
+
#
|
|
11
25
|
# @example Basic formatting
|
|
12
26
|
# money = Mint.money(1234.56, 'USD')
|
|
13
27
|
# money.to_s #=> "$1,234.56"
|
|
@@ -20,12 +34,22 @@ module Mint
|
|
|
20
34
|
# money.to_s(format: '%<amount>f %<symbol>s') #=> "1234.56 $"
|
|
21
35
|
# money.to_s(format: '%<symbol>s%<amount>+f') #=> "$+1234.56"
|
|
22
36
|
#
|
|
37
|
+
# @example Per-sign Hash format (accounting parentheses)
|
|
38
|
+
# loss = Mint.money(-1234.56, 'USD')
|
|
39
|
+
# loss.to_s(format: { negative: '(%<symbol>s%<amount>f)' }) #=> "($1,234.56)"
|
|
40
|
+
# Mint.money(0, 'BRL').to_s(format: { zero: '--' }) #=> "--"
|
|
41
|
+
#
|
|
23
42
|
# @example Padding and alignment
|
|
24
43
|
# money.to_s(format: '%<amount>10.2f') #=> " 1234.56"
|
|
25
44
|
# money.to_s(format: '%<symbol>s%<amount>010.2f') #=> "$0001234.56"
|
|
26
45
|
#
|
|
27
46
|
def to_s(format: '%<symbol>s%<amount>f', decimal: '.', thousand: ',', width: nil)
|
|
28
|
-
|
|
47
|
+
case format
|
|
48
|
+
when {}, '', nil then raise ArgumentError, 'format must not be empty or null'
|
|
49
|
+
when Hash then validate_format_hash!(format)
|
|
50
|
+
when String # noop
|
|
51
|
+
else raise ArgumentError, 'Invalid format'
|
|
52
|
+
end
|
|
29
53
|
|
|
30
54
|
formatted = format_amount(format)
|
|
31
55
|
|
|
@@ -43,17 +67,27 @@ module Mint
|
|
|
43
67
|
|
|
44
68
|
private
|
|
45
69
|
|
|
70
|
+
def validate_format_hash!(format)
|
|
71
|
+
unknown = format.keys - %i[positive negative zero]
|
|
72
|
+
|
|
73
|
+
raise ArgumentError, "Unknown format parameter(s): #{unknown.inspect}. " unless unknown.empty?
|
|
74
|
+
end
|
|
75
|
+
|
|
46
76
|
def format_amount(format)
|
|
47
77
|
format = { positive: format } if format.is_a?(String)
|
|
48
|
-
|
|
78
|
+
positive_format = format[:positive]
|
|
79
|
+
negative_format = format[:negative]
|
|
80
|
+
zero_format = format[:zero]
|
|
49
81
|
|
|
50
|
-
if amount.negative? &&
|
|
51
|
-
format =
|
|
82
|
+
if amount.negative? && negative_format
|
|
83
|
+
format = negative_format
|
|
52
84
|
value = -amount
|
|
53
|
-
elsif amount.zero? &&
|
|
54
|
-
format =
|
|
85
|
+
elsif amount.zero? && zero_format
|
|
86
|
+
format = zero_format
|
|
87
|
+
value = amount
|
|
55
88
|
else
|
|
56
|
-
format =
|
|
89
|
+
format = positive_format
|
|
90
|
+
value = amount
|
|
57
91
|
end
|
|
58
92
|
format ||= '%<symbol>s%<amount>f'
|
|
59
93
|
|