How to compare a string to dates in a find clause? - ruby-on-rails

I want to get models whom date is within a date range.
So I want to do something like
MyModel.find_all_by_field1_id_and_field2_id(value1, value2, :conditions => { :date => nb_days_ago..Date.yesterday })
The thing is, the date attribute of my model is a string (with the format "08-24-2010"), and I can't modify this.
So to compare it to my range of dates, I tried this:
MyModel.find_all_by_field1_id_and_field2_id(value1, value2, :conditions => { Date.strptime(:date, "%m-%d-%Y") => nb_days_ago..Date.yesterday })
But I get an error that basically says that strptime can't process the :date symbol. I think my solution is not good.
How can I compare my string to my range of dates ?
Thanks

You have to convert the DB string to date in the database rather than in Ruby code:
Model.all(:conditions => [ "STR_TO_DATE(date,'%m-%d-%Y') BETWEEN ? AND ? ",
nb_days_ago, Date.yesterday])
Better solution is to normalize your model by adding a shadow field.
class Model
after_save :create_shadow_fields
def create_shadow_fields
self.date_fld = Date.strptime(self.date_str, "%m-%d-%Y")
end
end
Now your query can be written as follows:
Model.all(:conditions => {:date_fld => nb_days_ago..Date.yesterday})
Don't forget to add an index on the date_fld column.
Edit 1
For SQLLite, first solution can be rewritten as follows:
Model.all(:conditions => [ "STRFTIME('%m-%d-%Y', date) BETWEEN ? AND ? ",
nb_days_ago, Date.yesterday])

First of all I do not envy your situation. That's a pretty ugly date format. The only thing I can think of is to generate an array of strings, in that format, representing ALL the days between your starting date and your finish date, then use the SQL "IN" syntax to find dates in that set (which you can do from within ActiveRecord's :conditions param).
For example, if you wanted to search to 10 days ago:
num = 10 #number of days ago for search range
# range starts at 1 because you specified yesterday
matching_date_strings = (1..num).to_a.map{|x| x.days.ago.strftime("%m-%d-%Y")}
=> ["08-24-2010", "08-23-2010", "08-22-2010", "08-21-2010", "08-20-2010"]
# then...
records = MyModel.all(:conditions => { :date => matching_date_strings })
# or in your case with field1 and field2
records = MyModel.find_all_by_field1_id_and_field2_id(value1, value2, :conditions => { :date => matching_date_strings })
The idea is this should generate SQL with something like "... WHERE date IN ("08-24-2010", "08-23-2010", "08-22-2010", "08-21-2010", "08-20-2010")

Related

ruby comparison of time and variable

I have two models, being an Employee and a WorkingPattern. An instance of an Employee belongs_to an Working Pattern.
The Working Pattern looks like this
:id => :integer,
:name => :string,
:mon => :boolean,
:tue => :boolean,
:wed => :boolean,
:thu => :boolean,
:fri => :boolean,
:sat => :boolean,
:sun => :boolean
I need to know if an Employee should be at work today. So, if today is a Tuesday and that employee's working pattern record reports that :tue = true then return true, etc. I don't have the option of renaming the fields on the WorkingPattern model to match the days names.
I know that
Time.now.strftime("%A")
will return the name of the day. Then I figured out I can get the first 3 characters of the string by doing
Time.now.strftime("%A")[0,3]
so now I have "Tue" returned as a string. Add in a downcase
Time.now.strftime("%A")[0,3].downcase
and now I have "tue", which matches the symbol for :tue on the WorkingPattern.
Now I need a way of checking the string against the correct day, ideally in a manner that doesn't mean 7 queries against the database for each employee!
Can anyone advise?
You can use %a for the abbreviated weekday name. And use send to dynamically invoke a method
employee.working_pattern.send(Time.now.strftime("%a").downcase)
Use send to invoke a method, stored in a variable, on an object.
Both of these are identical:
user.tue # true
user.send('tue') # true
You can access an attibute using [], just like a Hash. No need to use send or even attributes.
day = Time.now.strftime("%a").downcase
employee.working_pattern[day]
Butchering strings makes me feel a little uneasy
Construct a hash of day numbers:
{0 => :sun
1 => :mon,
2 => :tue,
...}
Then use Time.now.wday (or Time.zone.now.wday if you want to be timezone aware) to select the appropriate value from the hash
You can then use that string on employee.working_pattern in any of the ways described by the other answers:
employee.working_pattern.send(day_name)
employee.working_pattern.read_attribute[day_name]
enployee.working_pattern[day_name]

