bd_money 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "http://rubygems.org"
2
+
3
+ group :development do
4
+ gem "rspec", "~> 2.8.0"
5
+ gem "rdoc", "~> 3.12"
6
+ gem "bundler", "~> 1.0.0"
7
+ gem "jeweler", "~> 1.8.3"
8
+ gem "rcov", ">= 0"
9
+ gem "activerecord", "~> 2.3.5"
10
+ gem "sqlite3"
11
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+ Copyright (c) 2012 North Point Advisors, Inc.
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a
5
+ copy of this software and associated documentation files (the "Software"),
6
+ to deal in the Software without restriction, including without limitation
7
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
8
+ and/or sell copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in
12
+ all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20
+ DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,101 @@
1
+ = bd_money
2
+
3
+ This library makes it easier to deal with Money values, storing them as BigDecimal to avoid floating-point
4
+ math errors.
5
+
6
+ This library makes extensive use of ideas borrowed from the Money[https://github.com/collectiveidea/money] gem
7
+ from CollectiveIdea so please review their code and decide which gem to use according to your needs. We needed
8
+ an object that would keep numbers with more precision internally and display them with smaller precision if
9
+ required. Also we needed more control with regards to rounding methods.
10
+
11
+ == Money
12
+
13
+ Money objects use internally BigDecimal object to provide exact calculations.
14
+
15
+ === Precision
16
+
17
+ You can decide how many decimals you want to display using the second parameter on initialization (nil will
18
+ use the current Money class default).
19
+
20
+ m = Money.new 123.456789, 3 #=> 123.456
21
+ m.to_s(2) #=> 123.45
22
+
23
+ You can change the default precision for display purposes:
24
+
25
+ Money.precision = 2
26
+ m = Money.new 123.456789, 3 #=> 123.456
27
+ m.precision = 1
28
+ m.to_s #=> 123.4
29
+ m.to_s(2) #=> 123.45
30
+
31
+ == Rounding
32
+
33
+ BigDecimal numbers provide several rounding methods: up, down, half_up, half_down, half_even, ceiling and floor.
34
+ You can pass an option to decide what rounding method to use with your object.
35
+
36
+ m = Money.new 123.456, nil, :half_up
37
+ m.to_s(2) #=> 123.46
38
+ m.to_s(2, :floor) #=> 123.45
39
+
40
+ == Download
41
+
42
+ Preferred method of installation is gem:
43
+
44
+ gem install bd_money
45
+
46
+ You can find the source at:
47
+
48
+ http://github.com/NorthPointAdvisors/bd_money
49
+
50
+ == Rails
51
+
52
+ There is a rails extension that makes it easier to store money values in the database.
53
+
54
+ require 'bd_money/rails'
55
+
56
+ class Product < ActiveRecord::Base
57
+ money :price
58
+ validates_numericality_of :price, :greater_than => 0
59
+ end
60
+
61
+ This assumes that there is a price (decimal highly recommended) column in the database. You can also specify the
62
+ :precision and :round_mode option for more fine control of the results.
63
+
64
+ class Loan < ActiveRecord::Base
65
+ money :amount, :round_mode => :half_up
66
+ money :apr, :precision => 5, :round_mode => :floor
67
+ end
68
+
69
+ You can set the attribute to a String, Fixnum, or Float and it will call #to_money to
70
+ convert it to a Money object. This makes it convenient for using money fields in forms.
71
+
72
+ r = Loan.new :amount => "123.456", :apr => 0.123456
73
+ r.amount #=> 123.46
74
+ r.apr #=> 0.12345
75
+
76
+ Also notice that operating on a Money object will return another money object to help you maintain the extra
77
+ information in the BigDecimal amount.
78
+
79
+ twice = r.amount * 2 #=> 246.91
80
+ twice.class.name #=> "Money"
81
+
82
+ This has been tested with ActiveRecord 2.3.5. Please proceed with caution in any other environment. If you work out a
83
+ solution for other versions please let me know.
84
+
85
+ == Contributing to bd_money
86
+
87
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
88
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
89
+ * Fork the project.
90
+ * Start a feature/bugfix branch.
91
+ * Commit and push until you are happy with your contribution.
92
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
93
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
94
+
95
+ == Code
96
+
97
+ If you have any improvements please email them to aemadrid [at] gmail.com
98
+
99
+ == Copyright
100
+
101
+ Copyright (c) 2012 North Point Advisors, Inc. See LICENSE.txt for further details.
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "bd_money"
18
+ gem.homepage = "http://github.com/aemadrid/bd_money"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{Managing money objects sanely}
21
+ gem.description = %Q{This library makes it easier to deal with Money values, storing them as BigDecimal to avoid floating-point math errors.}
22
+ gem.email = "aemadrid@gmail.com"
23
+ gem.authors = ["Adrian Madrid"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ task :default => :spec
40
+
41
+ require 'rdoc/task'
42
+ Rake::RDocTask.new do |rdoc|
43
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "bd_money #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/bd_money.gemspec ADDED
@@ -0,0 +1,73 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{bd_money}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Adrian Madrid"]
12
+ s.date = %q{2012-03-09}
13
+ s.description = %q{This library makes it easier to deal with Money values, storing them as BigDecimal to avoid floating-point math errors.}
14
+ s.email = %q{aemadrid@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".rspec",
22
+ "Gemfile",
23
+ "LICENSE.txt",
24
+ "README.rdoc",
25
+ "Rakefile",
26
+ "VERSION",
27
+ "bd_money.gemspec",
28
+ "lib/bd_money.rb",
29
+ "lib/bd_money/bd_money.rb",
30
+ "lib/bd_money/core_extensions.rb",
31
+ "lib/bd_money/rails.rb",
32
+ "spec/bd_money_spec.rb",
33
+ "spec/core_extensions_spec.rb",
34
+ "spec/rails_spec.rb",
35
+ "spec/spec_helper.rb"
36
+ ]
37
+ s.homepage = %q{http://github.com/aemadrid/bd_money}
38
+ s.licenses = ["MIT"]
39
+ s.require_paths = ["lib"]
40
+ s.rubygems_version = %q{1.4.2}
41
+ s.summary = %q{Managing money objects sanely}
42
+
43
+ if s.respond_to? :specification_version then
44
+ s.specification_version = 3
45
+
46
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
47
+ s.add_development_dependency(%q<rspec>, ["~> 2.8.0"])
48
+ s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
49
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
50
+ s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
51
+ s.add_development_dependency(%q<rcov>, [">= 0"])
52
+ s.add_development_dependency(%q<activerecord>, ["~> 2.3.5"])
53
+ s.add_development_dependency(%q<sqlite3>, [">= 0"])
54
+ else
55
+ s.add_dependency(%q<rspec>, ["~> 2.8.0"])
56
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
57
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
58
+ s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
59
+ s.add_dependency(%q<rcov>, [">= 0"])
60
+ s.add_dependency(%q<activerecord>, ["~> 2.3.5"])
61
+ s.add_dependency(%q<sqlite3>, [">= 0"])
62
+ end
63
+ else
64
+ s.add_dependency(%q<rspec>, ["~> 2.8.0"])
65
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
66
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
67
+ s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
68
+ s.add_dependency(%q<rcov>, [">= 0"])
69
+ s.add_dependency(%q<activerecord>, ["~> 2.3.5"])
70
+ s.add_dependency(%q<sqlite3>, [">= 0"])
71
+ end
72
+ end
73
+
@@ -0,0 +1,210 @@
1
+ require 'bigdecimal'
2
+
3
+ class Money
4
+
5
+ ROUND_MODES = {
6
+ :ceiling => BigDecimal::ROUND_CEILING,
7
+ :down => BigDecimal::ROUND_DOWN,
8
+ :floor => BigDecimal::ROUND_FLOOR,
9
+ :half_down => BigDecimal::ROUND_HALF_DOWN,
10
+ :half_even => BigDecimal::ROUND_HALF_EVEN,
11
+ :half_up => BigDecimal::ROUND_HALF_UP,
12
+ :up => BigDecimal::ROUND_UP,
13
+ } unless const_defined?(:ROUND_MODES)
14
+
15
+ REMOVE_RE = %r{[$,_ ]} unless const_defined?(:REMOVE_RE)
16
+ VALID_RE = %r{^(-)?(\d)+(\.\d{1,12})?$} unless const_defined?(:VALID_RE)
17
+
18
+ include Comparable
19
+
20
+ class MoneyError < StandardError # :nodoc:
21
+ end
22
+
23
+ def initialize(value, precision = nil, round_mode = nil)
24
+ self.amount = value
25
+ self.precision = precision if precision
26
+ self.round_mode = round_mode if round_mode
27
+ end
28
+
29
+ def amount=(value)
30
+ if value.is_a?(BigDecimal)
31
+ @amount = value
32
+ else
33
+ str = self.class.clean value
34
+ raise MoneyError, "Invalid value [#{str}] (#{value.class.name})" unless self.class.valid?(str)
35
+ @amount = BigDecimal.new str.gsub REMOVE_RE, ''
36
+ end
37
+ end
38
+
39
+ def amount
40
+ @amount
41
+ end
42
+
43
+ def precision=(value)
44
+ raise "Unknown precision [#{value}]" unless value.is_a?(Integer)
45
+ @precision = value
46
+ end
47
+
48
+ def precision
49
+ @precision || self.class.precision
50
+ end
51
+
52
+ def round_mode=(value)
53
+ raise "Unknown rounding mode [#{value}]" unless ROUND_MODES.key?(value)
54
+ @round_mode = value
55
+ end
56
+
57
+ def round_mode
58
+ @round_mode || self.class.round_mode
59
+ end
60
+
61
+ def convert(value)
62
+ self.class.convert value
63
+ end
64
+
65
+ def eql?(other)
66
+ amount == convert(other).amount
67
+ end
68
+
69
+ def <=>(other)
70
+ amount <=> convert(other).amount
71
+ end
72
+
73
+ def +(other)
74
+ convert amount + convert(other).amount
75
+ end
76
+
77
+ def -(other)
78
+ convert amount - convert(other).amount
79
+ end
80
+
81
+ def *(other)
82
+ convert amount * convert(other).amount
83
+ end
84
+
85
+ def /(other)
86
+ convert amount / convert(other).amount
87
+ end
88
+
89
+ def **(other)
90
+ convert amount ** convert(other).amount.to_i
91
+ end
92
+
93
+ def %(other)
94
+ convert amount % convert(other).amount
95
+ end
96
+
97
+ def ^(other)
98
+ convert amount ^ convert(other).amount
99
+ end
100
+
101
+ def round_amount(this_precision = precision, this_round_mode = round_mode)
102
+ this_round_mode = BigDecimal.const_get("ROUND_#{this_round_mode.to_s.upcase}") if this_round_mode.is_a?(Symbol)
103
+ amount.round this_precision, this_round_mode
104
+ end
105
+
106
+ def round(this_precision = precision, this_round_mode = round_mode)
107
+ convert round_amount(this_precision, this_round_mode)
108
+ end
109
+
110
+ def to_i(this_round_mode = round_mode)
111
+ round_amount(0, this_round_mode).to_i
112
+ end
113
+
114
+ def to_f(this_precision = precision, this_round_mode = round_mode)
115
+ round_amount(this_precision, this_round_mode).to_i
116
+ end
117
+
118
+ def to_credit
119
+ convert amount.abs
120
+ end
121
+
122
+ def to_credit!
123
+ self.amount = amount.abs
124
+ self
125
+ end
126
+
127
+ def credit?
128
+ amount >= 0
129
+ end
130
+
131
+ def to_debit
132
+ convert amount.abs * -1
133
+ end
134
+
135
+ def to_debit!
136
+ self.amount = amount.abs * -1
137
+ self
138
+ end
139
+
140
+ def debit?
141
+ amount < 0
142
+ end
143
+
144
+ def zero?
145
+ amount == 0
146
+ end
147
+
148
+ def to_s(this_precision = precision, this_round_mode = round_mode)
149
+ this_round_mode = BigDecimal.const_get("ROUND_#{this_round_mode.to_s.upcase}") if this_round_mode.is_a?(Symbol)
150
+ amount_str = amount.round(this_precision, this_round_mode).to_s('F')
151
+ dollars, cents = amount_str.split('.')
152
+ return dollars if this_precision == 0
153
+ if cents.size >= this_precision
154
+ "#{dollars}.#{cents[0, this_precision]}"
155
+ else
156
+ "#{dollars}.#{cents}#{'0' * (this_precision - cents.size)}"
157
+ end
158
+ end
159
+
160
+ alias :inspect :to_s
161
+
162
+ def respond_to?(meth)
163
+ amount.respond_to?(meth) || super
164
+ end
165
+
166
+ def method_missing(meth, *args, &blk)
167
+ if amount.respond_to? meth
168
+ result = amount.send meth, *args, &blk
169
+ result.is_a?(::BigDecimal) ? convert(result) : result
170
+ else
171
+ super
172
+ end
173
+ end
174
+
175
+ class << self
176
+
177
+ def precision=(value)
178
+ raise "Unknown precision [#{value}]" unless value.is_a?(Integer)
179
+ @precision = value
180
+ end
181
+
182
+ def precision
183
+ @precision || 2
184
+ end
185
+
186
+ def round_mode=(value)
187
+ raise "Unknown rounding mode [#{value}]" unless ROUND_MODES.key?(value)
188
+ @round_mode = value
189
+ end
190
+
191
+ def round_mode
192
+ @round_mode || :half_up
193
+ end
194
+
195
+ def convert(value)
196
+ return value if value.is_a?(Money)
197
+ new value
198
+ end
199
+
200
+ def clean(value)
201
+ value.to_s.gsub REMOVE_RE, ''
202
+ end
203
+
204
+ def valid?(value)
205
+ !!value.to_s.match(VALID_RE)
206
+ end
207
+
208
+ end
209
+
210
+ end
@@ -0,0 +1,39 @@
1
+ class Numeric
2
+ # Convert the number to a +Money+ object.
3
+ #
4
+ # 100.to_money #=> 100.00
5
+ #
6
+ # Takes an optional precision, which defaults to 2
7
+ # Takes an optional round_mode which defaults to :half_up
8
+ def to_money(precision = nil, round_mode = nil)
9
+ Money.new self, precision, round_mode
10
+ end
11
+ end
12
+
13
+ class Float
14
+ # Convert the float to a +Money+ object.
15
+ #
16
+ # 3.75.to_money #=> 3.75
17
+ #
18
+ # Takes an optional precision, which defaults to 2
19
+ # Takes an optional round_mode which defaults to :half_up
20
+ def to_money(precision = nil, round_mode = nil)
21
+ Money.new self, precision, round_mode
22
+ end
23
+ end
24
+
25
+ class String
26
+ # Convert the String to a +Money+ object.
27
+ #
28
+ # '100'.to_money #=> 100.00
29
+ # '100.37'.to_money #=> 100.37
30
+ # '.37'.to_money #=> 0.37
31
+ # '$ 4.25'.to_money #=> 4.25
32
+ # '3,550.55'.to_money #=> 3550.55
33
+ #
34
+ # Takes an optional precision, which defaults to 2
35
+ # Takes an optional round_mode which defaults to :half_up
36
+ def to_money(precision = nil, round_mode = nil)
37
+ Money.new self, precision, round_mode
38
+ end
39
+ end
@@ -0,0 +1,44 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../lib/bd_money')
2
+
3
+ class Money
4
+
5
+ # Terrible hack to allow to quote money correctly
6
+ def quoted_id
7
+ amount
8
+ end
9
+
10
+ # This will help to save money objects correctly
11
+ def to_d
12
+ amount
13
+ end
14
+
15
+ end
16
+
17
+ module ActiveRecord #:nodoc:
18
+ module Acts #:nodoc:
19
+ module Money #:nodoc:
20
+ def self.included(base) #:nodoc:
21
+ base.extend ClassMethods
22
+ end
23
+
24
+ module ClassMethods
25
+ def money(name, options = {})
26
+ define_method "#{name}=" do |value|
27
+ if value.present?
28
+ self[name] = ::Money.new(value, options[:precision], options[:round_mode]).amount
29
+ else
30
+ self[name] = nil
31
+ end
32
+ end
33
+ define_method "#{name}" do
34
+ puts "#{name} : self[#{name}] (1) : #{self[name]} : #{self[name].class}"
35
+ return nil unless self[name].present?
36
+ ::Money.new self[name], options[:precision], options[:round_mode]
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ ActiveRecord::Base.send :include, ActiveRecord::Acts::Money
data/lib/bd_money.rb ADDED
@@ -0,0 +1,2 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/bd_money/bd_money')
2
+ require File.expand_path(File.dirname(__FILE__) + '/bd_money/core_extensions')
@@ -0,0 +1,344 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Money do
4
+
5
+ let(:amt) { '3.53' }
6
+ let(:neg_amt) { '-3.53' }
7
+ let(:other_amt) { 1.01 }
8
+ subject { Money.new amt, 2, :half_down }
9
+ let(:neg_subject) { Money.new neg_amt, 2, :half_down }
10
+
11
+ describe "class precision" do
12
+ it { Money.precision.should == 2 }
13
+ it "should support customization" do
14
+ old_value = Money.instance_variable_get :@precision
15
+ Money.precision = 3
16
+ Money.precision.should == 3
17
+ Money.instance_variable_set :@precision, old_value
18
+ end
19
+ it { expect { Money.precision = '3' }.to raise_error(RuntimeError) }
20
+ end
21
+
22
+ describe "class round mode" do
23
+ it { Money.round_mode.should == :half_up }
24
+ it "should support customization" do
25
+ old_value = Money.instance_variable_get :@round_mode
26
+ Money.round_mode = :floor
27
+ Money.round_mode.should == :floor
28
+ Money.instance_variable_set :@round_mode, old_value
29
+ end
30
+ it { expect { Money.round_mode = :not_there }.to raise_error(RuntimeError) }
31
+ end
32
+
33
+ describe "class convert" do
34
+ describe "strings" do
35
+ it { Money.convert('3.53').to_s.should == '3.53' }
36
+ it { Money.convert('-3.53').to_s.should == '-3.53' }
37
+ end
38
+ describe "integers" do
39
+ it { Money.convert(3).to_s.should == '3.00' }
40
+ it { Money.convert(-3).to_s.should == '-3.00' }
41
+ end
42
+ describe "floats" do
43
+ it { Money.convert(3.53).to_s.should == '3.53' }
44
+ it { Money.convert(-3.53).to_s.should == '-3.53' }
45
+ end
46
+ end
47
+
48
+ describe "class clean" do
49
+ it { Money.clean(1).should == '1' }
50
+ it { Money.clean(1.1).should == '1.1' }
51
+ it { Money.clean('$ 3,456,789.01').should == '3456789.01' }
52
+ it { Money.clean('3_456_789.01').should == '3456789.01' }
53
+ it { Money.clean('[ $ 3,456,789.01 ]').should == '[3456789.01]' }
54
+ it { Money.clean('You owe me $ 35.50').should == 'Youoweme35.50' }
55
+ end
56
+
57
+ describe "class valid?" do
58
+ it { Money.valid?(1).should be_true }
59
+ it { Money.valid?('1').should be_true }
60
+ it { Money.valid?(1.1).should be_true }
61
+ it { Money.valid?('3456789.01').should be_true }
62
+ it { Money.valid?('$ 3.45').should_not be_true }
63
+ it { Money.valid?('[3.45]').should_not be_true }
64
+ it { Money.valid?('0.01').should be_true }
65
+ it { Money.valid?('.01').should_not be_true }
66
+ it { Money.valid?('-0.01').should be_true }
67
+ it { Money.valid?('-.01').should_not be_true }
68
+ end
69
+
70
+ describe "initialize" do
71
+ describe "defaults" do
72
+ subject { Money.new '3.53' }
73
+ it { subject.to_s.should == '3.53' }
74
+ it { subject.precision.should == 2 }
75
+ it { subject.round_mode.should == :half_up }
76
+ end
77
+ describe "custom" do
78
+ subject { Money.new '3.53', 1, :floor }
79
+ it { subject.to_s.should == '3.5' }
80
+ it { subject.precision.should == 1 }
81
+ it { subject.round_mode.should == :floor }
82
+ end
83
+ describe "customize" do
84
+ describe "amount" do
85
+ it { subject.amount = '5.35'; subject.to_s.should == '5.35' }
86
+ end
87
+ describe "precision" do
88
+ it { subject.precision = 4; subject.precision.should == 4 }
89
+ it { expect { Money.precision = '4' }.to raise_error(RuntimeError) }
90
+ end
91
+ describe "round_mode" do
92
+ it { subject.round_mode = :floor; subject.round_mode.should == :floor }
93
+ it { expect { Money.round_mode = :not_there }.to raise_error(RuntimeError) }
94
+ end
95
+ end
96
+ end
97
+
98
+ describe "convert" do
99
+ describe "strings" do
100
+ it { subject.convert('3.53').to_s.should == '3.53' }
101
+ it { subject.convert('-3.53').to_s.should == '-3.53' }
102
+ end
103
+ describe "integers" do
104
+ it { subject.convert(3).to_s.should == '3.00' }
105
+ it { subject.convert(-3).to_s.should == '-3.00' }
106
+ end
107
+ describe "floats" do
108
+ it { subject.convert(3.53).to_s.should == '3.53' }
109
+ it { subject.convert(-3.53).to_s.should == '-3.53' }
110
+ end
111
+ end
112
+
113
+ describe Comparable do
114
+ let(:amt) { 5.35 }
115
+ describe "eql" do
116
+ it { subject.eql?(Money.new(amt)).should be_true }
117
+ it { subject.eql?(amt).should be_true }
118
+ it { subject.eql?(amt.to_s).should be_true }
119
+ end
120
+ describe "<=>" do
121
+ it { subject.<=>(amt).should == 0 }
122
+ it { subject.<=>(amt + 1).should == -1 }
123
+ it { subject.<=>(amt - 1).should == 1 }
124
+ end
125
+ end
126
+
127
+ describe "Operations" do
128
+ describe "+" do
129
+ subject { Money.new(amt) + other_amt }
130
+ it { should be_a Money }
131
+ it { should == 4.54 }
132
+ end
133
+ describe "-" do
134
+ subject { Money.new(amt) - other_amt }
135
+ it { should be_a Money }
136
+ it { should == 2.52 }
137
+ end
138
+ describe "*" do
139
+ subject { Money.new(amt) * other_amt }
140
+ it { should be_a Money }
141
+ it { subject.to_s.should == '3.57' }
142
+ end
143
+ describe "/" do
144
+ subject { Money.new(amt) / other_amt }
145
+ it { should be_a Money }
146
+ it { subject.to_s.should == '3.50' }
147
+ end
148
+ describe "**" do
149
+ subject { Money.new(amt) ** other_amt }
150
+ it { should be_a Money }
151
+ it { should == 3.53 }
152
+ end
153
+ describe "%" do
154
+ subject { Money.new(amt) % other_amt }
155
+ it { should be_a Money }
156
+ it { should == 0.50 }
157
+ end
158
+ describe "to_credit" do
159
+ it { subject.object_id.should_not == subject.to_credit.object_id }
160
+ it { subject.to_credit.should == 3.53 }
161
+ it { neg_subject.to_credit.should == 3.53 }
162
+ end
163
+ describe "to_credit!" do
164
+ it { o = Money.new(amt); o.object_id.to_s.should == o.to_credit!.object_id.to_s }
165
+ it { subject.to_credit!.should == 3.53 }
166
+ it { neg_subject.to_credit!.should == 3.53 }
167
+ end
168
+ describe "credit?" do
169
+ it { subject.should be_credit }
170
+ it { neg_subject.should_not be_credit }
171
+ end
172
+ describe "to_debit" do
173
+ it { subject.object_id.should_not == subject.to_credit.object_id }
174
+ it { subject.to_debit.should == -3.53 }
175
+ it { neg_subject.to_debit.should == -3.53 }
176
+ end
177
+ describe "to_debit!" do
178
+ it { subject.object_id.to_s.should == subject.to_debit!.object_id.to_s }
179
+ it { subject.to_debit!.should == -3.53 }
180
+ it { neg_subject.to_debit!.should == -3.53 }
181
+ end
182
+ describe "debit?" do
183
+ it { subject.should_not be_debit }
184
+ it { neg_subject.should be_debit }
185
+ end
186
+ describe "zero?" do
187
+ it { subject.should_not be_zero }
188
+ it { neg_subject.should_not be_zero }
189
+ it { Money.new('0').should be_zero }
190
+ it { Money.new('0.0').should be_zero }
191
+ end
192
+ end
193
+ describe "to_s" do
194
+ describe "no decimals" do
195
+ let(:amt) { '3' }
196
+ it { subject.to_s.should == "#{amt}.00" }
197
+ it { subject.to_s(0).should == amt }
198
+ it { subject.to_s(1).should == "#{amt}.0" }
199
+ it { subject.to_s(2).should == "#{amt}.00" }
200
+ it { subject.to_s(3).should == "#{amt}.000" }
201
+ it { subject.to_s(4).should == "#{amt}.0000" }
202
+ it { subject.to_s(5).should == "#{amt}.00000" }
203
+ it { subject.to_s(6).should == "#{amt}.000000" }
204
+ end
205
+ describe "one decimal" do
206
+ let(:amt) { '3.5' }
207
+ it { subject.to_s.should == "#{amt}0" }
208
+ it { subject.to_s(0).should == amt[0, 1] }
209
+ it { subject.to_s(1).should == amt[0, 3] }
210
+ it { subject.to_s(2).should == "#{amt}0" }
211
+ it { subject.to_s(3).should == "#{amt}00" }
212
+ it { subject.to_s(4).should == "#{amt}000" }
213
+ it { subject.to_s(5).should == "#{amt}0000" }
214
+ it { subject.to_s(6).should == "#{amt}00000" }
215
+ end
216
+ describe "two decimals" do
217
+ let(:amt) { '3.53' }
218
+ it { subject.to_s.should == amt }
219
+ it { subject.to_s(0).should == amt[0, 1] }
220
+ it { subject.to_s(1).should == amt[0, 3] }
221
+ it { subject.to_s(2).should == amt }
222
+ it { subject.to_s(3).should == "#{amt}0" }
223
+ it { subject.to_s(4).should == "#{amt}00" }
224
+ it { subject.to_s(5).should == "#{amt}000" }
225
+ it { subject.to_s(6).should == "#{amt}0000" }
226
+ end
227
+ describe "three decimals" do
228
+ let(:amt) { '3.534' }
229
+ it { subject.to_s.should == amt[0, 4] }
230
+ it { subject.to_s(0).should == amt[0, 1] }
231
+ it { subject.to_s(1).should == amt[0, 3] }
232
+ it { subject.to_s(2).should == amt[0, 4] }
233
+ it { subject.to_s(3).should == amt }
234
+ it { subject.to_s(4).should == "#{amt}0" }
235
+ it { subject.to_s(5).should == "#{amt}00" }
236
+ it { subject.to_s(6).should == "#{amt}000" }
237
+ end
238
+ describe "four decimals" do
239
+ let(:amt) { '3.5343' }
240
+ it { subject.to_s.should == amt[0, 4] }
241
+ it { subject.to_s(0).should == amt[0, 1] }
242
+ it { subject.to_s(1).should == amt[0, 3] }
243
+ it { subject.to_s(2).should == amt[0, 4] }
244
+ it { subject.to_s(3).should == amt[0, 5] }
245
+ it { subject.to_s(4).should == amt }
246
+ it { subject.to_s(5).should == "#{amt}0" }
247
+ it { subject.to_s(6).should == "#{amt}00" }
248
+ end
249
+ describe "five decimals" do
250
+ let(:amt) { '3.53434' }
251
+ it { subject.to_s.should == amt[0, 4] }
252
+ it { subject.to_s(0).should == amt[0, 1] }
253
+ it { subject.to_s(1).should == amt[0, 3] }
254
+ it { subject.to_s(2).should == amt[0, 4] }
255
+ it { subject.to_s(3).should == amt[0, 5] }
256
+ it { subject.to_s(4).should == amt[0, 6] }
257
+ it { subject.to_s(5).should == amt }
258
+ it { subject.to_s(6).should == "#{amt}0" }
259
+ end
260
+ describe "six decimals" do
261
+ let(:amt) { '3.534343' }
262
+ it { subject.to_s.should == amt[0, 4] }
263
+ it { subject.to_s(0).should == amt[0, 1] }
264
+ it { subject.to_s(1).should == amt[0, 3] }
265
+ it { subject.to_s(2).should == amt[0, 4] }
266
+ it { subject.to_s(3).should == amt[0, 5] }
267
+ it { subject.to_s(4).should == amt[0, 6] }
268
+ it { subject.to_s(5).should == amt[0, 7] }
269
+ it { subject.to_s(6).should == amt }
270
+ end
271
+ end
272
+ describe "forwarded" do
273
+ describe "power" do
274
+ subject { Money.new(amt).power 2 }
275
+ it { should be_a Money }
276
+ it { subject.to_s.should == '12.46' }
277
+ end
278
+ end
279
+ describe "Rounding" do
280
+ let(:pos_amt1) { Money.new '1.4' }
281
+ let(:pos_amt2) { Money.new '1.5' }
282
+ let(:pos_amt3) { Money.new '1.6' }
283
+ let(:neg_amt1) { Money.new '-1.4' }
284
+ let(:neg_amt2) { Money.new '-1.5' }
285
+ let(:neg_amt3) { Money.new '-1.6' }
286
+ describe "up" do
287
+ it { pos_amt1.to_i(:up).should == 2 }
288
+ it { pos_amt2.to_i(:up).should == 2 }
289
+ it { pos_amt3.to_i(:up).should == 2 }
290
+ it { neg_amt1.to_i(:up).should == -2 }
291
+ it { neg_amt2.to_i(:up).should == -2 }
292
+ it { neg_amt3.to_i(:up).should == -2 }
293
+ end
294
+ describe "down" do
295
+ it { pos_amt1.to_i(:down).should == 1 }
296
+ it { pos_amt2.to_i(:down).should == 1 }
297
+ it { pos_amt3.to_i(:down).should == 1 }
298
+ it { neg_amt1.to_i(:down).should == -1 }
299
+ it { neg_amt2.to_i(:down).should == -1 }
300
+ it { neg_amt3.to_i(:down).should == -1 }
301
+ end
302
+ describe "half_up" do
303
+ it { pos_amt1.to_i(:half_up).should == 1 }
304
+ it { pos_amt2.to_i(:half_up).should == 2 }
305
+ it { pos_amt3.to_i(:half_up).should == 2 }
306
+ it { neg_amt1.to_i(:half_up).should == -1 }
307
+ it { neg_amt2.to_i(:half_up).should == -2 }
308
+ it { neg_amt3.to_i(:half_up).should == -2 }
309
+ end
310
+ describe "half_down" do
311
+ it { pos_amt1.to_i(:half_down).should == 1 }
312
+ it { pos_amt2.to_i(:half_down).should == 1 }
313
+ it { pos_amt3.to_i(:half_down).should == 2 }
314
+ it { neg_amt1.to_i(:half_down).should == -1 }
315
+ it { neg_amt2.to_i(:half_down).should == -1 }
316
+ it { neg_amt3.to_i(:half_down).should == -2 }
317
+ end
318
+ describe "half_even" do
319
+ it { pos_amt1.to_i(:half_even).should == 1 }
320
+ it { pos_amt2.to_i(:half_even).should == 2 }
321
+ it { pos_amt3.to_i(:half_even).should == 2 }
322
+ it { neg_amt1.to_i(:half_even).should == -1 }
323
+ it { neg_amt2.to_i(:half_even).should == -2 }
324
+ it { neg_amt3.to_i(:half_even).should == -2 }
325
+ end
326
+ describe "ceiling" do
327
+ it { pos_amt1.to_i(:ceiling).should == 2 }
328
+ it { pos_amt2.to_i(:ceiling).should == 2 }
329
+ it { pos_amt3.to_i(:ceiling).should == 2 }
330
+ it { neg_amt1.to_i(:ceiling).should == -1 }
331
+ it { neg_amt2.to_i(:ceiling).should == -1 }
332
+ it { neg_amt3.to_i(:ceiling).should == -1 }
333
+ end
334
+ describe "floor" do
335
+ it { pos_amt1.to_i(:floor).should == 1 }
336
+ it { pos_amt2.to_i(:floor).should == 1 }
337
+ it { pos_amt3.to_i(:floor).should == 1 }
338
+ it { neg_amt1.to_i(:floor).should == -2 }
339
+ it { neg_amt2.to_i(:floor).should == -2 }
340
+ it { neg_amt3.to_i(:floor).should == -2 }
341
+ end
342
+ end
343
+
344
+ end
@@ -0,0 +1,32 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Numeric do
4
+ describe "to_money" do
5
+ subject { 3 }
6
+ it { subject.to_money.should == Money.new('3') }
7
+ it { (subject * -1).to_money.should == Money.new('-3') }
8
+ end
9
+ end
10
+
11
+ describe Float do
12
+ describe "to_money" do
13
+ subject { 3.53 }
14
+ it { subject.to_money.should == Money.new('3.53') }
15
+ it { (subject * -1).to_money.should == Money.new('-3.53') }
16
+ it { subject.to_money.precision.should == Money.precision }
17
+ it { subject.to_money.round_mode.should == Money.round_mode }
18
+ it { subject.to_money(3).precision.should == 3 }
19
+ it { subject.to_money(nil, :up).round_mode.should == :up }
20
+ end
21
+ end
22
+
23
+ describe String do
24
+ describe "to_money" do
25
+ subject { '3.53' }
26
+ it { subject.to_money.should == Money.new('3.53') }
27
+ it { subject.to_money.precision.should == Money.precision }
28
+ it { subject.to_money.round_mode.should == Money.round_mode }
29
+ it { subject.to_money(3).precision.should == 3 }
30
+ it { subject.to_money(nil, :up).round_mode.should == :up }
31
+ end
32
+ end
@@ -0,0 +1,77 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'rubygems'
3
+ require 'active_record'
4
+ require 'bd_money/rails'
5
+
6
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ':memory:')
7
+ ActiveRecord::Migration.verbose = false
8
+ ActiveRecord::Schema.define do
9
+ create_table :money_examples, :force => true do |t|
10
+ t.decimal :amount, :precision => 15, :scale => 2
11
+ t.decimal :apr, :precision => 7, :scale => 5
12
+ end
13
+ end
14
+
15
+ class DefaultLoanExample < ActiveRecord::Base
16
+ set_table_name "money_examples"
17
+
18
+ money :amount
19
+ money :apr
20
+ end
21
+
22
+ class BetterLoanExample < ActiveRecord::Base
23
+ set_table_name "money_examples"
24
+
25
+ money :amount, :precision => 2, :round_mode => :half_up
26
+ money :apr, :precision => 5, :round_mode => :floor
27
+ end
28
+
29
+ describe Money do
30
+ describe "default settings" do
31
+ it "should allow dynamic finders to work with money objects" do
32
+ record = DefaultLoanExample.create! :amount => '325.75', :apr => '0.01234'
33
+ DefaultLoanExample.find_by_amount(0.to_money).should be_nil
34
+ found = DefaultLoanExample.find_by_amount('325.75'.to_money)
35
+ found.should == record
36
+ found.amount.should be_a(Money)
37
+ found.amount.to_s.should == '325.75'
38
+ found.apr.should be_a(Money)
39
+ found.apr.to_s.should == '0.01'
40
+ end
41
+ end
42
+ describe "custom settings" do
43
+ it "should allow dynamic finders to work with money objects" do
44
+ record = BetterLoanExample.create! :amount => '123.45', :apr => '0.01234'
45
+ BetterLoanExample.find_by_amount(0.to_money).should be_nil
46
+ found = BetterLoanExample.find_by_amount('123.45'.to_money)
47
+ found.should == record
48
+ found.amount.should be_a(Money)
49
+ found.amount.to_s.should == '123.45'
50
+ found.apr.should be_a(Money)
51
+ found.apr.to_s.should == '0.01234'
52
+ end
53
+ end
54
+ describe "setter method" do
55
+ it "should pass on money values" do
56
+ DefaultLoanExample.new(:amount => 1.to_money).amount.should == 1.to_money
57
+ end
58
+
59
+ it "should convert string values to money objects" do
60
+ DefaultLoanExample.new(:amount => '2').amount.should == 2.to_money
61
+ end
62
+
63
+ it "should convert numeric values to money objects" do
64
+ DefaultLoanExample.new(:amount => 3).amount.should == 3.to_money
65
+ end
66
+
67
+ it "should treat blank values as nil" do
68
+ DefaultLoanExample.new(:amount => '').amount.should be_nil
69
+ end
70
+
71
+ it "should allow existing amounts to be set to nil with a blank value" do
72
+ me = DefaultLoanExample.new :amount => 500.to_money
73
+ me.update_attribute :amount, nil
74
+ me.amount.should be_nil
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'bd_money'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
metadata ADDED
@@ -0,0 +1,189 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bd_money
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Adrian Madrid
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-03-09 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ type: :development
23
+ version_requirements: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 47
29
+ segments:
30
+ - 2
31
+ - 8
32
+ - 0
33
+ version: 2.8.0
34
+ requirement: *id001
35
+ prerelease: false
36
+ name: rspec
37
+ - !ruby/object:Gem::Dependency
38
+ type: :development
39
+ version_requirements: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ hash: 31
45
+ segments:
46
+ - 3
47
+ - 12
48
+ version: "3.12"
49
+ requirement: *id002
50
+ prerelease: false
51
+ name: rdoc
52
+ - !ruby/object:Gem::Dependency
53
+ type: :development
54
+ version_requirements: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ~>
58
+ - !ruby/object:Gem::Version
59
+ hash: 23
60
+ segments:
61
+ - 1
62
+ - 0
63
+ - 0
64
+ version: 1.0.0
65
+ requirement: *id003
66
+ prerelease: false
67
+ name: bundler
68
+ - !ruby/object:Gem::Dependency
69
+ type: :development
70
+ version_requirements: &id004 !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ hash: 49
76
+ segments:
77
+ - 1
78
+ - 8
79
+ - 3
80
+ version: 1.8.3
81
+ requirement: *id004
82
+ prerelease: false
83
+ name: jeweler
84
+ - !ruby/object:Gem::Dependency
85
+ type: :development
86
+ version_requirements: &id005 !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ hash: 3
92
+ segments:
93
+ - 0
94
+ version: "0"
95
+ requirement: *id005
96
+ prerelease: false
97
+ name: rcov
98
+ - !ruby/object:Gem::Dependency
99
+ type: :development
100
+ version_requirements: &id006 !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ~>
104
+ - !ruby/object:Gem::Version
105
+ hash: 9
106
+ segments:
107
+ - 2
108
+ - 3
109
+ - 5
110
+ version: 2.3.5
111
+ requirement: *id006
112
+ prerelease: false
113
+ name: activerecord
114
+ - !ruby/object:Gem::Dependency
115
+ type: :development
116
+ version_requirements: &id007 !ruby/object:Gem::Requirement
117
+ none: false
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ hash: 3
122
+ segments:
123
+ - 0
124
+ version: "0"
125
+ requirement: *id007
126
+ prerelease: false
127
+ name: sqlite3
128
+ description: This library makes it easier to deal with Money values, storing them as BigDecimal to avoid floating-point math errors.
129
+ email: aemadrid@gmail.com
130
+ executables: []
131
+
132
+ extensions: []
133
+
134
+ extra_rdoc_files:
135
+ - LICENSE.txt
136
+ - README.rdoc
137
+ files:
138
+ - .document
139
+ - .rspec
140
+ - Gemfile
141
+ - LICENSE.txt
142
+ - README.rdoc
143
+ - Rakefile
144
+ - VERSION
145
+ - bd_money.gemspec
146
+ - lib/bd_money.rb
147
+ - lib/bd_money/bd_money.rb
148
+ - lib/bd_money/core_extensions.rb
149
+ - lib/bd_money/rails.rb
150
+ - spec/bd_money_spec.rb
151
+ - spec/core_extensions_spec.rb
152
+ - spec/rails_spec.rb
153
+ - spec/spec_helper.rb
154
+ has_rdoc: true
155
+ homepage: http://github.com/aemadrid/bd_money
156
+ licenses:
157
+ - MIT
158
+ post_install_message:
159
+ rdoc_options: []
160
+
161
+ require_paths:
162
+ - lib
163
+ required_ruby_version: !ruby/object:Gem::Requirement
164
+ none: false
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ hash: 3
169
+ segments:
170
+ - 0
171
+ version: "0"
172
+ required_rubygems_version: !ruby/object:Gem::Requirement
173
+ none: false
174
+ requirements:
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ hash: 3
178
+ segments:
179
+ - 0
180
+ version: "0"
181
+ requirements: []
182
+
183
+ rubyforge_project:
184
+ rubygems_version: 1.4.2
185
+ signing_key:
186
+ specification_version: 3
187
+ summary: Managing money objects sanely
188
+ test_files: []
189
+