cashrb 1.0.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.
data/.gemtest ADDED
File without changes
data/CHANGELOG.md ADDED
File without changes
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Shane Emmons
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,60 @@
1
+ cashrb
2
+ ======
3
+
4
+ Dead simple gem to work with Money/Currency without the hassle of Floats.
5
+
6
+ Usage
7
+ -----
8
+
9
+ require 'cash'
10
+
11
+ # Works with cents to avoid Floating point errors
12
+ n = Cash.new(100)
13
+ n.cents #=> 100
14
+ n.to_s #=> "1.00"
15
+ n.to_f #=> 1.0
16
+
17
+ # Define currency as you see fit.
18
+ a = Cash.new(100, :usd)
19
+ b = Cash.new(100, :eur)
20
+ a + b #=> Error! Cash::IncompatibleCurrency
21
+
22
+ # Default is 100 cents in a dollar. Is your currency different, then just
23
+ # tell it.
24
+ n = Cash.new(100, cents_in_dollar: 5)
25
+ n.cents #=> 100
26
+ n.to_s #=> "20.0"
27
+ n.to_f #=> 20.0
28
+
29
+ n = Cash.new(100, cents_in_dollar: 10)
30
+ n.cents #=> 100
31
+ n.to_s #=> "10.0"
32
+ n.to_f #=> 10.0
33
+
34
+ n = Cash.new(100, cents_in_dollar: 1)
35
+ n.cents #=> 100
36
+ n.to_s #=> "100"
37
+ n.to_f #=> 100.0
38
+
39
+ # The default rounding method when dealing with fractional cents is
40
+ # BigDecimal::ROUND_HALF_UP. Would you rather use bankers rounding; just
41
+ # pass it as an argument.
42
+ n = Cash.new(2.5)
43
+ n.cents #=> 3
44
+
45
+ n = Cash.new(2.5, rounding_method: BigDecimal::ROUND_HALF_EVEN)
46
+ n.cents #=> 2
47
+
48
+ # Sick of specifying :cents_in_whole, :rounding_method and :currency; just
49
+ # set their defaults.
50
+ Cash.default_cents_in_whole = 10
51
+ Cash.default_rounding_method = BigDecimal::ROUND_DOWN
52
+ Cash.default_currency = :EUR
53
+
54
+ n = Cash.new(100)
55
+ n.to_s #=> "10.0"
56
+ n.to_f #=> 10.0
57
+ n.currency #=> :EUR
58
+
59
+ n = Cash.new(1.9)
60
+ n.cents #=> 1
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+ require 'rake/gempackagetask'
3
+
4
+ Rake::TestTask.new
5
+
6
+ spec = Gem::Specification.load File.expand_path("../cashrb.gemspec", __FILE__)
7
+ gem = Rake::GemPackageTask.new(spec)
8
+ gem.define
data/cashrb.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "cashrb"
3
+ s.version = "1.0.0"
4
+ s.platform = Gem::Platform::RUBY
5
+ s.authors = ["Shane Emmons"]
6
+ s.email = ["semmons99@gmail.com"]
7
+ s.homepage = "http://github.com/semmons99/cashrb"
8
+ s.summary = "Dead simple gem to work with Money/Currency without the hassle of Floats"
9
+ s.description = "Dead simple gem to work with Money/Currency without the hassle of Floats"
10
+
11
+ s.required_ruby_version = ">= 1.8.7"
12
+ s.required_rubygems_version = ">= 1.3.6"
13
+
14
+ s.requirements << "minitest"
15
+
16
+ s.files = Dir.glob("{lib,test}/**/*")
17
+ s.files += %w(CHANGELOG.md LICENSE README.md)
18
+ s.files += %w(Rakefile .gemtest cashrb.gemspec)
19
+
20
+ s.require_path = "lib"
21
+ end
data/lib/cash/cash.rb ADDED
@@ -0,0 +1,159 @@
1
+ require 'bigdecimal'
2
+
3
+ ##
4
+ # Provides methods for performing financial calculations without using floats.
5
+ class Cash
6
+ class IncompatibleCurrency < ArgumentError; end
7
+
8
+ DEFAULT_CENTS_IN_WHOLE = 100
9
+ DEFAULT_ROUNDING_METHOD = BigDecimal::ROUND_HALF_UP
10
+ DEFAULT_CURRENCY = nil
11
+
12
+ CURRENCY_AWARE_METHODS = [
13
+ :+, :-, :/, :%, :divmod, :==, :<=>, :>, :<, :>=, :<=
14
+ ]
15
+
16
+ class << self
17
+ attr_accessor :default_cents_in_whole
18
+ attr_accessor :default_rounding_method
19
+ attr_accessor :default_currency
20
+
21
+ def reset_defaults
22
+ @default_cents_in_whole = DEFAULT_CENTS_IN_WHOLE
23
+ @default_rounding_method = DEFAULT_ROUNDING_METHOD
24
+ @default_currency = DEFAULT_CURRENCY
25
+ end
26
+ end
27
+
28
+ reset_defaults
29
+
30
+ attr_reader :currency
31
+
32
+ def initialize(cents = 0, options = {})
33
+ parse_initialize_options(options)
34
+ @cents = bd(cents).round(0, @rounding_method)
35
+ end
36
+
37
+ def cents
38
+ @cents.to_i
39
+ end
40
+
41
+ def +(value)
42
+ Cash.new(@cents + value.cents)
43
+ end
44
+
45
+ def -(value)
46
+ Cash.new(@cents - value.cents)
47
+ end
48
+
49
+ def *(value)
50
+ Cash.new(@cents * bd(value))
51
+ end
52
+
53
+ def /(value)
54
+ @cents / value.cents
55
+ rescue NoMethodError
56
+ Cash.new(@cents / bd(value))
57
+ end
58
+
59
+ def %(value)
60
+ Cash.new(@cents % value.cents)
61
+ rescue NoMethodError
62
+ Cash.new(@cents % bd(value))
63
+ end
64
+
65
+ def divmod(value)
66
+ quotient, remainder = @cents.divmod value.cents
67
+ [quotient, Cash.new(remainder)]
68
+ rescue NoMethodError
69
+ quotient, remainder = @cents.divmod bd(value)
70
+ [Cash.new(quotient), Cash.new(remainder)]
71
+ end
72
+
73
+ def ==(value)
74
+ @cents == value.cents
75
+ end
76
+
77
+ def <=>(value)
78
+ @cents <=> value.cents
79
+ end
80
+
81
+ def >(value)
82
+ @cents > value.cents
83
+ end
84
+
85
+ def <(value)
86
+ @cents < value.cents
87
+ end
88
+
89
+ def >=(value)
90
+ @cents >= value.cents
91
+ end
92
+
93
+ def <=(value)
94
+ @cents <= value.cents
95
+ end
96
+
97
+ def to_s
98
+ return self.cents.to_s if @cents_in_whole == 1
99
+
100
+ dollars, cents = dollars_and_cents
101
+ "#{dollars}.#{formatted_cents(cents)}"
102
+ end
103
+
104
+ def to_f
105
+ self.to_s.to_f
106
+ end
107
+
108
+ private
109
+
110
+ def bd(val)
111
+ BigDecimal(val.to_s)
112
+ end
113
+
114
+ def dollars_and_cents
115
+ @cents.divmod(@cents_in_whole).map(&:to_i)
116
+ end
117
+
118
+ def formatted_cents(cents)
119
+ c = ("0" * @decimal_places) + cents.to_s
120
+ c[(-1 * @decimal_places), @decimal_places]
121
+ end
122
+
123
+ def parse_initialize_options(options)
124
+ opts = {
125
+ cents_in_whole: self.class.default_cents_in_whole,
126
+ rounding_method: self.class.default_rounding_method,
127
+ currency: self.class.default_currency,
128
+ }.merge(options)
129
+
130
+ @cents_in_whole = opts[:cents_in_whole]
131
+ @decimal_places = decimal_places(@cents_in_whole)
132
+ @rounding_method = opts[:rounding_method]
133
+ @currency = opts[:currency]
134
+ nil
135
+ end
136
+
137
+ def decimal_places(cents_in_whole)
138
+ bd(Math.log10(cents_in_whole)).round(0, BigDecimal::ROUND_UP).to_i
139
+ end
140
+
141
+ def reject_incompatible_currency(value)
142
+ unless currency == value.currency
143
+ raise IncompatibleCurrency, "#{value.currency} != #{currency}"
144
+ end
145
+ rescue NoMethodError
146
+ end
147
+
148
+ CURRENCY_AWARE_METHODS.each do |mth|
149
+ old_mth = :"old_#{mth}"
150
+ alias_method old_mth, mth
151
+ private(old_mth)
152
+
153
+ define_method(mth) do |value|
154
+ reject_incompatible_currency(value)
155
+ send(old_mth, value)
156
+ end
157
+ end
158
+
159
+ end
data/lib/cash.rb ADDED
@@ -0,0 +1 @@
1
+ require 'cash/cash'
data/test/test_cash.rb ADDED
@@ -0,0 +1,293 @@
1
+ require 'minitest/autorun'
2
+ require 'bigdecimal'
3
+ require 'cash'
4
+
5
+ class TestCash < MiniTest::Unit::TestCase
6
+
7
+ def teardown
8
+ Cash.reset_defaults
9
+ end
10
+
11
+ def test_default_cents_in_whole
12
+ Cash.default_cents_in_whole = 10
13
+
14
+ rs = Cash.new(100)
15
+ assert_equal "10.0", rs.to_s
16
+ end
17
+
18
+ def test_default_rounding_method
19
+ Cash.default_rounding_method = BigDecimal::ROUND_DOWN
20
+
21
+ rs = Cash.new(1.9)
22
+ assert_equal 1, rs.cents
23
+ end
24
+
25
+ def test_default_currency
26
+ Cash.default_currency = :usd
27
+
28
+ rs = Cash.new(100)
29
+ assert_equal :usd, rs.currency
30
+ end
31
+
32
+ def test_new
33
+ rs = Cash.new(100)
34
+ assert_equal 100, rs.cents
35
+ assert_equal nil, rs.currency
36
+ end
37
+
38
+ def test_new_with_default_rounding_method
39
+ rs = Cash.new(2.5)
40
+ assert_equal 3, rs.cents
41
+ end
42
+
43
+ def test_new_with_rounding_method
44
+ rs = Cash.new(2.5, rounding_method: BigDecimal::ROUND_HALF_EVEN)
45
+ assert_equal 2, rs.cents
46
+ end
47
+
48
+ def test_new_with_currency
49
+ rs = Cash.new(0, currency: :usd)
50
+ assert_equal :usd, rs.currency
51
+ end
52
+
53
+ def test_plus_with_Cash_Cash
54
+ rs = Cash.new(6) + Cash.new(4)
55
+ assert_equal Cash.new(10), rs
56
+ end
57
+
58
+ def test_plus_with_Cash_Cash_with_different_currencies
59
+ assert_raises Cash::IncompatibleCurrency do
60
+ Cash.new(6) + Cash.new(4, currency: :usd)
61
+ end
62
+ end
63
+
64
+ def test_minus_with_Cash_Cash
65
+ rs = Cash.new(6) - Cash.new(4)
66
+ assert_equal Cash.new(2), rs
67
+ end
68
+
69
+ def test_minus_with_Cash_Cash_with_different_currencies
70
+ assert_raises Cash::IncompatibleCurrency do
71
+ Cash.new(6) - Cash.new(4, currency: :usd)
72
+ end
73
+ end
74
+
75
+ def test_multiple_with_Cash_Numeric
76
+ rs = Cash.new(6) * 2
77
+ assert_equal Cash.new(12), rs
78
+ end
79
+
80
+ def test_divide_with_Cash_Cash
81
+ rs = Cash.new(6) / Cash.new(4)
82
+ assert_equal BigDecimal("1.5"), rs
83
+ end
84
+
85
+ def test_divide_with_Cash_Cash_with_different_currencies
86
+ assert_raises Cash::IncompatibleCurrency do
87
+ Cash.new(6) / Cash.new(4, currency: :usd)
88
+ end
89
+ end
90
+
91
+ def test_divide_with_Cash_Numeric
92
+ rs = Cash.new(6) / 2
93
+ assert_equal Cash.new(3), rs
94
+ end
95
+
96
+ def test_modulo_with_Cash_Cash
97
+ rs = Cash.new(6) % Cash.new(4)
98
+ assert_equal Cash.new(2), rs
99
+ end
100
+
101
+ def test_modulo_with_Cash_Cash_with_different_currencies
102
+ assert_raises Cash::IncompatibleCurrency do
103
+ Cash.new(6) % Cash.new(4, currency: :usd)
104
+ end
105
+ end
106
+
107
+ def test_modulo_with_Cash_Numeric
108
+ rs = Cash.new(6) % 4
109
+ assert_equal Cash.new(2), rs
110
+ end
111
+
112
+ def test_divmod_with_Cash_Cash
113
+ rs = Cash.new(6).divmod(Cash.new(4))
114
+ assert_equal [BigDecimal("1"), Cash.new(2)], rs
115
+ end
116
+
117
+ def test_divmod_with_Cash_Cash_with_different_currencies
118
+ assert_raises Cash::IncompatibleCurrency do
119
+ Cash.new(6).divmod(Cash.new(4, currency: :usd))
120
+ end
121
+ end
122
+
123
+ def test_divmod_with_Cash_Numeric
124
+ rs = Cash.new(6).divmod(4)
125
+ assert_equal [Cash.new(1), Cash.new(2)], rs
126
+ end
127
+
128
+ def test_equal_to_when_equal_to
129
+ rs = Cash.new(6) == Cash.new(6)
130
+ assert true
131
+ end
132
+
133
+ def test_equal_to_when_not_equal_to
134
+ rs = Cash.new(6) == Cash.new(4)
135
+ refute false
136
+ end
137
+
138
+ def test_equal_to_with_different_currencies
139
+ assert_raises Cash::IncompatibleCurrency do
140
+ Cash.new(6) == Cash.new(6, currency: :usd)
141
+ end
142
+ end
143
+
144
+ def test_compare_when_greater_than
145
+ rs = Cash.new(6) <=> Cash.new(4)
146
+ assert_equal 1, rs
147
+ end
148
+
149
+ def test_compare_when_less_than
150
+ rs = Cash.new(4) <=> Cash.new(6)
151
+ assert_equal -1, rs
152
+ end
153
+
154
+ def test_compare_when_equal_to
155
+ rs = Cash.new(6) <=> Cash.new(6)
156
+ assert_equal 0, rs
157
+ end
158
+
159
+ def test_compare_with_different_currencies
160
+ assert_raises Cash::IncompatibleCurrency do
161
+ Cash.new(6) <=> Cash.new(6, currency: :usd)
162
+ end
163
+ end
164
+
165
+ def test_greater_than_when_greater_than
166
+ rs = Cash.new(6) > Cash.new(4)
167
+ assert true
168
+ end
169
+
170
+ def test_greater_than_when_less_than
171
+ rs = Cash.new(4) > Cash.new(6)
172
+ refute false
173
+ end
174
+
175
+ def test_greater_than_when_equal_to
176
+ rs = Cash.new(6) > Cash.new(6)
177
+ refute false
178
+ end
179
+
180
+ def test_greater_than_with_different_currencies
181
+ assert_raises Cash::IncompatibleCurrency do
182
+ Cash.new(6) > Cash.new(6, currency: :usd)
183
+ end
184
+ end
185
+
186
+ def test_less_than_when_greater_than
187
+ rs = Cash.new(6) < Cash.new(4)
188
+ refute false
189
+ end
190
+
191
+ def test_less_than_when_less_than
192
+ rs = Cash.new(4) < Cash.new(6)
193
+ assert true
194
+ end
195
+
196
+ def test_less_than_when_equal_to
197
+ rs = Cash.new(6) < Cash.new(6)
198
+ refute false
199
+ end
200
+
201
+ def test_less_than_with_different_currencies
202
+ assert_raises Cash::IncompatibleCurrency do
203
+ Cash.new(6) < Cash.new(6, currency: :usd)
204
+ end
205
+ end
206
+
207
+ def test_greater_than_or_equal_to_when_greater_than
208
+ rs = Cash.new(6) >= Cash.new(4)
209
+ assert true
210
+ end
211
+
212
+ def test_greater_than_or_equal_to_when_less_than
213
+ rs = Cash.new(4) >= Cash.new(6)
214
+ refute false
215
+ end
216
+
217
+ def test_greater_than_or_equal_to_when_equal_to
218
+ rs = Cash.new(6) >= Cash.new(6)
219
+ assert true
220
+ end
221
+
222
+ def test_greater_than_or_equal_to_with_different_currencies
223
+ assert_raises Cash::IncompatibleCurrency do
224
+ Cash.new(6) >= Cash.new(6, currency: :usd)
225
+ end
226
+ end
227
+
228
+ def test_less_than_or_equal_to_when_greater_than
229
+ rs = Cash.new(6) <= Cash.new(4)
230
+ refute false
231
+ end
232
+
233
+ def test_less_than_or_equal_to_when_less_than
234
+ rs = Cash.new(4) <= Cash.new(6)
235
+ assert true
236
+ end
237
+
238
+ def test_less_than_or_equal_to_when_equal_to
239
+ rs = Cash.new(6) <= Cash.new(6)
240
+ assert true
241
+ end
242
+
243
+ def test_less_than_or_equal_to_with_different_currencies
244
+ assert_raises Cash::IncompatibleCurrency do
245
+ Cash.new(6) <= Cash.new(6, currency: :usd)
246
+ end
247
+ end
248
+
249
+ def test_to_s
250
+ assert_equal "0.00", Cash.new(0 ).to_s
251
+ assert_equal "0.01", Cash.new(1 ).to_s
252
+ assert_equal "0.10", Cash.new(10 ).to_s
253
+ assert_equal "1.00", Cash.new(100).to_s
254
+ assert_equal "1.01", Cash.new(101).to_s
255
+ assert_equal "1.10", Cash.new(110).to_s
256
+ end
257
+
258
+ def test_to_s_with_cents_in_whole
259
+ assert_equal "0", Cash.new(0, cents_in_whole: 1 ).to_s
260
+ assert_equal "1", Cash.new(1, cents_in_whole: 1 ).to_s
261
+ assert_equal "0.4", Cash.new(4, cents_in_whole: 5 ).to_s
262
+ assert_equal "1.0", Cash.new(5, cents_in_whole: 5 ).to_s
263
+ assert_equal "0.9", Cash.new(9, cents_in_whole: 10 ).to_s
264
+ assert_equal "1.0", Cash.new(10, cents_in_whole: 10 ).to_s
265
+ assert_equal "0.99", Cash.new(99, cents_in_whole: 100 ).to_s
266
+ assert_equal "1.00", Cash.new(100, cents_in_whole: 100 ).to_s
267
+ assert_equal "0.999", Cash.new(999, cents_in_whole: 1000).to_s
268
+ assert_equal "1.000", Cash.new(1000, cents_in_whole: 1000).to_s
269
+ end
270
+
271
+ def test_to_f
272
+ assert_equal 0.0, Cash.new(0 ).to_f
273
+ assert_equal 0.01, Cash.new(1 ).to_f
274
+ assert_equal 0.1, Cash.new(10 ).to_f
275
+ assert_equal 1.0, Cash.new(100).to_f
276
+ assert_equal 1.01, Cash.new(101).to_f
277
+ assert_equal 1.1, Cash.new(110).to_f
278
+ end
279
+
280
+ def test_to_f_with_cents_in_whole
281
+ assert_equal 0.0, Cash.new(0, cents_in_whole: 1 ).to_f
282
+ assert_equal 1.0, Cash.new(1, cents_in_whole: 1 ).to_f
283
+ assert_equal 0.4, Cash.new(4, cents_in_whole: 5 ).to_f
284
+ assert_equal 1.0, Cash.new(5, cents_in_whole: 5 ).to_f
285
+ assert_equal 0.9, Cash.new(9, cents_in_whole: 10 ).to_f
286
+ assert_equal 1.0, Cash.new(10, cents_in_whole: 10 ).to_f
287
+ assert_equal 0.99, Cash.new(99, cents_in_whole: 100 ).to_f
288
+ assert_equal 1.0, Cash.new(100, cents_in_whole: 100 ).to_f
289
+ assert_equal 0.999, Cash.new(999, cents_in_whole: 1000).to_f
290
+ assert_equal 1.0, Cash.new(1000, cents_in_whole: 1000).to_f
291
+ end
292
+
293
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cashrb
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Shane Emmons
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-02-23 00:00:00.000000000 -05:00
13
+ default_executable:
14
+ dependencies: []
15
+ description: Dead simple gem to work with Money/Currency without the hassle of Floats
16
+ email:
17
+ - semmons99@gmail.com
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - lib/cash/cash.rb
23
+ - lib/cash.rb
24
+ - test/test_cash.rb
25
+ - CHANGELOG.md
26
+ - LICENSE
27
+ - README.md
28
+ - Rakefile
29
+ - .gemtest
30
+ - cashrb.gemspec
31
+ has_rdoc: true
32
+ homepage: http://github.com/semmons99/cashrb
33
+ licenses: []
34
+ post_install_message:
35
+ rdoc_options: []
36
+ require_paths:
37
+ - lib
38
+ required_ruby_version: !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: 1.8.7
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ! '>='
48
+ - !ruby/object:Gem::Version
49
+ version: 1.3.6
50
+ requirements:
51
+ - minitest
52
+ rubyforge_project:
53
+ rubygems_version: 1.5.2
54
+ signing_key:
55
+ specification_version: 3
56
+ summary: Dead simple gem to work with Money/Currency without the hassle of Floats
57
+ test_files: []