fixed_point_field 1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,62 @@
1
+ FixedPointField
2
+ ===============
3
+
4
+ Stores floating point values in an integer database column.
5
+
6
+ Most useful for currency, fixed_point_field is a library that lets you tell your
7
+ active record classes that some numeric column needs to be upscaled when saved
8
+ and downscaled when loaded, by some known number of decimal places. It also
9
+ generates stubs to directly access the fixed point version of the number, if
10
+ you need to use mathematics and remain exact, you can multiply the fixed width
11
+ version of your number by another integer and then down convert the result
12
+ manually. The formula for this is:
13
+ fixed_num.to_f / (base ** width)
14
+
15
+
16
+ Usage
17
+ =====
18
+
19
+ class Product < ActiveRecord::Base
20
+ fixed_point_field :price
21
+ end
22
+
23
+ prod = Product.new(:price => 12.75)
24
+ prod.price # => 12.75
25
+ prod.price_fixed # => 1275
26
+
27
+ # it is stored as an int
28
+ prod.send(:read_attribute, :price) # => 1275
29
+
30
+ # fixed point setter
31
+ prod.price_fixed = 1999
32
+ prod.price # => 19.99
33
+
34
+
35
+ Other widths and bases
36
+ ======================
37
+
38
+ This library was designed to be used to store American currency (USD), but may
39
+ be useful in other situations as well. To store a number with a fixed point
40
+ width of 10, use:
41
+
42
+ fixed_point_field :very_precise_column, :width => 10
43
+
44
+ You can also store numbers in other bases other than decimal numbers, but any
45
+ base that is not evenly divisible by 10 will give you rounding errors, negating
46
+ the value of the plugin. I'm not sure why you'd need this, but here it is:
47
+
48
+ fixed_point_field :base_twenty_column, :base => 20
49
+
50
+
51
+ Installation
52
+ ============
53
+
54
+ The preferred method of installation is through Rubygems. You can use
55
+ config.gem, bundler, or manually install the fixed_point_field gem, depending
56
+ on your version of Rails and desired usage.
57
+
58
+
59
+ Feedback
60
+ ========
61
+
62
+ powerup@rubidine.com
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ begin
2
+ require File.join(File.dirname(__FILE__), '..', '..', '..', 'config', 'environment')
3
+ rescue LoadError
4
+ raise "Please test from within your rails application: Unable to load environment.rb"
5
+ end
6
+ require 'rake'
7
+ require 'rake/testtask'
8
+ require 'rake/rdoctask'
9
+
10
+ desc 'Default: run unit tests.'
11
+ task :default => :test
12
+
13
+ desc 'Test the fixed_point_field plugin.'
14
+ Rake::TestTask.new(:test) do |t|
15
+ t.libs << 'lib'
16
+ t.pattern = 'test/**/*_test.rb'
17
+ t.verbose = true
18
+ end
19
+
20
+ desc 'Generate documentation for the fixed_point_field plugin.'
21
+ Rake::RDocTask.new(:rdoc) do |rdoc|
22
+ rdoc.rdoc_dir = 'rdoc'
23
+ rdoc.title = 'FixedPointField'
24
+ rdoc.options << '--line-numbers' << '--inline-source'
25
+ rdoc.rdoc_files.include('README')
26
+ rdoc.rdoc_files.include('lib/**/*.rb')
27
+ end
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'fixed_point_field'
2
+ ActiveRecord::Base.send :include, FixedPointField
@@ -0,0 +1,123 @@
1
+ # The MIT License
2
+ #
3
+ # Copyright (c) 2009 Rubidine <powerup@rubidine.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ # Fixed Point Field specifies that a field that is accessed like a float from
24
+ # a rails script should be stored as an integer in the database. This is only
25
+ # practical when the field always has a fixed number of digits after the
26
+ # decimal place, like how US Dollars have 2 digits of cents after the decimal.
27
+ # More information and examples are available in the README.
28
+ module FixedPointField
29
+
30
+ # When the module is included, in addition to adding the methods to instances
31
+ # of the class, we need to add methods to the class object, do this
32
+ # with extend.
33
+ def self.included kls
34
+ kls.send :extend, ClassMethods
35
+ end
36
+
37
+ # Set a fixed point column to the specificed value (fixed). This
38
+ # is wrapped behind a conversion for the default assignment operator,
39
+ # such that if you had called:
40
+ # <code>fixed_point_field :price</code>
41
+ # the method price= would convert and then call this function.
42
+ def set_fixed_point(column_name, value)
43
+ write_attribute(column_name, value)
44
+ end
45
+
46
+ # Set a column using a floating point column. This calls
47
+ # set_fixed_point after it has up-scaled the value to the specified
48
+ # number of digits. By default the width is 2 and base is 10, to
49
+ # work with USD currency.
50
+ # If you had called:
51
+ # <code>fixed_point_field :price</code>
52
+ # the method price= would be a direct call to this function.
53
+ def set_floating_point(column_name, value, width = 2, base = 10)
54
+ return if value == ''
55
+ set_fixed_point(column_name, (value.to_f * (base**width)).round)
56
+ end
57
+
58
+ # Retrieve the raw value of the field, which will be a Fixnum.
59
+ # This is wrapped behind the default getter, so it is fetched with this
60
+ # function, then down-converted and returned.
61
+ def read_fixed_point(column_name)
62
+ read_attribute(column_name)
63
+ end
64
+
65
+ # Reads the fixed point version and converts. Will return nil if the column
66
+ # value is nil. If you had called:
67
+ # <code>fixed_point_field :price</code>
68
+ # the method price would be a direct call to this function.
69
+ def read_floating_point(column_name, width = 2, base = 10)
70
+ (rv = read_fixed_point(column_name)) ? (rv.to_f / (base**width)) : nil
71
+ end
72
+
73
+ module FixedPointField::ClassMethods
74
+
75
+ # Calling this in an active record class will make getters / setters
76
+ # available for in integer field that return / accept values that are
77
+ # floating point. It will convert them to integer values by up-scaling
78
+ # the number by a certain number of decimal places. This is most useful
79
+ # for working with money, when the values can be stored as cents, but
80
+ # will most often be working with dollars, which always has two places
81
+ # after the decimal reserved for cents.
82
+ #
83
+ # Takes any number of field names to be converted, and an optional
84
+ # hash as the last argument. The hash can have keys of :width, which
85
+ # is the number of places beyond the decimal to use (default 2), and
86
+ # :base, which is the number system to use (default 10 [decimal]).
87
+ #
88
+ # This for a field named my_field, this will generate the methods
89
+ # * my_field - returns the value as a float
90
+ # * my_field_fixed - returns the value as it is stored (Fixnum)
91
+ # * my_field= - takes a floating point number and scales it appropriately
92
+ # * my_field_fixed= - direct setter for fixed point number
93
+ def fixed_point_field *fields
94
+ opts = (fields.pop if fields.last.is_a?(Hash)) || {}
95
+ opts[:width] ||= 2
96
+ opts[:base] ||= 10
97
+ fields.each do |field|
98
+ read_fixed_method = "#{field}_fixed"
99
+ read_float_method = "#{field}"
100
+ set_float_method = "#{field}="
101
+ set_fixed_method = "#{field}_fixed="
102
+
103
+ define_method(read_fixed_method) do
104
+ read_fixed_point(field)
105
+ end
106
+
107
+ define_method(read_float_method) do
108
+ read_floating_point(field, opts[:width], opts[:base])
109
+ end
110
+
111
+ define_method(set_float_method) do |value|
112
+ set_floating_point(field, value, opts[:width], opts[:base])
113
+ end
114
+
115
+ define_method(set_fixed_method) do |value|
116
+ set_fixed_point(field, value)
117
+ end
118
+
119
+ end
120
+ end
121
+ end
122
+
123
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :fixed_point_field do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,221 @@
1
+ # this doesn't stand alone, load rails
2
+ unless defined?(RAILS_ROOT)
3
+ ENV['RAILS_ENV'] = 'test'
4
+ begin
5
+ require File.join(File.dirname(__FILE__), '..', '..', '..', '..', 'config', 'environment')
6
+ rescue
7
+ raise "Please test from within your rails app vendor/plugins/THISPLUGIN: Unable to load config/environment.rb"
8
+ end
9
+ end
10
+
11
+ begin
12
+ require 'mocha'
13
+ rescue LoadError
14
+ raise "Please install the mocha gem to test fixed_point_column."
15
+ end
16
+
17
+ require 'test/unit'
18
+
19
+ class FixedPointFieldTest < Test::Unit::TestCase
20
+
21
+ class Sentinel < RuntimeError
22
+ end
23
+
24
+ class TestRecord < ActiveRecord::Base
25
+ def self.columns
26
+ [
27
+ ActiveRecord::ConnectionAdapters::Column.new(
28
+ 'a', #name
29
+ nil, #default
30
+ 'int(11)', # sql type
31
+ true #null
32
+ ),
33
+ ActiveRecord::ConnectionAdapters::Column.new(
34
+ 'b', #name
35
+ nil, #default
36
+ 'int(11)', # sql type
37
+ true #null
38
+ )
39
+ ]
40
+ end
41
+ end
42
+
43
+ def test_mixin_works
44
+ TestRecord.send(:fixed_point_field, :a)
45
+ instance = TestRecord.new
46
+
47
+ assert instance.respond_to?(:set_fixed_point)
48
+ assert instance.respond_to?(:set_floating_point)
49
+ assert instance.respond_to?(:read_fixed_point)
50
+ assert instance.respond_to?(:read_floating_point)
51
+
52
+ assert instance.respond_to?(:a_fixed)
53
+ assert instance.respond_to?(:a)
54
+ assert instance.respond_to?(:a_fixed=)
55
+ assert instance.respond_to?(:a=)
56
+ end
57
+
58
+ def test_sane_defaults
59
+ TestRecord.send(:fixed_point_field, :a)
60
+ instance = TestRecord.new
61
+
62
+ # make sure it works before the stub
63
+ instance.a = 10.3
64
+
65
+ # 2, 10 are our default width and base
66
+ instance.stubs(:set_floating_point).with(:a, 10.3, 2, 10).raises(Sentinel)
67
+ assert_raises(Sentinel) do
68
+ instance.a = 10.3
69
+ end
70
+ end
71
+
72
+ def test_mixin_works_with_options
73
+ TestRecord.send(:fixed_point_field, :a, {:width => 1})
74
+ instance = TestRecord.new
75
+
76
+ assert instance.respond_to?(:set_fixed_point)
77
+ assert instance.respond_to?(:set_floating_point)
78
+ assert instance.respond_to?(:read_fixed_point)
79
+ assert instance.respond_to?(:read_floating_point)
80
+
81
+ assert instance.respond_to?(:a_fixed)
82
+ assert instance.respond_to?(:a)
83
+ assert instance.respond_to?(:a_fixed=)
84
+ assert instance.respond_to?(:a=)
85
+ end
86
+
87
+ def test_options_are_followed
88
+ TestRecord.send(:fixed_point_field, :a, {:width => 1})
89
+ instance = TestRecord.new
90
+
91
+ # make sure it works before the stub
92
+ instance.a = 10.3
93
+
94
+ # third argument = width
95
+ instance.stubs(:set_floating_point).with(:a, 10.3, 1, 10).raises(Sentinel)
96
+ assert_raises(Sentinel) do
97
+ instance.a = 10.3
98
+ end
99
+
100
+ # make sure it works before the stub
101
+ instance.a
102
+
103
+ # second argument = width
104
+ instance.stubs(:read_floating_point).with(:a, 1, 10).raises(Sentinel)
105
+ assert_raises(Sentinel) do
106
+ instance.a
107
+ end
108
+ end
109
+
110
+ def test_set_float
111
+ TestRecord.send(:fixed_point_field, :a)
112
+ instance = TestRecord.new
113
+ instance.a = 10.3
114
+ assert_equal 1030, instance.send(:read_attribute, :a)
115
+ end
116
+
117
+ def test_set_fixed
118
+ TestRecord.send(:fixed_point_field, :a)
119
+ instance = TestRecord.new
120
+ instance.a_fixed = 103
121
+ assert_equal 103, instance.send(:read_attribute, :a)
122
+ end
123
+
124
+ def test_read_float
125
+ TestRecord.send(:fixed_point_field, :a)
126
+ instance = TestRecord.new
127
+ instance.send(:write_attribute, :a, 103)
128
+ assert_equal 1.03, instance.a
129
+ end
130
+
131
+ def test_read_fixed
132
+ TestRecord.send(:fixed_point_field, :a)
133
+ instance = TestRecord.new
134
+ instance.send(:write_attribute, :a, 103)
135
+ assert_equal 103, instance.a_fixed
136
+ end
137
+
138
+ def test_write_and_read_paring
139
+ TestRecord.send(:fixed_point_field, :a)
140
+ instance = TestRecord.new
141
+
142
+ instance.a = 10.3
143
+ assert_equal 10.3, instance.a
144
+ assert_equal 1030, instance.a_fixed
145
+
146
+ instance.a_fixed = 1310
147
+ assert_equal 13.10, instance.a
148
+ assert_equal 1310, instance.a_fixed
149
+ end
150
+
151
+ def test_multiple_fields_mixin
152
+ TestRecord.send(:fixed_point_field, :a, :b)
153
+ instance = TestRecord.new
154
+
155
+ assert instance.respond_to?(:set_fixed_point)
156
+ assert instance.respond_to?(:set_floating_point)
157
+ assert instance.respond_to?(:read_fixed_point)
158
+ assert instance.respond_to?(:read_floating_point)
159
+
160
+ assert instance.respond_to?(:a_fixed)
161
+ assert instance.respond_to?(:a)
162
+ assert instance.respond_to?(:a_fixed=)
163
+ assert instance.respond_to?(:a=)
164
+
165
+ assert instance.respond_to?(:b_fixed)
166
+ assert instance.respond_to?(:b)
167
+ assert instance.respond_to?(:b_fixed=)
168
+ assert instance.respond_to?(:b=)
169
+ end
170
+
171
+ def test_multiple_fields_read_write_pairing_without_collision
172
+ TestRecord.send(:fixed_point_field, :a, :b)
173
+ instance = TestRecord.new
174
+
175
+ instance.a = 10.3
176
+ instance.b = 1.1
177
+ assert_equal 10.3, instance.a
178
+ assert_equal 1030, instance.a_fixed
179
+ assert_equal 1.1, instance.b
180
+ assert_equal 110, instance.b_fixed
181
+
182
+ instance.a_fixed = 1310
183
+ instance.b_fixed = 570
184
+ assert_equal 13.10, instance.a
185
+ assert_equal 1310, instance.a_fixed
186
+ assert_equal 5.70, instance.b
187
+ assert_equal 570, instance.b_fixed
188
+ end
189
+
190
+ def test_multiple_fields_mixin_with_options
191
+ TestRecord.send(:fixed_point_field, :a, :b, {:width => 1})
192
+ instance = TestRecord.new
193
+
194
+ assert instance.respond_to?(:set_fixed_point)
195
+ assert instance.respond_to?(:set_floating_point)
196
+ assert instance.respond_to?(:read_fixed_point)
197
+ assert instance.respond_to?(:read_floating_point)
198
+
199
+ assert instance.respond_to?(:a_fixed)
200
+ assert instance.respond_to?(:a)
201
+ assert instance.respond_to?(:a_fixed=)
202
+ assert instance.respond_to?(:a=)
203
+
204
+ assert instance.respond_to?(:b_fixed)
205
+ assert instance.respond_to?(:b)
206
+ assert instance.respond_to?(:b_fixed=)
207
+ assert instance.respond_to?(:b=)
208
+ end
209
+
210
+ def test_base_twenty
211
+ TestRecord.send(:fixed_point_field, :a, :b, {:base => 20})
212
+ instance = TestRecord.new
213
+
214
+ instance.a = 10.3
215
+ assert_equal (10.3 * (20 ** 2)), instance.send(:read_attribute, :a)
216
+
217
+ # make sure it loads back okay as well
218
+ assert_equal 10.3, instance.a
219
+ end
220
+
221
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fixed_point_field
3
+ version: !ruby/object:Gem::Version
4
+ version: "1.0"
5
+ platform: ruby
6
+ authors:
7
+ - Todd Willey <todd@rubidine.com>
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-07-05 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Store numeric amounts with a known number of decial points, such as currency, as a whole number, for more precise (non-floating point) operations.
17
+ email: powerup@rubidine.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ files:
25
+ - README
26
+ - Rakefile
27
+ - init.rb
28
+ - lib/fixed_point_field.rb
29
+ - tasks/fixed_point_field_tasks.rake
30
+ - test/fixed_point_field_test.rb
31
+ has_rdoc: true
32
+ homepage: http://github.com/rubidine/fixed_point_field
33
+ licenses: []
34
+
35
+ post_install_message:
36
+ rdoc_options: []
37
+
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: "0"
45
+ version:
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">"
49
+ - !ruby/object:Gem::Version
50
+ version: 1.3.1
51
+ version:
52
+ requirements: []
53
+
54
+ rubyforge_project:
55
+ rubygems_version: 1.3.5
56
+ signing_key:
57
+ specification_version: 3
58
+ summary: ActiveRecord plugin for dealing with currency
59
+ test_files:
60
+ - test/fixed_point_field_test.rb