shopify-money 0.13.0 → 0.14.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -3
  3. data/Gemfile +2 -1
  4. data/README.md +20 -1
  5. data/Rakefile +1 -0
  6. data/bin/console +1 -0
  7. data/config/currency_historic.yml +152 -0
  8. data/config/currency_iso.yml +2623 -0
  9. data/config/currency_non_iso.yml +77 -0
  10. data/dev.yml +1 -1
  11. data/lib/money.rb +3 -0
  12. data/lib/money/allocator.rb +10 -11
  13. data/lib/money/core_extensions.rb +1 -0
  14. data/lib/money/currency.rb +1 -0
  15. data/lib/money/currency/loader.rb +28 -15
  16. data/lib/money/deprecations.rb +1 -0
  17. data/lib/money/errors.rb +1 -0
  18. data/lib/money/helpers.rb +5 -15
  19. data/lib/money/money.rb +3 -2
  20. data/lib/money/null_currency.rb +1 -0
  21. data/lib/money/version.rb +2 -1
  22. data/lib/money_accessor.rb +1 -0
  23. data/lib/money_column.rb +1 -0
  24. data/lib/money_column/active_record_hooks.rb +2 -1
  25. data/lib/money_column/active_record_type.rb +1 -0
  26. data/lib/money_column/railtie.rb +1 -0
  27. data/lib/rubocop/cop/money.rb +3 -0
  28. data/lib/rubocop/cop/money/missing_currency.rb +75 -0
  29. data/lib/shopify-money.rb +2 -0
  30. data/money.gemspec +7 -3
  31. data/spec/accounting_money_parser_spec.rb +1 -0
  32. data/spec/allocator_spec.rb +13 -0
  33. data/spec/core_extensions_spec.rb +2 -1
  34. data/spec/currency/loader_spec.rb +1 -0
  35. data/spec/currency_spec.rb +1 -0
  36. data/spec/helpers_spec.rb +1 -6
  37. data/spec/money_accessor_spec.rb +1 -0
  38. data/spec/money_column_spec.rb +1 -0
  39. data/spec/money_parser_spec.rb +1 -0
  40. data/spec/money_spec.rb +3 -2
  41. data/spec/null_currency_spec.rb +1 -0
  42. data/spec/rubocop/cop/money/missing_currency_spec.rb +108 -0
  43. data/spec/rubocop_helper.rb +10 -0
  44. data/spec/schema.rb +1 -0
  45. data/spec/spec_helper.rb +1 -5
  46. metadata +20 -27
  47. data/config/currency_historic.json +0 -157
  48. data/config/currency_iso.json +0 -2642
  49. 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
@@ -3,7 +3,7 @@
3
3
  ---
4
4
  name: money
5
5
  up:
6
- - ruby: 2.5.3
6
+ - ruby: 2.6.6
7
7
  - bundler
8
8
  commands:
9
9
  test: bundle exec rspec
@@ -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)
@@ -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
- if all_rational?(splits)
41
- allocations = splits.inject(0) { |sum, n| sum + n }
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 = (Helpers.value_to_decimal(subunits_to_split * ratio) / allocations).floor
121
+ frac = (subunits_to_split * ratio / allocations.to_r).floor
123
122
  left_over -= frac
124
123
  frac
125
124
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Allows Writing of 100.to_money for +Numeric+ types
2
3
  # 100.to_money => #<Money @cents=10000>
3
4
  # 100.37.to_money => #<Money @cents=10037>
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "money/currency/loader"
2
3
 
3
4
  class Money
@@ -1,25 +1,38 @@
1
- require 'json'
1
+ # frozen_string_literal: true
2
+ require 'yaml'
2
3
 
3
4
  class Money
4
5
  class Currency
5
6
  module Loader
6
- extend self
7
+ class << self
8
+ def load_currencies
9
+ currency_data_path = File.expand_path("../../../../config", __FILE__)
7
10
 
8
- CURRENCY_DATA_PATH = File.expand_path("../../../../config", __FILE__)
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
- def load_currencies
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
- def parse_currency_file(filename)
20
- json = File.read("#{CURRENCY_DATA_PATH}/#{filename}")
21
- json.force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
22
- JSON.parse(json)
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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  Money.class_eval do
2
3
  ACTIVE_SUPPORT_DEFINED = defined?(ActiveSupport)
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class Money
2
3
  class Error < StandardError
3
4
  end
@@ -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
- string_to_decimal(num)
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
@@ -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)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class Money
2
3
  class NullCurrency
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class Money
2
- VERSION = "0.13.0"
3
+ VERSION = "0.14.3"
3
4
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module MoneyAccessor
2
3
  def self.included(base)
3
4
  base.extend(ClassMethods)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require_relative 'money_column/active_record_hooks'
2
3
  require_relative 'money_column/active_record_type'
3
4
  require_relative 'money_column/railtie'
@@ -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?
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class MoneyColumn::ActiveRecordType < ActiveRecord::Type::Decimal
2
3
  def serialize(money)
3
4
  return nil unless money
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module MoneyColumn
2
3
  class Railtie < Rails::Railtie
3
4
  ActiveSupport.on_load :active_record do
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop/cop/money/missing_currency'
@@ -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
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'money'
@@ -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", "~> 5.0")
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.3.6")
21
- s.add_development_dependency("bigdecimal", "~> 1.3.2")
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) }
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'spec_helper'
2
3
 
3
4
  RSpec.describe AccountingMoneyParser do