shopify-money 2.0.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 591c60e355144246085ac254fe82b099c85ab4bf7f1e37672b8082331bd9d0aa
4
- data.tar.gz: 493384ced59c9a37ebc798b4cb499bd0532ea30243c8248704bf0df6f776ca0f
3
+ metadata.gz: 75ba2eb41fa1e50dac837fab61774cdb4a05407b030cb7ba2989b60de27bbfe4
4
+ data.tar.gz: e612602f0f07fe052e6a2e53046e7b6b9a05c56775a75c6a3e4d1f7984671a49
5
5
  SHA512:
6
- metadata.gz: 1867df0795206ccd5996b1d8471f23f60340ace3e5e685ddbb98f601402a7748aade38263dbc7f12971c1e4368919cb0122c53eacf0b96b96df44189be3ca32e
7
- data.tar.gz: 9ed8869fce8e66c4e9aee275ac3a28c24db47fd2ae8537f2ff3ee100541f122cd9b8b87e7ca102567b976548eb04b53b06f6c560f2e6e12dc3ca506e0bb87f01
6
+ metadata.gz: ffa8231621bc6d26f446425cab725e4a3fdcef5a5ec1c7bc240f6397c6fd07514863b56a44c89b413c4750f4a20eb8b218500ea2ab5c0120b7c158f5a24b9a46
7
+ data.tar.gz: 6b9582714d55a4e1150b01fcb3eada86751f6408b943811570470c88fda04a7e9db8fdc8a1da65ff46bdf465eb641be521ac474fe1ec6dcc952598d6c32594c0
data/.gitignore CHANGED
@@ -46,6 +46,5 @@ pkg
46
46
 
47
47
  # For rubinius:
48
48
  #*.rbc
49
- Gemfile.lock
50
49
 
