minting 1.8.2 → 1.9.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 +26 -9
- data/bin/bench_check +34 -9
- data/doc/Mint/Currency.html +66 -33
- data/doc/Mint/Money.html +491 -240
- data/doc/Mint/RangeStepPatch.html +1 -1
- data/doc/Mint/Registry.html +7 -3
- data/doc/Mint/Rounding.html +25 -14
- data/doc/Mint/UnknownCurrency.html +1 -1
- data/doc/Mint.html +18 -7
- data/doc/Minting.html +2 -2
- data/doc/_index.html +1 -1
- data/doc/file.README.html +27 -46
- data/doc/index.html +27 -46
- data/doc/method_list.html +51 -35
- data/doc/top-level-namespace.html +1 -1
- data/lib/minting/currency/currency.rb +6 -1
- data/lib/minting/mint/{locale_backend.rb → i18n.rb} +27 -0
- data/lib/minting/mint/mint.rb +10 -1
- data/lib/minting/mint/registry/zeros.rb +2 -0
- data/lib/minting/mint/rounding.rb +14 -1
- data/lib/minting/mint.rb +1 -2
- data/lib/minting/money/allocation/allocation.rb +1 -1
- data/lib/minting/money/allocation/split.rb +1 -1
- data/lib/minting/money/arithmetics/methods.rb +1 -1
- data/lib/minting/money/arithmetics/operators.rb +1 -1
- data/lib/minting/money/clamp.rb +2 -2
- data/lib/minting/money/coercion.rb +1 -1
- data/lib/minting/money/comparable.rb +1 -1
- data/lib/minting/money/constructors.rb +21 -11
- data/lib/minting/money/conversion.rb +1 -1
- data/lib/minting/money/format/formatting.rb +59 -29
- data/lib/minting/money/format/to_s.rb +44 -16
- data/lib/minting/money/money.rb +14 -5
- data/lib/minting/version.rb +1 -1
- metadata +2 -2
|
@@ -1,29 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Mint
|
|
4
|
-
#
|
|
4
|
+
# :nodoc:
|
|
5
5
|
class Money
|
|
6
6
|
private
|
|
7
7
|
|
|
8
|
-
# Resolves format
|
|
8
|
+
# Resolves the format template and amount based on the amount's sign
|
|
9
|
+
# (negative_format/zero_format may override both the template and negate the value)
|
|
9
10
|
# @private
|
|
10
|
-
def
|
|
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)
|
|
11
|
+
def resolve_format(format)
|
|
27
12
|
negative_format = format[:negative]
|
|
28
13
|
zero_format = format[:zero]
|
|
29
14
|
|
|
@@ -32,7 +17,7 @@ module Mint
|
|
|
32
17
|
elsif amount.zero? && zero_format
|
|
33
18
|
[zero_format, amount]
|
|
34
19
|
else
|
|
35
|
-
[format[:positive], amount]
|
|
20
|
+
[format[:positive] || '%<symbol>s%<amount>f', amount]
|
|
36
21
|
end
|
|
37
22
|
end
|
|
38
23
|
|
|
@@ -44,17 +29,62 @@ module Mint
|
|
|
44
29
|
raise ArgumentError, "Unknown format parameter(s): #{unknown.inspect}. " unless unknown.empty?
|
|
45
30
|
end
|
|
46
31
|
|
|
32
|
+
# Validates +decimal+ and +thousand+ separator arguments.
|
|
33
|
+
# @private
|
|
34
|
+
def validate_separators!(decimal:, thousand:)
|
|
35
|
+
case decimal
|
|
36
|
+
when '' then raise ArgumentError, 'decimal must be a non-empty'
|
|
37
|
+
when nil # :noop
|
|
38
|
+
when String
|
|
39
|
+
raise ArgumentError, "decimal and thousand cannot be identical: #{decimal.inspect}" if decimal == thousand
|
|
40
|
+
else raise ArgumentError, "decimal must be a String, false, or nil, got #{decimal.inspect}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
case thousand
|
|
44
|
+
when false, nil, String # :noop
|
|
45
|
+
else raise ArgumentError, "thousand must be a String, false, or nil, got #{thousand.inspect}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def apply_thousand_separator(string, decimal:, thousand:)
|
|
50
|
+
return string if !thousand || thousand.empty?
|
|
51
|
+
|
|
52
|
+
# Apply thousands only to the integral portion, using the decimal as boundary
|
|
53
|
+
parts = string.split(/(?<=\d)#{Regexp.escape(decimal)}(?=\d)/, 2)
|
|
54
|
+
parts[0].gsub!(/(\d)(?=(?:\d{3})+(?:[^\d]|$))/, "\\1#{thousand}")
|
|
55
|
+
parts.join(decimal)
|
|
56
|
+
end
|
|
57
|
+
|
|
47
58
|
# Applies a format template to produce a formatted string representation.
|
|
48
59
|
# @private
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
#
|
|
61
|
+
def format_amount(format, decimal:, thousand:)
|
|
62
|
+
subunit = currency.subunit
|
|
63
|
+
resolved_format, adjusted_amount = resolve_format(format)
|
|
64
|
+
|
|
65
|
+
# Inject the currency's subunit precision into %<amount>f specifiers
|
|
66
|
+
# e.g. '%<amount>f' becomes '%<amount>.2f' for USD
|
|
67
|
+
resolved_format = resolved_format.gsub(/%<amount>(\s*\+?\d*)f/, "%<amount>\\1.#{subunit}f")
|
|
68
|
+
|
|
69
|
+
# Zero-subunit currencies (e.g. JPY) have no fractional part —
|
|
70
|
+
# strip %<fractional>d specifiers entirely since there's no valid integer for "nothing"
|
|
71
|
+
resolved_format.gsub!(/%<fractional>[^%]*?d/, '') if subunit.zero?
|
|
72
|
+
|
|
73
|
+
result = Kernel.format(resolved_format, {
|
|
74
|
+
amount: adjusted_amount,
|
|
75
|
+
currency: currency_code,
|
|
76
|
+
symbol: currency.symbol,
|
|
77
|
+
integral: adjusted_amount.to_i,
|
|
78
|
+
fractional: fractional
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
# Substitute decimal first, while the dot is still unambiguous
|
|
82
|
+
result.gsub!(/(?<=\d)\.(?=\d)/, decimal) if decimal != '.'
|
|
83
|
+
|
|
84
|
+
return result if adjusted_amount.abs < 1000
|
|
85
|
+
|
|
86
|
+
# Apply thousands only to the integral portion, using the decimal as boundary
|
|
87
|
+
apply_thousand_separator(result, decimal:, thousand:)
|
|
58
88
|
end
|
|
59
89
|
end
|
|
60
90
|
end
|
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Mint
|
|
4
|
-
#
|
|
4
|
+
# :nodoc:
|
|
5
5
|
class Money
|
|
6
|
+
PRESETS = {
|
|
7
|
+
amount: { format: '%<amount>f' },
|
|
8
|
+
accounting: { format: { negative: '(%<symbol>s%<amount>f)' } },
|
|
9
|
+
european: { format: '%<amount>f %<symbol>s', decimal: ',', thousand: '.' },
|
|
10
|
+
currency: { format: '%<currency>s %<amount>f' }
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
6
13
|
# Formats money as a string with customizable format, thousand delimiter, and decimal
|
|
7
14
|
#
|
|
15
|
+
# @param preset [Symbol, nil] Named format preset, one of:
|
|
16
|
+
# +:accounting+, +:european+, +:amount+, +:currency+.
|
|
17
|
+
# When provided, expands to the preset's format options and merges
|
|
18
|
+
# with any explicit keyword arguments (kwargs override the preset).
|
|
8
19
|
# @param format [String, Hash, nil] Either a Format string with placeholders
|
|
9
|
-
# (%<symbol>s, %<amount>f, %<currency>s
|
|
10
|
-
# (:positive, :negative, :zero) each
|
|
11
|
-
# is convenient for sign-aware formats
|
|
20
|
+
# (%<symbol>s, %<amount>f, %<currency>s, %<integral>d, %<fractional>d),
|
|
21
|
+
# or a Hash with per-sign keys (:positive, :negative, :zero) each
|
|
22
|
+
# holding a format string. A Hash is convenient for sign-aware formats
|
|
23
|
+
# such as accounting parentheses:
|
|
12
24
|
#
|
|
13
25
|
# money.to_s(format: { negative: '(%<symbol>s%<amount>f)' })
|
|
14
26
|
#
|
|
@@ -23,8 +35,9 @@ module Mint
|
|
|
23
35
|
# When +nil+, falls back to +Mint.locale_backend+ if set, otherwise +"."+.
|
|
24
36
|
# @return [String] Formatted money string
|
|
25
37
|
#
|
|
26
|
-
# @raise [ArgumentError] if +
|
|
27
|
-
# is
|
|
38
|
+
# @raise [ArgumentError] if +preset+ is not a recognised name, or if
|
|
39
|
+
# +format+ is not a String or Hash, the Hash is empty, or the Hash
|
|
40
|
+
# contains an unrecognised key.
|
|
28
41
|
#
|
|
29
42
|
# @example Basic formatting
|
|
30
43
|
# money = Mint.money(1234.56, 'USD')
|
|
@@ -32,12 +45,25 @@ module Mint
|
|
|
32
45
|
# money.to_s(thousand: '.', decimal: ',') #=> "$1.234,56"
|
|
33
46
|
# money.to_s(decimal: ',', thousand: '') #=> "$1234,56"
|
|
34
47
|
#
|
|
48
|
+
# @example Preset formats
|
|
49
|
+
# loss = Mint.money(-1234.56, 'USD')
|
|
50
|
+
# loss.to_s(:accounting) #=> "($1,234.56)"
|
|
51
|
+
# money.to_s(:european) #=> "1.234,56 €"
|
|
52
|
+
# money.to_s(:amount) #=> "1234.56"
|
|
53
|
+
# money.to_s(:currency) #=> "USD 1234.56"
|
|
54
|
+
#
|
|
35
55
|
# @example Custom formats
|
|
36
56
|
# money.to_s(format: '%<amount>f') #=> "1234.56"
|
|
37
57
|
# money.to_s(format: '%<currency>s %<amount>f') #=> "USD 1234.56"
|
|
38
58
|
# money.to_s(format: '%<amount>f %<symbol>s') #=> "1234.56 $"
|
|
39
59
|
# money.to_s(format: '%<symbol>s%<amount>+f') #=> "$+1234.56"
|
|
40
60
|
#
|
|
61
|
+
# @example Integral & fractional parts
|
|
62
|
+
# money.to_s(format: '%<integral>d.%<fractional>02d') #=> "1234.56"
|
|
63
|
+
# price = Mint.money(0.99, 'USD')
|
|
64
|
+
# price.to_s(format: '%<integral>d dollars and %<fractional>02d cents')
|
|
65
|
+
# #=> "0 dollars and 99 cents"
|
|
66
|
+
#
|
|
41
67
|
# @example Per-sign Hash format (accounting parentheses)
|
|
42
68
|
# loss = Mint.money(-1234.56, 'USD')
|
|
43
69
|
# loss.to_s(format: { negative: '(%<symbol>s%<amount>f)' }) #=> "($1,234.56)"
|
|
@@ -50,7 +76,17 @@ module Mint
|
|
|
50
76
|
# @example Locale-aware formatting (with Mint.locale_backend set)
|
|
51
77
|
# money.to_s # decimal and thousand come from locale_backend
|
|
52
78
|
#
|
|
53
|
-
def to_s(format: nil, decimal: nil, thousand: nil, width: nil)
|
|
79
|
+
def to_s(preset = nil, format: nil, decimal: nil, thousand: nil, width: nil)
|
|
80
|
+
if preset
|
|
81
|
+
config = PRESETS.fetch(preset) { raise ArgumentError, "Unknown format preset: #{preset.inspect}" }
|
|
82
|
+
format ||= config[:format]
|
|
83
|
+
decimal ||= config[:decimal]
|
|
84
|
+
thousand ||= config[:thousand]
|
|
85
|
+
width ||= config[:width]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
validate_separators!(decimal:, thousand:)
|
|
89
|
+
|
|
54
90
|
format, decimal, thousand = resolve_locale_for(format, decimal, thousand)
|
|
55
91
|
|
|
56
92
|
case format
|
|
@@ -60,15 +96,7 @@ module Mint
|
|
|
60
96
|
else raise ArgumentError, 'Invalid format. Only String or Hash are accepted'
|
|
61
97
|
end
|
|
62
98
|
|
|
63
|
-
formatted = format_amount(format)
|
|
64
|
-
|
|
65
|
-
formatted.tr!('.', decimal) if decimal != '.'
|
|
66
|
-
|
|
67
|
-
unless thousand.empty?
|
|
68
|
-
# Regular expression courtesy of Money gem
|
|
69
|
-
# Matches digits followed by groups of 3 digits until non-digit or end
|
|
70
|
-
formatted.gsub!(/(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/, "\\1#{thousand}")
|
|
71
|
-
end
|
|
99
|
+
formatted = format_amount(format, decimal: decimal, thousand: thousand)
|
|
72
100
|
|
|
73
101
|
width ? formatted.rjust(width) : formatted
|
|
74
102
|
end
|
data/lib/minting/money/money.rb
CHANGED
|
@@ -13,7 +13,9 @@ require_relative 'format/formatting'
|
|
|
13
13
|
require_relative 'format/to_s'
|
|
14
14
|
|
|
15
15
|
module Mint
|
|
16
|
-
#
|
|
16
|
+
# Represents a monetary value paired with a currency.
|
|
17
|
+
# Money objects are immutable and support arithmetic, comparison,
|
|
18
|
+
# formatting, allocation, and parsing operations.
|
|
17
19
|
class Money
|
|
18
20
|
# The default display format pattern for formatting monetary values.
|
|
19
21
|
# Uses `%<symbol>s` for the currency symbol and `%<amount>f` for the rounded amount.
|
|
@@ -33,10 +35,17 @@ module Mint
|
|
|
33
35
|
#
|
|
34
36
|
# @return [Integer] the amount in fractional units
|
|
35
37
|
# @example
|
|
36
|
-
# Mint.money(1234.56, 'USD').
|
|
37
|
-
# Mint.money(1000, 'JPY').
|
|
38
|
-
# Mint.money(123.456, 'IQD').
|
|
39
|
-
def
|
|
38
|
+
# Mint.money(1234.56, 'USD').subunits #=> 123456
|
|
39
|
+
# Mint.money(1000, 'JPY').subunits #=> 1000
|
|
40
|
+
# Mint.money(123.456, 'IQD').subunits #=> 123456
|
|
41
|
+
def subunits = (amount * currency.fractional_multiplier).to_i
|
|
42
|
+
|
|
43
|
+
# Returns the fractional part of the amount.
|
|
44
|
+
# @example
|
|
45
|
+
# Mint.money(1234.56, 'USD').fractional #=> 56
|
|
46
|
+
# Mint.money(1000, 'JPY').fractional #=> 0
|
|
47
|
+
# Mint.money(123.456, 'IQD').fractional #=> 456
|
|
48
|
+
def fractional = ((amount.abs % 1) * currency.fractional_multiplier).to_i
|
|
40
49
|
|
|
41
50
|
# Generates a stable hash key for Money instances.
|
|
42
51
|
#
|
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.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gilson Ferraz
|
|
@@ -75,7 +75,7 @@ files:
|
|
|
75
75
|
- lib/minting/mint/dsl/range.rb
|
|
76
76
|
- lib/minting/mint/dsl/string.rb
|
|
77
77
|
- lib/minting/mint/dsl/top_level.rb
|
|
78
|
-
- lib/minting/mint/
|
|
78
|
+
- lib/minting/mint/i18n.rb
|
|
79
79
|
- lib/minting/mint/mint.rb
|
|
80
80
|
- lib/minting/mint/parser/parser.rb
|
|
81
81
|
- lib/minting/mint/parser/separators.rb
|