tiny-sharp-rb 0.0.1

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.
@@ -0,0 +1,346 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Money
4
+ module Arithmetic
5
+ # Wrapper for coerced numeric values to distinguish
6
+ # when numeric was on the 1st place in operation.
7
+ CoercedNumeric = Struct.new(:value) do
8
+ # Proxy #zero? method to skip unnecessary typecasts. See #- and #+.
9
+ def zero?
10
+ value.zero?
11
+ end
12
+ end
13
+
14
+ # Returns a money object with changed polarity.
15
+ #
16
+ # @return [Money]
17
+ #
18
+ # @example
19
+ # - Money.new(100) #=> #<Money @fractional=-100>
20
+ def -@
21
+ dup_with(fractional: -fractional)
22
+ end
23
+
24
+ # Checks whether two Money objects have the same currency and the same
25
+ # amount. Checks against objects that are not Money or a subclass will
26
+ # always return false.
27
+ #
28
+ # @param [Money] other_money Value to compare with.
29
+ #
30
+ # @return [Boolean]
31
+ #
32
+ # @example
33
+ # Money.new(1_00).eql?(Money.new(1_00)) #=> true
34
+ # Money.new(1_00).eql?(Money.new(1_01)) #=> false
35
+ # Money.new(1_00, "USD").eql?(Money.new(1_00, "GBP")) #=> false
36
+ # Money.new(0, "USD").eql?(Money.new(0, "EUR")) #=> false
37
+ # Money.new(1_00).eql?("1.00") #=> false
38
+ #
39
+ # @see Money.strict_eql_compare
40
+ def eql?(other_money)
41
+ if other_money.is_a?(Money)
42
+ if !Money.strict_eql_compare && fractional == 0 && other_money.fractional == 0
43
+ warn "[DEPRECATION] Comparing 0 #{currency} with 0 " \
44
+ "#{other_money.currency} using `#eql?` will return false in " \
45
+ "future versions of Money. Opt-in to the new behavior by " \
46
+ "setting `Money.strict_eql_compare = true`."
47
+ return true
48
+ end
49
+
50
+ fractional == other_money.fractional && currency == other_money.currency
51
+ else
52
+ false
53
+ end
54
+ end
55
+
56
+ # Compares two Money objects. If money objects have a different currency it
57
+ # will attempt to convert the currency.
58
+ #
59
+ # @param [Money] other Value to compare with.
60
+ #
61
+ # @return [Integer]
62
+ #
63
+ # @raise [TypeError] when other object is not Money
64
+ #
65
+ def <=>(other)
66
+ unless other.is_a?(Money)
67
+ return unless other.respond_to?(:zero?) && other.zero?
68
+ return other.is_a?(CoercedNumeric) ? 0 <=> fractional : fractional <=> 0
69
+ end
70
+
71
+ # Always allow comparison with zero
72
+ if zero? || other.zero?
73
+ return fractional <=> other.fractional
74
+ end
75
+
76
+ other = other.exchange_to(currency)
77
+ fractional <=> other.fractional
78
+ rescue Money::Bank::UnknownRate
79
+ end
80
+
81
+ # Uses Comparable's implementation but raises ArgumentError if non-zero
82
+ # numeric value is given.
83
+ def ==(other)
84
+ if other.is_a?(Numeric) && !other.zero?
85
+ raise ArgumentError, 'Money#== supports only zero numerics'
86
+ end
87
+ super
88
+ end
89
+
90
+ # Test if the amount is positive. Returns +true+ if the money amount is
91
+ # greater than 0, +false+ otherwise.
92
+ #
93
+ # @return [Boolean]
94
+ #
95
+ # @example
96
+ # Money.new(1).positive? #=> true
97
+ # Money.new(0).positive? #=> false
98
+ # Money.new(-1).positive? #=> false
99
+ def positive?
100
+ fractional > 0
101
+ end
102
+
103
+ # Test if the amount is negative. Returns +true+ if the money amount is
104
+ # less than 0, +false+ otherwise.
105
+ #
106
+ # @return [Boolean]
107
+ #
108
+ # @example
109
+ # Money.new(-1).negative? #=> true
110
+ # Money.new(0).negative? #=> false
111
+ # Money.new(1).negative? #=> false
112
+ def negative?
113
+ fractional < 0
114
+ end
115
+
116
+ # @!method +(other)
117
+ # Returns a new Money object containing the sum of the two operands' monetary
118
+ # values. If +other_money+ has a different currency then its monetary value
119
+ # is automatically exchanged to this object's currency using +exchange_to+.
120
+ #
121
+ # @param [Money] other Other +Money+ object to add.
122
+ # @return [Money]
123
+ #
124
+ # @example
125
+ # Money.new(100) + Money.new(100) #=> #<Money @fractional=200>
126
+
127
+ # @!method -(other)
128
+ # Returns a new Money object containing the difference between the two
129
+ # operands' monetary values. If +other_money+ has a different currency then
130
+ # its monetary value is automatically exchanged to this object's currency
131
+ # using +exchange_to+.
132
+ #
133
+ # @param [Money] other Other +Money+ object to subtract.
134
+ # @return [Money]
135
+ #
136
+ # @example
137
+ # Money.new(100) - Money.new(99) #=> #<Money @fractional=1>
138
+ [:+, :-].each do |op|
139
+ non_zero_message = lambda do |value|
140
+ "Can't add or subtract a non-zero #{value.class.name} value"
141
+ end
142
+
143
+ define_method(op) do |other|
144
+ case other
145
+ when Money
146
+ other = other.exchange_to(currency)
147
+ new_fractional = fractional.public_send(op, other.fractional)
148
+ dup_with(fractional: new_fractional)
149
+ when CoercedNumeric
150
+ raise TypeError, non_zero_message.call(other.value) unless other.zero?
151
+ dup_with(fractional: other.value.public_send(op, fractional))
152
+ when Numeric
153
+ raise TypeError, non_zero_message.call(other) unless other.zero?
154
+ self
155
+ else
156
+ raise TypeError, "Unsupported argument type: #{other.class.name}"
157
+ end
158
+ end
159
+ end
160
+
161
+ # Multiplies the monetary value with the given number and returns a new
162
+ # +Money+ object with this monetary value and the same currency.
163
+ #
164
+ # Note that you can't multiply a Money object by an other +Money+ object.
165
+ #
166
+ # @param [Numeric] value Number to multiply by.
167
+ #
168
+ # @return [Money] The resulting money.
169
+ #
170
+ # @raise [TypeError] If +value+ is NOT a number.
171
+ #
172
+ # @example
173
+ # Money.new(100) * 2 #=> #<Money @fractional=200>
174
+ #
175
+ def *(value)
176
+ value = value.value if value.is_a?(CoercedNumeric)
177
+ if value.is_a? Numeric
178
+ dup_with(fractional: fractional * value)
179
+ else
180
+ raise TypeError, "Can't multiply a #{self.class.name} by a #{value.class.name}'s value"
181
+ end
182
+ end
183
+
184
+ # Divides the monetary value with the given number and returns a new +Money+
185
+ # object with this monetary value and the same currency.
186
+ # Can also divide by another +Money+ object to get a ratio.
187
+ #
188
+ # +Money/Numeric+ returns +Money+. +Money/Money+ returns +Float+.
189
+ #
190
+ # @param [Money, Numeric] value Number to divide by.
191
+ #
192
+ # @return [Money] The resulting money if you divide Money by a number.
193
+ # @return [Float] The resulting number if you divide Money by a Money.
194
+ #
195
+ # @example
196
+ # Money.new(100) / 10 #=> #<Money @fractional=10>
197
+ # Money.new(100) / Money.new(10) #=> 10.0
198
+ #
199
+ def /(value)
200
+ if value.is_a?(self.class)
201
+ exchanged = value.exchange_to(currency)
202
+ raise ZeroDivisionError, "divided by Money(0)" if exchanged.zero?
203
+ fractional / as_d(exchanged.fractional).to_f
204
+ else
205
+ raise TypeError, 'Can not divide by Money' if value.is_a?(CoercedNumeric)
206
+
207
+ value = as_d(value)
208
+ raise ZeroDivisionError, "divided by zero" if value.zero?
209
+ dup_with(fractional: fractional / value)
210
+ end
211
+ end
212
+
213
+ # Synonym for +#/+.
214
+ #
215
+ # @param [Money, Numeric] value Number to divide by.
216
+ #
217
+ # @return [Money] The resulting money if you divide Money by a number.
218
+ # @return [Float] The resulting number if you divide Money by a Money.
219
+ #
220
+ # @see #/
221
+ #
222
+ def div(value)
223
+ self / value
224
+ end
225
+
226
+ # Divide money by money or fixnum and return array containing quotient and
227
+ # modulus.
228
+ #
229
+ # @param [Money, Integer] val Number to divmod by.
230
+ #
231
+ # @return [Array<Money,Money>,Array<Integer,Money>]
232
+ #
233
+ # @example
234
+ # Money.new(100).divmod(9) #=> [#<Money @fractional=11>, #<Money @fractional=1>]
235
+ # Money.new(100).divmod(Money.new(9)) #=> [11, #<Money @fractional=1>]
236
+ def divmod(val)
237
+ if val.is_a?(Money)
238
+ divmod_money(val)
239
+ else
240
+ divmod_other(val)
241
+ end
242
+ end
243
+
244
+ def divmod_money(val)
245
+ cents = val.exchange_to(currency).cents
246
+ raise ZeroDivisionError, "divided by Money(0)" if cents == 0
247
+ quotient, remainder = fractional.divmod(cents)
248
+ [quotient, dup_with(fractional: remainder)]
249
+ end
250
+ private :divmod_money
251
+
252
+ def divmod_other(val)
253
+ val = as_d(val)
254
+ raise ZeroDivisionError, "divided by zero" if val.zero?
255
+ quotient, remainder = fractional.divmod(val)
256
+ [dup_with(fractional: quotient), dup_with(fractional: remainder)]
257
+ end
258
+ private :divmod_other
259
+
260
+ # Equivalent to +self.divmod(val)[1]+
261
+ #
262
+ # @param [Money, Integer] val Number take modulo with.
263
+ #
264
+ # @return [Money]
265
+ #
266
+ # @example
267
+ # Money.new(100).modulo(9) #=> #<Money @fractional=1>
268
+ # Money.new(100).modulo(Money.new(9)) #=> #<Money @fractional=1>
269
+ def modulo(val)
270
+ divmod(val)[1]
271
+ end
272
+
273
+ # Synonym for +#modulo+.
274
+ #
275
+ # @param [Money, Integer] val Number take modulo with.
276
+ #
277
+ # @return [Money]
278
+ #
279
+ # @see #modulo
280
+ def %(val)
281
+ modulo(val)
282
+ end
283
+
284
+ # If different signs +self.modulo(val) - val+ otherwise +self.modulo(val)+
285
+ #
286
+ # @param [Money, Integer] val Number to rake remainder with.
287
+ #
288
+ # @return [Money]
289
+ #
290
+ # @example
291
+ # Money.new(100).remainder(9) #=> #<Money @fractional=1>
292
+ def remainder(val)
293
+ if val.is_a?(Money) && currency != val.currency
294
+ val = val.exchange_to(currency)
295
+ end
296
+
297
+ if (fractional < 0 && val < 0) || (fractional > 0 && val > 0)
298
+ self.modulo(val)
299
+ else
300
+ self.modulo(val) - (val.is_a?(Money) ? val : dup_with(fractional: val))
301
+ end
302
+ end
303
+
304
+ # Return absolute value of self as a new Money object.
305
+ #
306
+ # @return [Money]
307
+ #
308
+ # @example
309
+ # Money.new(-100).abs #=> #<Money @fractional=100>
310
+ def abs
311
+ dup_with(fractional: fractional.abs)
312
+ end
313
+
314
+ # Test if the money amount is zero.
315
+ #
316
+ # @return [Boolean]
317
+ #
318
+ # @example
319
+ # Money.new(100).zero? #=> false
320
+ # Money.new(0).zero? #=> true
321
+ def zero?
322
+ fractional == 0
323
+ end
324
+
325
+ # Test if the money amount is non-zero. Returns this money object if it is
326
+ # non-zero, or nil otherwise, like +Numeric#nonzero?+.
327
+ #
328
+ # @return [Money, nil]
329
+ #
330
+ # @example
331
+ # Money.new(100).nonzero? #=> #<Money @fractional=100>
332
+ # Money.new(0).nonzero? #=> nil
333
+ def nonzero?
334
+ fractional != 0 ? self : nil
335
+ end
336
+
337
+ # Used to make Money instance handle the operations when arguments order is reversed
338
+ # @return [Array]
339
+ #
340
+ # @example
341
+ # 2 * Money.new(10) #=> #<Money @fractional=20>
342
+ def coerce(other)
343
+ [self, CoercedNumeric.new(other)]
344
+ end
345
+ end
346
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Money
4
+ module Constructors
5
+
6
+ # Create a new money object with value 0.
7
+ #
8
+ # @param [Currency, String, Symbol] currency The currency to use.
9
+ #
10
+ # @return [Money]
11
+ #
12
+ # @example
13
+ # Money.empty #=> #<Money @fractional=0>
14
+ def empty(currency = default_currency)
15
+ new(0, currency)
16
+ end
17
+ alias_method :zero, :empty
18
+
19
+
20
+ # Creates a new Money object of the given value, using the Canadian
21
+ # dollar currency.
22
+ #
23
+ # @param [Integer] cents The cents value.
24
+ #
25
+ # @return [Money]
26
+ #
27
+ # @example
28
+ # n = Money.ca_dollar(100)
29
+ # n.cents #=> 100
30
+ # n.currency #=> #<Money::Currency id: cad>
31
+ def ca_dollar(cents)
32
+ new(cents, "CAD")
33
+ end
34
+ alias_method :cad, :ca_dollar
35
+
36
+
37
+ # Creates a new Money object of the given value, using the American dollar
38
+ # currency.
39
+ #
40
+ # @param [Integer] cents The cents value.
41
+ #
42
+ # @return [Money]
43
+ #
44
+ # @example
45
+ # n = Money.us_dollar(100)
46
+ # n.cents #=> 100
47
+ # n.currency #=> #<Money::Currency id: usd>
48
+ def us_dollar(cents)
49
+ new(cents, "USD")
50
+ end
51
+ alias_method :usd, :us_dollar
52
+
53
+
54
+ # Creates a new Money object of the given value, using the Euro currency.
55
+ #
56
+ # @param [Integer] cents The cents value.
57
+ #
58
+ # @return [Money]
59
+ #
60
+ # @example
61
+ # n = Money.euro(100)
62
+ # n.cents #=> 100
63
+ # n.currency #=> #<Money::Currency id: eur>
64
+ def euro(cents)
65
+ new(cents, "EUR")
66
+ end
67
+ alias_method :eur, :euro
68
+
69
+
70
+ # Creates a new Money object of the given value, in British pounds.
71
+ #
72
+ # @param [Integer] pence The pence value.
73
+ #
74
+ # @return [Money]
75
+ #
76
+ # @example
77
+ # n = Money.pound_sterling(100)
78
+ # n.fractional #=> 100
79
+ # n.currency #=> #<Money::Currency id: gbp>
80
+ def pound_sterling(pence)
81
+ new(pence, "GBP")
82
+ end
83
+ alias_method :gbp, :pound_sterling
84
+
85
+ end
86
+ end