Rails convert integer value before save - ruby-on-rails

I have an database with an enginze size for the car
Therefore user can write something like 2.5 (like in liters) or 2500 (cc)
Later on I have an sorting and it should using 999-9999 values to compare
I came up the function below, but I would like it be more flexible. Moreover, 2.5 causing the result of 2000 now because looks like Rails converting value before triggering before_save
How do I make convert right and detect if there is an point or comma in input?
before_save :covert_engine
private
def covert_engine
if self.car_engine_size.present?
if Math.log10(self.car_engine_size).to_i + 1 < 4
self.car_engine_size = self.car_engine_size * 1000
end
end
end
P.S. self.car_engine_size is an integer in database

If you want the user to be able to use different units of input I would make it explicit to the user by letting them select the unit.
Start by creating a virtual attribute
class Car
attr_accessor :engine_size_unit
end
Add the attribute to the form and whitelist it in the controller
<%= f.number_field :engine_size %>
<%= f.select :engine_size_unit, ['cc', 'l']) %>
Then convert the value in the model using the value before the typecast
class Car
before_save :covert_engine,
if: -> { car_engine_size.present? && engine_size_unit == 'l' }
def covert_engine
converted = car_engine_size_before_type_cast * 1000
self[:car_engine_size] = converted.to_i
end
end

Related

Define and use array of text values corresponding to numeric values

I'm new to the Ruby on Rails environment, so I am stuck on what might be a simple question: I'm looking to define some text strings/labels that correspond to a numeric value. These values will be stored in the database and then used in my code instead of the numeric values.
In C, I would to something like this:
#define Accounting 0
#define Engineering 1
#define Education 2
...to be used like this:
if (field_of_study == Accounting) ...
I want to be able to do this in Rails controllers/views. I currently have to do something like this in my views to display items:
<tr>
<td><%= link_to user.name, user %></td>
<% if user.studyField == 0 %>
<td>Accounting</td>
<% elsif user.studyField == 1 %>
<td>Engineering</td>
<% elsif user.studyField == 2 %>
<td>Education</td>
<% end %>
</tr>
I would also like to use the text strings/labels in a drop-down menu in the form_for form and then save it using the numeric identifier. Do I need a before_save method to translate the two or is their an automatic way of doing this?
You might find this helpful: Ruby on Rails: Where to define global constants?.
In Rails, since all models are autoloaded by default, you might find it convenient to define your constants in the models, as follows
class User < ActiveRecord::Base
ACCOUNTING = 0
ENGINEERING = 1
EDUCATION = 2
end
or even
class User < ActiveRecord::Base
FIELDS = { accounting: 0, engineering: 1, education: 2 }
end
These can be used anywhere with User::ACCOUNTING or User::FIELDS[:accounting]. To use the second version inside a form, you can use
select('user', 'study_field', User::FIELDS)
Refer to select for more details.
There are a couple of ways to do this. You can assign the constants to integers and they should be saved to the database as integers:
# config/initializers/constants.rb
Accounting = 0
Engineering = 1
This is a bit ugly because Accounting is literally equal to zero. In Rails console:
Accounting == 0
=> true
However, this is probably the most straightforward way to meet your requirement and it looks like this is how your approached the problem with C.

Performing row operations on ActiveRecord objects prior to an aggregate function

