minting 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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: []