minting 1.6.3 → 1.7.2

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +71 -113
  3. data/Rakefile +25 -3
  4. data/bin/bench_check +46 -0
  5. data/doc/Mint/Currency.html +1139 -0
  6. data/doc/Mint/CurrencyRegistry.html +511 -0
  7. data/doc/Mint/Money.html +3859 -0
  8. data/doc/Mint/RangeStepPatch.html +277 -0
  9. data/doc/Mint/UnknownCurrency.html +136 -0
  10. data/doc/Mint.html +911 -0
  11. data/doc/Minting.html +142 -0
  12. data/doc/_index.html +173 -0
  13. data/doc/class_list.html +54 -0
  14. data/doc/css/common.css +1 -0
  15. data/doc/css/full_list.css +206 -0
  16. data/doc/css/style.css +1089 -0
  17. data/doc/file.README.html +275 -0
  18. data/doc/file_list.html +59 -0
  19. data/doc/frames.html +22 -0
  20. data/doc/index.html +275 -0
  21. data/doc/js/app.js +801 -0
  22. data/doc/js/full_list.js +334 -0
  23. data/doc/js/jquery.js +4 -0
  24. data/doc/method_list.html +518 -0
  25. data/doc/top-level-namespace.html +151 -0
  26. data/lib/minting/{mint/currency → currency}/currency.rb +8 -0
  27. data/lib/minting/{mint/currency → currency}/currency_registry.rb +1 -1
  28. data/lib/minting/{mint/currency → currency}/world_currencies.rb +1 -1
  29. data/lib/minting/mint/aliases.rb +3 -0
  30. data/lib/minting/mint/dsl/numeric.rb +23 -0
  31. data/lib/minting/mint/dsl/range.rb +67 -0
  32. data/lib/minting/mint/dsl/string.rb +12 -0
  33. data/lib/minting/mint/{dsl.rb → dsl/top_level.rb} +3 -18
  34. data/lib/minting/mint/mint.rb +17 -3
  35. data/lib/minting/mint/{parser.rb → parser/parser.rb} +17 -29
  36. data/lib/minting/mint/parser/separators.rb +39 -0
  37. data/lib/minting/mint.rb +19 -8
  38. data/lib/minting/money/allocation/allocation.rb +25 -0
  39. data/lib/minting/money/allocation/split.rb +41 -0
  40. data/lib/minting/money/arithmetics/methods.rb +27 -0
  41. data/lib/minting/money/{arithmetics.rb → arithmetics/operators.rb} +5 -26
  42. data/lib/minting/money/clamp.rb +66 -0
  43. data/lib/minting/money/coercion.rb +18 -11
  44. data/lib/minting/money/comparable.rb +6 -0
  45. data/lib/minting/money/constructors.rb +13 -3
  46. data/lib/minting/money/format/formatting.rb +44 -0
  47. data/lib/minting/money/{formatting.rb → format/to_s.rb} +5 -42
  48. data/lib/minting/money/money.rb +0 -58
  49. data/lib/minting/version.rb +1 -1
  50. data/minting.gemspec +5 -2
  51. metadata +42 -11
  52. data/lib/minting/money/allocation.rb +0 -59
@@ -13,7 +13,9 @@ module Mint
13
13
  checked_currency = Mint.currency(currency)
14
14
  raise ArgumentError, "Currency not found (#{currency})" unless checked_currency
15
15
 
16
- new(checked_currency.normalize_amount(amount), checked_currency)
16
+ amount = checked_currency.normalize_amount(amount)
17
+
18
+ amount.zero? ? Mint.zero(checked_currency) : new(amount, checked_currency)
17
19
  end
18
20
 
19
21
  # Builds a Money from a fractional (smallest-unit) Integer amount.
@@ -41,7 +43,8 @@ module Mint
41
43
  raise ArgumentError, "Currency not found (#{currency})" unless checked_currency
42
44
 
43
45
  amount = Rational(fractional, checked_currency.fractional_multiplier)
