minting 1.7.3 → 1.8.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 +25 -8
- data/doc/Mint/Currency.html +535 -18
- data/doc/Mint/Money.html +659 -185
- data/doc/Mint/RangeStepPatch.html +1 -1
- data/doc/Mint/Registry.html +19 -2
- data/doc/Mint/Rounding.html +495 -0
- data/doc/Mint/UnknownCurrency.html +1 -1
- data/doc/Mint.html +187 -399
- data/doc/Minting.html +2 -2
- data/doc/_index.html +8 -1
- data/doc/agents/api_review-2026-06-15.md +0 -13
- data/doc/agents/copilot-instructions.md +70 -0
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +19 -9
- data/doc/index.html +19 -9
- data/doc/method_list.html +130 -42
- data/doc/top-level-namespace.html +1 -1
- data/lib/minting/currency/currency.rb +41 -2
- data/lib/minting/mint/dsl/range.rb +1 -0
- data/lib/minting/mint/mint.rb +10 -37
- data/lib/minting/mint/parser/parser.rb +50 -15
- data/lib/minting/mint/parser/separators.rb +10 -8
- data/lib/minting/mint/registry/zeros.rb +2 -0
- data/lib/minting/mint/rounding.rb +51 -0
- data/lib/minting/mint.rb +1 -0
- data/lib/minting/money/allocation/allocation.rb +1 -2
- data/lib/minting/money/allocation/split.rb +1 -1
- data/lib/minting/money/arithmetics/methods.rb +2 -2
- data/lib/minting/money/arithmetics/operators.rb +6 -6
- data/lib/minting/money/clamp.rb +1 -1
- data/lib/minting/money/coercion.rb +1 -1
- data/lib/minting/money/comparable.rb +6 -0
- data/lib/minting/money/constructors.rb +60 -12
- data/lib/minting/money/money.rb +0 -6
- data/lib/minting/version.rb +1 -1
- metadata +4 -1
|
@@ -10,24 +10,26 @@ module Mint
|
|
|
10
10
|
# @private
|
|
11
11
|
def classify_separators(numeric)
|
|
12
12
|
case [numeric.count('.'), numeric.count(',')]
|
|
13
|
-
in [0, 1] if numeric[-4] == ',' then :thousands_comma
|
|
14
|
-
in [0, 1] then :decimal_comma
|
|
15
|
-
in [0, 0] | [1, 0] then :decimal_period
|
|
16
|
-
in [p, c] if p > 1 && c > 1 then :
|
|
17
|
-
in [p, c] if p > 0 && c > 0 then :mixed
|
|
18
|
-
else :thousands
|
|
13
|
+
in [0, 1] if numeric[-4] == ',' then :thousands_comma # Comma is a thousand separator
|
|
14
|
+
in [0, 1] then :decimal_comma # Only one comma: decimal (e.g. 19,99 or 1,4 or 1,2345).
|
|
15
|
+
in [0, 0] | [1, 0] then :decimal_period # e.g. "1500" or "34.21".
|
|
16
|
+
in [p, c] if p > 1 && c > 1 then :invalid # Both separators appear multiple times
|
|
17
|
+
in [p, c] if p > 0 && c > 0 then :mixed # Commas and dots: the rightmost one is the decimal
|
|
18
|
+
else :thousands # Multiple of the same separator only (e.g. 1,234,567)
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
# Converts locale-specific decimal/thousand separators into a plain decimal string.
|
|
23
23
|
# @private
|
|
24
|
-
def
|
|
24
|
+
def parse_separators(numeric)
|
|
25
|
+
return nil unless numeric.match?(/\d/)
|
|
26
|
+
|
|
25
27
|
case classify_separators(numeric)
|
|
26
28
|
when :decimal_period then numeric # Nothing to normalize (e.g. "1500" or "34.21").
|
|
27
29
|
when :decimal_comma then numeric.tr(',', '.') # Only one comma: decimal (e.g. 19,99 or 1,234).
|
|
28
30
|
when :thousands_comma then numeric.delete(',')
|
|
29
31
|
when :thousands then numeric.delete('.,')
|
|
30
|
-
when :
|
|
32
|
+
when :invalid then nil
|
|
31
33
|
when :mixed # Commas and dots: the rightmost one is the decimal separator.
|
|
32
34
|
if numeric.rindex(',') > numeric.rindex('.')
|
|
33
35
|
numeric.delete('.').tr(',', '.')
|
|
@@ -9,6 +9,8 @@ module Mint
|
|
|
9
9
|
# @return [Money] a frozen zero-Money
|
|
10
10
|
# @api private
|
|
11
11
|
def self.zero_for(currency)
|
|
12
|
+
raise ArgumentError, "Expect a Currency param. (#{currency})" unless currency.is_a?(Currency)
|
|
13
|
+
|
|
12
14
|
MUTEX.synchronize do
|
|
13
15
|
@zeros ||= {}
|
|
14
16
|
@zeros[currency] ||= Mint::Money.send(:new, 0, currency)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mint
|
|
4
|
+
# Rounding-mode dispatch table and block-scoped context.
|
|
5
|
+
# @api private
|
|
6
|
+
module Rounding
|
|
7
|
+
# Maps mode symbols to their corresponding +Rational+ rounding lambdas.
|
|
8
|
+
# @return [Hash{Symbol => Proc}]
|
|
9
|
+
# @api private
|
|
10
|
+
MODES = {
|
|
11
|
+
half_up: ->(amount, ndigits) { amount.round(ndigits, half: :up) },
|
|
12
|
+
half_down: ->(amount, ndigits) { amount.round(ndigits, half: :down) },
|
|
13
|
+
floor: ->(amount, ndigits) { amount.floor(ndigits) },
|
|
14
|
+
ceil: ->(amount, ndigits) { amount.ceil(ndigits) },
|
|
15
|
+
truncate: ->(amount, ndigits) { amount.truncate(ndigits) },
|
|
16
|
+
down: ->(amount, ndigits) { amount.truncate(ndigits) }
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
# Returns the currently active rounding mode, falling back to +:half_up+.
|
|
20
|
+
# @api private
|
|
21
|
+
# @return [Symbol]
|
|
22
|
+
def self.current_mode
|
|
23
|
+
Thread.current[:minting_rounding_mode] || :half_up
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Rounds +amount+ to +ndigits+ using the currently scoped rounding mode.
|
|
27
|
+
# @api private
|
|
28
|
+
# @param amount [Numeric]
|
|
29
|
+
# @param ndigits [Integer]
|
|
30
|
+
# @return [Rational]
|
|
31
|
+
def self.apply(amount, ndigits)
|
|
32
|
+
MODES.fetch(current_mode).call(amount.to_r, ndigits)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Sets a rounding mode for the duration of a block, restoring the
|
|
36
|
+
# previous mode on exit (even on exception).
|
|
37
|
+
# @api private
|
|
38
|
+
# @param mode [Symbol]
|
|
39
|
+
# @yield block to execute with the mode active
|
|
40
|
+
# @raise [ArgumentError] on unknown mode
|
|
41
|
+
def self.with_mode(mode)
|
|
42
|
+
raise ArgumentError, "Unknown rounding mode: #{mode}" unless MODES.key?(mode)
|
|
43
|
+
|
|
44
|
+
prev = Thread.current[:minting_rounding_mode]
|
|
45
|
+
Thread.current[:minting_rounding_mode] = mode
|
|
46
|
+
yield
|
|
47
|
+
ensure
|
|
48
|
+
Thread.current[:minting_rounding_mode] = prev
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/minting/mint.rb
CHANGED
|
@@ -17,8 +17,7 @@ module Mint
|
|
|
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
|
-
|
|
21
|
-
amounts = proportions.map { |rate| Rational(amount * rate, whole).round(subunit) }
|
|
20
|
+
amounts = proportions.map { |rate| currency.normalize_amount(Rational(amount * rate, whole)) }
|
|
22
21
|
allocate_left_over(amounts: amounts, left_over: amount - amounts.sum)
|
|
23
22
|
end
|
|
24
23
|
end
|
|
@@ -17,7 +17,7 @@ module Mint
|
|
|
17
17
|
def split(slices)
|
|
18
18
|
raise ArgumentError, 'Slices quantity must be an poitive integer' unless slices.positive? && slices.integer?
|
|
19
19
|
|
|
20
|
-
fraction = (amount / slices)
|
|
20
|
+
fraction = currency.normalize_amount(amount / slices)
|
|
21
21
|
allocate_left_over(amounts: Array.new(slices, fraction),
|
|
22
22
|
left_over: amount - (fraction * slices))
|
|
23
23
|
end
|
|
@@ -6,7 +6,7 @@ module Mint
|
|
|
6
6
|
# Returns the absolute value of the monetary amount as a new {Money} instance.
|
|
7
7
|
#
|
|
8
8
|
# @return [Money] the absolute value
|
|
9
|
-
def abs =
|
|
9
|
+
def abs = copy_with(amount: amount.abs)
|
|
10
10
|
|
|
11
11
|
# Returns true if the monetary amount is less than zero.
|
|
12
12
|
#
|
|
@@ -22,6 +22,6 @@ module Mint
|
|
|
22
22
|
# Enables standard ranges and stepping (e.g. `1.dollar..10.dollars`).
|
|
23
23
|
#
|
|
24
24
|
# @return [Money] successor Money instance
|
|
25
|
-
def succ =
|
|
25
|
+
def succ = copy_with(amount: amount + currency.minimum_amount)
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -11,7 +11,7 @@ module Mint
|
|
|
11
11
|
def +(addend)
|
|
12
12
|
case addend
|
|
13
13
|
in 0 then self
|
|
14
|
-
in Money if same_currency?(addend) then
|
|
14
|
+
in Money if same_currency?(addend) then copy_with(amount: amount + addend.amount)
|
|
15
15
|
else raise TypeError, "#{addend} can't be added to #{self}"
|
|
16
16
|
end
|
|
17
17
|
end
|
|
@@ -24,7 +24,7 @@ module Mint
|
|
|
24
24
|
def -(subtrahend)
|
|
25
25
|
case subtrahend
|
|
26
26
|
when 0 then return self
|
|
27
|
-
when Money then return
|
|
27
|
+
when Money then return copy_with(amount: amount - subtrahend.amount) if same_currency?(subtrahend)
|
|
28
28
|
end
|
|
29
29
|
raise TypeError, "#{subtrahend} can't be subtracted from #{self}"
|
|
30
30
|
end
|
|
@@ -32,7 +32,7 @@ module Mint
|
|
|
32
32
|
# Unary negation operator. Returns a new {Money} instance with the inverted sign.
|
|
33
33
|
#
|
|
34
34
|
# @return [Money] negated Money instance
|
|
35
|
-
def -@ =
|
|
35
|
+
def -@ = copy_with(amount: -amount)
|
|
36
36
|
|
|
37
37
|
# Performs multiplication of the monetary value by a standard scalar Numeric.
|
|
38
38
|
#
|
|
@@ -42,7 +42,7 @@ module Mint
|
|
|
42
42
|
def *(multiplicand)
|
|
43
43
|
raise TypeError, "#{self} can't be multiplied by #{multiplicand}" unless multiplicand.is_a?(Numeric)
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
copy_with(amount: amount * multiplicand)
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# Performs division of the monetary value by a scalar Numeric or identical currency {Money}.
|
|
@@ -53,7 +53,7 @@ module Mint
|
|
|
53
53
|
# @raise [ZeroDivisionError] if division by zero is attempted
|
|
54
54
|
def /(divisor)
|
|
55
55
|
case divisor
|
|
56
|
-
when Numeric then return
|
|
56
|
+
when Numeric then return copy_with(amount: amount / divisor)
|
|
57
57
|
when Money then return amount / divisor.amount if same_currency? divisor
|
|
58
58
|
end
|
|
59
59
|
raise TypeError, "#{self} can't be divided by #{divisor}"
|
|
@@ -65,7 +65,7 @@ module Mint
|
|
|
65
65
|
# @return [Money] reult of amount ** exponent
|
|
66
66
|
# @raise [TypeError] if exponent is not Numeric
|
|
67
67
|
def **(exponent)
|
|
68
|
-
return
|
|
68
|
+
return copy_with(amount: amount**exponent) if exponent.is_a?(Numeric)
|
|
69
69
|
|
|
70
70
|
raise TypeError, "#{self} can't be powered by #{exponent}"
|
|
71
71
|
end
|
data/lib/minting/money/clamp.rb
CHANGED
|
@@ -44,7 +44,7 @@ module Mint
|
|
|
44
44
|
# Multiplies a Money object by the wrapped numeric value.
|
|
45
45
|
# This is the standard coercion path for `Numeric * Money`.
|
|
46
46
|
def *(other)
|
|
47
|
-
other.
|
|
47
|
+
other.copy_with(amount: @value * other.amount)
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
# @private
|
|
@@ -44,6 +44,12 @@ module Mint
|
|
|
44
44
|
# @return [self, nil] self if amount is non-zero, nil otherwise
|
|
45
45
|
def nonzero? = amount.nonzero?
|
|
46
46
|
|
|
47
|
+
# Helper method to verify if another Money has the identical currency.
|
|
48
|
+
#
|
|
49
|
+
# @param other [Money] the target currency to compare
|
|
50
|
+
# @return [Boolean] true if currencies match, false otherwise
|
|
51
|
+
def same_currency?(other) = other.currency == currency
|
|
52
|
+
|
|
47
53
|
# @return [Boolean] true if amount is zero
|
|
48
54
|
def zero? = amount.zero?
|
|
49
55
|
end
|
|
@@ -7,13 +7,56 @@ module Mint
|
|
|
7
7
|
# @param amount [Numeric] The monetary amount
|
|
8
8
|
# @param currency [Currency, String] The currency code or currency object
|
|
9
9
|
# @raise [ArgumentError] If amount is not numeric or currency is invalid
|
|
10
|
-
def self.
|
|
10
|
+
def self.from(amount, currency)
|
|
11
11
|
raise ArgumentError, 'amount must be Numeric' unless amount.is_a?(Numeric)
|
|
12
12
|
|
|
13
13
|
currency = Currency.resolve!(currency)
|
|
14
14
|
amount = currency.normalize_amount(amount)
|
|
15
15
|
|
|
16
|
-
amount.zero? ?
|
|
16
|
+
amount.zero? ? currency.zero : new(amount, currency)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Parses a human-readable money string into a {Money} object.
|
|
20
|
+
#
|
|
21
|
+
# Returns +nil+ when the input is invalid or currency cannot be determined.
|
|
22
|
+
#
|
|
23
|
+
# @param input [String] Amount input, optionally including a currency symbol or code
|
|
24
|
+
# @param currency [String, Symbol, Currency, nil] ISO code when not present in +input+
|
|
25
|
+
# @return [Money, nil]
|
|
26
|
+
#
|
|
27
|
+
# @example With explicit currency
|
|
28
|
+
# Money.parse('19.99', 'USD') #=> [USD 19.99]
|
|
29
|
+
# Money.parse('garbage', 'USD') #=> nil
|
|
30
|
+
#
|
|
31
|
+
# @example With symbol or code in the string
|
|
32
|
+
# Money.parse('$19.99') #=> [USD 19.99]
|
|
33
|
+
# Money.parse('USD 1,234.56') #=> [USD 1234.56]
|
|
34
|
+
def self.parse(input, currency = nil) = Mint.parse(input, currency)
|
|
35
|
+
|
|
36
|
+
# Like {.parse} but raises on failure.
|
|
37
|
+
#
|
|
38
|
+
# @param input [String] Amount input, optionally including a currency symbol or code
|
|
39
|
+
# @param currency [String, Symbol, Currency, nil] ISO code when not present in +input+
|
|
40
|
+
# @return [Money]
|
|
41
|
+
# @raise [ArgumentError] when +input+ is invalid or currency cannot be determined
|
|
42
|
+
#
|
|
43
|
+
# @example
|
|
44
|
+
# Money.parse!('19.99', 'USD') #=> [USD 19.99]
|
|
45
|
+
# Money.parse!('garbage', 'USD') #=> ArgumentError
|
|
46
|
+
def self.parse!(input, currency = nil) = Mint.parse!(input, currency)
|
|
47
|
+
|
|
48
|
+
# Returns a frozen zero Money in the given currency.
|
|
49
|
+
#
|
|
50
|
+
# @param currency [String, Currency] a currency code or object
|
|
51
|
+
# @return [Money] a frozen zero-Money
|
|
52
|
+
# @raise [ArgumentError] if the currency can't be resolved
|
|
53
|
+
def self.zero(currency) = Currency.resolve!(currency).zero
|
|
54
|
+
|
|
55
|
+
# Backwards-compatible alias for previous API
|
|
56
|
+
# TODO: deprecate in a future major release
|
|
57
|
+
def self.create(amount, currency)
|
|
58
|
+
warn 'Money.create is now deprecated. Use Money.from'
|
|
59
|
+
from(amount, currency)
|
|
17
60
|
end
|
|
18
61
|
|
|
19
62
|
# Builds a Money from a fractional (smallest-unit) Integer amount.
|
|
@@ -39,31 +82,36 @@ module Mint
|
|
|
39
82
|
|
|
40
83
|
currency = Currency.resolve!(currency)
|
|
41
84
|
amount = Rational(fractional, currency.fractional_multiplier)
|
|
42
|
-
amount.zero? ?
|
|
85
|
+
amount.zero? ? currency.zero : new(amount, currency)
|
|
43
86
|
end
|
|
44
87
|
|
|
45
88
|
# Returns a new Money object with the specified amount, or self if unchanged.
|
|
46
89
|
# This is the primary method for creating a modified copy of a Money instance
|
|
47
90
|
# while preserving immutability.
|
|
48
91
|
#
|
|
49
|
-
# @param
|
|
92
|
+
# @param amount [Numeric] The new monetary amount
|
|
50
93
|
# @return [Money] A new Money object with the new amount, or self if the amount is unchanged
|
|
51
94
|
# @example
|
|
52
95
|
# price = Mint.money(10.00, 'USD')
|
|
53
|
-
# price.
|
|
54
|
-
# price.
|
|
55
|
-
def
|
|
56
|
-
|
|
96
|
+
# price.copy_with(amount: 15.00) #=> [USD 15.00]
|
|
97
|
+
# price.copy_with(amount: 10.00) #=> [USD 10.00] (returns self)
|
|
98
|
+
def copy_with(amount:)
|
|
99
|
+
amount = currency.normalize_amount(amount)
|
|
57
100
|
|
|
58
|
-
if
|
|
101
|
+
if amount == self.amount
|
|
59
102
|
self
|
|
60
|
-
elsif
|
|
61
|
-
|
|
103
|
+
elsif amount.zero?
|
|
104
|
+
currency.zero
|
|
62
105
|
else
|
|
63
|
-
Money.new(
|
|
106
|
+
Money.new(amount, currency)
|
|
64
107
|
end
|
|
65
108
|
end
|
|
66
109
|
|
|
110
|
+
def mint(new_amount)
|
|
111
|
+
warn 'Money#mint is now deprecated and will be removed in v2'
|
|
112
|
+
copy_with(amount: new_amount)
|
|
113
|
+
end
|
|
114
|
+
|
|
67
115
|
private
|
|
68
116
|
|
|
69
117
|
# Initializes a new Money object with the given amount and currency.
|
data/lib/minting/money/money.rb
CHANGED
|
@@ -49,11 +49,5 @@ module Mint
|
|
|
49
49
|
def inspect
|
|
50
50
|
Kernel.format "[#{currency_code} %0.#{currency.subunit}f]", amount
|
|
51
51
|
end
|
|
52
|
-
|
|
53
|
-
# Helper method to verify if another object has the identical currency.
|
|
54
|
-
#
|
|
55
|
-
# @param other [Currency] the target currency to compare
|
|
56
|
-
# @return [Boolean] true if currencies match, false otherwise
|
|
57
|
-
def same_currency?(other) = other.currency == currency
|
|
58
52
|
end
|
|
59
53
|
end
|
data/lib/minting/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: minting
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gilson Ferraz
|
|
@@ -42,10 +42,12 @@ files:
|
|
|
42
42
|
- doc/Mint/Money.html
|
|
43
43
|
- doc/Mint/RangeStepPatch.html
|
|
44
44
|
- doc/Mint/Registry.html
|
|
45
|
+
- doc/Mint/Rounding.html
|
|
45
46
|
- doc/Mint/UnknownCurrency.html
|
|
46
47
|
- doc/Minting.html
|
|
47
48
|
- doc/_index.html
|
|
48
49
|
- doc/agents/api_review-2026-06-15.md
|
|
50
|
+
- doc/agents/copilot-instructions.md
|
|
49
51
|
- doc/agents/expired/AGENTS.md
|
|
50
52
|
- doc/agents/expired/copilot-instructions.md
|
|
51
53
|
- doc/agents/expired/gemini_gem_evaluation.md
|
|
@@ -81,6 +83,7 @@ files:
|
|
|
81
83
|
- lib/minting/mint/registry/registry.rb
|
|
82
84
|
- lib/minting/mint/registry/symbols.rb
|
|
83
85
|
- lib/minting/mint/registry/zeros.rb
|
|
86
|
+
- lib/minting/mint/rounding.rb
|
|
84
87
|
- lib/minting/money/allocation/allocation.rb
|
|
85
88
|
- lib/minting/money/allocation/split.rb
|
|
86
89
|
- lib/minting/money/arithmetics/methods.rb
|