Rails: Storing BigDecimal in Database - ruby-on-rails

I am handling a bunch of BigDecimals, which I want to store to my database. Ideally without any loss of accuracy. I don't know the nicest way to achieve that.
This came to my mind:
t.decimal :value, precision: 1000, scale: 30
It does not look like a good way to approach this problem. 1. it still compromises accuracy. 2. It's is getting unnecessarily large.
Is there a way to store the object, e.g.: #<BigDecimal:586a238,'0.563E0',9(36)> to the database (within a text column) and then re-initialize it as a BigDecimal?

You might want to look into composed_of.
But I prefer using custom getter and setter methods because I thing they are easier to read and to understand.
Example: Imagine your attribute is named foo and you want to use BigDecimal in the app but store the value as a string in the database:
def foo
BigDecimal.new(read_attribute(:foo))
end
def foo=(foo)
write_attribute(:foo, foo.to_s)
end

By default a PostgreSQL Decimal has a range of "up to 131072 digits before the decimal point; up to 16383 digits after the decimal point". Is that not enough?
https://www.postgresql.org/docs/9.1/static/datatype-numeric.html
Just use:
t.decimal :value

Related

Rails converting string with float value to integer

I have a model that has an attribute named value this is saved in the db as an integer.
I input the number in the view using a text_field, I am also performing a method on it :before_save that takes the value (something like 21.37) and using the money gem convert it to just cents.
However, it seems that before I can even perform the method that converts it from a float to an integer it is being converted to some kind of integer and the decimal is being lost.
I have tested this by outputting value in the method that runs before_save: and it round it to 21
Does anyone know why this might be happening, Im not performing any other changes to it.
I'm guessing you're doing something like Coupon.new(params) in your controller, and since Rails knows Coupon's value should be an integer, it helpfully calls to_i on params[:value] for you under the covers, which turns 21.37 into 21. Here's a rather inelegant but effective fix:
params[:value] ~= /.*\.(\d+)/
params[:value] = params[:value].to_f * (10**$1.length) if $1
Do that before you do Coupon.new(params).
Another option is to simply assume you don't care about anything after the 2nd decimal place (you're dealing with cents after all). Then you can simply do params[:value] = params[:value].to_f.round(2) * 100.
Also you should consider using number_field instead of text_field in your view so you can at least be sure you're getting a number. Something like number_field(:coupon, :value, step: 0.01) should ensure your users can enter decimals.
I agree with #kitkat seems like something that would happen in the model layer. You might be able to implement your conversion to cents logic in 'before_validation' or perhaps a custom setter for 'coupon'.

Storing Prices with Cents in Rails

Ok I have a Stripe charge to which I am applying taxes. Stripe takes in a number as cents, so it leaves you with a number like 10015 instead of 100.15.
In my controller, I am sending the number to ActiveRecord as 10015/100.0
When I retrieve it, it gives me #<BigDecimal:7fca81f71130,'0.1243E3',18(27)>>
Whats going on ?
I tried
rails g migration add_expense_to_user expense:integer
and
rails g migration add_expense_to_user expense:decimal
to whose migration I added
add_column :user, :expense, :decimal, precision: 6, scale: 2
which is the current setup.
How do I store / retrieve the value if it is stored as 10015/100
The BigDecimal is just the type that Rails uses for decimal types in DBs. 0.1243E3 is scientific notation, short for 0.1243 x 10³ - ie. 124.3
You can get a regular float from it by just using the .to_f method in Ruby, or you can pass a BigDecimal into other Rails helpers, like number_to_currency(the_big_decimal) will produce "$124.30"
So, in other words, with the BigDecimal you probably already have what you're asking for in this question.
When you access the data, you need to call .to_f.
In irb:
a = BigDecimal.new(5, 2)
a
=> #<BigDecimal:1ad7c98,'0.5E1',9(27)>
a.to_f
=> 5.0
The value is stored inside the BigDecimal object with arbitrary precision. The "common" representation of non-integer values (i.e. floats) however doesn't provide this precision. Thus, BigDecimal provides various options to convert its value to other types.
You can e.g use expense.to_f to get a floating point representation of the BigDecimal value (and thus loosing the precision along the way). Alternatively, if you just want to print the value, you could use one of the to_s method to format the value as a string:
expense.to_s
# => "124.3"
See the BigDecimal documentation for details.

Big Integers and Custom Validation

