Rails 6 number field with comma - ruby-on-rails

When a user inputs a number value into a field that contains a comma, for example: 1,000,000. When the form is submitted and saved, the value becomes 1. The column type is a t.bigint
How do I prevent this across all my numeric fields?

The key thing about the thousand delimiter is that it will always be followed by three digits. So a regex lookahead would make sense. Also I'd do this processing in the controller rather than the model as it's a function of the input from the form.
So if I had an object Foo with a some_number attribute, in the foos controller I'd do something like:
def foo_params
params.require(:foo).permit(:some_number).tap do |foo|
foo[:some_number] = foo[:some_number]&.split(/[\.\,](?=\d{3})/).join
end
end
That will convert "3,000" and "3.000" into "3000", but leave "3.12" as "3.12".
It will also convert "23,345,555,444.3" into "23345555444.3"

I don't know if it's the best way to do this, but I usually use a custom setter, lets say your column is total, then I do something like:
def total=(value)
value = value.gsub(/[\.,],'') if value.is_a?(String) # remove , and .
write_attribute(:total, value)
end
Now you can assign anything like "1.000" or "2,343,111" and it will strip comma and dot.
If you want to be more permissive with the values (in french you could write "1 000"), you can use /\D/ for the regexp to remove anything that's not a digit.

When space ( ) or period (.) is used as a thousand separator and comma (,) as decimal separator, ie: "772 067,48". First it removes space, then period and replaces comma with period.
So not directly applicable to OPs question but I found the page through a search engine and others may do as well.
def foo_params
params.require(:foo).permit(:year, :quarter, :first).tap do |foo|
foo[:first] = foo[:first]&.gsub(' ', '').gsub('.', '').gsub(',', '.')
end

Related

Rails Amounts in Thousands Are Truncated

In my Rails 5 app, I read in a feed for products. In the JSON, when the price is over $1,000, it the JSON has a comma, like 1,000.
My code seems to be truncating it, so it's storing as 1 instead of 1,000.
All other fields are storing correctly. Can someone please tell me what I'm doing wrong?
In this example, the reg_price saves as 2, instead of 2590.
json sample (for reg_price field):
[
{
"reg_price": "2,590"
}
]
schema
create_table "products", force: :cascade do |t|
t.decimal "reg_price", precision: 10, scale: 2
end
model
response = open_url(url_string).to_s
products = JSON.parse(response)
products.each do |product|
product = Product.new(
reg_price: item['reg_price']
)
product.save
end
You are not doing anything wrong. Decimals don't work with comma separator. I'm not sure there is a nice way to fix the thing. But as an option you could define a virtual attribute:
def reg_price=(reg_price)
self[:reg_price] = reg_price.gsub(',', '')
end
The reason this is happening has nothing to do with Rails.
JSON is a pretty simple document structure and doesn't have any support for number separators. The values in your JSON document are strings.
When you receive a String as input and you want to store it as an Integer, you need to cast it to the appropriate type.
Ruby has built in support for this, and Rails is using it: "1".to_s #=> 1
The particular heuristic Ruby uses to convert a string to an integer is to take any number up to a non-numerical character and cast it as an integer. Commas are non-numeric, at least by default, in Ruby.
The solution is to convert the string value in your JSON to an integer using another method. You can do this any of these ways:
Cast the string to an integer before sending it to your ActiveRecord model.
Alter the string in such a way that the default Ruby casting will cast the string into the expected value.
Use a custom caster to handle the casting for this particular attribute (inside of ActiveRecord and ActiveModel).
The solution proposed by #Danil follows #2 above, and it has some shortcomings (as #tadman pointed out).
A more robust way of handling this without getting down in the mud is to use a library like Delocalize, which will automatically handle numeric string parsing and casting with consideration for separators used by the active locale. See this excellent answer by Benoit Garret for more information.

Arbitrary-length LIKE clause in Ruby on Rails ActiveRecord

I'm attempting to write a Ruby method which accepts an array of strings (for example, ["EG", "K", "C"], and returns all records from a database table where the icao_code field starts with any of those strings (for example, KORD, EGLL, and CYVR would all match). The length of the array will vary, and it will be input by a user, so it needs to be sanitized.
If I were only searching for a single string, I could do something like Airport.where("icao_code LIKE ?", "#{icao_start}%"). However, since I need to search against an arbitrary number of strings, I can't use that syntax.
Right now, I've got it working as follows:
def in_region(icao_starts)
where_clause = icao_starts.map{|i| "icao_code LIKE '#{i}%'"}.join(" OR ")
return Airport.where(where_clause)
end
However, I'm a bit worried using a setup like this with untrusted user input, since I suspect it would be vulnerable to SQL injection.
Is there a better way to get the same result in a more secure way?
You could consider something like this:
def in_region(icao_starts)
where_clause = "icao_code LIKE '#?%' OR " * icao_starts.length
return Airport.where(where_clause.sub(/\ OR\ $/, ''), *icao_starts)
end
This will build up a (potentially very long?) string with ? placeholders. The *icao_starts will expand that array into arguments to the where clause, so each ? will end up getting safely replaced. The sub(/\ OR\ $/, '') simply trims off the final OR (you could append 1=0 instead if you wanted).
If I were you I would also perform a .uniq on icao_starts before you do anything, truncate the array at some sensible upper length limit, and also have a whitelist of permitted values (oh, forget that, I thought users were searching by airport code). That should be pretty much infallible.
You are right about not interpolating user input into your SQL query. This is dangerous and makes your code vulnerable for SQLI attacks.
def in_region(icao_starts)
conditions = icao_starts.map { "icao_code LIKE ?"}
Airport.where(conditions.join(' OR '), *icao_starts.map { |name| "#{name}%"})
end
It is pretty similar than the solution of bogardpd but does not use a Regexp to get rid of the last " OR"