Rename a certain key in a hash

I have a column car_details with 2000 entries, each of which is a hash of info that looks like this:
{"capacity"=>"0",
"wheels"=>"6",
"weight"=>"3000",
"engine_type"=>"Diesel",
"horsepower"=>"350",
"fuel_capacity"=>"35",
"fuel_consumption"=>"30"}
Some cars have more details, some have less. I want to rename the "fuel_consumption" key to "mpg" on every car that has that key.
Well, a previous answer will generate 2000 requests, but you can use the REPLACE function instead. Both MySQL and PostgreSQL have that, so it will be like:
Car.update_all("car_details = REPLACE(car_details, 'fuel_consumption', 'mpg')")
Take a look at the update_all method for the conditions.
See also PostgreSQL string functions and MySQL string functions.
Answer posted by #Ivan Shamatov works very well and is particular important to have good performances on huge databases.
I tried it with a PostgreSQL database, on a jsonb column.
To let it works we have to pay same attention to data type casting.
For example on a User model like this:
User < ActiveRecord::Base {
:id => :integer,
:created_at => :datetime,
:updated_at => :datetime,
:email => :string,
:first_name => :string,
:last_name => :string,
:custom_data => :jsonb
}
My goal was to rename a key, inside custom_data jsonb field.
For example custom_data hash content from:
{
"foo" => "bar",
"date" => "1980-07-10"
}
to:
{
"new_foo" => "bar",
"date" => "1980-07-10"
}
For all users records present into my db.
We can execute this query:
old_key = 'foo'
new_key = 'new_foo'
User.update_all("custom_data = REPLACE(custom_data::text, '#{old_key}'::text, '#{new_key}'::text)::jsonb")
This will only replace the target key (old_key), inside our jsonb hash, without changing hash values or other hash keys.
Note ::text and ::jsonb type casting!
As far as I know, there is no easy way to update a serialized column in a data table en masse with raw SQL. The best way I can think of would be to do something like:
Car.find_each do |car|
mpg = car.car_details.delete("fuel_consumption")
car.car_details["mpg"] = mpg if mpg
car.save
end
This is assuming that you are using Active Record and your model is called "Car".

How do I group these objects into a hash?

I have an event modal, which has a datetime field titled scheduled_time. I need to create a hash that has a day name in a certain format ('mon', 'tue' etc) as the key, and the count of events that take place on that day as the value. How can I do this?
{
'mon' => 2,
'tue' => 4,
'wed' => 3,
'thu' => 5,
'fri' => 12,
'sat' => 11,
'sun' => 7,
}
I'm using Rails 3.2.0 and Ruby 1.9.2
The easiest would be to use count with a :group option:
h = Model.count(:group => %q{to_char(scheduled_time, 'dy')})
The specific function that you'd GROUP BY would, as usual, depend on the database; the to_char approach above would work with PostgreSQL, with MySQL you could use date_format and lower:
h = Model.count(:group => %q{lower(date_format(scheduled_time, '%a'))})
For SQLite you'd probably use strftime with a %w format and then convert the numbers to strings by hand.
Note that using Model.count(:group => ...) will give you a Hash with holes in it: if there aren't any entries in the table for that day then the Hash won't have a key for it. If you really want seven keys all the time then create a Hash with zeros:
h = { 'mon' => 0, 'tue' => 0, ... }
and then merge the count results into it:
h.merge!(Model.count(:group => ...))

