shopify-money 0.13.0 → 0.14.3
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 +4 -4
- data/.travis.yml +2 -3
- data/Gemfile +2 -1
- data/README.md +20 -1
- data/Rakefile +1 -0
- data/bin/console +1 -0
- data/config/currency_historic.yml +152 -0
- data/config/currency_iso.yml +2623 -0
- data/config/currency_non_iso.yml +77 -0
- data/dev.yml +1 -1
- data/lib/money.rb +3 -0
- data/lib/money/allocator.rb +10 -11
- data/lib/money/core_extensions.rb +1 -0
- data/lib/money/currency.rb +1 -0
- data/lib/money/currency/loader.rb +28 -15
- data/lib/money/deprecations.rb +1 -0
- data/lib/money/errors.rb +1 -0
- data/lib/money/helpers.rb +5 -15
- data/lib/money/money.rb +3 -2
- data/lib/money/null_currency.rb +1 -0
- data/lib/money/version.rb +2 -1
- data/lib/money_accessor.rb +1 -0
- data/lib/money_column.rb +1 -0
- data/lib/money_column/active_record_hooks.rb +2 -1
- data/lib/money_column/active_record_type.rb +1 -0
- data/lib/money_column/railtie.rb +1 -0
- data/lib/rubocop/cop/money.rb +3 -0
- data/lib/rubocop/cop/money/missing_currency.rb +75 -0
- data/lib/shopify-money.rb +2 -0
- data/money.gemspec +7 -3
- data/spec/accounting_money_parser_spec.rb +1 -0
- data/spec/allocator_spec.rb +13 -0
- data/spec/core_extensions_spec.rb +2 -1
- data/spec/currency/loader_spec.rb +1 -0
- data/spec/currency_spec.rb +1 -0
- data/spec/helpers_spec.rb +1 -6
- data/spec/money_accessor_spec.rb +1 -0
- data/spec/money_column_spec.rb +1 -0
- data/spec/money_parser_spec.rb +1 -0
- data/spec/money_spec.rb +3 -2
- data/spec/null_currency_spec.rb +1 -0
- data/spec/rubocop/cop/money/missing_currency_spec.rb +108 -0
- data/spec/rubocop_helper.rb +10 -0
- data/spec/schema.rb +1 -0
- data/spec/spec_helper.rb +1 -5
- metadata +20 -27
- data/config/currency_historic.json +0 -157
- data/config/currency_iso.json +0 -2642
- data/config/currency_non_iso.json +0 -82
@@ -0,0 +1,77 @@
|
|
1
|
+
---
|
2
|
+
jep:
|
3
|
+
priority: 100
|
4
|
+
iso_code: JEP
|
5
|
+
name: Jersey Pound
|
6
|
+
symbol: "£"
|
7
|
+
disambiguate_symbol: JEP
|
8
|
+
alternate_symbols: []
|
9
|
+
subunit: Penny
|
10
|
+
subunit_to_unit: 100
|
11
|
+
symbol_first: true
|
12
|
+
html_entity: "£"
|
13
|
+
decimal_mark: "."
|
14
|
+
thousands_separator: ","
|
15
|
+
iso_numeric: ''
|
16
|
+
smallest_denomination: 1
|
17
|
+
ggp:
|
18
|
+
priority: 100
|
19
|
+
iso_code: GGP
|
20
|
+
name: Guernsey Pound
|
21
|
+
symbol: "£"
|
22
|
+
disambiguate_symbol: GGP
|
23
|
+
alternate_symbols: []
|
24
|
+
subunit: Penny
|
25
|
+
subunit_to_unit: 100
|
26
|
+
symbol_first: true
|
27
|
+
html_entity: "£"
|
28
|
+
decimal_mark: "."
|
29
|
+
thousands_separator: ","
|
30
|
+
iso_numeric: ''
|
31
|
+
smallest_denomination: 1
|
32
|
+
imp:
|
33
|
+
priority: 100
|
34
|
+
iso_code: IMP
|
35
|
+
name: Isle of Man Pound
|
36
|
+
symbol: "£"
|
37
|
+
disambiguate_symbol: IMP
|
38
|
+
alternate_symbols:
|
39
|
+
- M£
|
40
|
+
subunit: Penny
|
41
|
+
subunit_to_unit: 100
|
42
|
+
symbol_first: true
|
43
|
+
html_entity: "£"
|
44
|
+
decimal_mark: "."
|
45
|
+
thousands_separator: ","
|
46
|
+
iso_numeric: ''
|
47
|
+
smallest_denomination: 1
|
48
|
+
xfu:
|
49
|
+
priority: 100
|
50
|
+
iso_code: XFU
|
51
|
+
name: UIC Franc
|
52
|
+
symbol: ''
|
53
|
+
disambiguate_symbol: XFU
|
54
|
+
alternate_symbols: []
|
55
|
+
subunit: ''
|
56
|
+
subunit_to_unit: 100
|
57
|
+
symbol_first: true
|
58
|
+
html_entity: ''
|
59
|
+
decimal_mark: "."
|
60
|
+
thousands_separator: ","
|
61
|
+
iso_numeric: ''
|
62
|
+
smallest_denomination: ''
|
63
|
+
gbx:
|
64
|
+
priority: 100
|
65
|
+
iso_code: GBX
|
66
|
+
name: British Penny
|
67
|
+
symbol: ''
|
68
|
+
disambiguate_symbol: GBX
|
69
|
+
alternate_symbols: []
|
70
|
+
subunit: ''
|
71
|
+
subunit_to_unit: 1
|
72
|
+
symbol_first: true
|
73
|
+
html_entity: ''
|
74
|
+
decimal_mark: "."
|
75
|
+
thousands_separator: ","
|
76
|
+
iso_numeric: ''
|
77
|
+
smallest_denomination: 1
|
data/dev.yml
CHANGED
data/lib/money.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require_relative 'money/money_parser'
|
2
3
|
require_relative 'money/helpers'
|
3
4
|
require_relative 'money/currency'
|
@@ -10,3 +11,5 @@ require_relative 'money/accounting_money_parser'
|
|
10
11
|
require_relative 'money/core_extensions'
|
11
12
|
require_relative 'money_accessor'
|
12
13
|
require_relative 'money_column' if defined?(ActiveRecord)
|
14
|
+
|
15
|
+
require_relative 'rubocop/cop/money' if defined?(RuboCop)
|
data/lib/money/allocator.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'delegate'
|
3
|
+
|
1
4
|
class Money
|
2
5
|
class Allocator < SimpleDelegator
|
3
6
|
def initialize(money)
|
@@ -37,11 +40,8 @@ class Money
|
|
37
40
|
# Money.new(10.01, "USD").allocate([0.5, 0.5], :roundrobin_reverse)
|
38
41
|
# #=> [#<Money value:5.00 currency:USD>, #<Money value:5.01 currency:USD>]
|
39
42
|
def allocate(splits, strategy = :roundrobin)
|
40
|
-
|
41
|
-
|
42
|
-
else
|
43
|
-
allocations = splits.inject(0) { |sum, n| sum + Helpers.value_to_decimal(n) }
|
44
|
-
end
|
43
|
+
splits.map!(&:to_r)
|
44
|
+
allocations = splits.inject(0, :+)
|
45
45
|
|
46
46
|
if (allocations - BigDecimal("1")) > Float::EPSILON
|
47
47
|
raise ArgumentError, "splits add to more than 100%"
|
@@ -81,14 +81,11 @@ class Money
|
|
81
81
|
maximums_total = maximums.reduce(Money.new(0, allocation_currency), :+)
|
82
82
|
|
83
83
|
splits = maximums.map do |max_amount|
|
84
|
-
next(0) if maximums_total.zero?
|
84
|
+
next(Rational(0)) if maximums_total.zero?
|
85
85
|
Money.rational(max_amount, maximums_total)
|
86
86
|
end
|
87
87
|
|
88
|
-
total_allocatable = [
|
89
|
-
value * allocation_currency.subunit_to_unit,
|
90
|
-
maximums_total.value * allocation_currency.subunit_to_unit
|
91
|
-
].min
|
88
|
+
total_allocatable = [maximums_total.subunits, self.subunits].min
|
92
89
|
|
93
90
|
subunits_amounts, left_over = amounts_from_splits(1, splits, total_allocatable)
|
94
91
|
|
@@ -116,10 +113,12 @@ class Money
|
|
116
113
|
end
|
117
114
|
|
118
115
|
def amounts_from_splits(allocations, splits, subunits_to_split = subunits)
|
116
|
+
raise ArgumentError, "All splits values must be of type Rational." unless all_rational?(splits)
|
117
|
+
|
119
118
|
left_over = subunits_to_split
|
120
119
|
|
121
120
|
amounts = splits.collect do |ratio|
|
122
|
-
frac = (
|
121
|
+
frac = (subunits_to_split * ratio / allocations.to_r).floor
|
123
122
|
left_over -= frac
|
124
123
|
frac
|
125
124
|
end
|
data/lib/money/currency.rb
CHANGED
@@ -1,25 +1,38 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'yaml'
|
2
3
|
|
3
4
|
class Money
|
4
5
|
class Currency
|
5
6
|
module Loader
|
6
|
-
|
7
|
+
class << self
|
8
|
+
def load_currencies
|
9
|
+
currency_data_path = File.expand_path("../../../../config", __FILE__)
|
7
10
|
|
8
|
-
|
11
|
+
currencies = {}
|
12
|
+
currencies.merge! YAML.load_file("#{currency_data_path}/currency_historic.yml")
|
13
|
+
currencies.merge! YAML.load_file("#{currency_data_path}/currency_non_iso.yml")
|
14
|
+
currencies.merge! YAML.load_file("#{currency_data_path}/currency_iso.yml")
|
15
|
+
deep_deduplicate!(currencies)
|
16
|
+
end
|
9
17
|
|
10
|
-
|
11
|
-
currencies = {}
|
12
|
-
currencies.merge! parse_currency_file("currency_historic.json")
|
13
|
-
currencies.merge! parse_currency_file("currency_non_iso.json")
|
14
|
-
currencies.merge! parse_currency_file("currency_iso.json")
|
15
|
-
end
|
16
|
-
|
17
|
-
private
|
18
|
+
private
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
def deep_deduplicate!(data)
|
21
|
+
case data
|
22
|
+
when Hash
|
23
|
+
return data if data.frozen?
|
24
|
+
data.transform_keys! { |k| deep_deduplicate!(k) }
|
25
|
+
data.transform_values! { |v| deep_deduplicate!(v) }
|
26
|
+
data.freeze
|
27
|
+
when Array
|
28
|
+
return data if data.frozen?
|
29
|
+
data.map! { |d| deep_deduplicate!(d) }.freeze
|
30
|
+
when String
|
31
|
+
-data
|
32
|
+
else
|
33
|
+
data.duplicable? ? data.freeze : data
|
34
|
+
end
|
35
|
+
end
|
23
36
|
end
|
24
37
|
end
|
25
38
|
end
|
data/lib/money/deprecations.rb
CHANGED
data/lib/money/errors.rb
CHANGED
data/lib/money/helpers.rb
CHANGED
@@ -5,7 +5,6 @@ class Money
|
|
5
5
|
module Helpers
|
6
6
|
module_function
|
7
7
|
|
8
|
-
NUMERIC_REGEX = /\A\s*[\+\-]?(\d+|\d*\.\d+)\s*\z/
|
9
8
|
DECIMAL_ZERO = BigDecimal(0).freeze
|
10
9
|
MAX_DECIMAL = 21
|
11
10
|
|
@@ -25,7 +24,11 @@ class Money
|
|
25
24
|
when Rational
|
26
25
|
BigDecimal(num, MAX_DECIMAL)
|
27
26
|
when String
|
28
|
-
|
27
|
+
decimal = BigDecimal(num, exception: false)
|
28
|
+
return decimal if decimal
|
29
|
+
|
30
|
+
Money.deprecate("using Money.new('#{num}') is deprecated and will raise an ArgumentError in the next major release")
|
31
|
+
DECIMAL_ZERO
|
29
32
|
else
|
30
33
|
raise ArgumentError, "could not parse as decimal #{num.inspect}"
|
31
34
|
end
|
@@ -54,18 +57,5 @@ class Money
|
|
54
57
|
raise ArgumentError, "could not parse as currency #{currency.inspect}"
|
55
58
|
end
|
56
59
|
end
|
57
|
-
|
58
|
-
def string_to_decimal(num)
|
59
|
-
if num =~ NUMERIC_REGEX
|
60
|
-
return BigDecimal(num)
|
61
|
-
end
|
62
|
-
|
63
|
-
Money.deprecate("using Money.new('#{num}') is deprecated and will raise an ArgumentError in the next major release")
|
64
|
-
begin
|
65
|
-
BigDecimal(num)
|
66
|
-
rescue ArgumentError
|
67
|
-
DECIMAL_ZERO
|
68
|
-
end
|
69
|
-
end
|
70
60
|
end
|
71
61
|
end
|
data/lib/money/money.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'forwardable'
|
2
3
|
|
3
4
|
class Money
|
@@ -30,8 +31,8 @@ class Money
|
|
30
31
|
end
|
31
32
|
alias_method :empty, :zero
|
32
33
|
|
33
|
-
def parse(*args)
|
34
|
-
parser.parse(*args)
|
34
|
+
def parse(*args, **kwargs)
|
35
|
+
parser.parse(*args, **kwargs)
|
35
36
|
end
|
36
37
|
|
37
38
|
def from_cents(cents, currency = nil)
|
data/lib/money/null_currency.rb
CHANGED
data/lib/money/version.rb
CHANGED
data/lib/money_accessor.rb
CHANGED
data/lib/money_column.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module MoneyColumn
|
2
3
|
module ActiveRecordHooks
|
3
4
|
def self.included(base)
|
@@ -57,7 +58,7 @@ module MoneyColumn
|
|
57
58
|
if options[:currency_read_only]
|
58
59
|
currency_source = Money::Helpers.value_to_currency(currency_raw_source)
|
59
60
|
if currency_raw_source && !money.currency.compatible?(currency_source)
|
60
|
-
Money.deprecate("[money_column] currency mismatch between #{currency_source} and #{money.currency}.")
|
61
|
+
Money.deprecate("[money_column] currency mismatch between #{currency_source} and #{money.currency} in column #{column}.")
|
61
62
|
end
|
62
63
|
else
|
63
64
|
self[options[:currency_column]] = money.currency.to_s unless money.no_currency?
|
data/lib/money_column/railtie.rb
CHANGED
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Money
|
6
|
+
class MissingCurrency < Cop
|
7
|
+
# `Money.new()` without a currency argument cannot guarantee correctness:
|
8
|
+
# - no error raised for cross-currency computation (e.g. 5 CAD + 5 USD)
|
9
|
+
# - #subunits returns wrong values for 0 and 3 decimals currencies
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# # bad
|
13
|
+
# Money.new(123.45)
|
14
|
+
# Money.new
|
15
|
+
# "1,234.50".to_money
|
16
|
+
#
|
17
|
+
# # good
|
18
|
+
# Money.new(123.45, 'CAD')
|
19
|
+
# "1,234.50".to_money('CAD')
|
20
|
+
#
|
21
|
+
|
22
|
+
def_node_matcher :money_new, <<~PATTERN
|
23
|
+
(send (const nil? :Money) {:new :from_amount :from_cents} $...)
|
24
|
+
PATTERN
|
25
|
+
|
26
|
+
def_node_matcher :to_money_without_currency?, <<~PATTERN
|
27
|
+
(send _ :to_money)
|
28
|
+
PATTERN
|
29
|
+
|
30
|
+
def_node_matcher :to_money_block?, <<~PATTERN
|
31
|
+
(send _ _ (block_pass (sym :to_money)))
|
32
|
+
PATTERN
|
33
|
+
|
34
|
+
def on_send(node)
|
35
|
+
money_new(node) do |_amount, currency_arg|
|
36
|
+
return if currency_arg
|
37
|
+
|
38
|
+
add_offense(node, message: 'Money is missing currency argument')
|
39
|
+
end
|
40
|
+
|
41
|
+
if to_money_block?(node) || to_money_without_currency?(node)
|
42
|
+
add_offense(node, message: 'to_money is missing currency argument')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def autocorrect(node)
|
47
|
+
currency = cop_config['ReplacementCurrency']
|
48
|
+
return unless currency
|
49
|
+
|
50
|
+
receiver, method, _ = *node
|
51
|
+
|
52
|
+
lambda do |corrector|
|
53
|
+
money_new(node) do |amount, currency_arg|
|
54
|
+
return if currency_arg
|
55
|
+
|
56
|
+
corrector.replace(
|
57
|
+
node.loc.expression,
|
58
|
+
"#{receiver.source}.#{method}(#{amount&.source || 0}, '#{currency}')"
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
if to_money_without_currency?(node)
|
63
|
+
corrector.insert_after(node.loc.expression, "('#{currency}')")
|
64
|
+
elsif to_money_block?(node)
|
65
|
+
corrector.replace(
|
66
|
+
node.loc.expression,
|
67
|
+
"#{receiver.source}.#{method} { |x| x.to_money('#{currency}') }"
|
68
|
+
)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/money.gemspec
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
2
3
|
require_relative "lib/money/version"
|
3
4
|
|
4
5
|
Gem::Specification.new do |s|
|
@@ -12,13 +13,16 @@ Gem::Specification.new do |s|
|
|
12
13
|
s.licenses = "MIT"
|
13
14
|
s.summary = "Shopify's money gem"
|
14
15
|
|
16
|
+
s.metadata['allowed_push_host'] = "https://rubygems.org"
|
17
|
+
|
15
18
|
s.add_development_dependency("bundler", ">= 1.5")
|
16
19
|
s.add_development_dependency("simplecov", ">= 0")
|
17
|
-
s.add_development_dependency("rails", "~>
|
20
|
+
s.add_development_dependency("rails", "~> 6.0")
|
18
21
|
s.add_development_dependency("rspec", "~> 3.2")
|
19
22
|
s.add_development_dependency("database_cleaner", "~> 1.6")
|
20
|
-
s.add_development_dependency("sqlite3", "~> 1.
|
21
|
-
|
23
|
+
s.add_development_dependency("sqlite3", "~> 1.4.2")
|
24
|
+
|
25
|
+
s.required_ruby_version = '>= 2.6'
|
22
26
|
|
23
27
|
s.files = `git ls-files`.split($/)
|
24
28
|
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|