I am trying to calculate a weighted average of a variable in my model based on a second variable in my model and I'm having trouble finding a way to do it through ActiveRecord.
class Employer < ActiveRecord::Base
attr_accessible :name, :number_of_employees, :average_age
def self.wt_avg_age
#return sum(number_of_employee * average_age)/sum(number_of_employees)
end
end
In straight SQL, I would use:
SELECT id, SUM(number_of_employees*average_age)/SUM(number_of_employees)
FROM employer
GROUP BY name
Can I execute something like this on an ActiveRecord relation in an eloquent way (i.e., without pulling down separate arrays and iterating through every record to get my numerator)? I have tried different combinations using .select(), .pluck(), and sum() without any luck. I'm having trouble getting the ActiveRecord object to perform the sumproduct.
You should be able to do something like:
Employer.select("name, (SUM(number_of_employees*average_age)/SUM(number_of_employees)) as sum").group(:name)
That will return Employer instances to you, but they will only have the .name and .sum attributes on them. This will run the exact SQL query that you wanted.
It looks like ActiveRecord::Calculations#sum takes a block:
# File activerecord/lib/active_record/relation/calculations.rb, line 92
def sum(*args)
if block_given?
self.to_a.sum(*args) {|*block_args| yield(*block_args)}
else
calculate(:sum, *args)
end
end
(also see http://api.rubyonrails.org/classes/Enumerable.html#method-i-sum)
So you might try:
def self.wt_avg_age
numerator = self.all.sum { |e| e.number_of_employee * e.average_age }
denominator = self.sum :number_of_employees
return numerator / denominator
end
Take a try, maybe it can works:
def self.wt_avg_age
a = Employer.sum("number_of_employee * average_age")
b = Employer.sum('number_of_employees')
a/b
end

Custom formatting of float within form_for

Is there any way to control the float format in a form field?
I want to format a float like an integer if the modulus is 0, otherwise display the float as is. I overrode the Model accessor to do this formatting.
When an edit form is loaded, I would like to have the following transformations:
stored value | accessor returns | form field shows
---------------------------------------------------
1.0 | 1 | 1
1.5 | 1.5 | 1.5
However, form_for appears to be accessing the attribute directly, thereby displaying the float as is.
Any ideas on how to get around this? Thanks.
Using
def my_float
raw = read_attribute(:my_float)
if raw == raw.to_i
raw.to_i
else
raw
end
end
within form_for will not work as noted before. Tried multiple times. IMHO it is one of more severe design issues with rails. In general you don't have straight forward (restful) access to model from your view.
You could override the attribute reader, something like this:
def myfloat
if #myfloat == #myfloat.to_i
#myfloat.to_i
else
#myfloat
end
end
Now the returned value are correctly formatted for your form and still usable in your application.
I do believe it will work when you do something like this:
<%= f.text_field :field_attribute, :value => format_method(f.object.field_attribute) %>
format_method is whatever method you use within the model to override the formatting when accessing it that way.
Veger's solution will work if you use read_attribute to get the "raw" value:
def myfloat
raw = read_attribute(:myfloat)
if raw == raw.to_i
raw.to_i
else
raw
end
end
As always when comparing floats to integers, you'd want to be careful about rounding.
You can override respond_to? in the Model to stop value_before_type_cast from being called.
def respond_to?(*args)
if args.first.to_s == "my_float_before_type_cast"
false
else
super
end
end
And then you need also:
def my_float
raw = read_attribute(:my_float)
if raw == raw.to_i
raw.to_i
else
raw
end
end

Rails, using time_select on a non active record model

I am trying to use a time_select to input a time into a model that will then perform some calculations.
the time_select helper prepares the params that is return so that it can be used in a multi-parameter assignment to an Active Record object.
Something like the following
Parameters: {"commit"=>"Calculate", "authenticity_token"=>"eQ/wixLHfrboPd/Ol5IkhQ4lENpt9vc4j0PcIw0Iy/M=", "calculator"=>{"time(2i)"=>"6", "time(3i)"=>"10", "time(4i)"=>"17", "time(5i)"=>"15", "time(1i)"=>"2009"}}
My question is, what is the best way to use this format in a non-active record model. Also on a side note. What is the meaning of the (5i), (4i) etc.? (Other than the obvious reason to distinguish the different time values, basically why it was named this way)
Thank you
You can create a method in the non active record model as follows
# This will return a Time object from provided hash
def parse_calculator_time(hash)
Time.parse("#{hash['time1i']}-#{hash['time2i']}-#{hash['time3i']} #{hash['time4i']}:#{hash['time5i']}")
end
You can then call the method from the controller action as follows
time_object = YourModel.parse_calculator_time(params[:calculator])
It may not be the best solution, but it is simple to use.
Cheers :)
The letter after the number stands for the type to which you wish it to be cast. In this case, integer. It could also be f for float or s for string.
I just did this myself and the easiest way that I could find was to basically copy/paste the Rails code into my base module (or abstract object).
I copied the following functions verbatim from ActiveRecord::Base
assign_multiparameter_attributes(pairs)
extract_callstack_for_multiparameter_attributes(pairs)
type_cast_attribute_value(multiparameter_name, value)
find_parameter_position(multiparameter_name)
I also have the following methods which call/use them:
def setup_parameters(params = {})
new_params = {}
multi_parameter_attributes = []
params.each do |k,v|
if k.to_s.include?("(")
multi_parameter_attributes << [ k.to_s, v ]
else
new_params[k.to_s] = v
end
end
new_params.merge(assign_multiparameter_attributes(multi_parameter_attributes))
end
# Very simplified version of the ActiveRecord::Base method that handles only dates/times
def execute_callstack_for_multiparameter_attributes(callstack)
attributes = {}
callstack.each do |name, values|
if values.empty?
send(name + '=', nil)
else
value = case values.size
when 2 then t = Time.new; Time.local(t.year, t.month, t.day, values[0], values[min], 0, 0)
when 5 then t = Time.time_with_datetime_fallback(:local, *values)
when 3 then Date.new(*values)
else nil
end
attributes[name.to_s] = value
end
end
attributes
end
If you find a better solution, please let me know :-)

