Querying a datetime range while taking into account the time zone - ruby-on-rails

I'm having a lot of trouble with a query. I don't really know how to explain this well, but I'm going to try.
Basically, we have several objects with a 'posted_at' field that keeps the date and time something was posted, with the time zone, in a datetime field. I need to query and get a range by date with those objects.
Previously, I was converting that to Date and comparing it to another Date object. The query was something like this:
Date(posted_at) >= :start_date AND Date(posted_at) <= :end_date
However, when Postgre converted it to Date, it lost the timezone info which caused innacurate results to the query.
So, I changed to this:
if start_date then
start_time = Time.zone.parse("#{start_date.year}-#{start_date.month}-#{start_date.day}")
conditions << "posted_at >= :start"
hash[:start] = start_time
end
if end_date then
end_time = Time.zone.parse("#{end_date.year}-#{end_date.month}-#{end_date.day}").end_of_day
conditions << "posted_at <= :end"
hash[:end] = end_time
end
While this gets me the accurate results, it also has horrible performance and is causing some timeouts in my application.
I couldn't find any other way to do this query and still keep the accurate results. Would anyone have some advice or ideas?
Thank you in advance.

You never want to store timezone information in your database.
Here's a read that discusses some of the pitfalls:
http://derickrethans.nl/storing-date-time-in-database.html
You'll get better results as tadman suggests: add a new field with your timestamp at time zone 'utc', and index it. You'll then be able to grab stuff using posted_at between ? and ?.

You may have more luck converting your start and end times to UTC which would render the time-zone mostly irrelevant when making the query itself. This is done easily enough:
start_date.to_time.to_datetime.beginning_of_day.utc
end_date.to_time.to_datetime.end_of_day.utc
You can also adjust your query to be:
posted_at BETWEEN :start AND :end
Be sure to have an index on the fields you're searching, too, or you will get horrible performance.

Related

Rails - Check if a date belongs to a certain month in where query

Using Rails 6.
I have an ElectricityUsage model, with a Date field, date. I want to extract all the values for amount only for the current month. How would I accomplish this?
What I immediately attempted was the following:
ElectricityUsage.where(habitat: current_user.reservations[0].room.habitat).where(date.month: Date.today.month)
But that obviously doesn't work, and it wouldn't even account for the year, either. My DB is running on PostgreSQL, if that makes a difference.
You can use where with Date.current.all_month, which basically is just translated into a query using BETWEEN where the start date is the first day of the month and end date is the last one:
ElectricityUsage.where(habitat: current_user.reservations[0].room.habitat, date: Date.current.all_month)
This should work for you
ElectricityUsage.where(habitat: current_user.reservations[0].room.habitat).where("EXTRACT(MONTH FROM date) = ?", Date.current.month)
PostgreSQL Date/Time Functions and Operators
Might be a delayed response but you can use date_queries gem
model ElectricityUsage < ActiveRecord::Base
date_queries_for :date
end
Then you can simply use ElectricityUsage.dates_in_this_month to find all the records that false in current month

Rails where based on association

I'm trying to query items based on an association but getting a bit confused with how to phrase the date part of my query. The dates are stored in text format like "2018-12-25".
year = params[:ridden_in]
#items = Item.joins(:cycle).where(cycles: { (:date.to_date).year: year })
Anyone able to help out where i'm going wrong?
The easiest way to do this is to use SQL functions. You could try to hide the SQL behind AREL but that very quickly becomes an incomprehensible mess.
One way would be straight string manipulation:
Item.joins(:cycle).where('substring(cycles."date" from 1 for 4) = ?', year)
or slightly more future-proof since your date will eventually become a real date:
Item.joins(:cycle).where('extract(year from cycles."date"::date) = ?', year)
Item.joins(:cycle).where('extract(year from cast(cycles."date" as date)) = ?', year)
Both of those assume that your date columns really do follow the ISO8601 date format.
If you're doing this a lot then you could add a partial index on cycles.date (i.e. index the result of whichever function you end up using).

Timezone independent due date comparison in rails scope

Consider the following case. I have a Voucher model with a datetime activation_due_date field and user model that has up-to-date information about his location (timezone, UTC offset).
I want to check if he requests voucher activation before due date in any of available time zones. For instance, If a due date is set to 28.08.2018 23:59 UTC
I want my scope before_activation_due to check if he requests something before 28.08.2018 - 23:59 in his current time zone so my due date is not something fixed - it depends on users location - In one place it can be after due date and in the other before.
I have tried the following approach.
models/voucher.rb
scope :before_activation_due, lambda { |user|
where('activation_due_date > ? ', Time.current.to_utc + user.utc_offset)
}
My questions are:

