Why is my floating point rounded in Rails? - ruby-on-rails

I have a model that uses floating points to store geolocations. It stores the data in MySQL, using ActiveRecord, Rails 3.2.x.
c = Camping.new()
c.latitude=51.77802459999999
c.save
c.latitude
=> 51.77802459999999
c.reload
c.latitude
=> 51.778
When looking in the database, I confirm it is stored as 51.778. Other numbers are stored with more precision, I have the idea the 0 is what makes either ActiveRecord or MySQL decide to omit the rest.
What decides that the number could or should be chopped off after three digits? Is there anything to control this? Is float the correct type to store this in?
I want a precision of 6, never more, but could be less, or padded. So when 51.7780 is provided, it might be stored as 51.7780 or as 51.778000. When 51.77802459999999is provided, it could be stored as 51.778025 or as the number with full precision; I don't really care.
Relevant part from schema.rb:
create_table "campings", :force => true do |t|
#...
t.float "latitude"
t.float "longitude"
end

Apparently, MySQL is the problem, as float is described as an approximated value.
You can use decimal instead, and specify the precision and scale:
create_table "campings", :force => true do |t|
#...
t.decimal "latitude", :precision => 15, :scale => 10
t.decimal "longitude", :precision => 15, :scale => 10
end

Related

How do I get Rails to accept decimals?

I am creating some table data in a Rails-React application.
I had creating this piece of data here in console:
2.3.3 :024 > Crop.create date: Date.today, cropname: 'Radishes', ismetric: false, bagspackaged: '20', unitweight: '0.5', totalweight: '10'
Today I realized that Rails did not accept the 0.5 decimal for unitweight and no matter how I try to update it in console, it does not save.
This is my schema.rb file:
ActiveRecord::Schema.define(version: 20171004224716) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "crops", force: :cascade do |t|
t.date "date"
t.string "cropname"
t.boolean "ismetric"
t.integer "bagspackaged"
t.integer "unitweight"
t.integer "totalweight"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
Two issues here
First you have given the data type integer to unitweight and totalweight, while you should have given it decimal or float in order to accept and store fractions. decimal data type with precision is better as it will give you more accurate result as stated below in the comments' section.
when you use decimal you can control it by The precision which is total number of digits in a number, whereas scale is number of digits following the decimal point.
here is an example
add_column :tickets, :price, :decimal, precision: 5, scale: 2
this will allow you to store decimal numbers like these 60.00, 80.99 and 100.00
Second you are passing string to integer, it is not a problem because rails will convert it to integer as long as it is a valid integer otherwise it will be 0. But generally it is not a good practice.
I would avoid rolling back your crops table, it would just be more work. It is up to you.
I would just do:
rails g migration ChangeUnitweightToFloat
Inside that file I would configure like so:
class ChangeUnitweightToFloat < ActiveRecord::Migration
def change
change_column :crops, :unitweight, :float
end
end
With these two steps, you should be go to go.
For future reference, please keep in mind that if you want to work with decimals, it will either be a t.decimal or t.float.
That isn't a decimal, it's a string. Don't put quotes around your numeric literals.
Crop.create(
date: Date.today,
cropname: 'Radishes',
ismetric: false,
bagspackaged: 20,
unitweight: 0.5,
totalweight: 10
)
You could use a decimal (or float) type field instead of an integer:
create_table "crops", force: :cascade do |t|
t.decimal "unitweight"
end
and then don't use quotes around the value:
2.3.3 :024 > Crop.create date: Date.today, cropname: 'Radishes', ismetric: false, bagspackaged: '20', unitweight: 0.5, totalweight: '10'

Calculate average of column value in Ruby on Rails

I'm kind of new to Ruby on Rails. I have a profile model which has_many courses taken.
create_table "profiles", force: :cascade do |t|
t.string "pname"
t.float "current_gpa"
end
and
create_table "courses", force: :cascade do |t|
t.integer "course_number"
t.float "gpa"
end
I want to calculate the average gpa: current_gpa = sum of gpa of courses taken / num of course taken. How can I do this?
You should consider reading some documentation - obviously it's quick to get a answer on SO but sometimes the docs can lead you to something you didn't know to look for or ask for.
That said, the fastest way is to use average
profile.courses.average(:gpa)
This will give you an average. Or you can do it the long way, if for some reason you need make modifications in between.
profile.courses.sum(:gpa) / profile.courses.count
An additional note here... Average returns floating point numbers or BigDecimal which is often represented in decimal notation. This can be confusing and may not be what you are looking for. You might explore adding something like: .to_int, .to_f, .truncate, .round, etc...
Person.average(:age) # => 0.387e2
Person.average(:age).to_i # => 38
Person.average(:age).to_f # => 38.7
Person.average(:age).to_f.round # => 39
You can use ActiveRecord::Calculations average:
profile.courses.average(:gpa)

Permutating an existing array to seed a Rails database

