Searchlogic doesn't convert the time properly for datetime conditions - timezone

The author of Searchlogic says that it is delegated to A::R converter, but at least in our case this didn't cover the usual cases. Local time was 'interpreted' as UTC and therefore was moved by one hour (CET).
How can I do that properly?
I add our current workaround as an answer, hopefully it helps somebody!

We've added the following method to the application controller:
protected
def parse_datetime_fields(hash, key)
value = hash[key]
return unless value
hash[key] = Time.zone.parse(value)
end
And then before creating the searchlogic object we 'preprocess' the params hash:
if params[:search]
parse_datetime_fields(params[:search], :begin_greater_than)
parse_datetime_fields(params[:search], :begin_less_than)
end
#search = Record.search(params[:search])
Any clearer better and nicer solutions/ideas are very appreciated :)!
our environment.rb:
config.time_zone = 'Bern'
config.active_record.default_timezone = :utc

Related

How to create array with hours in ruby

I need to create the following Array:
array_time = [00:00:00, 00:00:01, ..., 23:59:59]
Is there a way to generate this type of hash with all hours of the day in ruby?
Because then I will need to create the following Hash:
hash = { "time" => { "'00:00:00'" => "23:59:59" } }
And I would like to check if the sub-Hash under key "time" uses keys in the correct format, for example:
hash["time"].each do |key|
array_time.includes key
end
Assuming that you're happy with Strings, this is a simple way to do it:
array_time = ("00".."23").flat_map do |h|
("00".."59").flat_map do |m|
("00".."59").map do |s|
"#{h}:#{m}:#{s}"
end
end
end
array_time.length
# => 86400
array_time.first(5)
# => ["00:00:00", "00:00:01", "00:00:02", "00:00:03", "00:00:04"]
array_time.last(5)
#=> ["23:59:55", "23:59:56", "23:59:57", "23:59:58", "23:59:59"]
However, if your goal is:
and I would like to check if the hash time is in the correct format, example:
hash["time"].each do |key|
array_time.include? key
end
Then that's really not the most efficient way to go about it.
First off, Hash lookups are much faster than Array#include?, so you really want to use a Hash and treat it a Set:
time_set = Hash[
("00".."23").flat_map do |h|
("00".."59").flat_map do |m|
("00".."59").map do |s|
["#{h}:#{m}:#{s}", true]
end
end
end
]
time_set
# => {"00:00:00"=>true,
# "00:00:01"=>true,
# "00:00:02"=>true,
# ...
# "23:59:58"=>true,
# "23:59:59"=>true}
And then perform your lookups like this:
hash[:time].each do |time_str|
time_set[time_str]
end
But even this is not great. Not always at least.
If you know you need to perform this check very often, with arbitrary values, and with a lot of values to check, then yes, pre-computing the lookup set once and storing it in a constant could make sense. So you'd use TIME_SET = ... instead of time_set = ....
But if this is performed sporadically, you're just much better off validating the time strings one by one. For example:
TIME_REGEX = %r{^([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$}.freeze
hash[:time].each do |time_str|
TIME_REGEX === time_str
end
Assuming an array of strings is acceptable, here is one way to do it.
time_iterator = Time.at(1_500_076_800) # 2017-07-15 00:00:00 +0000
end_time = time_iterator + 1.day
array_time = []
while time_iterator < end_time
array_time << time_iterator.strftime('%T')
time_iterator += 1.second
end
Apparently in Ruby 1.9 they removed the ability to step-iterate over a time range, so a while loop seems to be preferred now.
I do think that if you're just trying to validate the format of a time-like string (HH:MM:SS) then there are much better ways to accomplish this. A simple RegEx would do it, or something similar.

Reducing Rails Queries (N+1?)

So in my past application, I was somewhat familiar with using .includes in Rails, but for some reason I'm having a bit of a difficult time in my current scenario.
Here's what I'm working with:
# If non-existent, create. Otherwise, update.
existing_data = Page.all
updated_data = {}
new_records = []
#latest_page_data.each do |key, value|
existing_record = existing_data.find_by(symbol: key)
if existing_record != nil
updated_data[existing_record.id] = value
else
new_records << Page.new(value)
end
end
if !new_records.empty?
Page.import new_reocrds
end
if !updated_data.empty?
Page.update(updated_data.keys, updated_data.values)
end
end
The problem that I'm having is that the .find_by portion of the code results in a query every single iteration of #latest_page_data. I guess I would think that existing_data would hold all of the data it needs in memory, but obviously it doesn't work that way.
So next, I tried something like this:
# If non-existent, create. Otherwise, update.
existing_data = Page.includes(:id, :symbol)
updated_data = {}
new_records = []
#latest_currency_data.each do |key, value|
existing_record = existing_data.find_by(symbol: key)
but then rails throws an error, stating:
ActiveRecord::AssociationNotFoundError (Association named 'id' was not
found on Page; perhaps you misspelled it?):
so I can't use this example to find the id and symbol attributes.
I tried to take out :id in the Page.includes method, but I need to be able to get to the ID attribute in order to update the respective record later down in the code.
I've also saw some other posts pertaining to this topic, but I think the problem I may be running into is I'm not dealing with associations (and I believe that's what .includes is for? If this is the case, is there any other way that I can reduce all of the queries that I'm submitting here?
The includes method is used to preload associated models. I think what you are looking for is a select. Modifying your code to use select, do this :
existing_data = Page.select(:id, :symbol).load
updated_data = {}
new_records = []
#latest_currency_data.each do |key, value|
existing_record = existing_data.find_by(symbol: key)
if existing_record
updated_data[existing_record.id] = value
else
new_records << Page.new(value)
end
end
The drawbacks of using select over pluck is that since Rails constructs an object for you, so it is slower than a pluck. Benchmark: pluck vs select
Rather than trying to figure out a way to do it in Rails (since I'm not familiar with the 100% correct/accurate Rails way), I just decided to use .pluck and convert it into a hash to get the data that I'm looking for:
existing_data = Page.pluck(:id, :symbol)
existing_data = Hash[*existing_data.flatten]
updated_data = {}
new_records = []
#latest_currency_data.each do |key, value|
if existing_data.values.include? key
id = existing_data.find{|k,v| v.include? key}[0]
updated_data[id] = value
else
new_records << Page.new(value)
end
end
If anyone has a better way, it'd be gladly appreciated. Thanks!

Where is the Rails method that converts data from `datetime_select` into a DateTime object?

When I use <%= f.datetime_select :somedate %> in a form, it generates HTML like:
<select id="some_date_1i" name="somedate1(1i)"> #year
<select id="some_date_2i" name="somedate1(2i)"> #month
<select id="some_date_3i" name="somedate1(3i)"> #day
<select id="some_date_4i" name="somedate1(4i)"> #hour
<select id="some_date_5i" name="somedate1(5i)"> #minute
When that form is submitted, the somedate1(<n>i) values are received:
{"date1(1i)"=>"2011", "date1(2i)"=>"2", "date1(3i)"=>"21", "date1(4i)"=>"19", "date1(5i)"=>"25"}
How can I convert that into a DateTime object?
I could write my own method to do this, but since Rails already is able to do the conversion, I was wondering if I could call that Rails method to do it for me?
I don't know where to look for that method.
I'm ultimately trying to solve "How to handle date/times in POST parameters?" and this question is the first step in trying to find a solution to that other problem.
This conversion happens within ActiveRecord when you save your model.
You could work around it with something like this:
somedate = DateTime.new(params["date1(1i)"].to_i,
params["date1(2i)"].to_i,
params["date1(3i)"].to_i,
params["date1(4i)"].to_i,
params["date1(5i)"].to_i)
DateTime::new is an alias of DateTime::civil (ruby-doc)
The start of that code path, seems to be right about here:
https://github.com/rails/rails/blob/d90b4e2/activerecord/lib/active_record/base.rb#L1811
That was tricky to find! I hope this helps you find what you need
Hi I have added the following on the ApplicationController, and it does this conversion.
#extract a datetime object from params, useful for receiving datetime_select attributes
#out of any activemodel
def parse_datetime_params params, label, utc_or_local = :local
begin
year = params[(label.to_s + '(1i)').to_sym].to_i
month = params[(label.to_s + '(2i)').to_sym].to_i
mday = params[(label.to_s + '(3i)').to_sym].to_i
hour = (params[(label.to_s + '(4i)').to_sym] || 0).to_i
minute = (params[(label.to_s + '(5i)').to_sym] || 0).to_i
second = (params[(label.to_s + '(6i)').to_sym] || 0).to_i
return DateTime.civil_from_format(utc_or_local,year,month,mday,hour,minute,second)
rescue => e
return nil
end
end
Had to do something very similar, and ended up using this method:
def time_value(hash, field)
Time.zone.local(*(1..5).map { |i| hash["#{field}(#{i}i)"] })
end
time = time_value(params, 'start_time')
See also: TimeZone.local
Someone else here offered solution of using DateTime.new, but that won't work in Postgresql. That will save the record as is, that is, as it was inserted in form, and thus it won't save in database as utc time, if you are using "timestamp without timezone". I spent hours trying to figure out this one, and the solution was to use Time.new rather than DateTime.new:
datetime = Time.new(params["fuel_date(1i)"].to_i, params["fuel_date(2i)"].to_i,
params["fuel_date(3i)"].to_i, params["fuel_date(4i)"].to_i,
params["fuel_date(5i)"].to_i)
I had this issue with Rails 4. It turned out I forgot to add the permitted params to my controller:
def event_params
params.require(:event).permit(....., :start_time, :end_time,...)
end
This is the method I use - it returns the deleted keys as a new hash.
class Hash
def delete_by_keys(*keys)
keys.each_with_object({}) { |k, h| h[k] = delete(k) if include? k }
end
end

Rails 3: setting the timezone to the current users timezone

I put this in Application Controller:
before_filter :set_timezone
def set_timezone
Time.zone = current_user.time_zone
end
But I always get the error:
undefined method time_zone for #<User:0xa46e358>
and I just don't know why...
I hope someone can help
Further to Jesse's answer, I should add that you can generally avoid adding a new column in db and just create a custom method in user model and make use of cookie to get the user's
timezone:
in client-side (js):
function set_time_zone_offset() {
var current_time = new Date();
$.cookie('time_zone', current_time.getTimezoneOffset());
}
in Application Controller:
before_filter :set_timezone
def set_timezone
min = request.cookies["time_zone"].to_i
Time.zone = ActiveSupport::TimeZone[-min.minutes]
end
Max -- the ryandaigle.com article you mentioned links to this writeup where you need to create a migration to add "time_zone" as an attribute to the user
(this is from the article, in rails 2.x syntax)
$ script/generate scaffold User name:string time_zone:string
$ rake db:migrate
later
<%= f.time_zone_select :time_zone, TimeZone.us_zones %>
That's why your .time_zone is returning a method_missing -- you haven't stored the time_zone on the user yet.
function set_time_zone_offset() {
var current_time = new Date();
$.cookie('time_zone', current_time.getTimezoneOffset());
}
This is not correct, because time offset is not constant, it depends on daylight saving time periods.
Rails expects the standard time offset when calling ActiveSupport::TimeZone[-min.minutes].
ex: in France at date 09/03/2013 10:50:12 +02:00, your javascript will return -120 as offset where ActiveSupport will need -60 to resolve France timezone.
Then you need to check if this is a daylight saving time period in JS then if this is the case you will have to substract one hour to the offset to get the right value used by Rails.

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 :-)

Resources