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 +62 -0
- data/Rakefile +27 -0
- data/init.rb +2 -0
- data/lib/fixed_point_field.rb +123 -0
- data/tasks/fixed_point_field_tasks.rake +4 -0
- data/test/fixed_point_field_test.rb +221 -0
- metadata +60 -0
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,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,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
|