I would like to seed my Rails app database with the permutation of an existing array of objects, and am unsure about the best way to go about this.
I currently have a Country model, with the following attributes:
create_table :countries do |t|
t.string :name
t.float :latitude_dec
t.float :longitude_dec
t.timestamps null: false
end
I have seeded this model from a .yaml file (as these attributes are static), and now would like to use these records to seed a CountryPair model (where the attributes are also static). This model will have the following attributes:
create_table :country_pairs do |t|
t.string :country_a
t.string :country_b
t.string :pair_name
t.float :country_a_latitude_dec
t.float :country_b_latitude_dec
t.float :country_a_longitude_dec
t.float :country_b_longitude_dec
t.float :distance
t.timestamps null: false
end
The aim is to permutate the array of Country objects, and create a CountryPair object from each permutation (and seed the database with the output). I understand the Ruby array#permutation method, but am unsure about how to pull out the appropriate values into the new array of CountryPair objects. The order of countries in the pair is important here, so I'd like to use permutations rather than combinations.
Ultimately, I'd also like to calculate the distance between the country pairs, but I'm hoping to start figuring that out once I have the CountryPair model filled!!
This is my first foray back into Rails after a five year absence, so apologies if I've got some of the terminology/methodology wrong - please do ask for clarification if any further information is required! Thanks in advance!
You can add this snippet to your seeds.rb after the Countries are seeded.
Country.all.permutation(2) do |p|
CountryPair.create(
country_a: p[0].name,
country_b: p[1].name,
pair_name: p[0]name + p[1].name,
country_a_latitude_dec: p[0].latitude.dec,
country_b_latitude_dec: p[1].latitude.dec,
country_a_longitude_dec: p[0].longitude.dec,
country_b_longitude_dec: p[1].longitude.dec,
distance: # An algorithm to calculate distance
)
end
Then run it with: rake db:setup

Does PostgreSQL support precision and scale for Rails?

I have an Rails application that defines a migration that contains a decimal with precision 8 and scale 2. The database I have set up is PostgreSQL 9.1 database.
class CreateMyModels < ActiveRecord::Migration
def change
create_table :my_models do |t|
t.decimal :multiplier, precison: 8, scale: 2
t.timestamps
end
end
end
When I run rake db:migrate, the migration happens successfully, but I noticed an error when I was trying to run a MyModel.find_or_create_by_multiplier. If I ran the following command twice, the object would get created twice:
MyModel.find_or_create_by_multiplier(multiplier: 0.07)
I am assuming this should create the object during the first call and then find the object during the second call. Unfortunately, this does not seem to be happening with the multiplier set to 0.07.
This DOES work as expected for every other number I have thrown at the above command. The following commands work as expected (creating the object during the first call and then finding the object during the second call).
MyModel.find_or_create_by_multiplier(multiplier: 1.0)
MyModel.find_or_create_by_multiplier(multiplier: 0.05)
MyModel.find_or_create_by_multiplier(multiplier: 0.071)
When I view the PostgreSQL database description of the MyModel table, I notice that the table does not have a restriction on the numeric column.
Column | Type | Modifiers
-------------+-----------------------------+-------------------------------------------------------
id | integer | not null default nextval('my_models_id_seq'::regclass)
multiplier | numeric |
created_at | timestamp without time zone | not null
updated_at | timestamp without time zone | not null
My db/schema.rb file also does not state the precision and scale:
ActiveRecord::Schema.define(:version => 20121206202800) do
...
create_table "my_models", :force => true do |t|
t.decimal "multiplier"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
...
So my first question is, why do I not see precision and scale pushed down to PostgreSQL when I migrate? Documentation states that it should be supported.
My second question is, why is 0.07 not correctly comparing using the MyModel.find_or_create_by_multiplier(multiplier: 0.07) command? (If I need to open another question for this, I will).
This is embarrassing...
I have precision misspelled.
Changing the migration to:
t.decimal :multiplier, precision: 8, scale: 2
fixed everything.
PostgreSQL 9.1 will let you declare a column in any of these ways.
column_name decimal
column_name numeric
column_name decimal(8, 2)
column_name numeric(8, 2)
If you look at that column using, say, pgAdminIII, it will show you exactly how it was created. If you (or Rails) created the column as numeric, it will say "numeric". If you (or Rails) created the column as decimal(8, 2), it will say "decimal(8, 2)".
So it looks to me like Rails is not passing precision and scale to PostgreSQL. Instead, it's simply telling PostgreSQL to create that column with type "numeric". Rails docs suggest it should not be doing that.
Example syntax in that link is different from yours.
td.column(:bill_gates_money, :decimal, :precision => 15, :scale => 2)
I was using :numeric at first. Although ActiveRecord changed it to :decimal for me, both :precision and :scale were ignored.
# 202001010000000_add_my_col_to_my_table.rb
add_column :my_table, :my_col :numeric, percision: 3, scale: 2, comment: 'Foobar'
# schema.rb
t.decimal "my_col", comment: 'Foobar'
Simply change to :decimal in migration file fixed it for me:
# 202001010000000_add_my_col_to_my_table.rb
add_column :my_table, :my_col :decimal, percision: 3, scale: 2, comment: 'Foobar'
# schema.rb
t.decimal "my_col", precision: 3, scale: 2, comment: 'Foobar'

Preventing a Ruby on Rails notification daemon from emailing the same information more than once

Part of a real estate application I'm writing will allow a user to subscribe to a location and if a new property becomes available in that location then the user will receive an email notifying them of this.
I plan on runnning a background process once every few hours to handle the matching.
Right now I have a model called location and the plan is to add another model called notification. The location model has a latitude and longitude and so will the notification model.
Something like:
create_table "locations", :force => true do |t|
t.decimal "lat", :precision => 15, :scale => 10
t.decimal "lng", :precision => 15, :scale => 10
end
create_table "notifications", :force => true do |t|
t.decimal "lat", :precision => 15, :scale => 10
t.decimal "lng", :precision => 15, :scale => 10
t.string "user_name"
t.string "user_email"
end
The obvious thing to do is loop through the list of locations and if one matches that in a notification then send a mail to the user_email defined in the notification model.
What I'm trying to avoid is sending the same email about a location to the same user more than once. What with the process running every few hours.
I thought of adding another field to the notification model called "has_been_mailed" which would be set to true once a mail is sent but then that means the won't get any future updates of other locations added.
Does anyone have any suggestions as to the best way to implement this?
Without changing your model much, you should have the timestamp fields created_at and updated_at in both tables.
With that, you could query only for locations and notifications that have been updated since the last time you ran your mailer script.

Resources