Supporting timezones in a Rails application - ruby-on-rails

I added a calendar to my rails application. We have events with a start date and an end date (both includes hours and minutes). This ones are fields in the events table, actually their format is datetime. When you select the start date, end date, title and description (among others) for an specific event the the parameters hash is something like this:
parameters = {"event" => {"title" => "a title", "description" => "a description", "start_date" => "2012-09-23 23:45", "end_date" => "2012-09-24 15:32"}}
In the events controller i have something like this:
Event.create(:title => params[:event][:title],
:description => params[:event][:description],
:start_date => params[:event][:start_date].present? ? DateTime.strptime(params[:event][:start_date], "%Y-%m-%d %H:%M") : nil,
:end_date => params[:event][:end_date].present? ? DateTime.strptime(params[:event][:end_date], "%Y-%m-%d %H:%M") : nil,
...
)
Well, when this event is stored into the database (mysql) the value for start date is "2012-09-23 20:45" while the value for end date is "2012-09-24 12:32". I understand that Rails is automatically dealing with timezones. My problem is that when I want to fetch all the events for a specific time (for example, all events that occurs today) I do:
query = "((start_date between ? AND ?) OR (end_date between ? AND ?))"
query << "title NOT LIKE ? AND ..."
self.events.where(query,
Time.now.beginning_of_day, Time.now.end_of_day,
Time.now.beginning_of_day, Time.now.end_of_day,
some_restriction_here, ...)
I get the wrong events! (it is obvious why, the date and time is different in the database from the inputs). There's and obvious solution also, bring all the events into memory and the date and time will be as the original again but this is expensive in terms of resources. By the other hand this don't solve the problem of timezones, if someone in china crate an event the value in the database will be relative to the time offset of the server localization. I could have a field somewhere where the user can set a time zone and work with this storing a specific date for such user and working through this, what is your suggestion about the best way to deal with this?

Rails stores all dates in database in the UTC format. So one way is to always do operations in UTC and in the view convert the displayed dates to current time zone. The other way is to change Rails timezone on the beginning of every action. All parsed times and dates will then be in your custom time zone.
def action_name
Time.zone = "Prague"
# ... logic
end

Related

Rails - How to group dates by time when they're in different timezones?