I'm somewhat new to Rails and I'm trying to learn about custom validations.
One common requirement in Brazil are CPF/CNPJ/RG fields. They are a type of identification number and follow a specific format.
For example:
CPFs are 11 digit numbers. They follow this pattern: xxx.xxx.xxx-xx
I'm trying to store them in an Integer field but I'm getting (Using Postgres):
PG::Error: ERROR: value "xxxxxxxxxxx" is out of range for type
integer
What is the proper way to store this? Bigint (How?)? A string?
My second question is:
How can I specify a custom validation (a method) for this field that could be called somewhat like this:
class User < AR::Base
validates :cpf, presence: true, unique: true, cpf: true
Assuming performance is not critical, strings are fine. That way you can keep the dots and dashes. As mentioned by others in this thread, bigint or numeric may be far more performant if that's a concern.
If you keep the field a string, you can easily validate it with regex:
validates_format_of :cpf, with: /^[0-9]{3}\.[0-9]{3}\.[0-9]{3}\-[0-9]{2}$/
For small tables, just store as text to preserve the format.
For big tables, performance and storage size may be an issue. If your pattern is guaranteed, you may very well store the number as bigint and format it on retrieval with to_char():
Write:
SELECT translate('111.222.333-55', '.-', '')::bigint
This also serves as partial validation. Only digits, . and - are allowed in your string. The pattern might still be violated, you have to check explicitly with something like #Michael provided.
Read:
SELECT to_char(11122233355, 'FM000"."000"."000"-"00')
Returns:
111.222.333-55
Don't forget the leading FM in the pattern to remove the leading whitespace (where a negative sign might go for numbers).
A bigint occupies 8 bytes on disk and can easily store 11-digit numbers.
text (or varchar) need 1 byte plus the actual string, which amounts to 15 bytes in your case.
Plus, processing bigint is generally a bit faster than processing text of equal length..
Personally I would always store these values as bigint and apply formatting on input/output (as Erwin suggests) or in the application.
The main reasons are storage efficiency (as Erwin mentions) and efficiency of comparison. When you compare 11111111112 to 11111111113 as text, PostgreSQL will use language-specific collation rules that are correct for text, but may not be what you want for numbers. They're also slow; a recent question on SO reported a five-fold speed-up in text comparisons by using the COLLATE "C" option to force plain POSIX collations; numeric collations are faster again.
Most of these standard numbers have their own internal check-sums, often a variant of the Luhn algorithm. Validating these in a CHECK constraint is likely to be a good idea. You can implement a Luhn algorithm check against integers pretty easily in PL/PgSQL or plain SQL; I wrote some samples on the PostgreSQL wiki.
Whatever you do, make sure you have a CHECK constraint on the column that validates the number on storage, so you don't get invalid and nonsensical values stored.

Setting default precision for all decimal numbers in rails

Is there a way to force Rails to output all decimal numbers with a certain precision? All of the decimal fields in my database are currency amounts, so I'd like decimal numbers to show by default with a precision of 2 (i.e. 2.40). I know I can use a helper function like "number_to_currency" to do this to each individual number, but that seems a bit tedious and unnecessary.
If you're concerned about overriding "to_s" on Float having possible unforeseen side-effects, your next best bet is probably to just create a new method, but still as a core extension. Something like this:
class Float
def c
sprintf('%.2f', self)
end
end
Then can't have any unforeseen consequences, and then anywhere you'd want to display the number with two decimal places, you'd just call .c. For example:
message = "The account balance is $#{amount.c}."
Not automatic, but not much extra typing, and no possible side-effects that overriding to_s could potentially cause.
Well if you don't mind monkey patching ruby, you can add a something like this (put it in an initializer at "/config/initializers/core_extensions.rb"):
class Float
def to_s
sprintf('%.2f', self)
end
end

Correct datatype for latitude and longitude? (in activerecord)

Should I store latitude and longitude as strings or floats (or something else)?
(I'm using activerecord / ruby on rails, if that matters).
Update:
Mysql in development and postgresql in production (why does it matter?)
This is what I use:
add_column :table_name, :lat, :decimal, {:precision=>10, :scale=>6}
add_column :table_name, :lng, :decimal, {:precision=>10, :scale=>6}
If you need to do more complex geographical calculations, you can investigate PostGIS for Postgresql or MySQL Spatial Extensions for MySQL. Otherwise, a float (double precision might be a good idea) should work.
Edit: It looks like the GeoRuby library includes a Rails extension for working with the spatial/GIS extensions for both of the aforementioned databases.
If you're not using a spatially-enabled database, Google Maps recommends using floats of size (10,6). That gives you 6 digits after the decimal - if you want more precision, you can adjust accordingly.
I would suggest using floats, although it doesn't really make that much of a difference. Floats are easier to do calculations on if you ever desire that in the future.
Generally you want Lat/Long stored in the largest float type you have. At some latitudes (eg: near the equator) very small changes in longitude can equate to large differences in terms of surface distance.
I suppose if it is a field which you won't ever want to do any math on, you could use a string. I'd avoid that though.
Since they're fixed precision, you should convert and store as integer for a significant performance improvement.
(SEE http://www.postgresql.org/docs/9.1/static/datatype-numeric.html#DATATYPE-NUMERIC-DECIMAL)

Resources