Is this a right approach? If not, what is the proper way for dealing with such cases?
How to test such a scope? The current timestamp is probably taken from a database server when comparing datetimes during query execution so I am not sure how to mock it in my specs.
Thanks in advance.
You can store just the timezone of the user, not the offset, then do:
where('activation_due_date > ? ', Time.now.utc.in_time_zone(user.timezone))
where timezone is any valid timezone shown in
rake time:zones
That'd be the more rails-y way to do things at least. But I don't think storing an offset then manually adding it to the time is a bad approach.
To test this, you can manually insert any date you want in to your database. Then you can use a gem like https://github.com/travisjeffery/timecop to travel in time to that point, and test your scope:
Voucher.create(activation_due_date: '2018-01-02 00:00:00')
format = '%Y-%m-%d %H:%M:%S %Z'
time = DateTime.strptime("2018-01-02 00:00:00 Central Time (US & Canada)",format)
Timecop.travel(time)
Voucher.before_activation_due.all ...
One approach is to convert the activation_due_date into the timezone of the user. As you say "my due date is not something fixed - it depends on users location".
To do this as a scope the easiest thing would be to use your databases timezone functions. This depends on which database you are using, but in PostgreSQL it will be something like:-
where('activation_due_date AT TIME ZONE ? > NOW() ', user.timezone)
An even simpler way would be to do a string comparison
where('to_char(activation_due_date, 'YYYYMMDDHH24MISS') > ?', Time.current.in_time_zone(user.timezone).strftime('%Y%m%d%H%M%S');
In this case we are saying what is the time on the wall for the user, and is it less than time in the database (which is "stored" in UTC).

Ruby on rails. Timezones and scopes

I need help trying to use timezones in a scope. Below is an example of what I'm trying to achieve.
All dates are stored in UTC time:
EG. A user in timezone UTC +1 posts a gig that will start at 20:00. On the gig show page, everyone will see this as 20:00, which is correct.
My problem arises with the fact that I must 'expire' gigs, so they no longer show up in search after the date has passed.
Until now I have been using a scope;
scope :expired, -> { where('date <= ?', Time.current.to_datetime) }
so then gig.expired would show all the gigs where the gig.date is in the past.
The problem is that as the user is in UTC +1 timezone, for him the gig will 'expire' at 21:00, not 20:00, as it expires in UTC not UTC+1.
EDIT:
I am now saving the timezone as an attribute of the gig model upon creation.
I was already using geocoder for location, which saves coordinates.
Using the timezone gem, I can lookup the coordinates and extract the timezone, which is saved as a string to the model.
Eg. In a view, #gig.timezone displays Europe/Amsterdam (or whatever timezone was saved)
Or <%= #gig.date.in_time_zone(#gig.timezone) %> displays 2016-03-17 21:38:00 +0100
My problem now, is using this with the previous scope.
If I try;
def self.expired
Gig.where('date < ?', Time.now.in_time_zone("Europe/Amsterdam"))
end
Rails still 'expires' the gigs in UTC time.
How can I add the gigs' saved timezone, and use that in the scope?
I have never used the TimeZone gem, but if you're having difficulty using it in a scope, you could consider instead simply using it in a method that returns what you want.
Fwiw, I think you're potentially making your app difficult to maintain if you're saving different times for different time zones. I could see showing the user a date that's converted to their time zone...but they should be saved in a uniform fashion. This code, if it's relying on the time zone of the current user is going to struggle. It's important to compare apples to apples.
How is date being saved? If it's being saved as UTC time just compare it to UTC time.
If you prefer to use a scope I would do something like this:
scope :expired, -> { where('date < ?', Date.today) }
Alternatively you could do something like this:
def self.expired
Gig.where('date < ?', Date.today)
end
Ok, after a couple of days trying to make this work I came up with a solution. If anyone can do any better I'd love to hear about it.
On gig creation I lookup the timezone with the timezone gem and save the resulting :string to the timezone attribute I created in the gig model
Longitude and latitude are available as attributes, as the gig has a location which is set in the create form;
timezone = Timezone.lookup(#gig.latitude, #gig.longitude)
#gig.update_attributes(timezone: timezone)
Then I added another column gigzonetime to the gig model. I set this to the gig.date with the timezone offset subtracted.
gigzonetime = #gig.date - #gig.date.in_time_zone(#gig.timezone).utc_offset
#gig.update_attributes(gigzonetime: gigzonetime)
On the show and index pages I display <%= #gig.date %> to show the local time in which it will be displayed, and in my scope I compare to the new attribute gigzonetime so it expires at the correct time.
scope :expired, -> { where('gigzonetime <= ?', Time.current.to_datetime) }
This seems to be working for all timezones (future and past)
It also seems to work correctly for daylight saving time differences.

ruby timezone conversion issues

I have a scenario in which i get a timestamp and i need to search for all bookings for that date in that timestamp. The timestamp is in users respective timezone and all the records in the database are stored in UTC. so naturally i need to convert that timestamp back to UTC and then search.
Here's something that i'm doing:
Booking.where("date_time >= '#{DateTime.parse(timestamp).in_time_zone('UTC').beginning_of_day}' and date_time <= '#{DateTime.parse(timestamp).in_time_zone('UTC').end_of_day}'")
which basically means to fetch all bookings from the beginning of day till the end
However, when i use the following query it gives me a different result:
Booking.where("date_time >= '#{DateTime.parse(timestamp).beginning_of_day.in_time_zone('UTC')}' and date_time <= '#{DateTime.parse(timestamp).end_of_day.in_time_zone('UTC')}'")
I'm wondering which one is actually the correct statement to use in my use case and i would appreciate some input here.
I wouldn't use either one.
This one:
DateTime.parse(timestamp).in_time_zone('UTC').beginning_of_day
gives you the beginning of the UTC day, not the beginning of the local-time-zone-day offset to UTC. In short, it is incorrect and won't give you what you're looking for.
This one:
DateTime.parse(timestamp).beginning_of_day.in_time_zone('UTC')
is correct as it changes the time to the beginning of the day in the local time zone and then converts the timestamp to UTC.
If you let ActiveRecord deal with the quoting using a placeholder, then it will apply the UTC adjustment itself.
I'd also use < t.tomorrow.beginning_of_day rather than <= t.end_of_day to avoid timestamp truncation and precision issues; the end of the day is considered to be at 23:59:59.999... and that could leave a little tiny window for errors to creep in. I'm being pretty pedantic here, you might not care about this.
I'd probably do it more like this:
t = DateTime.parse(timestamp)
Booking.where('date_time >= :start and date_time < :end',
:start => t.beginning_of_day,
:end => t.tomorrow.beginning_of_day
)

Resources