44
- new(amount, checked_currency)
46
+
47
+ amount.zero? ? Mint.zero(checked_currency) : new(amount, checked_currency)
45
48
  end
46
49
 
47
50
  # Returns a new Money object with the specified amount, or self if unchanged.
@@ -56,7 +59,14 @@ module Mint
56
59
  # price.mint(10.00) #=> [USD 10.00] (returns self)
57
60
  def mint(new_amount)
58
61
  new_amount = currency.normalize_amount(new_amount)
59
- new_amount == amount ? self : Money.new(new_amount, currency)
62
+
63
+ if new_amount == amount
64
+ self
65
+ elsif new_amount.zero?
66
+ Mint.zero(currency)
67
+ else
68
+ Money.new(new_amount, currency)
69
+ end
60
70
  end
61
71
 
62
72
  private
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # Formatting functionality for Money objects
5
+ class Money
6
+ private
7
+
8
+ # Selects the appropriate format template and value based on the amount's sign.
9
+ # @private
10
+ def select_format(format)
11
+ negative_format = format[:negative]
12
+ zero_format = format[:zero]
13
+
14
+ if amount.negative? && negative_format
15
+ [negative_format, -amount]
16
+ elsif amount.zero? && zero_format
17
+ [zero_format, amount]
18
+ else
19
+ [format[:positive], amount]
20
+ end
21
+ end
22
+
23
+ # Validates that format hash contains only known keys.
24
+ # @private
25
+ def validate_format_hash(format)
26
+ unknown = format.keys - %i[positive negative zero]
27
+
28
+ raise ArgumentError, "Unknown format parameter(s): #{unknown.inspect}. " unless unknown.empty?
29
+ end
30
+
31
+ # Applies a format template to produce a formatted string representation.
32
+ # @private
33
+ def format_amount(format)
34
+ format, value = select_format(format)
35
+ format ||= '%<symbol>s%<amount>f'
36
+ # Automatically adjust decimal places based on currency subunit if missing
37
+ format = format.gsub(/%<amount>(\s*\+?\d*)f/, "%<amount>\\1.#{currency.subunit}f")
38
+
39
+ refs = format.scan(/%<(\w+)>/).flatten.map(&:to_sym)
40
+ all_args = { amount: value, currency: currency_code, symbol: currency.symbol }
41
+ Kernel.format(format, **all_args.slice(*refs))
42
+ end
43
+ end
44
+ end
@@ -45,10 +45,10 @@ module Mint
45
45
  #
46
46
  def to_s(format: '%<symbol>s%<amount>f', decimal: '.', thousand: ',', width: nil)
47
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'
48
+ when {}, '' then raise ArgumentError, 'format must not be empty'
49
+ when Hash then validate_format_hash(format)
50
+ when String then format = { positive: format }
51
+ else raise ArgumentError, 'Invalid format. Only String or Hash are accepted'
52
52
  end
53
53
 
54
54
  formatted = format_amount(format)