Rails 4 ts_range not persisting

I'm having an issue with Rails 4's support for Postgresql's ts_range data type. Here is the code that I am trying to persist:
before_validation :set_appointment
attr_accessor :starting_tsrange, :ending_tsrange
def set_appointment
self.appointment = convert_to_utc(starting_tsrange)...convert_to_utc(ending_tsrange)
end
def convert_to_utc
ActiveSupport::TimeZone.new("America/New_York").parse(time_string).utc
end
Basically I set an instance variable for the beginning and end of the appointment ts_range with two strings representing date_times. Before validation it converts them to utc and saves those values to the appointment attribute which should then be persisted. It sets things correctly but when I try to retrieve the record, the appointment attribute is now nil. Why is this code not working as expected?
Figured out the subtle bug in the code. The issue here is with the triple dot range operator. If we get two values that are the exact same time. The triple dot will say include everything from time a up until time b if they are the same exact time, then nothing will be included and the result will be nil. This can be visualized with the code below
(1...1).to_a # []
(1..1).to_a # [1]
So the way to fix this is to not use the triple dot notation when using ranges that can have the same value for a time. Use the double dot notation instead.

Stripping commas from Integers or decimals in rails

Is there a gsub equivalent for integers or decimals? Should gsub work with integers? Basically I'm just trying to enter decimal into a ruby form and what the user to be able to use commas. For example, I want the user to be able to enter 1,000.99.
I've tried using
before_save :strip_commas
def strip_commas
self.number = self.number.gsub(",", "")
end
but get the following error "undefined method `gsub' for 8:Fixnum" where "8" is replaced with whatever number the user enters.
If your field is a Fixnum, it will never have commas, as Rails will have to convert the user input into a number in order to store it there.
However, it will do that by calling to_i on the input string, which is not what you want.
overriding the normal setter to something like
def price=(num)
num.gsub!(',','') if num.is_a?(String)
self[:price] = num.to_i
end
Not tested, but something resembling this should work...
You need to get at the commas while the input is still a string.
Edit:
As noted in comments, if you want to accept decimals, and create something not an integer, you need a different conversion than String.to_i. Also, different countries have different conventions for numeric punctuation, so this isn't a complete solution.
try self.number.gsub(/\D/, ''). That is, remove everything that isn't a digit. Regexen make no distinction between integers, floats, decimals, etc. It's all strings. And Rails won't convert it correctly for you, because it just calls #to_i or #to_f on it.
EDIT:
actually: self.number.gsub(/[^\d\.]/, '').to_f: everything that isn't a digit or decimal point, and convert it to a float.

Rails validates_format_of

I want to use validates_format_of to validate a comma separated string with only letters (small and caps), and numbers.
So.
example1, example2, 22example44, ex24
not:
^&*, <> , asfasfsdafas<#%$#
Basically I want to have users enter comma separated words(incl numbers) without special characters.
I'll use it to validate tags from acts_as_taggable_on. (i don't want to be a valid tag for example.
Thanks in advance.
You can always test out regular expressions at rubular, you would find that both tiftiks and Tims regular expressions work albeit with some strange edge cases with whitespace.
Tim's solution can be extended to include leading and trailing whitespace and that should then do what you want as follows :-
^\s*[A-Za-z0-9]+(\s*,\s*[A-Za-z0-9]+)*\s*$
Presumably when you have validated the input string you will want to turn it into an array of tags to iterate over. You can do this as follows :-
array_var = string_var.delete(' ').split(',')
^([a-zA-Z0-9]+,\s*)*[a-zA-Z0-9]+$
Note that this regex doesn't match values with whitespace, so it won't match multiple words like "abc xyz, fgh qwe". It matches any amount of whitespace after commas. You might not need ^ or $ if validates_format_of tries to match the whole string, I've never used Rails so I don't know about that.
^[A-Za-z0-9]+([ \t]*,[ \t]*[A-Za-z0-9]+)*$
should match a CSV line that only contains those characters, whether it's just one value or many.

Resources