51
- .rspec_status
50
+ .rspec_status
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.0
data/Gemfile.lock ADDED
@@ -0,0 +1,234 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ shopify-money (2.2.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ actioncable (6.1.7.7)
10
+ actionpack (= 6.1.7.7)
11
+ activesupport (= 6.1.7.7)
12
+ nio4r (~> 2.0)
13
+ websocket-driver (>= 0.6.1)
14
+ actionmailbox (6.1.7.7)
15
+ actionpack (= 6.1.7.7)
16
+ activejob (= 6.1.7.7)
17
+ activerecord (= 6.1.7.7)
18
+ activestorage (= 6.1.7.7)
19
+ activesupport (= 6.1.7.7)
20
+ mail (>= 2.7.1)
21
+ actionmailer (6.1.7.7)
22
+ actionpack (= 6.1.7.7)
23
+ actionview (= 6.1.7.7)
24
+ activejob (= 6.1.7.7)
25
+ activesupport (= 6.1.7.7)
26
+ mail (~> 2.5, >= 2.5.4)
27
+ rails-dom-testing (~> 2.0)
28
+ actionpack (6.1.7.7)
29
+ actionview (= 6.1.7.7)
30
+ activesupport (= 6.1.7.7)
31
+ rack (~> 2.0, >= 2.0.9)
32
+ rack-test (>= 0.6.3)
33
+ rails-dom-testing (~> 2.0)
34
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
35
+ actiontext (6.1.7.7)
36
+ actionpack (= 6.1.7.7)
37
+ activerecord (= 6.1.7.7)
38
+ activestorage (= 6.1.7.7)
39
+ activesupport (= 6.1.7.7)
40
+ nokogiri (>= 1.8.5)
41
+ actionview (6.1.7.7)
42
+ activesupport (= 6.1.7.7)
43
+ builder (~> 3.1)
44
+ erubi (~> 1.4)
45
+ rails-dom-testing (~> 2.0)
46
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
47
+ activejob (6.1.7.7)
48
+ activesupport (= 6.1.7.7)
49
+ globalid (>= 0.3.6)
50
+ activemodel (6.1.7.7)
51
+ activesupport (= 6.1.7.7)
52
+ activerecord (6.1.7.7)
53
+ activemodel (= 6.1.7.7)
54
+ activesupport (= 6.1.7.7)
55
+ activestorage (6.1.7.7)
56
+ actionpack (= 6.1.7.7)
57
+ activejob (= 6.1.7.7)
58
+ activerecord (= 6.1.7.7)
59
+ activesupport (= 6.1.7.7)
60
+ marcel (~> 1.0)
61
+ mini_mime (>= 1.1.0)
62
+ activesupport (6.1.7.7)
63
+ concurrent-ruby (~> 1.0, >= 1.0.2)
64
+ i18n (>= 1.6, < 2)
65
+ minitest (>= 5.1)
66
+ tzinfo (~> 2.0)
67
+ zeitwerk (~> 2.3)
68
+ ast (2.4.2)
69
+ builder (3.2.4)
70
+ byebug (11.1.3)
71
+ coderay (1.1.3)
72
+ concurrent-ruby (1.2.3)
73
+ crass (1.0.6)
74
+ database_cleaner (1.99.0)
75
+ date (3.3.4)
76
+ diff-lcs (1.5.1)
77
+ docile (1.4.0)
78
+ erubi (1.12.0)
79
+ globalid (1.2.1)
80
+ activesupport (>= 6.1)
81
+ i18n (1.14.4)
82
+ concurrent-ruby (~> 1.0)
83
+ jaro_winkler (1.5.6)
84
+ loofah (2.22.0)
85
+ crass (~> 1.0.2)
86
+ nokogiri (>= 1.12.0)
87
+ mail (2.8.1)
88
+ mini_mime (>= 0.1.1)
89
+ net-imap
90
+ net-pop
91
+ net-smtp
92
+ marcel (1.0.4)
93
+ method_source (1.0.0)
94
+ mini_mime (1.1.5)
95
+ minitest (5.22.3)
96
+ net-imap (0.4.10)
97
+ date
98
+ net-protocol
99
+ net-pop (0.1.2)
100
+ net-protocol
101
+ net-protocol (0.2.2)
102
+ timeout
103
+ net-smtp (0.5.0)
104
+ net-protocol
105
+ nio4r (2.7.1)
106
+ nokogiri (1.16.3-aarch64-linux)
107
+ racc (~> 1.4)
108
+ nokogiri (1.16.3-arm-linux)
109
+ racc (~> 1.4)
110
+ nokogiri (1.16.3-arm64-darwin)
111
+ racc (~> 1.4)
112
+ nokogiri (1.16.3-x86-linux)
113
+ racc (~> 1.4)
114
+ nokogiri (1.16.3-x86_64-darwin)
115
+ racc (~> 1.4)
116
+ nokogiri (1.16.3-x86_64-linux)
117
+ racc (~> 1.4)
118
+ parallel (1.24.0)
119
+ parser (3.3.0.5)
120
+ ast (~> 2.4.1)
121
+ racc
122
+ pry (0.14.2)
123
+ coderay (~> 1.1)
124
+ method_source (~> 1.0)
125
+ pry-byebug (3.10.1)
126
+ byebug (~> 11.0)
127
+ pry (>= 0.13, < 0.15)
128
+ racc (1.7.3)
129
+ rack (2.2.9)
130
+ rack-test (2.1.0)
131
+ rack (>= 1.3)
132
+ rails (6.1.7.7)
133
+ actioncable (= 6.1.7.7)
134
+ actionmailbox (= 6.1.7.7)
135
+ actionmailer (= 6.1.7.7)
136
+ actionpack (= 6.1.7.7)
137
+ actiontext (= 6.1.7.7)
138
+ actionview (= 6.1.7.7)
139
+ activejob (= 6.1.7.7)
140
+ activemodel (= 6.1.7.7)
141
+ activerecord (= 6.1.7.7)
142
+ activestorage (= 6.1.7.7)
143
+ activesupport (= 6.1.7.7)
144
+ bundler (>= 1.15.0)
145
+ railties (= 6.1.7.7)
146
+ sprockets-rails (>= 2.0.0)
147
+ rails-dom-testing (2.2.0)
148
+ activesupport (>= 5.0.0)
149
+ minitest
150
+ nokogiri (>= 1.6)
151
+ rails-html-sanitizer (1.6.0)
152
+ loofah (~> 2.21)
153
+ nokogiri (~> 1.14)
154
+ railties (6.1.7.7)
155
+ actionpack (= 6.1.7.7)
156
+ activesupport (= 6.1.7.7)
157
+ method_source
158
+ rake (>= 12.2)
159
+ thor (~> 1.0)
160
+ rainbow (3.1.1)
161
+ rake (13.2.0)
162
+ rexml (3.2.6)
163
+ rspec (3.13.0)
164
+ rspec-core (~> 3.13.0)
165
+ rspec-expectations (~> 3.13.0)
166
+ rspec-mocks (~> 3.13.0)
167
+ rspec-core (3.13.0)
168
+ rspec-support (~> 3.13.0)
169
+ rspec-expectations (3.13.0)
170
+ diff-lcs (>= 1.2.0, < 2.0)
171
+ rspec-support (~> 3.13.0)
172
+ rspec-mocks (3.13.0)
173
+ diff-lcs (>= 1.2.0, < 2.0)
174
+ rspec-support (~> 3.13.0)
175
+ rspec-support (3.13.1)
176
+ rubocop (0.81.0)
177
+ jaro_winkler (~> 1.5.1)
178
+ parallel (~> 1.10)
179
+ parser (>= 2.7.0.1)
180
+ rainbow (>= 2.2.2, < 4.0)
181
+ rexml
182
+ ruby-progressbar (~> 1.7)
183
+ unicode-display_width (>= 1.4.0, < 2.0)
184
+ ruby-progressbar (1.13.0)
185
+ simplecov (0.22.0)
186
+ docile (~> 1.1)
187
+ simplecov-html (~> 0.11)
188
+ simplecov_json_formatter (~> 0.1)
189
+ simplecov-html (0.12.3)
190
+ simplecov_json_formatter (0.1.4)
191
+ sprockets (4.2.1)
192
+ concurrent-ruby (~> 1.0)
193
+ rack (>= 2.2.4, < 4)
194
+ sprockets-rails (3.4.2)
195
+ actionpack (>= 5.2)
196
+ activesupport (>= 5.2)
197
+ sprockets (>= 3.0.0)
198
+ sqlite3 (1.7.3-aarch64-linux)
199
+ sqlite3 (1.7.3-arm-linux)
200
+ sqlite3 (1.7.3-arm64-darwin)
201
+ sqlite3 (1.7.3-x86-linux)
202
+ sqlite3 (1.7.3-x86_64-darwin)
203
+ sqlite3 (1.7.3-x86_64-linux)
204
+ thor (1.3.1)
205
+ timeout (0.4.1)
206
+ tzinfo (2.0.6)
207
+ concurrent-ruby (~> 1.0)
208
+ unicode-display_width (1.8.0)
209
+ websocket-driver (0.7.6)
210
+ websocket-extensions (>= 0.1.0)
211
+ websocket-extensions (0.1.5)
212
+ zeitwerk (2.6.13)
213
+
214
+ PLATFORMS
215
+ aarch64-linux
216
+ arm-linux
217
+ arm64-darwin
218
+ x86-linux
219
+ x86_64-darwin
220
+ x86_64-linux
221
+
222
+ DEPENDENCIES
223
+ bundler
224
+ database_cleaner (~> 1.6)
225
+ pry-byebug
226
+ rails (~> 6.0)
227
+ rspec (~> 3.2)
228
+ rubocop (~> 0.81.0)
229
+ shopify-money!
230
+ simplecov
231
+ sqlite3
232
+
233
+ BUNDLED WITH
234
+ 2.5.3
data/README.md CHANGED
@@ -11,7 +11,8 @@ money_column expects a DECIMAL(21,3) database field.
11
11
  - Provides a `Money::Currency` class which encapsulates all information about a monetary unit.
12
12
  - Represents monetary values as decimals. No need to convert your amounts every time you use them. Easily understand the data in your DB.
13
13
  - Does NOT provide APIs for exchanging money from one currency to another.
14
- - Will not lose pennies during divisions
14
+ - Will not lose pennies during divisions. For instance, given $1 / 3 the resulting chunks will be .34, .33, and .33. Notice that one chunk is larger than the others, so the result still adds to $1.
15
+ - Allows callers to select a rounding strategy when dividing, to determine the order in which leftover pennies are given out.
15
16
 
16
17
  ## Installation
17
18
 
@@ -57,6 +58,30 @@ m.allocate([Rational(2, 3), Rational(1, 3)]).map(&:value) == [666.67, 333.33]
57
58
  m.allocate_max_amounts([500, 300, 200]).map(&:value) == [500, 300, 200]
58
59
  m.allocate_max_amounts([500, 300, 300]).map(&:value) == [454.55, 272.73, 272.72]
59
60
 
61
+ ## Selectable rounding strategies during division
62
+
63
+ # Assigns leftover subunits left to right
64
+ m = Money::Allocator.new(Money.new(10.55, "USD"))
65
+ monies = m.allocate([0.25, 0.5, 0.25], :roundrobin)
66
+ #monies[0] == 2.64 <-- gets 1 penny
67
+ #monies[1] == 5.28 <-- gets 1 penny
68
+ #monies[2] == 2.63 <-- gets no penny
69
+
70
+ # Assigns leftover subunits right to left
71
+ m = Money::Allocator.new(Money.new(10.55, "USD"))
72
+ monies = m.allocate([0.25, 0.5, 0.25], :roundrobin_reverse)
73
+ #monies[0] == 2.63 <-- gets no penny
74
+ #monies[1] == 5.28 <-- gets 1 penny
75
+ #monies[2] == 2.64 <-- gets 1 penny
76
+
77
+ # Assigns leftover subunits to the nearest whole subunit
78
+ m = Money::Allocator.new(Money.new(10.55, "USD"))
79
+ monies = m.allocate([0.25, 0.5, 0.25], :nearest)
80
+ #monies[0] == 2.64 <-- gets 1 penny
81
+ #monies[1] == 5.27 <-- gets no penny
82
+ #monies[2] == 2.64 <-- gets 1 penny
83
+ # $2.6375 is closer to the next whole penny than $5.275
84
+
60
85
  # Clamp
61
86
  Money.new(50, "USD").clamp(1, 100) == Money.new(50, "USD")
62
87
 
data/dev.yml CHANGED
@@ -3,7 +3,7 @@
3
3
  ---
4
4
  name: money
5
5
  up:
6
- - ruby: 3.3.0
6
+ - ruby
7
7
  - bundler
8
8
  commands:
9
9
  test: bundle exec rspec
@@ -9,18 +9,27 @@ class Money
9
9
 
10
10
  ONE = BigDecimal("1")
11
11
 
12
- # Allocates money between different parties without losing pennies.
13
- # After the mathematically split has been performed, left over pennies will
14
- # be distributed round-robin amongst the parties. This means that parties
15
- # listed first will likely receive more pennies than ones that are listed later
12
+ # Allocates money between different parties without losing subunits. A "subunit"
13
+ # in this context is the smallest unit of a currency that can be divided no
14
+ # further. In USD the unit is dollars and the subunit is cents. In JPY the unit
15
+ # is yen and the subunit is also yen. So given $1 divided by 3, the resulting subunits
16
+ # should be [34¢, 33¢, 33¢]. Notice that one of these chunks is larger than the other
17
+ # two, because we cannot transact in amounts less than 1 subunit.
18
+ #
19
+ # After the mathematically split has been performed, left over subunits will
20
+ # be distributed round-robin or nearest-subunit strategy amongst the parties.
21
+ # Round-robin strategy has the virtue of being easier to understand, while
22
+ # nearest-subunit is a more complex alogirthm that results in the most fair
23
+ # distribution.
16
24
  #
17
25
  # @param splits [Array<Numeric>]
18
26
  # @param strategy Symbol
19
27
  # @return [Array<Money>]
20
28
  #
21
29
  # Strategies:
22
- # - `:roundrobin` (default): leftover pennies will be accumulated starting from the first allocation left to right
23
- # - `:roundrobin_reverse`: leftover pennies will be accumulated starting from the last allocation right to left
30
+ # - `:roundrobin` (default): leftover subunits will be accumulated starting from the first allocation left to right
31
+ # - `:roundrobin_reverse`: leftover subunits will be accumulated starting from the last allocation right to left
32
+ # - `:nearest`: leftover subunits will by given first to the party closest to the next whole subunit
24
33
  #
25
34
  # @example
26
35
  # Money.new(5, "USD").allocate([0.50, 0.25, 0.25])
@@ -38,29 +47,49 @@ class Money
38
47
  # Money.new(30, "USD").allocate([Rational(2, 3), Rational(1, 3)])
39
48
  # #=> [#<Money value:20.00 currency:USD>, #<Money value:10.00 currency:USD>]
40
49
 
41
- # @example left over pennies distributed reverse order when using roundrobin_reverse strategy
50
+ # @example left over subunits distributed reverse order when using roundrobin_reverse strategy
42
51
  # Money.new(10.01, "USD").allocate([0.5, 0.5], :roundrobin_reverse)
43
52
  # #=> [#<Money value:5.00 currency:USD>, #<Money value:5.01 currency:USD>]
53
+
54
+ # @examples left over subunits distributed by nearest strategy
55
+ # Money.new(10.55, "USD").allocate([0.25, 0.5, 0.25], :nearest)
56
+ # #=> [#<Money value:2.64 currency:USD>, #<Money value:5.27 currency:USD>, #<Money value:2.64 currency:USD>]
57
+
44
58
  def allocate(splits, strategy = :roundrobin)
59
+ if splits.empty?
60
+ raise ArgumentError, 'at least one split must be provided'
61
+ end
62
+
45
63
  splits.map!(&:to_r)
46
64
  allocations = splits.inject(0, :+)
47
65
 
48
66
  if (allocations - ONE) > Float::EPSILON
49
- raise ArgumentError, "splits add to more than 100%"
67
+ raise ArgumentError, "allocations add to more than 100%"
50
68
  end
51
69
 
52
70
  amounts, left_over = amounts_from_splits(allocations, splits)
53
71
 
72
+ order = case strategy
73
+ when :roundrobin
74
+ (0...left_over).to_a
75
+ when :roundrobin_reverse
76
+ (0...amounts.length).to_a.reverse
77
+ when :nearest
78
+ rank_by_nearest(amounts)
79
+ else
80
+ raise ArgumentError, "Invalid strategy. Valid options: :roundrobin, :roundrobin_reverse, :nearest"
81
+ end
82
+
54
83
  left_over.to_i.times do |i|
55
- amounts[allocation_index_for(strategy, amounts.length, i)] += 1
84
+ amounts[order[i]][:whole_subunits] += 1
56
85
  end
57
86
 
58
- amounts.collect { |subunits| Money.from_subunits(subunits, currency) }
87
+ amounts.map { |amount| Money.from_subunits(amount[:whole_subunits], currency) }
59
88
  end
60
89
 
61
90
  # Allocates money between different parties up to the maximum amounts specified.
62
- # Left over pennies will be assigned round-robin up to the maximum specified.
63
- # Pennies are dropped when the maximums are attained.
91
+ # Left over subunits will be assigned round-robin up to the maximum specified.
92
+ # Subunits are dropped when the maximums are attained.
64
93
  #
65
94
  # @example
66
95
  # Money.new(30.75).allocate_max_amounts([Money.new(26), Money.new(4.75)])
@@ -90,6 +119,7 @@ class Money
90
119
  total_allocatable = [maximums_total.subunits, self.subunits].min
91
120
 
92
121
  subunits_amounts, left_over = amounts_from_splits(1, splits, total_allocatable)
122
+ subunits_amounts.map! { |amount| amount[:whole_subunits] }
93
123
 
94
124
  subunits_amounts.each_with_index do |amount, index|
95
125
  break unless left_over > 0
@@ -119,10 +149,14 @@ class Money
119
149
 
120
150
  left_over = subunits_to_split
121
151
 
122
- amounts = splits.collect do |ratio|
123
- frac = (subunits_to_split * ratio / allocations.to_r).floor
124
- left_over -= frac
125
- frac
152
+ amounts = splits.map do |ratio|
153
+ whole_subunits = (subunits_to_split * ratio / allocations.to_r).floor
154
+ fractional_subunits = (subunits_to_split * ratio / allocations.to_r).to_f - whole_subunits
155
+ left_over -= whole_subunits
156
+ {
157
+ :whole_subunits => whole_subunits,
158
+ :fractional_subunits => fractional_subunits
159
+ }
126
160
  end
127
161
 
128
162
  [amounts, left_over]
@@ -132,15 +166,13 @@ class Money
132
166
  splits.all? { |split| split.is_a?(Rational) }
133
167
  end
134
168
 
135
- def allocation_index_for(strategy, length, idx)
136
- case strategy
137
- when :roundrobin
138
- idx % length
139
- when :roundrobin_reverse
140
- length - (idx % length) - 1
141
- else
142
- raise ArgumentError, "Invalid strategy. Valid options: :roundrobin, :roundrobin_reverse"
143
- end
169
+ # Given a list of decimal numbers, return a list ordered by which is nearest to the next whole number.
170
+ # For instance, given inputs [1.1, 1.5, 1.9] the correct ranking is 2, 1, 0. This is because 1.9 is nearly 2.
171
+ # Note that we are not ranking by absolute size, we only care about the distance between our input number and
172
+ # the next whole number. Similarly, given the input [9.1, 5.5, 3.9] the correct ranking is *still* 2, 1, 0. This
173
+ # is because 3.9 is nearer to 4 than 9.1 is to 10.
174
+ def rank_by_nearest(amounts)
175
+ amounts.each_with_index.sort_by{ |amount, i| 1 - amount[:fractional_subunits] }.map(&:last)
144
176
  end
145
177
  end
146
178
  end
data/lib/money/helpers.rb CHANGED
@@ -16,6 +16,8 @@ class Money
16
16
  def value_to_decimal(num)
17
17
  value =
18
18
  case num
19
+ when Money
20
+ num.value
19
21
  when BigDecimal
20
22
  num
21
23
  when nil, 0, ''
data/lib/money/money.rb CHANGED
@@ -10,6 +10,30 @@ class Money
10
10
  attr_reader :value, :currency
11
11
  def_delegators :@value, :zero?, :nonzero?, :positive?, :negative?, :to_i, :to_f, :hash
12
12
 
13
+ class ReverseOperationProxy
14
+ include Comparable
15
+
16
+ def initialize(value)
17
+ @value = value
18
+ end
19
+
20
+ def <=>(other)
21
+ -(other <=> @value)
22
+ end
23
+
24
+ def +(other)
25
+ other + @value
26
+ end
27
+
28
+ def -(other)
29
+ -(other - @value)
30
+ end
31
+
32
+ def *(other)
33
+ other * @value
34
+ end
35
+ end
36
+
13
37
  class << self
14
38
  extend Forwardable
15
39
  attr_accessor :config
@@ -21,6 +45,8 @@ class Money
21
45
  end
22
46
 
23
47
  def new(value = 0, currency = nil)
48
+ return new_from_money(value, currency) if value.is_a?(Money)
49
+
24
50
  value = Helpers.value_to_decimal(value)
25
51
  currency = Helpers.value_to_currency(currency)
26
52
 
@@ -76,6 +102,20 @@ class Money
76
102
  Money.current_currency = old_currency
77
103
  end
78
104
  end
105
+
106
+ private
107
+
108
+ def new_from_money(amount, currency)
109
+ return amount if amount.currency.compatible?(Helpers.value_to_currency(currency))
110
+
111
+ msg = "Money.new is attempting to change currency of an existing money object"
112
+ if Money.config.legacy_deprecations
113
+ Money.deprecate("#{msg}. A Money::IncompatibleCurrencyError will raise in the next major release")
114
+ return Money.new(amount.value, currency)
115
+ else
116
+ raise Money::IncompatibleCurrencyError, msg
117
+ end
118
+ end
79
119
  end
80
120
  configure
81
121
 
@@ -162,43 +202,22 @@ class Money
162
202
  value == other.value
163
203
  end
164
204
 
165
- class ReverseOperationProxy
166
- include Comparable
167
-
168
- def initialize(value)
169
- @value = value
170
- end
171
-
172
- def <=>(other)
173
- -(other <=> @value)
174
- end
175
-
176
- def +(other)
177
- other + @value
178
- end
179
-
180
- def -(other)
181
- -(other - @value)
182
- end
183
-
184
- def *(other)
185
- other * @value
186
- end
187
- end
188
-
189
205
  def coerce(other)
190
206
  raise TypeError, "Money can't be coerced into #{other.class}" unless other.is_a?(Numeric)
191
207
  [ReverseOperationProxy.new(other), self]
192
208
  end
193
209
 
194
- def to_money(curr = nil)
195
- if !curr.nil? && no_currency?
196
- return Money.new(value, curr)
210
+ def to_money(new_currency = nil)
211
+ if new_currency.nil?
212
+ return self
213
+ end
214
+
215
+ if no_currency?
216
+ return Money.new(value, new_currency)
197
217
  end
198
218
 
199
- curr = Helpers.value_to_currency(curr)
200
- unless currency.compatible?(curr)
201
- msg = "mathematical operation not permitted for Money objects with different currencies #{curr} and #{currency}"
219
+ unless currency.compatible?(Helpers.value_to_currency(new_currency))
220
+ msg = "to_money is attempting to change currency of an existing money object from #{currency} to #{new_currency}"
202
221
  if Money.config.legacy_deprecations
203
222
  Money.deprecate("#{msg}. A Money::IncompatibleCurrencyError will raise in the next major release")
204
223
  else
@@ -293,12 +312,12 @@ class Money
293
312
  #
294
313
  # @param [2] number of parties.
295
314
  #
296
- # @return [Array<Money, Money, Money>]
315
+ # @return [Enumerable<Money, Money, Money>]
297
316
  #
298
317
  # @example
299
- # Money.new(100, "USD").split(3) #=> [Money.new(34), Money.new(33), Money.new(33)]
318
+ # Money.new(100, "USD").split(3) #=> Enumerable[Money.new(34), Money.new(33), Money.new(33)]
300
319
  def split(num)
301
- calculate_splits(num).sum([]) { |value, count| Array.new(count, value) }
320
+ Splitter.new(self, num)
302
321
  end
303
322
 
304
323
  # Calculate the splits evenly without losing pennies.
@@ -313,17 +332,7 @@ class Money
313
332
  # @example
314
333
  # Money.new(100, "USD").calculate_splits(3) #=> {Money.new(34) => 1, Money.new(33) => 2}
315
334
  def calculate_splits(num)
316
- raise ArgumentError, "need at least one party" if num < 1
317
- subunits = self.subunits
318
- low = Money.from_subunits(subunits / num, currency)
319
- high = Money.from_subunits(low.subunits + 1, currency)
320
-
321
- num_high = subunits % num
322
-
323
- {}.tap do |result|
324
- result[high] = num_high if num_high > 0
325
- result[low] = num - num_high
326
- end
335
+ Splitter.new(self, num).split.dup
327
336
  end
328
337
 
329
338
  # Clamps the value to be within the specified minimum and maximum. Returns
@@ -350,10 +359,24 @@ class Money
350
359
  private
351
360
 
352
361
  def arithmetic(money_or_numeric)
353
- raise TypeError, "#{money_or_numeric.class.name} can't be coerced into Money" unless money_or_numeric.respond_to?(:to_money)
354
- other = money_or_numeric.to_money(currency)
362
+ case money_or_numeric
363
+ when Money
364
+ unless currency.compatible?(money_or_numeric.currency)
365
+ msg = "mathematical operation not permitted for Money objects with different currencies #{money_or_numeric.currency} and #{currency}."
366
+ if Money.config.legacy_deprecations
367
+ Money.deprecate("#{msg}. A Money::IncompatibleCurrencyError will raise in the next major release")
368
+ else
369
+ raise Money::IncompatibleCurrencyError, msg
370
+ end
371
+ end
372
+ yield(money_or_numeric)
373
+
374
+ when Numeric
375
+ yield(Money.new(money_or_numeric, currency))
355
376
 
356
- yield(other)
377
+ else
378
+ raise TypeError, "#{money_or_numeric.class.name} can't be coerced into Money"
379
+ end
357
380
  end
358
381
 
359
382
  def calculated_currency(other)