shopify-money 2.0.0 → 2.2.0

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