@@ -61,44 +61,7 @@ module Mint
61
61
  formatted.gsub!(/(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/, "\\1#{thousand}")
62
62
  end
63
63
 
64
- formatted = formatted.rjust(width) if width
65
- formatted
66
- end
67
-
68
- private
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
-
76
- def format_amount(format)
77
- format = { positive: format } if format.is_a?(String)
78
- positive_format = format[:positive]
79
- negative_format = format[:negative]
80
- zero_format = format[:zero]
81
-
82
- if amount.negative? && negative_format
83
- format = negative_format
84
- value = -amount
85
- elsif amount.zero? && zero_format
86
- format = zero_format
87
- value = amount
88
- else
89
- format = positive_format
90
- value = amount
91
- end
92
- format ||= '%<symbol>s%<amount>f'
93
-
94
- # Automatically adjust decimal places based on currency subunit if missing
95
- adjusted_format = format
96
- .gsub(/%<amount>(\s*\+?\d*)f/, "%<amount>\\1.#{currency.subunit}f")
97
-
98
- Kernel.format(adjusted_format,
99
- amount: value,
100
- currency: currency_code,
101
- symbol: currency.symbol)
64
+ width ? formatted.rjust(width) : formatted
102
65
  end
103
66
  end
104
67
  end
@@ -43,63 +43,5 @@ module Mint
43
43
  # @param other [Currency] the target currency to compare
44
44
  # @return [Boolean] true if currencies match, false otherwise
45
45
  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
46
  end
105
47
  end
@@ -3,5 +3,5 @@
3
3
  # Root namespace for the Minting library.
4
4
  module Minting
5
5
  # Current version of the Minting gem.
6
- VERSION = '1.6.3'
6
+ VERSION = '1.7.2'
7
7
  end
data/minting.gemspec CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  lib = File.expand_path('lib', __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require 'minting/version'
@@ -15,18 +17,19 @@ Gem::Specification.new do |s|
15
17
  # Prevent pushing this gem to RubyGems.org.
16
18
  # To allow pushes either set the 'allowed_push_host' to allow pushing to
17
19
  # a single host or delete this section to allow pushing to any host.
18
- raise 'RubyGems 3.2 or newer is required' unless s.respond_to?(:metadata)
20
+ raise 'RubyGems 3.3 or newer is required' unless s.respond_to?(:metadata)
19
21
 
20
22
  s.metadata = {
21
23
  'bug_tracker_uri' => "#{s.homepage}/issues",
22
24
  'changelog_uri' => "#{s.homepage}/blob/master/CHANGELOG.md",
25
+ 'documentation_uri' => 'https://www.rubydoc.info/gems/minting',
23
26
  'homepage_uri' => s.homepage,
24
27
  'source_code_uri' => s.homepage,
25
28
  'allowed_push_host' => 'https://rubygems.org',
26
29
  'rubygems_mfa_required' => 'true'
27
30
  }
28
31
 
29
- s.required_ruby_version = '>= 3.2.0'
32
+ s.required_ruby_version = '>= 3.3.0'
30
33
  s.add_dependency 'bigdecimal', '>= 4.0'
31
34
 
32
35
  s.files = Dir.glob('{bin,doc,lib}/**/*')
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.6.3
4
+ version: 1.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gilson Ferraz
@@ -32,31 +32,61 @@ files:
32
32
  - LICENSE
33
33
  - README.md
34
34
  - Rakefile
35
+ - bin/bench_check
35
36
  - bin/check-currencies
36
37
  - bin/console
37
38
  - bin/setup
39
+ - doc/Mint.html
40
+ - doc/Mint/Currency.html
41
+ - doc/Mint/CurrencyRegistry.html
42
+ - doc/Mint/Money.html
43
+ - doc/Mint/RangeStepPatch.html
44
+ - doc/Mint/UnknownCurrency.html
45
+ - doc/Minting.html
46
+ - doc/_index.html
38
47
  - doc/agents/AGENTS.md
39
48
  - doc/agents/copilot-instructions.md
40
49
  - doc/agents/gemini_gem_evaluation.md
41
50
  - doc/agents/recommendations.md
42
51
  - doc/agents/rubocop-issues.md
52
+ - doc/class_list.html
53
+ - doc/css/common.css
54
+ - doc/css/full_list.css
55
+ - doc/css/style.css
56
+ - doc/file.README.html
57
+ - doc/file_list.html
58
+ - doc/frames.html
59
+ - doc/index.html
60
+ - doc/js/app.js
61
+ - doc/js/full_list.js
62
+ - doc/js/jquery.js
63
+ - doc/method_list.html
64
+ - doc/top-level-namespace.html
43
65
  - lib/minting.rb
66
+ - lib/minting/currency/currency.rb
67
+ - lib/minting/currency/currency_registry.rb
68
+ - lib/minting/currency/world_currencies.rb
44
69
  - lib/minting/data/world-currencies.yaml
45
70
  - lib/minting/mint.rb
46
71
  - lib/minting/mint/aliases.rb
47
- - lib/minting/mint/currency/currency.rb
48
- - lib/minting/mint/currency/currency_registry.rb
49
- - lib/minting/mint/currency/world_currencies.rb
50
- - lib/minting/mint/dsl.rb
72
+ - lib/minting/mint/dsl/numeric.rb
73
+ - lib/minting/mint/dsl/range.rb
74
+ - lib/minting/mint/dsl/string.rb
75
+ - lib/minting/mint/dsl/top_level.rb
51
76
  - lib/minting/mint/mint.rb
52
- - lib/minting/mint/parser.rb
53
- - lib/minting/money/allocation.rb
54
- - lib/minting/money/arithmetics.rb
77
+ - lib/minting/mint/parser/parser.rb
78
+ - lib/minting/mint/parser/separators.rb
79
+ - lib/minting/money/allocation/allocation.rb
80
+ - lib/minting/money/allocation/split.rb
81
+ - lib/minting/money/arithmetics/methods.rb
82
+ - lib/minting/money/arithmetics/operators.rb
83
+ - lib/minting/money/clamp.rb
55
84
  - lib/minting/money/coercion.rb
56
85
  - lib/minting/money/comparable.rb
57
86
  - lib/minting/money/constructors.rb
58
87
  - lib/minting/money/conversion.rb
59
- - lib/minting/money/formatting.rb
88
+ - lib/minting/money/format/formatting.rb
89
+ - lib/minting/money/format/to_s.rb
60
90
  - lib/minting/money/money.rb
61
91
  - lib/minting/version.rb
62
92
  - minting.gemspec
@@ -66,6 +96,7 @@ licenses:
66
96
  metadata:
67
97
  bug_tracker_uri: https://github.com/gferraz/minting/issues
68
98
  changelog_uri: https://github.com/gferraz/minting/blob/master/CHANGELOG.md
99
+ documentation_uri: https://www.rubydoc.info/gems/minting
69
100
  homepage_uri: https://github.com/gferraz/minting
70
101
  source_code_uri: https://github.com/gferraz/minting
71
102
  allowed_push_host: https://rubygems.org
@@ -77,14 +108,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
77
108
  requirements:
78
109
  - - ">="
79
110
  - !ruby/object:Gem::Version
80
- version: 3.2.0
111
+ version: 3.3.0
81
112
  required_rubygems_version: !ruby/object:Gem::Requirement
82
113
  requirements:
83
114
  - - ">="
84
115
  - !ruby/object:Gem::Version
85
116
  version: '0'
86
117
  requirements: []
87
- rubygems_version: 4.0.9
118
+ rubygems_version: 4.0.10
88
119
  specification_version: 4
89
120
  summary: Library to manipulate currency values
90
121
  test_files: []
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mint
4
- # Allocation and splitting
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
- # Splits the monetary amount into a given quantity of equal parts.
26
- # Disperses any fractional subunit rounding differences across the initial slots
27
- # so that the sum is preserved.
28
- #
29
- # @param quantity [Integer] the number of equal parts to divide the money into (must be > 0)
30
- # @return [Array<Money>] the list of newly split Money objects
31
- # @raise [ArgumentError] if quantity is not a positive integer
32
- #
33
- # @example Even split
34
- # money = Mint.money(10.00, 'USD')
35
- # money.split(3) #=> [[USD 3.34], [USD 3.33], [USD 3.33]]
36
- def split(quantity)
37
- unless quantity.positive? && quantity.integer?
38
- raise ArgumentError,
39
- 'quantity must be an integer > 0'
40
- end
41
-
42
- fraction = (amount / quantity).round(currency.subunit)
43
- allocate_left_over!(amounts: Array.new(quantity, fraction),
44
- left_over: amount - (fraction * quantity))
45
- end
46
-
47
- private
48
-
49
- def allocate_left_over!(amounts:, left_over:)
50
- if left_over.nonzero?
51
- minimum = currency.minimum_amount
52
- minimum = -minimum if left_over.negative?
53
- last_slot = (left_over / minimum).to_i - 1
54
- (0..last_slot).each { |slot| amounts[slot] += minimum }
55
- end
56
- amounts.map { |amount| Money.new(amount, currency) }
57
- end
58
- end
59
- end