Querying embedded objects in Mongoid/rails 3 ("Lower than", Min operators and sorting)

I am using rails 3 with mongoid.
I have a collection of Stocks with an embedded collection of Prices :
class Stock
include Mongoid::Document
field :name, :type => String
field :code, :type => Integer
embeds_many :prices
class Price
include Mongoid::Document
field :date, :type => DateTime
field :value, :type => Float
embedded_in :stock, :inverse_of => :prices
I would like to get the stocks whose the minimum price since a given date is lower than a given price p, and then be able to sort the prices for each stock.
But it looks like Mongodb does not allow to do it.
Because this will not work:
#stocks = Stock.Where(:prices.value.lt => p)
Also, it seems that mongoDB can not sort embedded objects.
So, is there an alternative in order to accomplish this task ?
Maybe i should put everything in one collection so that i could easily run the following query:
#stocks = Stock.Where(:prices.lt => p)
But i really want to get results grouped by stock names after my query (distinct stocks with an array of ordered prices for example). I have heard about map/reduce with the group function but i am not sure how to use it correctly with Mongoid.
http://www.mongodb.org/display/DOCS/Aggregation
The equivalent in SQL would be something like this:
SELECT name, code, min(price) from Stock WHERE price<p GROUP BY name, code
Thanks for your help.
MongoDB / Mongoid do allow you to do this. Your example will work, the syntax is just incorrect.
#stocks = Stock.Where(:prices.value.lt => p) #does not work
#stocks = Stock.where('prices.value' => {'$lt' => p}) #this should work
And, it's still chainable so you can order by name as well:
#stocks = Stock.where('prices.value' => {'$lt' => p}).asc(:name)
Hope this helps.
I've had a similar problem... here's what I suggest:
scope :price_min, lambda { |price_min| price_min.nil? ? {} : where("price.value" => { '$lte' => price_min.to_f }) }
Place this scope in the parent model. This will enable you to make queries like:
Stock.price_min(1000).count
Note that my scope only works when you actually insert some data there. This is very handy if you're building complex queries with Mongoid.
Good luck!
Very best,
Ruy
MongoDB does allow querying of embedded documents, http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-ValueinanEmbeddedObject
What you're missing is a scope on the Price model, something like this:
scope :greater_than, lambda {|value| { :where => {:value.gt => value} } }
This will let you pass in any value you want and return a Mongoid collection of prices with the value greater than what you passed in. It'll be an unsorted collection, so you'll have to sort it in Ruby.
prices.sort {|a,b| a.value <=> b.value}.each {|price| puts price.value}
Mongoid does have a map_reduce method to which you pass two string variables containing the Javascript functions to execute map/reduce, and this would probably be the best way of doing what you need, but the code above will work for now.

MongoDB ruby dates

I have a collection with an index on :created_at (which in this particular case should be a date)
From rails what is the proper way to save an entry and then retrieve it by the date?
I'm trying something like:
Model:
field :created_at, :type => Time
script:
Col.create(:created_at => Time.parse(another_model.created_at).to_s
and
Col.find(:all, :conditions => { :created_at => Time.parse(same thing) })
and it's not returning anything
The Mongo driver and various ORMs handle Date, Time and DateTime objects just fine; there's no reason to cast them to strings.
Col.create(:created_at => another_model.created_at)
And finding:
Col.all(:created_at => another_model.created_at)
You don't want to be setting strings, because dates are stored internally as BSON Date objects, and are indexed and searched as such. If you save them as strings, you won't be able to do things like greater than/less than/range comparisons effectively.
Col.create(:created_at => Time.parse(another_model.created_at).to_s)
That line would pass your time object as a String, take off the to_s to send it to the type parsing layer in your ORM (MongoMapper or Mongoid) as a Time object. That's the only error I can see that would cause it to not work.

Resources