I have a series of Appointments where the date and time is stored under start_date as DateTime. I'd like to categorize them as starting in the Morning, Daytime, or Evening. I created an array of hashes with labels and ranges I used for a SQL statement where I convert the start_date records into seconds using CAST(EXTRACT (EPOCH FROM start_time) AS INT)
TIME_RANGES = [
{label: "Morning", min: 0, max: 32399},
{label: "Daytime", min: 32400, max: 61199},
{label: "Evening", min: 61200, max: 86399}
]
cases = TIME_RANGES.map do |r|
"when CAST (EXTRACT (EPOCH FROM start_date) AS INT) % 86400 between '#{r[:min]}' and '#{r[:max]}' then '#{r[:label]}'"
end
time_ranges = Appointment.select("count(*) as n, case #{time_cases.join(' ')} end as time_range").group('time_range')
This takes the number of seconds in a day (86400) and labels the appointments based on the modulo of the start_date with 86400. However, a number of appointments take place in different timezones, but they're all stored as UTC. So an appointment at 08:00 AM EST is equivalent to one at 07:00 AM CST, but both are stored internally as 12:00 PM UTC. This would cause the appointments to be incorrectly labelled as "Daytime" when they're intended to be "Morning" (from the perspective of the User booking it).
Ideally, I would like some way to convert the start_date based on the User's timezone to make it look like it occurred in UTC. So I would want a 12:00 PM EST appointment to be labelled as if it were a 12:00 PM UTC appointment instead of 04:00 PM UTC. More specifically, I would like to subtract 14400 seconds from the converted start_date before performing the modulo.
I can join Appoinments to Users, which contains the User's timezone. How can I incorporate this information into my query above, so that a modified start_date is used for each record, depending on the User's timezone in that same record?
I know I could accomplish this with a loop of each timezone and adding/substracting a specific amount of seconds in each loop, then combining the results of all the loops, but I was wondering if there was a way to do it in one query.
Per my comment, I am assuming we have three tables: appointments, users, and preferences. In appointments we have start_date and user_id. In users we have preference_id. In preferences we have some column that names the time zone, so I'll call that tz_name.
Note: Postgres timezone functions are messy. I would highly recommend you read up on them. This excellent article is a good place to start.
It is possible to use pure SQL to generate the time ranges and return a grouped result. A pure SQL solution would be best if you need to label and group many records (thousands or more) at a time.
Assuming you are working with 1000 records or fewer at a time, you'll probably want to use Rails scopes, as this will give you an ActiveRecord result. Then you'll do your grouping using Ruby's Array methods.
That solution would look something like this:
# app/user.rb
class User < ApplicationRecord
belongs_to :preference
has_many :appointments
end
# app/appointment.rb
class Appointment < ApplicationRecord
belongs_to :user
scope :with_seconds, lambda {
joins(user: :preference)
.select('appointments.*, extract(epoch from timezone(tz_name, start_date::timestamptz)::time)::int as seconds')
}
# This method is optional. If it is excluded, calling #seconds
# on an appointment instance will raise an error if the
# .with_seconds scope has not been applied.
def seconds
attributes['seconds']
end
def time_range
return nil unless seconds.present?
if seconds.between?(0, 32_399)
'Morning'
elsif seconds.between?(32_400, 61_199)
'Daytime'
else
'Evening'
end
end
end
The select portion of the scope probably deserves some explanation.
start_date::timestamptz: Take the start_date, which is stored as a Postgres timestamp, and convert it into a Postgres timestamp with time zone in the time zone of the Postgres server (presumably UTC).
timezone(tz_name, start_date::timestamptz): Convert the timestamp with time zone back into a timestamp type in the local time of the tz_name time zone.
timezone(tz_name, start_date::timestamptz)::time: Drop the date and keep the time component.
Then we extract epoch from that time component, which converts it into seconds.
Finally we convert the result to an integer to avoid anything falling through the cracks when we determine the time range.
Now you can do:
Appointment.all.with_seconds.group_by(&:time_range)
or
user = User.first
user.appointments.with_seconds.group_by(&:time_range)
For a pure SQL solution that will return ids grouped under the three time ranges, add this method to your Appointment model:
def self.grouped_by_time_range
current_scope = with_seconds.to_sql
query = <<~SQL
with converted_seconds as (#{current_scope})
select array_agg(id) as ids, case when seconds < 32400 then 'Morning'
when seconds < 61200 then 'Daytime'
else 'Evening' end as time_range
from converted_seconds
group by time_range
SQL
result = ActiveRecord::Base.connection.execute(query.squish)
result.to_a
end
If you don't need strictly SQL based solution you might use Ruby's select method to extract this appointments as in this example:
(I am assuming there is some kind of tz_name field in appointment model which holds timezone name)
morning_appointments = Appointment.all.select do |a|
a.start_date.in_time_zone(a.tz_name).hour > 6 && a.start_date.in_time_zone(a.tz_name).hour < 20
end
Edit:
Thanks #moveson for pointing out my mistake, I changed solution a bit.

comparison Operators in thinking Sphinix

I have a model with attributes start_date and end_date. I have search form where user will put the date and I should get a data from the model if date is in between start_date and end_date.
how should I create a query with thinking sphinx.
You will need to do something like the following:
Add both start_date and end_date as attributes (not fields) to your model's Sphinx index.
Translate form params into a date or time value
Use range filters to limit search queries.
I've opted for very large windows of time, but essentially this ensures the given date is equal to or larger than the start date and less than or equal to the end date.
beginning, ending = Time.utc(1970), Time.utc(2030)
Model.search :with => {
:start_date => beginning..date_from_params,
:end_date => date_from_params..ending
}

Really odd issues with Time when it used within a scope

There is a page in my Rails-app where tours should be displayed so that the start_date field is equal to tomorrow's date (in GMT+00)
I use default timezone in application.rb
# Set Time.zone default to the specified zone and make Active Record ...
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
# config.time_zone = 'Central Time (US & Canada)'
However there in is a problem. Instead of the page displaying tour dates starting from tomorrow, I see dates from today and even yesterday.
I put to the page the following information:
Time.zone (GMT+00:00) UTC
Date.today_local_zone 2013-11-20
Date.today 2013-11-20
Time.now 2013-11-20 00:48:21 +0000
Code in my controller:
puts "... #{ Tour.upcoming.order('start_date ASC').to_sql }"
#tours = Tour.upcoming.order('start_date ASC')
And the scope in Tour model
class Tour < ActiveRecord::Base
attr_accessible :start_date, :title
scope :upcoming, where('tours.start_date > ?', Date.today_local_zone)
end
Briefly about my today_local_zone method:
class Date
def self.today_local_zone
Time.zone.now.to_date
end
end
Here is a line from my logs (the date the query is different from the date in the logs)
2013-11-20T00:48:21.178380+00:00 app[web.1]: ... SELECT "tours".* FROM "tours" WHERE (tours.start_date > '2013-11-19') ORDER BY start_date ASC
After heroku restart or deploy dates besome correct
In heroku console heroku run rails c all dates are correct too
I decided to start another application from scratch and to deploy in on heroku. And result remained the same.
I ping this page every 5 minutes with pingdom. Date become correct after hour or two or even 23 after midnight. i.e. value of lag is different each day.
UPD:
I tried to log value of Time.zone.now.to_s.to_date. It's value is correct.
Please also look at my gist with:
Gemfile
Screenshot of webpage, including all values and text of generated query
I missed this on first review. The problem is in your scope statement:
scope :upcoming, where('tours.start_date > ?', Date.today_local_zone)
This definition is wrong. It loads Date.today_local_zone when the class is loaded. You need to wrap it in a Proc. So it should be:
scope :upcoming, lambda { where('tours.start_date > ?', Date.today_local_zone) }
which ensures that Date.today_local_zone is executed each time the scope is used.

Ruby on Rails - Date Comparison

I am trying to compare dates for an app in rails. I have tried this link Comparing dates in rails. But this is not what I want, suppose I have a filter according to dates, then I am doing
<% jobs = #user.jobs.where(:curr_date => (Date.today))%>
Here, curr_date is a "date" attribute chosen by me.
This code works for entries which have today's date, but if I want all entries which have date between today and a week before, what should I do?
Thanx! :)
You can simply use
jobs = #user.jobs.where(:curr_date => (DateTime.now..1.week.ago))
Updated to DateTime.now or you can also use Date.today.to_time otherwise there will be an error for comparing a Date and a Time
I think something like that should work:
<% jobs = #user.jobs.where(['curr_date < ? AND curr_date > ?', Date.today, 1.week.ago])) %>
comparison_date_1 <=> comparison_date_2 might be useful to to you which returns -1,1,0 accordingly and you can populate them......
Haven't tried this in a console but I think it should work:
# You need to implement the #week_before method
#user.jobs.where(:curr_date => (Date.week_before..Date.today))
Active Record Querying - Array Conditions Check under Range conditions (2.2.2)

rails date ranges for same date

Query below works fine when user selects different dates but when user selects same dates like 3/5/2011 and 3/5/2011 it returns nothings. How can i handle this ? If user selects same dates, i want it to find clients which are created at that date.
Client.where(:created_at => date_from..date_to)
You may need to modify your query as below to get between beginning_of_day and end_of_day
Client.where(:created_at => date_from.beginning_of_day..date_to.end_of_day)
You could create a helper method:
def date_range(from, to)
from == to ? from : from..to
end
Client.where(:created_at=>date_range(date_from,date_to))

Resources