cashrb 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []