fixed_point_field 1.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/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