minting 0.3.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c15130af9a863ae0ed77f63bd1fe5c9bf4c89e2f64bf116be16e5ace6407ede7
4
+ data.tar.gz: 749015ea7226c1ea4eafaa71d25461a812b29c0ec11541d686894aa55675bfe7
5
+ SHA512:
6
+ metadata.gz: 797bd7de317d1038b3b5bf154cea1f0386dd7901bc598822820a51b680f600d0fce15219f8533f5da342092378639b6c663fe979b79ce7ac07578c1c13b9e59b
7
+ data.tar.gz: 0fc814e5e15fc40915baef2cef206c55a773072b67587b9e32373898bdb806194b78d63cf85e804c16939aabed753d7ad2b82e7db2948f749fa017ede7ca1172
data/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # Minting
2
+
3
+ Yet another Ruby library for dealing with money and currency.
4
+
5
+ Work in progress, please wait release 1.0.0
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'minting', git: 'https://github.com/gferraz/minting.git'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself with:
20
+
21
+ $ git clone https://github.com/gferraz/minting.git
22
+ $ cd minting && gem build minting.gemspec
23
+ $ gem install minting-0.2.0.gem
24
+
25
+ ## Usage
26
+
27
+ ```ruby
28
+ require 'minting'
29
+
30
+ # 10.00 USD
31
+ ten_dollars = Mint.money(10, 'USD') #=> [USD 10.00]
32
+ ten_dollars.to_i #=> 10
33
+ ten_dollars.currency_code #=> "USD"
34
+
35
+ # Comparisons
36
+ ten_dollars = Mint.money(10, 'USD')
37
+
38
+ ten_dollars == Mint.money(10, 'USD') #=> true
39
+ ten_dollars == Mint.money(11, 'USD') #=> false
40
+ ten_dollars == Mint.money(10, 'EUR') #=> false
41
+
42
+ ten_dollars.eql? Mint.money(10, 'USD') #=> true
43
+ ten_dollars.hash == Mint.money(10, 'USD').hash #=> true
44
+
45
+ # Format (uses Kernel.format internally)
46
+ price = Mint.money(9.99, 'USD')
47
+
48
+ price.to_s #=> "$9.99",
49
+ price.to_s(format: '%<amount>d') #=> "9",
50
+ price.to_s(format: '%<symbol>s%<amount>f') #=> "$9.99",
51
+ price.to_s(format: '%<symbol>s%<amount>+f') #=> "$+9.99",
52
+ (-price).to_s(format: '%<amount>f') #=> "-9.99",
53
+
54
+ # Format with padding
55
+ price_in_euros = euro.money(12.34, 'EUR')
56
+
57
+ price.to_s(format: '--%<amount>7d') #=> "-- 9"
58
+ price.to_s(format: ' %<amount>10f %<currency>s') #=> " 9.99 USD"
59
+ (-price).to_s(format: ' %<amount>10f') #=> " -9.99"
60
+
61
+ price_in_euros.to_s(format: '%<symbol>2s%<amount>+10f') #=> " € +12.34"
62
+
63
+ # Json serialization
64
+
65
+ price.to_json # "{"currency": "USD", "amount": "9.99"}
66
+
67
+ # Allocation and split
68
+
69
+ ten_dollars.split(3) #=> [[USD 3.34], [USD 3.33], [USD 3.33]]
70
+ ten_dollars.split(7) #=> [[USD 1.42], [USD 1.43], [USD 1.43], [USD 1.43], [USD 1.43], [USD 1.43], [USD 1.43]]
71
+
72
+ ten_dollars.allocate([1, 2, 3]) #=> [[USD 1.67], [USD 3.33], [USD 5.00]]
73
+
74
+ # Numeric refinements
75
+ uning Mint
76
+
77
+ 1.dollar == Mint.money(1, 'USD') #=> true
78
+ 3.euros == Mint.money(2, 'EUR') #=> true
79
+ 4.mint('USD') == 4.dollars #=> true
80
+ 4.to_money('USD') == 4.dollars #=> true
81
+ ```
82
+
83
+ ## Release 1.0 Plan
84
+
85
+ - Rails
86
+ - Localization: I18n
87
+ - Arithmetics: div, mod
88
+ - Mint.parse
89
+
90
+ ## Contributing
91
+
92
+ Bug reports and pull requests are welcome on GitHub at https://github.com/gferraz/minting.
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rubocop/rake_task'
3
+ require 'rake/testtask'
4
+
5
+ CLOBBER.include %w[tmp]
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << 'test'
9
+ t.libs << 'lib'
10
+ t.test_files = FileList['test/**/*_test.rb']
11
+ t.ruby_opts << '-rtest_helper.rb'
12
+ end
13
+
14
+ Rake::TestTask.new(:bench) do |t|
15
+ t.libs = %w[lib test]
16
+ t.pattern = 'test/**/*_benchmark.rb'
17
+ end
18
+
19
+ RuboCop::RakeTask.new(:cop)
20
+
21
+ task default: :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'minting'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,2 @@
1
+ require 'minting/currency/mint_currency'
2
+ require 'minting/currency/currency'
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # Represents a specific currency unit, identified by ISO 4217 alphabetic ocde
5
+ #
6
+ # @see https://www.iso.org/iso-4217-currency-codes.html
7
+ class Currency
8
+ attr_reader :code, :subunit, :symbol
9
+
10
+ def inspect
11
+ "<Currency:(#{code} #{symbol} #{subunit})>"
12
+ end
13
+
14
+ def minimum_amount
15
+ @minimum_amount ||= 10r**-subunit
16
+ end
17
+
18
+ private
19
+
20
+ def initialize(code, subunit:, symbol:)
21
+ @code = code
22
+ @subunit = subunit
23
+ @symbol = symbol
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ # Mint is a library to operate with monetary values
2
+ module Mint
3
+ refine Numeric do
4
+ def reais
5
+ Mint.money(self, 'BRL')
6
+ end
7
+
8
+ def dollars
9
+ Mint.money(self, 'USD')
10
+ end
11
+
12
+ def euros
13
+ Mint.money(self, 'EUR')
14
+ end
15
+
16
+ def to_money(currency)
17
+ Mint.money(self, currency)
18
+ end
19
+
20
+ alias_method :dollar, :dollars
21
+ alias_method :euro, :euros
22
+ alias_method :mint, :to_money
23
+ end
24
+
25
+ refine String do
26
+ def to_money(currency)
27
+ Mint.money(to_r, currency)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc
4
+ module Mint
5
+ def self.money(amount, currency_code)
6
+ Money.new(amount, currency(currency_code))
7
+ end
8
+
9
+ def self.currency(currency)
10
+ case currency
11
+ when Currency
12
+ currency
13
+ when Symbol
14
+ currencies[currency.to_s]
15
+ else
16
+ currencies[currency]
17
+ end
18
+ end
19
+
20
+ def self.register_currency(code, subunit: 2, symbol: '$')
21
+ code = code.to_s
22
+ currencies[code] || register_currency!(code, subunit: subunit, symbol: symbol)
23
+ end
24
+
25
+ def self.register_currency!(code, subunit:, symbol: '')
26
+ code = code.to_s
27
+ unless code.match?(/^[A-Z_]+$/)
28
+ raise ArgumentError,
29
+ "Currency code must be String or Symbol ('USD', :EUR)"
30
+ end
31
+ if currencies[code]
32
+ raise KeyError,
33
+ "Currency: #{code} already registered"
34
+ end
35
+
36
+ currencies[code] =
37
+ Currency.new(code, subunit: subunit.to_i, symbol: symbol.to_s).freeze
38
+ end
39
+
40
+ def self.currencies
41
+ @currencies ||= {
42
+ 'AUD' => Currency.new('AUD', subunit: 2, symbol: '$'),
43
+ 'BRL' => Currency.new('BRL', subunit: 2, symbol: 'R$'),
44
+ 'CAD' => Currency.new('CAD', subunit: 2, symbol: 'R$'),
45
+ 'CHF' => Currency.new('CHF', subunit: 2, symbol: 'Fr'),
46
+ 'CNY' => Currency.new('CNY', subunit: 2, symbol: '¥'),
47
+ 'EUR' => Currency.new('EUR', subunit: 2, symbol: '€'),
48
+ 'GBP' => Currency.new('GBP', subunit: 2, symbol: '£'),
49
+ 'JPY' => Currency.new('JPY', subunit: 0, symbol: '¥'),
50
+ 'MXN' => Currency.new('MXN', subunit: 2, symbol: '$'),
51
+ 'NZD' => Currency.new('NZD', subunit: 2, symbol: '$'),
52
+ 'PEN' => Currency.new('PEN', subunit: 2, symbol: 'S/.'),
53
+ 'SEK' => Currency.new('SEK', subunit: 2, symbol: 'kr'),
54
+ 'USD' => Currency.new('USD', subunit: 2, symbol: '$')
55
+ }
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ require 'minting/mint/currency'
2
+ require 'minting/mint/refinements'
3
+ require 'minting/mint/registry'
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # :nodoc
5
+ # split and allocation methods
6
+ class Money
7
+ def allocate(proportions)
8
+ raise ArgumentError, 'Need at least 1 proportion element' if proportions.empty?
9
+
10
+ whole = proportions.sum.to_r
11
+ allocation = proportions.map { |rate| mint(amount * rate.to_r / whole) }
12
+ left_over = self - allocation.sum
13
+ allocate_left_over(allocation, left_over)
14
+ end
15
+
16
+ def split(quantity)
17
+ unless quantity.positive? && quantity.integer?
18
+ raise ArgumentError,
19
+ 'quantitity must be an integer > 0'
20
+ end
21
+
22
+ fraction = self / quantity
23
+ allocation = Array.new(quantity, fraction)
24
+ left_over = self - (fraction * quantity)
25
+ allocate_left_over(allocation, left_over)
26
+ end
27
+
28
+ private
29
+
30
+ def allocate_left_over(allocation, left_over)
31
+ minimum = currency.minimum_amount
32
+ minimum = mint(left_over.positive? ? minimum : -minimum)
33
+
34
+ slots = (left_over / minimum).to_i - 1
35
+ (0..slots).each { |slot| allocation[slot] += minimum }
36
+ allocation
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # :nodoc
5
+ # Arithmetic funcions for money ojects
6
+ class Money
7
+ def abs
8
+ mint(amount.abs)
9
+ end
10
+
11
+ def negative?
12
+ amount.negative?
13
+ end
14
+
15
+ def positive?
16
+ amount.positive?
17
+ end
18
+
19
+ def +(addend)
20
+ return mint(amount + addend.amount) if same_currency?(addend)
21
+ return self unless addend.is_a?(Money) || addend.nonzero?
22
+
23
+ raise TypeError, "#{addend} can't be added to #{self}"
24
+ end
25
+
26
+ def -(subtrahend)
27
+ return self if subtrahend.zero?
28
+ return mint(amount - subtrahend.amount) if same_currency?(subtrahend)
29
+
30
+ raise TypeError, "#{subtrahend} can't be subtracted from #{self}"
31
+ end
32
+
33
+ def -@
34
+ mint(-amount)
35
+ end
36
+
37
+ def *(multiplicand)
38
+ return mint(amount * multiplicand.to_r) if multiplicand.is_a?(Numeric)
39
+
40
+ raise TypeError, "#{self} can't be multiplied by #{multiplicand}"
41
+ end
42
+
43
+ def /(divisor)
44
+ return mint(amount / divisor) if divisor.is_a?(Numeric)
45
+ return amount / divisor.amount if same_currency? divisor
46
+
47
+ raise TypeError, "#{self} can't be divided by #{divisor}"
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # :nodoc
5
+ # Coercion logic
6
+ class Money
7
+ def coerce(other)
8
+ [CoercedNumber.new(other), self]
9
+ end
10
+
11
+ # :nodoc
12
+ # Coerced Number contains the arithmetic logic for numeric compatible ops.
13
+ class CoercedNumber
14
+ include Comparable
15
+
16
+ def initialize(value)
17
+ @value = value
18
+ end
19
+
20
+ def +(other)
21
+ return other if @value.zero?
22
+
23
+ raise_coercion_error(:+, other)
24
+ end
25
+
26
+ def -(other)
27
+ return -other if @value.zero?
28
+
29
+ raise_coercion_error(:-, other)
30
+ end
31
+
32
+ def *(other)
33
+ other.mint(@value * other.amount)
34
+ end
35
+
36
+ def /(other)
37
+ raise_coercion_error(:/, other)
38
+ end
39
+
40
+ def <=>(other)
41
+ return nil if @value.nil? || other.nil?
42
+ return @value <=> other.amount if @value.zero? || other.zero?
43
+
44
+ raise_coercion_error(:<=>, other)
45
+ end
46
+
47
+ def raise_coercion_error(operation, operand)
48
+ raise TypeError,
49
+ "#{self} #{operation} #{operand} : incompatible operands"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # :nodoc
5
+ # Comparision methods
6
+ class Money
7
+ include Comparable
8
+
9
+ # @return true if both are zero, or both have same amount and same currency
10
+ def ==(other)
11
+ return true if other.is_a?(Numeric) && zero? && other.zero?
12
+ return false unless other.is_a?(Mint::Money)
13
+ return false if nonzero? && currency != other.currency
14
+
15
+ amount == other.amount
16
+ end
17
+
18
+ # @example
19
+ # two_usd == Mint.money(2r, Currency['USD']) #=> [$ 2.00]
20
+ # two_usd > 0 #=> true
21
+ # two_usd > Mint.money(1r, Currency['USD']) #=> true
22
+ # two_usd > 1
23
+ # => TypeError: [$ 2.00] can't be compared to 1
24
+ # two_usd > Mint.money(2, Currency['BRL'])
25
+ # => TypeError: [$ 2.00] can't be compared to [R$ 2.00]
26
+ #
27
+ def <=>(other)
28
+ case other
29
+ when Numeric
30
+ return amount <=> other if other.zero?
31
+ when Mint::Money
32
+ return amount <=> other.amount if currency == other.currency
33
+ end
34
+ raise TypeError, "#{inspect} can't be compared to #{other.inspect}"
35
+ end
36
+
37
+ def eql?(other)
38
+ self == other
39
+ end
40
+
41
+ def hash
42
+ @hash ||= zero? ? 0.hash : [amount, currency].hash
43
+ end
44
+
45
+ def nonzero?
46
+ amount.nonzero?
47
+ end
48
+
49
+ def zero?
50
+ amount.zero?
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # Conversion logic
5
+ class Money
6
+ def to_d
7
+ amount.to_d 0
8
+ end
9
+
10
+ def to_f
11
+ amount.to_f
12
+ end
13
+
14
+ def to_html(format = DEFAULT_FORMAT)
15
+ title = Kernel.format("#{currency_code} %0.#{currency.subunit}f", amount)
16
+ %(<data class='money' title='#{title}'>#{to_s(format: format)}</data>)
17
+ end
18
+
19
+ def to_i
20
+ amount.to_i
21
+ end
22
+
23
+ def to_json(*_args)
24
+ subunit = currency.subunit
25
+ Kernel.format(
26
+ %({"currency": "#{currency_code}", "amount": "%0.#{subunit}f"}),
27
+ amount
28
+ )
29
+ end
30
+
31
+ def to_r
32
+ amount
33
+ end
34
+
35
+ def to_s(format: '%<symbol>s%<amount>f', delimiter: false, separator: '.')
36
+ format = format.gsub(/%<amount>(\+?\d*)f/,
37
+ "%<amount>\\1.#{currency.subunit}f")
38
+ formatted = Kernel.format(format, amount: amount, currency: currency_code,
39
+ symbol: currency.symbol)
40
+ if delimiter
41
+ # Thanks Money gem for the regular expression
42
+ formatted.gsub!(/(\d)(?=(?:\d{3})+(?:[^\d]{1}|$))/, "\\1#{delimiter}")
43
+ end
44
+ formatted.tr!('.', separator) if separator != '.'
45
+ formatted
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mint
4
+ # Money is a value object for monetary values
5
+ # Money is immutable
6
+ class Money
7
+ DEFAULT_FORMAT = '%<symbol>s%<amount>f'
8
+
9
+ attr_reader :amount, :currency
10
+
11
+ def initialize(amount, currency)
12
+ raise ArgumentError, 'amount must be Numeric' unless amount.is_a?(Numeric)
13
+
14
+ unless currency.is_a?(Currency)
15
+ raise ArgumentError,
16
+ 'currency must be a Currency object'
17
+ end
18
+
19
+ @amount = amount.to_r.round(currency.subunit)
20
+ @currency = currency
21
+ end
22
+
23
+ def currency_code
24
+ currency.code
25
+ end
26
+
27
+ def mint(new_amount)
28
+ new_amount.to_r == amount ? self : Money.new(new_amount, currency)
29
+ end
30
+
31
+ def inspect
32
+ Kernel.format "[#{currency_code} %0.#{currency.subunit}f]", amount
33
+ end
34
+
35
+ def same_currency?(other)
36
+ other.respond_to?(:currency) && other.currency == currency
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,6 @@
1
+ require 'minting/money/allocation'
2
+ require 'minting/money/arithmetics'
3
+ require 'minting/money/coercion'
4
+ require 'minting/money/comparable'
5
+ require 'minting/money/conversion'
6
+ require 'minting/money/money'
@@ -0,0 +1,3 @@
1
+ module Minting
2
+ VERSION = '0.3.0'.freeze
3
+ end
data/lib/minting.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'minting/mint'
2
+ require 'minting/money'
3
+ require 'minting/version'
data/minting.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'minting/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'minting'
7
+ s.summary = 'Library to manipulate currency values'
8
+ s.description = s.summary
9
+ s.homepage = 'https://github.com/gferraz/minting'
10
+ s.version = Minting::VERSION
11
+ s.authors = ['Gilson Ferraz']
12
+ s.email = []
13
+ s.license = 'MIT'
14
+
15
+ # Prevent pushing this gem to RubyGems.org.
16
+ # To allow pushes either set the 'allowed_push_host' to allow pushing to
17
+ # a single host or delete this section to allow pushing to any host.
18
+ raise 'RubyGems 3.0 or newer is required' unless s.respond_to?(:metadata)
19
+
20
+ s.metadata = {
21
+ 'bug_tracker_uri' => "#{s.homepage}/issues",
22
+ 'changelog_uri' => "#{s.homepage}/blob/master/CHANGELOG.md",
23
+ 'homepage_uri' => s.homepage,
24
+ 'source_code_uri' => s.homepage,
25
+ 'allowed_push_host' => 'https://rubygems.org',
26
+ 'rubygems_mfa_required' => 'true'
27
+ }
28
+
29
+ s.required_ruby_version = '>= 3.0.0'
30
+
31
+ s.files = Dir.glob('{bin,doc,lib}/**/*')
32
+ s.files += %w[minting.gemspec Rakefile README.md]
33
+
34
+ s.bindir = 'bin'
35
+ s.require_paths = ['lib']
36
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minting
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Gilson Ferraz
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-05-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Library to manipulate currency values
14
+ email: []
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - README.md
20
+ - Rakefile
21
+ - bin/console
22
+ - bin/setup
23
+ - lib/minting.rb
24
+ - lib/minting/currency.rb
25
+ - lib/minting/mint.rb
26
+ - lib/minting/mint/currency.rb
27
+ - lib/minting/mint/refinements.rb
28
+ - lib/minting/mint/registry.rb
29
+ - lib/minting/money.rb
30
+ - lib/minting/money/allocation.rb
31
+ - lib/minting/money/arithmetics.rb
32
+ - lib/minting/money/coercion.rb
33
+ - lib/minting/money/comparable.rb
34
+ - lib/minting/money/conversion.rb
35
+ - lib/minting/money/money.rb
36
+ - lib/minting/version.rb
37
+ - minting.gemspec
38
+ homepage: https://github.com/gferraz/minting
39
+ licenses:
40
+ - MIT
41
+ metadata:
42
+ bug_tracker_uri: https://github.com/gferraz/minting/issues
43
+ changelog_uri: https://github.com/gferraz/minting/blob/master/CHANGELOG.md
44
+ homepage_uri: https://github.com/gferraz/minting
45
+ source_code_uri: https://github.com/gferraz/minting
46
+ allowed_push_host: https://rubygems.org
47
+ rubygems_mfa_required: 'true'
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: 3.0.0
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.5.9
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: Library to manipulate currency values
67
+ test_files: []