money2 7.0.0.rc1
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 +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +23 -0
- data/.rspec +1 -0
- data/.travis.yml +24 -0
- data/AUTHORS +126 -0
- data/CHANGELOG.md +619 -0
- data/CONTRIBUTING.md +17 -0
- data/Gemfile +16 -0
- data/LICENSE +23 -0
- data/README.md +438 -0
- data/Rakefile +17 -0
- data/config/currency_backwards_compatible.json +107 -0
- data/config/currency_iso.json +2449 -0
- data/config/currency_non_iso.json +66 -0
- data/lib/money.rb +390 -0
- data/lib/money/allocate.rb +86 -0
- data/lib/money/arithmetic.rb +233 -0
- data/lib/money/bank/base.rb +137 -0
- data/lib/money/bank/single_currency.rb +25 -0
- data/lib/money/bank/variable_exchange.rb +252 -0
- data/lib/money/class_attribute.rb +26 -0
- data/lib/money/currency.rb +402 -0
- data/lib/money/currency/heuristics.rb +150 -0
- data/lib/money/currency/loader.rb +29 -0
- data/lib/money/currency_methods.rb +139 -0
- data/lib/money/formatter.rb +404 -0
- data/lib/money/formatter/to_string.rb +9 -0
- data/lib/money/rates_store/memory.rb +120 -0
- data/lib/money/unaccent.rb +18 -0
- data/lib/money/v6_compatibility.rb +5 -0
- data/lib/money/v6_compatibility/arithmetic.rb +61 -0
- data/lib/money/v6_compatibility/bank_rounding_block.rb +38 -0
- data/lib/money/v6_compatibility/currency_id.rb +29 -0
- data/lib/money/v6_compatibility/format.rb +53 -0
- data/lib/money/v6_compatibility/fractional.rb +74 -0
- data/lib/money/version.rb +3 -0
- data/lib/money2.rb +1 -0
- data/money.gemspec +31 -0
- metadata +207 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
class Money
|
2
|
+
module RatesStore
|
3
|
+
|
4
|
+
# Class for thread-safe storage of exchange rate pairs.
|
5
|
+
# Used by instances of +Money::Bank::VariableExchange+.
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# store = Money::RatesStore::Memory.new
|
9
|
+
# store.add_rate 'USD', 'CAD', 0.98
|
10
|
+
# store.get_rate 'USD', 'CAD' # => 0.98
|
11
|
+
# # iterates rates
|
12
|
+
# store.each_rate {|iso_from, iso_to, rate| puts "#{from} -> #{to}: #{rate}" }
|
13
|
+
class Memory
|
14
|
+
INDEX_KEY_SEPARATOR = '_TO_'.freeze
|
15
|
+
|
16
|
+
# Initializes a new +Money::RatesStore::Memory+ object.
|
17
|
+
#
|
18
|
+
# @param [Hash] opts Optional store options.
|
19
|
+
# @option opts [Boolean] :without_mutex disables the usage of a mutex
|
20
|
+
# @param [Hash] rt Optional initial exchange rate data.
|
21
|
+
def initialize(opts = {}, rt = {})
|
22
|
+
@options, @index = opts, rt
|
23
|
+
@mutex = Mutex.new
|
24
|
+
@in_transaction = false
|
25
|
+
end
|
26
|
+
|
27
|
+
# Registers a conversion rate and returns it. Uses +Mutex+ to synchronize data access.
|
28
|
+
#
|
29
|
+
# @param [String] currency_iso_from Currency to exchange from.
|
30
|
+
# @param [String] currency_iso_to Currency to exchange to.
|
31
|
+
# @param [Numeric] rate Rate to use when exchanging currencies.
|
32
|
+
#
|
33
|
+
# @return [Numeric]
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
# store = Money::RatesStore::Memory.new
|
37
|
+
# store.add_rate("USD", "CAD", 1.24515)
|
38
|
+
# store.add_rate("CAD", "USD", 0.803115)
|
39
|
+
def add_rate(currency_iso_from, currency_iso_to, rate)
|
40
|
+
transaction { index[rate_key_for(currency_iso_from, currency_iso_to)] = rate }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Retrieve the rate for the given currencies. Uses +Mutex+ to synchronize data access.
|
44
|
+
# Delegates to +Money::RatesStore::Memory+
|
45
|
+
#
|
46
|
+
# @param [String] currency_iso_from Currency to exchange from.
|
47
|
+
# @param [String] currency_iso_to Currency to exchange to.
|
48
|
+
#
|
49
|
+
# @return [Numeric]
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# store = Money::RatesStore::Memory.new
|
53
|
+
# store.add_rate("USD", "CAD", 1.24515)
|
54
|
+
#
|
55
|
+
# store.get_rate("USD", "CAD") #=> 1.24515
|
56
|
+
def get_rate(currency_iso_from, currency_iso_to)
|
57
|
+
transaction { index[rate_key_for(currency_iso_from, currency_iso_to)] }
|
58
|
+
end
|
59
|
+
|
60
|
+
def marshal_dump
|
61
|
+
[self.class, index, options]
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# Wraps block execution in a thread-safe transaction
|
66
|
+
def transaction(&block)
|
67
|
+
if @in_transaction || options[:without_mutex]
|
68
|
+
block.call self
|
69
|
+
else
|
70
|
+
@mutex.synchronize do
|
71
|
+
@in_transaction = true
|
72
|
+
result = block.call
|
73
|
+
@in_transaction = false
|
74
|
+
result
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Iterate over rate tuples (iso_from, iso_to, rate)
|
80
|
+
#
|
81
|
+
# @yieldparam iso_from [String] Currency ISO string.
|
82
|
+
# @yieldparam iso_to [String] Currency ISO string.
|
83
|
+
# @yieldparam rate [Numeric] Exchange rate.
|
84
|
+
#
|
85
|
+
# @return [Enumerator]
|
86
|
+
#
|
87
|
+
# @example
|
88
|
+
# store.each_rate do |iso_from, iso_to, rate|
|
89
|
+
# puts [iso_from, iso_to, rate].join
|
90
|
+
# end
|
91
|
+
def each_rate(&block)
|
92
|
+
enum = Enumerator.new do |yielder|
|
93
|
+
index.each do |key, rate|
|
94
|
+
iso_from, iso_to = key.split(INDEX_KEY_SEPARATOR)
|
95
|
+
yielder.yield iso_from, iso_to, rate
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
block_given? ? enum.each(&block) : enum
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
attr_reader :index, :options
|
105
|
+
|
106
|
+
# Return the rate hashkey for the given currencies.
|
107
|
+
#
|
108
|
+
# @param [String] currency_iso_from The currency to exchange from.
|
109
|
+
# @param [String] currency_iso_to The currency to exchange to.
|
110
|
+
#
|
111
|
+
# @return [String]
|
112
|
+
#
|
113
|
+
# @example
|
114
|
+
# rate_key_for("USD", "CAD") #=> "USD_TO_CAD"
|
115
|
+
def rate_key_for(currency_iso_from, currency_iso_to)
|
116
|
+
[currency_iso_from, currency_iso_to].join(INDEX_KEY_SEPARATOR).upcase
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
begin
|
2
|
+
require 'sixarm_ruby_unaccent'
|
3
|
+
rescue LoadError
|
4
|
+
raise 'Money gem doesnt install sixarm_ruby_unaccent by default. ' \
|
5
|
+
'Add it to your gemfile if you use Currency.analyze'
|
6
|
+
end
|
7
|
+
|
8
|
+
# Overwrites unaccent method of sixarm_ruby_unaccent.
|
9
|
+
class String
|
10
|
+
def unaccent
|
11
|
+
accentmap = ACCENTMAP
|
12
|
+
accentmap.delete("\u{0142}") # Delete ł symbol from ACCENTMAP used in PLN currency
|
13
|
+
accentmap.delete("\u{010D}") # Delete č symbol from ACCENTMAP used in CZK currency
|
14
|
+
accentmap.delete("\u{FDFC}") # Delete ﷼ symbol from ACCENTMAP used in IRR, SAR and YER currencies
|
15
|
+
accentmap.delete("\u{20A8}") # Delete ₨ symbol from ACCENTMAP used in INR, LKR, MUR, NPR, PKR and SCR currencies
|
16
|
+
split(//u).map {|c| accentmap[c] || c }.join("")
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class Money
|
2
|
+
module V6Compatibility
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def arithmetic
|
6
|
+
Money.prepend Arithmetic
|
7
|
+
end
|
8
|
+
|
9
|
+
module Arithmetic
|
10
|
+
# Wrapper for coerced numeric values to distinguish
|
11
|
+
# when numeric was on the 1st place in operation.
|
12
|
+
CoercedNumeric = Struct.new(:value) do
|
13
|
+
# Proxy #zero? method to skip unnecessary typecasts. See #- and #+.
|
14
|
+
def zero?
|
15
|
+
value.zero?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def coerce(value)
|
20
|
+
[self, CoercedNumeric.new(value)]
|
21
|
+
end
|
22
|
+
|
23
|
+
def <=>(other)
|
24
|
+
if !other.is_a?(Money) && other.respond_to?(:zero?) && other.zero?
|
25
|
+
return other.is_a?(CoercedNumeric) ? 0 <=> fractional : fractional <=> 0
|
26
|
+
end
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
# Uses Comparable's implementation but raises ArgumentError if non-zero
|
31
|
+
# numeric value is given.
|
32
|
+
def ==(other)
|
33
|
+
if other.is_a?(Numeric) && !other.zero?
|
34
|
+
raise ArgumentError, 'Money#== supports only zero numerics'
|
35
|
+
end
|
36
|
+
super
|
37
|
+
end
|
38
|
+
|
39
|
+
def +(other)
|
40
|
+
return self if !other.is_a?(Money) && other.zero?
|
41
|
+
super
|
42
|
+
end
|
43
|
+
|
44
|
+
def -(other)
|
45
|
+
return self if !other.is_a?(Money) && other.zero?
|
46
|
+
super
|
47
|
+
end
|
48
|
+
|
49
|
+
def *(other)
|
50
|
+
other = other.value if other.is_a?(CoercedNumeric)
|
51
|
+
super
|
52
|
+
end
|
53
|
+
|
54
|
+
def /(other)
|
55
|
+
raise TypeError, 'Can not divide by Money' if other.is_a?(CoercedNumeric)
|
56
|
+
super
|
57
|
+
end
|
58
|
+
alias_method :div, :/
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Money
|
2
|
+
module V6Compatibility
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def bank_rounding_block
|
6
|
+
Bank::VariableExchange.prepend BankRoundingBlock
|
7
|
+
end
|
8
|
+
|
9
|
+
module BankRoundingBlock
|
10
|
+
def exchange_with(from, to_currency, &block)
|
11
|
+
to_currency = Currency.wrap(to_currency)
|
12
|
+
if from.currency == to_currency
|
13
|
+
from
|
14
|
+
else
|
15
|
+
rate = get_rate(from.currency, to_currency)
|
16
|
+
unless rate
|
17
|
+
raise Bank::UnknownRate, "No conversion rate known for " \
|
18
|
+
"'#{from.currency.code}' -> '#{to_currency}'"
|
19
|
+
end
|
20
|
+
new_fractional = exchange(from.fractional, rate, &block).to_d
|
21
|
+
from.send(:build_new, new_fractional / to_currency.subunit_to_unit, to_currency)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def exchange(value, rate, &block)
|
26
|
+
rate = BigDecimal.new(rate.to_s) unless rate.is_a?(BigDecimal)
|
27
|
+
ex = rate * value
|
28
|
+
if block_given?
|
29
|
+
yield ex
|
30
|
+
elsif rounding_method
|
31
|
+
rounding_method.call(ex)
|
32
|
+
else
|
33
|
+
ex
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Money
|
2
|
+
module V6Compatibility
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def currency_id
|
6
|
+
Currency.prepend(CurrencyId)
|
7
|
+
Currency.instances.clear
|
8
|
+
end
|
9
|
+
|
10
|
+
module CurrencyId
|
11
|
+
def initialize(*)
|
12
|
+
super
|
13
|
+
@id = @code.downcase.to_sym
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_sym
|
17
|
+
@code.to_sym
|
18
|
+
end
|
19
|
+
|
20
|
+
def code
|
21
|
+
symbol || @code
|
22
|
+
end
|
23
|
+
|
24
|
+
def iso_code
|
25
|
+
@code
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class Money
|
2
|
+
module V6Compatibility
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def format
|
6
|
+
Money.formatter = Formatter
|
7
|
+
Money.prepend Formatting
|
8
|
+
end
|
9
|
+
|
10
|
+
module Formatting
|
11
|
+
def format(old_rule = nil, **options)
|
12
|
+
options = {old_rule => true} if old_rule.is_a?(Symbol)
|
13
|
+
super(options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Formatter < Money::Formatter
|
18
|
+
def prepare_rules
|
19
|
+
super
|
20
|
+
{
|
21
|
+
translate: :translate_symbol,
|
22
|
+
south_asian_number_formatting: :south_asian,
|
23
|
+
thousands_separator: :delimiter,
|
24
|
+
decimal_mark: :separator,
|
25
|
+
rounded_infinite_precision: :round,
|
26
|
+
}.each do |old_key, new_key|
|
27
|
+
rules[new_key] = rules[old_key] if rules.key?(old_key)
|
28
|
+
end
|
29
|
+
|
30
|
+
symbol_position = self.symbol_position
|
31
|
+
if rules.key?(:symbol_after_without_space) && symbol_position == :after
|
32
|
+
rules[:symbol_space] = !rules[:symbol_after_without_space]
|
33
|
+
end
|
34
|
+
if rules.key?(:symbol_before_without_space) && symbol_position == :before
|
35
|
+
rules[:symbol_space] = !rules[:symbol_before_without_space]
|
36
|
+
end
|
37
|
+
|
38
|
+
localize_formatting_rules
|
39
|
+
end
|
40
|
+
|
41
|
+
def localize_formatting_rules
|
42
|
+
if currency.code == 'JPY' && I18n.locale == :ja
|
43
|
+
rules[:symbol] = '円' unless rules[:symbol] == false
|
44
|
+
rules[:symbol_position] = :after
|
45
|
+
rules[:symbol_space] = false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
alias_method :thousands_separator, :delimiter
|
50
|
+
alias_method :decimal_mark, :separator
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
class Money
|
2
|
+
module V6Compatibility
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def fractional
|
6
|
+
Money.prepend Fractional
|
7
|
+
Money.extend Fractional::ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module Fractional
|
11
|
+
module ClassMethods
|
12
|
+
def from_amount(amount, currency = nil, bank = nil)
|
13
|
+
Numeric === amount or raise ArgumentError, "'amount' must be numeric"
|
14
|
+
new(amount, currency, bank, true)
|
15
|
+
end
|
16
|
+
|
17
|
+
def from_subunits(amount, currency = nil, bank = nil)
|
18
|
+
raise 'Use .new'
|
19
|
+
end
|
20
|
+
|
21
|
+
def new(val, currency = nil, bank = nil, from_amount = false)
|
22
|
+
return super(val, currency, bank) if val.is_a?(self) || from_amount
|
23
|
+
currency = Currency.wrap(currency || default_currency)
|
24
|
+
amount = as_d(val) / currency.subunit_to_unit
|
25
|
+
super(amount, currency, bank)
|
26
|
+
end
|
27
|
+
|
28
|
+
def as_d(value)
|
29
|
+
if value.respond_to?(:to_d)
|
30
|
+
value.is_a?(Rational) ? value.to_d(conversion_precision) : value.to_d
|
31
|
+
else
|
32
|
+
BigDecimal.new(value.to_s)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_f
|
38
|
+
amount.to_f
|
39
|
+
end
|
40
|
+
|
41
|
+
def yaml_initialize(_tag, attrs)
|
42
|
+
super
|
43
|
+
fractional = attrs['fractional']
|
44
|
+
@amount = as_d(fractional) / currency.subunit_to_unit if fractional
|
45
|
+
end
|
46
|
+
|
47
|
+
def round_to_nearest_cash_value
|
48
|
+
value = super * currency.subunit_to_unit
|
49
|
+
self.class.infinite_precision ? value : value.to_i
|
50
|
+
end
|
51
|
+
|
52
|
+
def %(other)
|
53
|
+
other = other.to_d / currency.subunit_to_unit unless other.is_a?(Money)
|
54
|
+
super
|
55
|
+
end
|
56
|
+
alias_method :modulo, :%
|
57
|
+
|
58
|
+
def remainder(other)
|
59
|
+
other = other.to_d / currency.subunit_to_unit unless other.is_a?(Money)
|
60
|
+
super
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def build_new(amount, currency = self.currency, bank = self.bank)
|
66
|
+
self.class.new(amount, currency, bank, true)
|
67
|
+
end
|
68
|
+
|
69
|
+
def as_d(num)
|
70
|
+
self.class.as_d(num)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/money2.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'money'
|
data/money.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'money/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'money2'
|
8
|
+
s.version = Money::VERSION
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = ['Shane Emmons']
|
11
|
+
s.email = ['shane@emmons.io']
|
12
|
+
s.homepage = 'http://rubymoney.github.io/money'
|
13
|
+
s.summary = 'A Ruby Library for dealing with money and currency conversion.'
|
14
|
+
s.description = 'A Ruby Library for dealing with money and currency conversion.'
|
15
|
+
s.license = 'MIT'
|
16
|
+
|
17
|
+
s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^spec/}) }
|
18
|
+
s.bindir = 'exe'
|
19
|
+
s.executables = s.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
s.require_paths = ['lib']
|
21
|
+
|
22
|
+
s.add_dependency 'i18n', ['>= 0.6.4', '<= 0.7.0']
|
23
|
+
|
24
|
+
s.add_development_dependency 'bundler', '~> 1.3'
|
25
|
+
s.add_development_dependency 'rake'
|
26
|
+
s.add_development_dependency 'rspec', '~> 3.4.0'
|
27
|
+
s.add_development_dependency 'rspec-its', '~> 1.1.0'
|
28
|
+
s.add_development_dependency 'yard', '~> 0.8'
|
29
|
+
s.add_development_dependency 'kramdown', '~> 1.1'
|
30
|
+
s.add_development_dependency 'sixarm_ruby_unaccent', ['>= 1.1.1', '< 2']
|
31
|
+
end
|