Rails / ActiveRecord: field normalization

I'm trying to remove the commas from a field in a model. I want the user to type a number, i.e. 10,000 and that number should be stored in the database as 10000. I was hoping that I could do some model-side normalization to remove the comma. I don't want to depend on the view or controller to properly format my data.
I tried:
before_validation :normalize
def normalize
self['thenumber'] = self['thenumber'].to_s.gsub(',','')
end
no worky.
http://github.com/mdeering/attribute_normalizer looks like a promising solution to this common problem. Here are a few examples from the home page:
# By default it will strip leading and trailing whitespace
# and set to nil if blank.
normalize_attributes :author, :publisher
# Using one of our predefined normalizers.
normalize_attribute :price, :with => :currency
# You can also define your normalization block inline.
normalize_attribute :title do |value|
value.is_a?(String) ? value.titleize.strip : value
end
So in your case you might do something like this:
normalize_attribute :title do |value|
value.to_s.gsub(',', '')
end
I think you're doing it right. This test passes:
test "should remove commas from thenumber" do
f = Foo.new(:thenumber => "10,000")
f.save
f = Foo.find(f.id)
assert f.thenumber == "10000"
end
And I used your code.
class Foo < ActiveRecord::Base
before_validation :normalize
def normalize
self['thenumber'] = self['thenumber'].to_s.gsub(',','')
end
end
Now, my schema is set up for thenumber to be a string though, not an integer.
Started
.
Finished in 0.049666 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
If you wanted to store this in the db as an integer, then you definitely need to override the setter:
def thenumber=(value)
self['thenumber'] = value.to_s.gsub(',','').to_i
end
If you do it your way, with an integer column, it gets truncated by AR....
>> f.thenumber = "10,000"
=> "10,000"
>> f.thenumber
=> 10
That's a little-known thing with Ruby and integers... it auto-casts by truncating anything that's no longer an integer.
irb(main):004:0> i = "155-brian-hogan".to_i
=> 155
Can be cool for things like
/users/155-brian-hogan
#user = User.find_by_id(params[:id])
But not so cool for what you're doing.
So either change the col to a string and use the filter, or change the setter :)
Good luck!
The problem with doing it that way is that for a while, the non-normalized stuff will exist in the object; if you have code that works on the attributes before stuff gets normalised, then that will be a problem.
You could define a setter:
def thenumber=(value)
# normalise stuff here, call write_attribute
end
Unfortunately I think a lot of the Rails form stuff writes the attributes directly, which is one of the reasons I don't tend to use it.
Or you could normalise the params in the controller before you pass them through.
Does ruby let you interchange between a . and [''] ?
I don't know, I'll try later, but I think you are supposed to use .
self.thenumber = self.thenumber.to_s.gsub(',','')
You should return true from your before_validation method, otherwise if the expression being assigned to self['thenumber'] ends up being nil or false, the data will not be saved, per the Rails documention:
If a before_* callback returns false,
all the later callbacks and the
associated action are cancelled.
Ostensibly, you are trying to normalize here then check the result of the normalization with your Rails validations, which will decide if nil/false/blank are okay or not.
before_validation :normalize
def normalize
self['thenumber'] = self['thenumber'].to_s.gsub(',','')
return true
end

Resources