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.
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