money2 7.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|