minting 1.7.0 → 1.7.3
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 +80 -112
- data/Rakefile +13 -1
- data/bin/bench_check +46 -0
- data/doc/Mint/Currency.html +446 -46
- data/doc/Mint/CurrencyRegistry.html +7 -7
- data/doc/Mint/Money.html +203 -177
- data/doc/Mint/RangeStepPatch.html +277 -0
- data/doc/Mint/Registry.html +842 -0
- data/doc/Mint/UnknownCurrency.html +2 -2
- data/doc/Mint.html +385 -66
- data/doc/Minting.html +3 -3
- data/doc/_index.html +24 -9
- data/doc/agents/api_review-2026-06-15.md +342 -0
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +97 -89
- data/doc/index.html +97 -89
- data/doc/method_list.html +97 -25
- data/doc/top-level-namespace.html +13 -5
- data/lib/minting/currency/currency.rb +75 -0
- data/lib/minting/mint/aliases.rb +3 -0
- data/lib/minting/mint/dsl/{refinements.rb → numeric.rb} +6 -5
- data/lib/minting/mint/dsl/range.rb +31 -18
- data/lib/minting/mint/dsl/string.rb +12 -0
- data/lib/minting/mint/dsl/top_level.rb +3 -0
- data/lib/minting/mint/locale_backend.rb +29 -0
- data/lib/minting/mint/mint.rb +28 -12
- data/lib/minting/mint/parser/parser.rb +62 -0
- data/lib/minting/mint/parser/separators.rb +39 -0
- data/lib/minting/mint/registry/registration.rb +33 -0
- data/lib/minting/mint/registry/registry.rb +38 -0
- data/lib/minting/mint/registry/symbols.rb +49 -0
- data/lib/minting/mint/registry/zeros.rb +18 -0
- data/lib/minting/mint.rb +13 -16
- data/lib/minting/money/allocation/allocation.rb +25 -0
- data/lib/minting/money/{allocation.rb → allocation/split.rb} +1 -19
- data/lib/minting/money/arithmetics/methods.rb +27 -0
- data/lib/minting/money/{arithmetics.rb → arithmetics/operators.rb} +0 -21
- data/lib/minting/money/clamp.rb +66 -0
- data/lib/minting/money/coercion.rb +10 -0
- data/lib/minting/money/comparable.rb +6 -0
- data/lib/minting/money/constructors.rb +14 -9
- data/lib/minting/money/format/formatting.rb +60 -0
- data/lib/minting/money/{formatting.rb → format/to_s.rb} +13 -36
- data/lib/minting/money/money.rb +12 -58
- data/lib/minting/version.rb +1 -1
- metadata +29 -19
- data/lib/minting/mint/currency/currency.rb +0 -36
- data/lib/minting/mint/currency/currency_registry.rb +0 -67
- data/lib/minting/mint/currency/world_currencies.rb +0 -16
- data/lib/minting/mint/parser.rb +0 -85
- /data/doc/agents/{AGENTS.md → expired/AGENTS.md} +0 -0
- /data/doc/agents/{copilot-instructions.md → expired/copilot-instructions.md} +0 -0
- /data/doc/agents/{gemini_gem_evaluation.md → expired/gemini_gem_evaluation.md} +0 -0
- /data/doc/agents/{recommendations.md → expired/recommendations.md} +0 -0
- /data/doc/agents/{rubocop-issues.md → expired/rubocop-issues.md} +0 -0
|
@@ -3,25 +3,6 @@
|
|
|
3
3
|
module Mint
|
|
4
4
|
# Allocation and splitting
|
|
5
5
|
class Money
|
|
6
|
-
# Proportionally allocates the monetary amount among a list of ratios.
|
|
7
|
-
# Disperses any subunit rounding amounts across the initial slots
|
|
8
|
-
# @param proportions [Array<Numeric>] a list of numeric proportions/ratios to allocate by
|
|
9
|
-
# @return [Array<Money>] the list of newly allocated Money objects
|
|
10
|
-
# @raise [ArgumentError] if the proportions list is empty or sums to zero
|
|
11
|
-
#
|
|
12
|
-
# @example Proportional allocation
|
|
13
|
-
# money = Mint.money(10.00, 'USD')
|
|
14
|
-
# money.allocate([1, 2, 3]) #=> [[USD 1.67], [USD 3.33], [USD 5.00]]
|
|
15
|
-
def allocate(proportions)
|
|
16
|
-
whole = proportions.sum.to_r
|
|
17
|
-
raise ArgumentError, 'Need at least 1 proportion element' if proportions.empty?
|
|
18
|
-
raise ArgumentError, 'Proportions total must not be zero' if whole.zero?
|
|
19
|
-
|
|
20
|
-
subunit = currency.subunit
|
|
21
|
-
amounts = proportions.map { |rate| Rational(amount * rate, whole).round(subunit) }
|
|
22
|
-
allocate_left_over(amounts: amounts, left_over: amount - amounts.sum)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
6
|
# Splits the monetary amount into a given quantity of equal parts.
|
|
26
7
|
# Disperses any fractional subunit rounding differences across the initial slots
|
|
27
8
|
# so that the sum is preserved.
|
|
@@ -46,6 +27,7 @@ module Mint
|
|
|
46
27
|
# Distributes any leftover amount across the allocation slots by adjusting
|
|
47
28
|
# individual amounts by the currency's minimum unit, and converting to Money.
|
|
48
29
|
# Caution: amounts array is mutated by this method
|
|
30
|
+
# @private
|
|
49
31
|
def allocate_left_over(amounts:, left_over:)
|
|
50
32
|
if left_over.nonzero?
|
|
51
33
|
minimum = currency.minimum_amount
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mint
|
|
4
|
+
# Money Arithmetics
|
|
5
|
+
class Money
|
|
6
|
+
# Returns the absolute value of the monetary amount as a new {Money} instance.
|
|
7
|
+
#
|
|
8
|
+
# @return [Money] the absolute value
|
|
9
|
+
def abs = mint(amount.abs)
|
|
10
|
+
|
|
11
|
+
# Returns true if the monetary amount is less than zero.
|
|
12
|
+
#
|
|
13
|
+
# @return [Boolean] true if negative, false otherwise
|
|
14
|
+
def negative? = amount.negative?
|
|
15
|
+
|
|
16
|
+
# Returns true if the monetary amount is greater than zero.
|
|
17
|
+
#
|
|
18
|
+
# @return [Boolean] true if positive, false otherwise
|
|
19
|
+
def positive? = amount.positive?
|
|
20
|
+
|
|
21
|
+
# Returns the successor of the Money instance by adding the minimum possible subunit amount.
|
|
22
|
+
# Enables standard ranges and stepping (e.g. `1.dollar..10.dollars`).
|
|
23
|
+
#
|
|
24
|
+
# @return [Money] successor Money instance
|
|
25
|
+
def succ = mint(amount + currency.minimum_amount)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -3,27 +3,6 @@
|
|
|
3
3
|
module Mint
|
|
4
4
|
# Money Arithmetics
|
|
5
5
|
class Money
|
|
6
|
-
# Returns the absolute value of the monetary amount as a new {Money} instance.
|
|
7
|
-
#
|
|
8
|
-
# @return [Money] the absolute value
|
|
9
|
-
def abs = mint(amount.abs)
|
|
10
|
-
|
|
11
|
-
# Returns true if the monetary amount is less than zero.
|
|
12
|
-
#
|
|
13
|
-
# @return [Boolean] true if negative, false otherwise
|
|
14
|
-
def negative? = amount.negative?
|
|
15
|
-
|
|
16
|
-
# Returns true if the monetary amount is greater than zero.
|
|
17
|
-
#
|
|
18
|
-
# @return [Boolean] true if positive, false otherwise
|
|
19
|
-
def positive? = amount.positive?
|
|
20
|
-
|
|
21
|
-
# Returns the successor of the Money instance by adding the minimum possible subunit amount.
|
|
22
|
-
# Enables standard ranges and stepping (e.g. `1.dollar..10.dollars`).
|
|
23
|
-
#
|
|
24
|
-
# @return [Money] successor Money instance
|
|
25
|
-
def succ = mint(amount + currency.minimum_amount)
|
|
26
|
-
|
|
27
6
|
# Performs addition with another {Money} instance or standard zero Numeric.
|
|
28
7
|
#
|
|
29
8
|
# @param addend [Money, Numeric] the value to add
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mint
|
|
4
|
+
# Money clamp
|
|
5
|
+
class Money
|
|
6
|
+
# Constrains +self+ to the inclusive range [+min+, +max+].
|
|
7
|
+
#
|
|
8
|
+
# Bounds may be:
|
|
9
|
+
# - nil meaning no boundary
|
|
10
|
+
# - same-currency {Money} or Range
|
|
11
|
+
# - Numeric amount, or Range
|
|
12
|
+
#
|
|
13
|
+
# Numeric is interpreted as an amount in +self+'s currency, so the common
|
|
14
|
+
# pricing idiom +price.clamp(0, 100)+ reads as "0 to 100 in the same
|
|
15
|
+
# currency as +price+".
|
|
16
|
+
#
|
|
17
|
+
# When +self+ is already in range the receiver is returned (no new object
|
|
18
|
+
# allocated). When out of range, the nearest bound is returned as a new
|
|
19
|
+
# frozen {Money} in +self+'s currency.
|
|
20
|
+
#
|
|
21
|
+
# @param min_or_range [Money, Numeric, Range, nil] lower bound (inclusive), or range
|
|
22
|
+
# @param max [Money, Numeric, nil] upper bound (inclusive)
|
|
23
|
+
# @return [Money] +self+ if in range, otherwise the nearer bound
|
|
24
|
+
# @raise [ArgumentError] if +min+ or +max+ is not a Money, Numeric or nil; if
|
|
25
|
+
# a Money operand has a different currency; if +min+ > +max+;
|
|
26
|
+
# if min is a Range, and max is not nil
|
|
27
|
+
#
|
|
28
|
+
# @example In range
|
|
29
|
+
# Mint.money(5, 'USD').clamp(0, 10) #=> [USD 5.00] (returns self)
|
|
30
|
+
#
|
|
31
|
+
# @example Out of range, with Numeric bounds
|
|
32
|
+
# Mint.money(50, 'USD').clamp(0, 10) #=> [USD 10.00]
|
|
33
|
+
#
|
|
34
|
+
# @example Out of range, with Money bounds
|
|
35
|
+
# loss = Mint.money(-5, 'USD')
|
|
36
|
+
# floor = Mint.money(0, 'USD')
|
|
37
|
+
# ceil = Mint.money(10, 'USD')
|
|
38
|
+
# loss.clamp(floor, ceil) #=> [USD 0.00]
|
|
39
|
+
#
|
|
40
|
+
# @example Subunit-0 currency (JPY)
|
|
41
|
+
# Mint.money(500, 'JPY').clamp(0, 100) #=> [JPY 100]
|
|
42
|
+
def clamp(min_or_range, max = nil)
|
|
43
|
+
if min_or_range.is_a?(Range)
|
|
44
|
+
raise(ArgumentError, "Either amount range alone or two amounts accepted: #{max}") if max
|
|
45
|
+
|
|
46
|
+
min, max = min_or_range.minmax
|
|
47
|
+
else
|
|
48
|
+
min = min_or_range
|
|
49
|
+
end
|
|
50
|
+
mint(amount.clamp(normalize_boundary(min), normalize_boundary(max)))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Converts a clamp boundary to a numeric amount.
|
|
56
|
+
# @private
|
|
57
|
+
def normalize_boundary(boundary)
|
|
58
|
+
case boundary
|
|
59
|
+
in NilClass | Numeric then boundary
|
|
60
|
+
in Money if same_currency?(boundary) then boundary.amount
|
|
61
|
+
in Money then raise ArgumentError, "oundary currency must be: #{currency_code}"
|
|
62
|
+
else raise ArgumentError, "Boundary must be Numeric or Money #{boundary}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -23,6 +23,8 @@ module Mint
|
|
|
23
23
|
def initialize(value) = @value = value
|
|
24
24
|
|
|
25
25
|
# @private
|
|
26
|
+
# Adds a CoercedNumber to a Money object.
|
|
27
|
+
# Only zero is a valid additive identity (returns the Money unchanged).
|
|
26
28
|
def +(other)
|
|
27
29
|
raise_coercion_error(:+, other) unless @value.zero?
|
|
28
30
|
|
|
@@ -30,6 +32,8 @@ module Mint
|
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
# @private
|
|
35
|
+
# Subtracts a Money object from a CoercedNumber.
|
|
36
|
+
# Only zero is valid (returns the negated Money).
|
|
33
37
|
def -(other)
|
|
34
38
|
raise_coercion_error(:-, other) unless @value.zero?
|
|
35
39
|
|
|
@@ -37,15 +41,20 @@ module Mint
|
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
# @private
|
|
44
|
+
# Multiplies a Money object by the wrapped numeric value.
|
|
45
|
+
# This is the standard coercion path for `Numeric * Money`.
|
|
40
46
|
def *(other)
|
|
41
47
|
other.mint(@value * other.amount)
|
|
42
48
|
end
|
|
43
49
|
|
|
44
50
|
# @private
|
|
51
|
+
# Divides a CoercedNumber by a Money object.
|
|
52
|
+
# Not a meaningful operation (what currency is the result?).
|
|
45
53
|
def /(other)
|
|
46
54
|
raise_coercion_error(:/, other)
|
|
47
55
|
end
|
|
48
56
|
|
|
57
|
+
# @private
|
|
49
58
|
# Only zero is dimensionless and comparable to Money.
|
|
50
59
|
# e.g. 0 < price is meaningful; 0.5 < price is not (what currency is 0.5?).
|
|
51
60
|
def <=>(other)
|
|
@@ -56,6 +65,7 @@ module Mint
|
|
|
56
65
|
|
|
57
66
|
private
|
|
58
67
|
|
|
68
|
+
# Raises a TypeError with a descriptive message for unsupported coercions.
|
|
59
69
|
def raise_coercion_error(operation, operand)
|
|
60
70
|
raise TypeError, "#{@value} #{operation} #{operand} : incompatible operands"
|
|
61
71
|
end
|
|
@@ -14,6 +14,10 @@ module Mint
|
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# Strict equality — both amount and currency must match exactly.
|
|
18
|
+
# Unlike ==, does not treat zero as equivalent across currencies.
|
|
19
|
+
#
|
|
20
|
+
# @return [Boolean]
|
|
17
21
|
def eql?(other)
|
|
18
22
|
other.is_a?(Mint::Money) &&
|
|
19
23
|
amount == other.amount &&
|
|
@@ -37,8 +41,10 @@ module Mint
|
|
|
37
41
|
end
|
|
38
42
|
end
|
|
39
43
|
|
|
44
|
+
# @return [self, nil] self if amount is non-zero, nil otherwise
|
|
40
45
|
def nonzero? = amount.nonzero?
|
|
41
46
|
|
|
47
|
+
# @return [Boolean] true if amount is zero
|
|
42
48
|
def zero? = amount.zero?
|
|
43
49
|
end
|
|
44
50
|
end
|
|
@@ -10,10 +10,10 @@ module Mint
|
|
|
10
10
|
def self.create(amount, currency)
|
|
11
11
|
raise ArgumentError, 'amount must be Numeric' unless amount.is_a?(Numeric)
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
currency = Currency.resolve!(currency)
|
|
14
|
+
amount = currency.normalize_amount(amount)
|
|
15
15
|
|
|
16
|
-
new(
|
|
16
|
+
amount.zero? ? Mint.zero(currency) : new(amount, currency)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# Builds a Money from a fractional (smallest-unit) Integer amount.
|
|
@@ -37,11 +37,9 @@ module Mint
|
|
|
37
37
|
def self.from_fractional(fractional, currency)
|
|
38
38
|
raise ArgumentError, 'fractional must be an Integer' unless fractional.is_a?(Integer)
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
amount = Rational(fractional, checked_currency.fractional_multiplier)
|
|
44
|
-
new(amount, checked_currency)
|
|
40
|
+
currency = Currency.resolve!(currency)
|
|
41
|
+
amount = Rational(fractional, currency.fractional_multiplier)
|
|
42
|
+
amount.zero? ? Mint.zero(currency) : new(amount, currency)
|
|
45
43
|
end
|
|
46
44
|
|
|
47
45
|
# Returns a new Money object with the specified amount, or self if unchanged.
|
|
@@ -56,7 +54,14 @@ module Mint
|
|
|
56
54
|
# price.mint(10.00) #=> [USD 10.00] (returns self)
|
|
57
55
|
def mint(new_amount)
|
|
58
56
|
new_amount = currency.normalize_amount(new_amount)
|
|
59
|
-
|
|
57
|
+
|
|
58
|
+
if new_amount == amount
|
|
59
|
+
self
|
|
60
|
+
elsif new_amount.zero?
|
|
61
|
+
Mint.zero(currency)
|
|
62
|
+
else
|
|
63
|
+
Money.new(new_amount, currency)
|
|
64
|
+
end
|
|
60
65
|
end
|
|
61
66
|
|
|
62
67
|
private
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mint
|
|
4
|
+
# Formatting functionality for Money objects
|
|
5
|
+
class Money
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# Resolves format/decimal/thousand from locale_backend when not explicitly given.
|
|
9
|
+
# @private
|
|
10
|
+
def resolve_locale_for(format, decimal, thousand)
|
|
11
|
+
locale = locale_backend
|
|
12
|
+
[format || locale[:format] || '%<symbol>s%<amount>f',
|
|
13
|
+
decimal || locale[:decimal] || '.',
|
|
14
|
+
thousand || locale[:thousand] || ',']
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def locale_backend
|
|
18
|
+
bk = Mint.locale_backend
|
|
19
|
+
return {} unless bk.respond_to?(:call)
|
|
20
|
+
|
|
21
|
+
bk.call
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Selects the appropriate format template and value based on the amount's sign.
|
|
25
|
+
# @private
|
|
26
|
+
def select_format(format)
|
|
27
|
+
negative_format = format[:negative]
|
|
28
|
+
zero_format = format[:zero]
|
|
29
|
+
|
|
30
|
+
if amount.negative? && negative_format
|
|
31
|
+
[negative_format, -amount]
|
|
32
|
+
elsif amount.zero? && zero_format
|
|
33
|
+
[zero_format, amount]
|
|
34
|
+
else
|
|
35
|
+
[format[:positive], amount]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Validates that format hash contains only known keys.
|
|
40
|
+
# @private
|
|
41
|
+
def validate_format_hash(format)
|
|
42
|
+
unknown = format.keys - %i[positive negative zero]
|
|
43
|
+
|
|
44
|
+
raise ArgumentError, "Unknown format parameter(s): #{unknown.inspect}. " unless unknown.empty?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Applies a format template to produce a formatted string representation.
|
|
48
|
+
# @private
|
|
49
|
+
def format_amount(format)
|
|
50
|
+
format, value = select_format(format)
|
|
51
|
+
format ||= '%<symbol>s%<amount>f'
|
|
52
|
+
# Automatically adjust decimal places based on currency subunit if missing
|
|
53
|
+
format = format.gsub(/%<amount>(\s*\+?\d*)f/, "%<amount>\\1.#{currency.subunit}f")
|
|
54
|
+
|
|
55
|
+
refs = format.scan(/%<(\w+)>/).flatten.map(&:to_sym)
|
|
56
|
+
all_args = { amount: value, currency: currency_code, symbol: currency.symbol }
|
|
57
|
+
Kernel.format(format, **all_args.slice(*refs))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -5,7 +5,7 @@ module Mint
|
|
|
5
5
|
class Money
|
|
6
6
|
# Formats money as a string with customizable format, thousand delimiter, and decimal
|
|
7
7
|
#
|
|
8
|
-
# @param format [String, Hash] Either a Format string with placeholders
|
|
8
|
+
# @param format [String, Hash, nil] Either a Format string with placeholders
|
|
9
9
|
# (%<symbol>s, %<amount>f, %<currency>s), or a Hash with per-sign keys
|
|
10
10
|
# (:positive, :negative, :zero) each holding a format string. A Hash
|
|
11
11
|
# is convenient for sign-aware formats such as accounting parentheses:
|
|
@@ -15,8 +15,12 @@ module Mint
|
|
|
15
15
|
# Missing keys fall back to the module default, so a Hash with only
|
|
16
16
|
# :negative will still format positives sensibly. The valid keys are
|
|
17
17
|
# :positive, :negative, :zero; anything else raises ArgumentError.
|
|
18
|
-
#
|
|
19
|
-
#
|
|
18
|
+
# When +nil+, falls back to +Mint.locale_backend+ if set, otherwise
|
|
19
|
+
# +"%<symbol>s%<amount>f"+.
|
|
20
|
+
# @param thousand [String, false, nil] Thousands delimiter (e.g., ',' for 1,000).
|
|
21
|
+
# When +nil+, falls back to +Mint.locale_backend+ if set, otherwise +","+.
|
|
22
|
+
# @param decimal [String, nil] Decimal separator (e.g., '.' or ',').
|
|
23
|
+
# When +nil+, falls back to +Mint.locale_backend+ if set, otherwise +"."+.
|
|
20
24
|
# @return [String] Formatted money string
|
|
21
25
|
#
|
|
22
26
|
# @raise [ArgumentError] if +format+ is not a String or Hash, the Hash
|
|
@@ -43,7 +47,12 @@ module Mint
|
|
|
43
47
|
# money.to_s(format: '%<amount>10.2f') #=> " 1234.56"
|
|
44
48
|
# money.to_s(format: '%<symbol>s%<amount>010.2f') #=> "$0001234.56"
|
|
45
49
|
#
|
|
46
|
-
|
|
50
|
+
# @example Locale-aware formatting (with Mint.locale_backend set)
|
|
51
|
+
# money.to_s # decimal and thousand come from locale_backend
|
|
52
|
+
#
|
|
53
|
+
def to_s(format: nil, decimal: nil, thousand: nil, width: nil)
|
|
54
|
+
format, decimal, thousand = resolve_locale_for(format, decimal, thousand)
|
|
55
|
+
|
|
47
56
|
case format
|
|
48
57
|
when {}, '' then raise ArgumentError, 'format must not be empty'
|
|
49
58
|
when Hash then validate_format_hash(format)
|
|
@@ -63,37 +72,5 @@ module Mint
|
|
|
63
72
|
|
|
64
73
|
width ? formatted.rjust(width) : formatted
|
|
65
74
|
end
|
|
66
|
-
|
|
67
|
-
private
|
|
68
|
-
|
|
69
|
-
def select_format(format)
|
|
70
|
-
negative_format = format[:negative]
|
|
71
|
-
zero_format = format[:zero]
|
|
72
|
-
|
|
73
|
-
if amount.negative? && negative_format
|
|
74
|
-
[negative_format, -amount]
|
|
75
|
-
elsif amount.zero? && zero_format
|
|
76
|
-
[zero_format, amount]
|
|
77
|
-
else
|
|
78
|
-
[format[:positive], amount]
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def validate_format_hash(format)
|
|
83
|
-
unknown = format.keys - %i[positive negative zero]
|
|
84
|
-
|
|
85
|
-
raise ArgumentError, "Unknown format parameter(s): #{unknown.inspect}. " unless unknown.empty?
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def format_amount(format)
|
|
89
|
-
format, value = select_format(format)
|
|
90
|
-
format ||= '%<symbol>s%<amount>f'
|
|
91
|
-
# Automatically adjust decimal places based on currency subunit if missing
|
|
92
|
-
format = format.gsub(/%<amount>(\s*\+?\d*)f/, "%<amount>\\1.#{currency.subunit}f")
|
|
93
|
-
|
|
94
|
-
refs = format.scan(/%<(\w+)>/).flatten.map(&:to_sym)
|
|
95
|
-
all_args = { amount: value, currency: currency_code, symbol: currency.symbol }
|
|
96
|
-
Kernel.format(format, **all_args.slice(*refs))
|
|
97
|
-
end
|
|
98
75
|
end
|
|
99
76
|
end
|
data/lib/minting/money/money.rb
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'allocation/allocation'
|
|
4
|
+
require_relative 'allocation/split'
|
|
5
|
+
require_relative 'arithmetics/methods'
|
|
6
|
+
require_relative 'arithmetics/operators'
|
|
7
|
+
require_relative 'clamp'
|
|
8
|
+
require_relative 'coercion'
|
|
9
|
+
require_relative 'comparable'
|
|
10
|
+
require_relative 'constructors'
|
|
11
|
+
require_relative 'conversion'
|
|
12
|
+
require_relative 'format/formatting'
|
|
13
|
+
require_relative 'format/to_s'
|
|
14
|
+
|
|
3
15
|
module Mint
|
|
4
16
|
# Money constructors
|
|
5
17
|
class Money
|
|
@@ -43,63 +55,5 @@ module Mint
|
|
|
43
55
|
# @param other [Currency] the target currency to compare
|
|
44
56
|
# @return [Boolean] true if currencies match, false otherwise
|
|
45
57
|
def same_currency?(other) = other.currency == currency
|
|
46
|
-
|
|
47
|
-
# Constrains +self+ to the inclusive range [+min+, +max+].
|
|
48
|
-
#
|
|
49
|
-
# Bounds may be:
|
|
50
|
-
# - nil meaning no boundary
|
|
51
|
-
# - same-currency {Money} or Range
|
|
52
|
-
# - Numeric amount, or Range
|
|
53
|
-
#
|
|
54
|
-
# Numeric is interpreted as an amount in +self+'s currency, so the common
|
|
55
|
-
# pricing idiom +price.clamp(0, 100)+ reads as "0 to 100 in the same
|
|
56
|
-
# currency as +price+".
|
|
57
|
-
#
|
|
58
|
-
# When +self+ is already in range the receiver is returned (no new object
|
|
59
|
-
# allocated). When out of range, the nearest bound is returned as a new
|
|
60
|
-
# frozen {Money} in +self+'s currency.
|
|
61
|
-
#
|
|
62
|
-
# @param min_or_range [Money, Numeric, Range, nil] lower bound (inclusive), or range
|
|
63
|
-
# @param max [Money, Numeric, nil] upper bound (inclusive)
|
|
64
|
-
# @return [Money] +self+ if in range, otherwise the nearer bound
|
|
65
|
-
# @raise [ArgumentError] if +min+ or +max+ is not a Money, Numeric or nil; if
|
|
66
|
-
# a Money operand has a different currency; if +min+ > +max+;
|
|
67
|
-
# if min is a Range, and max is not nil
|
|
68
|
-
#
|
|
69
|
-
# @example In range
|
|
70
|
-
# Mint.money(5, 'USD').clamp(0, 10) #=> [USD 5.00] (returns self)
|
|
71
|
-
#
|
|
72
|
-
# @example Out of range, with Numeric bounds
|
|
73
|
-
# Mint.money(50, 'USD').clamp(0, 10) #=> [USD 10.00]
|
|
74
|
-
#
|
|
75
|
-
# @example Out of range, with Money bounds
|
|
76
|
-
# loss = Mint.money(-5, 'USD')
|
|
77
|
-
# floor = Mint.money(0, 'USD')
|
|
78
|
-
# ceil = Mint.money(10, 'USD')
|
|
79
|
-
# loss.clamp(floor, ceil) #=> [USD 0.00]
|
|
80
|
-
#
|
|
81
|
-
# @example Subunit-0 currency (JPY)
|
|
82
|
-
# Mint.money(500, 'JPY').clamp(0, 100) #=> [JPY 100]
|
|
83
|
-
def clamp(min_or_range, max = nil)
|
|
84
|
-
if min_or_range.is_a?(Range)
|
|
85
|
-
raise(ArgumentError, "Either amount range alone or two amounts accepted: #{max}") if max
|
|
86
|
-
|
|
87
|
-
min, max = min_or_range.minmax
|
|
88
|
-
else
|
|
89
|
-
min = min_or_range
|
|
90
|
-
end
|
|
91
|
-
mint(amount.clamp(normalize_boundary(min), normalize_boundary(max)))
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
private
|
|
95
|
-
|
|
96
|
-
def normalize_boundary(boundary)
|
|
97
|
-
case boundary
|
|
98
|
-
in NilClass | Numeric then boundary
|
|
99
|
-
in Money if same_currency?(boundary) then boundary.amount
|
|
100
|
-
in Money then raise ArgumentError, "oundary currency must be: #{currency_code}"
|
|
101
|
-
else raise ArgumentError, "Boundary must be Numeric or Money #{boundary}"
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
58
|
end
|
|
105
59
|
end
|
data/lib/minting/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: minting
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.7.
|
|
4
|
+
version: 1.7.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gilson Ferraz
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: bigdecimal
|
|
@@ -33,6 +32,7 @@ files:
|
|
|
33
32
|
- LICENSE
|
|
34
33
|
- README.md
|
|
35
34
|
- Rakefile
|
|
35
|
+
- bin/bench_check
|
|
36
36
|
- bin/check-currencies
|
|
37
37
|
- bin/console
|
|
38
38
|
- bin/setup
|
|
@@ -40,14 +40,17 @@ files:
|
|
|
40
40
|
- doc/Mint/Currency.html
|
|
41
41
|
- doc/Mint/CurrencyRegistry.html
|
|
42
42
|
- doc/Mint/Money.html
|
|
43
|
+
- doc/Mint/RangeStepPatch.html
|
|
44
|
+
- doc/Mint/Registry.html
|
|
43
45
|
- doc/Mint/UnknownCurrency.html
|
|
44
46
|
- doc/Minting.html
|
|
45
47
|
- doc/_index.html
|
|
46
|
-
- doc/agents/
|
|
47
|
-
- doc/agents/
|
|
48
|
-
- doc/agents/
|
|
49
|
-
- doc/agents/
|
|
50
|
-
- doc/agents/
|
|
48
|
+
- doc/agents/api_review-2026-06-15.md
|
|
49
|
+
- doc/agents/expired/AGENTS.md
|
|
50
|
+
- doc/agents/expired/copilot-instructions.md
|
|
51
|
+
- doc/agents/expired/gemini_gem_evaluation.md
|
|
52
|
+
- doc/agents/expired/recommendations.md
|
|
53
|
+
- doc/agents/expired/rubocop-issues.md
|
|
51
54
|
- doc/class_list.html
|
|
52
55
|
- doc/css/common.css
|
|
53
56
|
- doc/css/full_list.css
|
|
@@ -62,24 +65,33 @@ files:
|
|
|
62
65
|
- doc/method_list.html
|
|
63
66
|
- doc/top-level-namespace.html
|
|
64
67
|
- lib/minting.rb
|
|
68
|
+
- lib/minting/currency/currency.rb
|
|
65
69
|
- lib/minting/data/world-currencies.yaml
|
|
66
70
|
- lib/minting/mint.rb
|
|
67
71
|
- lib/minting/mint/aliases.rb
|
|
68
|
-
- lib/minting/mint/
|
|
69
|
-
- lib/minting/mint/currency/currency_registry.rb
|
|
70
|
-
- lib/minting/mint/currency/world_currencies.rb
|
|
72
|
+
- lib/minting/mint/dsl/numeric.rb
|
|
71
73
|
- lib/minting/mint/dsl/range.rb
|
|
72
|
-
- lib/minting/mint/dsl/
|
|
74
|
+
- lib/minting/mint/dsl/string.rb
|
|
73
75
|
- lib/minting/mint/dsl/top_level.rb
|
|
76
|
+
- lib/minting/mint/locale_backend.rb
|
|
74
77
|
- lib/minting/mint/mint.rb
|
|
75
|
-
- lib/minting/mint/parser.rb
|
|
76
|
-
- lib/minting/
|
|
77
|
-
- lib/minting/
|
|
78
|
+
- lib/minting/mint/parser/parser.rb
|
|
79
|
+
- lib/minting/mint/parser/separators.rb
|
|
80
|
+
- lib/minting/mint/registry/registration.rb
|
|
81
|
+
- lib/minting/mint/registry/registry.rb
|
|
82
|
+
- lib/minting/mint/registry/symbols.rb
|
|
83
|
+
- lib/minting/mint/registry/zeros.rb
|
|
84
|
+
- lib/minting/money/allocation/allocation.rb
|
|
85
|
+
- lib/minting/money/allocation/split.rb
|
|
86
|
+
- lib/minting/money/arithmetics/methods.rb
|
|
87
|
+
- lib/minting/money/arithmetics/operators.rb
|
|
88
|
+
- lib/minting/money/clamp.rb
|
|
78
89
|
- lib/minting/money/coercion.rb
|
|
79
90
|
- lib/minting/money/comparable.rb
|
|
80
91
|
- lib/minting/money/constructors.rb
|
|
81
92
|
- lib/minting/money/conversion.rb
|
|
82
|
-
- lib/minting/money/formatting.rb
|
|
93
|
+
- lib/minting/money/format/formatting.rb
|
|
94
|
+
- lib/minting/money/format/to_s.rb
|
|
83
95
|
- lib/minting/money/money.rb
|
|
84
96
|
- lib/minting/version.rb
|
|
85
97
|
- minting.gemspec
|
|
@@ -94,7 +106,6 @@ metadata:
|
|
|
94
106
|
source_code_uri: https://github.com/gferraz/minting
|
|
95
107
|
allowed_push_host: https://rubygems.org
|
|
96
108
|
rubygems_mfa_required: 'true'
|
|
97
|
-
post_install_message:
|
|
98
109
|
rdoc_options: []
|
|
99
110
|
require_paths:
|
|
100
111
|
- lib
|
|
@@ -109,8 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
109
120
|
- !ruby/object:Gem::Version
|
|
110
121
|
version: '0'
|
|
111
122
|
requirements: []
|
|
112
|
-
rubygems_version:
|
|
113
|
-
signing_key:
|
|
123
|
+
rubygems_version: 4.0.10
|
|
114
124
|
specification_version: 4
|
|
115
125
|
summary: Library to manipulate currency values
|
|
116
126
|
test_files: []
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Mint
|
|
4
|
-
# Represents a specific currency unit, identified by ISO 4217 alphabetic code.
|
|
5
|
-
# Currency objects are immutable and define the properties of a monetary unit
|
|
6
|
-
# including its subunit precision, display symbol, and formatting rules.
|
|
7
|
-
#
|
|
8
|
-
# @see https://www.iso.org/iso-4217-currency-codes.html
|
|
9
|
-
# @attr_reader code [String] ISO 4217 currency code (e.g., "USD", "EUR")
|
|
10
|
-
# @attr_reader subunit [Integer] Number of decimal places (0 for JPY, 2 for USD, 3 for IQD)
|
|
11
|
-
# @attr_reader symbol [String] Display symbol (e.g., "$", "€", "R$")
|
|
12
|
-
# @attr_reader priority [Integer] Parser precedence for symbol detection
|
|
13
|
-
# @attr_reader country [String, nil] Associated country code
|
|
14
|
-
# @attr_reader name [String, nil] Currency name
|
|
15
|
-
# @attr_reader fractional_multiplier [Integer] 10^subunit, used for fractional conversions
|
|
16
|
-
# @attr_reader minimum_amount [Rational] Smallest representable amount (1/fractional_multiplier)
|
|
17
|
-
Currency = Data.define(:code, :subunit, :symbol, :priority, :country, :name,
|
|
18
|
-
:fractional_multiplier) do
|
|
19
|
-
def initialize(code:, symbol:, subunit: 0, priority: 0, country: nil, name: nil)
|
|
20
|
-
subunit = subunit.to_i
|
|
21
|
-
priority = priority.to_i
|
|
22
|
-
fractional_multiplier = 10**subunit
|
|
23
|
-
super(code:, subunit:, symbol:, priority:, country:, name:,
|
|
24
|
-
fractional_multiplier:)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def inspect = "<Currency:(#{code} #{symbol} #{subunit} #{name})>"
|
|
28
|
-
|
|
29
|
-
def minimum_amount = Rational(1, fractional_multiplier)
|
|
30
|
-
|
|
31
|
-
# Normalizes numeric amounts for this currency
|
|
32
|
-
# 1. Converts to Rational
|
|
33
|
-
# 2. Rounds to respect currency subunit
|
|
34
|
-
def normalize_amount(amount) = amount.to_r.round(subunit)
|
|
35
|
-
end
|
|
36
|
-
end
|