DateTime arithmetic in an ActiveRecord query (PostgreSQL) - ruby-on-rails

I have two datetime columns in a User/users table: created_at and birthdate. I'd like to find users whose birthdate is less than 13 years before their creation date.
The equivalent Rails if statement would be ((created_at - birthdate) < 13.years)
How do I translate this to an ActiveRecord query (one that'll work on PostgreSQL)? Is this even possible, or will I have to iterate over all records manually?

The easiest way to do this is to use an interval, then it is pretty much a straight transliteration of the Rails version:
User.where(%q{created_at - birthdate < interval '13 years'})
The difference between two timestamps (or a timestamp and a date) is an interval so you just need the appropriate value on the right side of your comparison.

You simply have to formulate that in PostgreSQL syntax inside your where clause.
For MySQL this would look similar to this using the datediff function:
User.where("DATEDIFF(created_at, birthdate) > (13 * 365)")
13*356 is there to represent 3 years in days since datediff returns difference in days.
I would then encapsulate that in a scope-like function like the following:
class User < ActiveRecord::Model
def self.age_difference(years)
where("DATEDIFF(created_at, birthdate) > (? * 365)", years)
end
end
So you can call it:
User.age_difference(13).each do |user|
puts user.inspect
end
I guess it's similar in Postgres.

Related

Rails 5 and PostgreSQL 'Interval' data type

Does Rails really not properly support PostgreSQL's interval data type?
I had to use this Stack Overflow answer from 2013 to create an interval column, and now it looks like I'll need to use this piece of code from 2013 to get ActiveRecord to treat the interval as something other than a string.
Is that how it is? Am I better off just using an integer data type to represent the number of minutes instead?
From Rails 5.1, you can use postgres 'Interval' Data Type, so you can do things like this in a migration:
add_column :your_table, :new_column, :interval, default: "2 weeks"
Although ActiveRecord only treat interval as string, but if you set the IntervalStyle to iso_8601 in your postgresql database, it will display the interval in iso8601 style: 2 weeks => P14D
execute "ALTER DATABASE your_database SET IntervalStyle = 'iso_8601'"
You can then directly parse the column to a ActiveSupport::Duration
In your model.rb
def new_column
ActiveSupport::Duration.parse self[:new_column]
end
More infomation of ISO8601 intervals can be find at https://en.wikipedia.org/wiki/ISO_8601#Time_intervals
I had a similar issue and went with defining reader method for the particular column on the ActiveRecord model. Like this:
class DivingResults < ActiveRecord::Base
# This overrides the same method for db column, generated by rails
def underwater_duration
interval_from_db = super
time_parts = interval_from_db.split(':')
if time_parts.size > 1 # Handle formats like 17:04:41.478432
units = %i(hours minutes seconds)
in_seconds = time_parts
.map.with_index { |t,i| t.to_i.public_send(units[i]) }
.reduce(&:+) # Turn each part to seconds and then sum
ActiveSupport::Duration.build in_seconds
else # Handle formats in seconds
ActiveSupport::Duration.build(interval_from_db.to_i)
end
end
end
This allows to use ActiveSupport::Duration instance elsewhere. Hopefully Rails will start handling the PostgreSQL interval data type automatically in near future.
A more complete and integrated solution is available in Rails 6.1
The current answers suggest overriding readers and writers in the models. I took the alter database suggestion and built a gem for ISO8601 intervals, ar_interval.
It provides a simple ActiveRecord::Type that deals with the serialization and casting of ISO8601 strings for you!
The tests include examples for how to use it.
If there is interest, the additional formats Sam Soffes demonstrates could be included in the tests
Similar to Madis' solution, this one handles fractions of a second and ISO8601 durations:
def duration
return nil unless (value = super)
# Handle ISO8601 duration
return ActiveSupport::Duration.parse(value) if value.start_with?('P')
time_parts = value.split(':')
if time_parts.size > 1
# Handle formats like 17:04:41.478432
units = %i[hours minutes seconds]
in_seconds = time_parts.map.with_index { |t, i| t.to_f.public_send(units[i]) }.reduce(&:+)
ActiveSupport::Duration.build in_seconds
else
# Handle formats in seconds
ActiveSupport::Duration.build(value)
end
end
def duration=(value)
unless value.is_a?(String)
value = ActiveSupport::Duration.build(value).iso8601
end
self[:duration] = value
end
This assumes you setup your database like Leo mentions in his answer. No idea why sometimes they come back from Postgres in the PT42S format and sometimes in the 00:00:42.000 format :/

How to use where clause on methods rather than database columns?

One problem I often run into in Rails is this:
Let's say I have an invoices table with a date and a days column.
How can I retrieve all invoices which are due?
class Invoice < ActiveRecord::Base
def self.due
where("due_date > ?", Date.today) # this doesn't work because there is no database column "due_date"
end
private
def due_date
date + days
end
end
Can anybody tell me how to do this without having to add a database column due_date to my invoices table?
Thanks for any help.
In PostgreSQL, adding an integer to a date adds that many days:
date '2001-09-28' + integer '7' = date '2001-10-05'
so you can simply say:
where('due_date + days > :today', :today => Date.today)
However, SQLite doesn't really have a date type at all, it stores dates as ISO 8601 strings. That means that adding a number to a date will end up concatenating the strings and that's sort of useless. SQLite does have a date function though:
date(timestring, modifier, modifier, ...)
[...]
All five date and time functions take a time string as an argument. The time string is followed by zero or more modifiers.
so you can say things like date('2014-01-22', '+ 11 days') to do your date arithmetic. That leaves you with this:
where("date(due_date, '+' || days || ' days') > :today", :today => Date.today)
Thankfully, ISO 8601 date strings compare properly as strings so > still works.
Now you're stuck with two versions of the same simple query. You could check what sort of thing self.connection is to differentiate between dev/SQLite and production/PostgreSQL or you could look at Rails.env.production?. This of course leaves a hole in your test suite.
I think you should stop developing on top of SQLite if you intend on deploying on top of PostgreSQL and you should do that right now to minimize the pain and suffering. The truth is that any non-trivial application will be wedded to the database you use in production or you will have to expend significant effort (including running your test suite against all the different databases you use) to maintain database portability. Database independence is a nice idea in theory but wholly impractical unless someone is prepared to cover the non-trivial costs (in time and treasure) that such independence requires. ORMs won't protect you from the differences between databases unless your application is yet another "15 minute blog" toy.
You could do something like:
class Invoice < ActiveRecord::Base
def self.due
Invoice.all.select { |invoice| invoice.due_date > Date.today }
end
private
def due_date
date + days
end
end

Rails query timestamp between two hours

I have a problem in Ruby on Rails, where I need to allow a user to set a two time columns, and then query all instances of that model that have the current time within the hours of the two timestamps. Days do not matter, just within the times specified be the start and end timestamps.
Thanks,
Brian
Something like this should work (assuming MySQL):
Model.where("TIME(created_at) BETWEEN '07:00:00' AND '09:00:00'")
You can convert a Time instance to this format with:
Time.now.strftime("%T") #=> "23:02:25"
If the start time is larger than the end time you could reverse the logic, so instead of:
Model.where("TIME(created_at) BETWEEN '23:00:00' AND '01:59:59'")
You could use:
Model.where("TIME(created_at) NOT BETWEEN '02:00:00' AND '22:59:59'")
You might be looking for something like
Model.where("starts_at >= ? AND ends_at =< ?", Time.current, Time.current)
Alternatively, you can use placeholder conditions to make the query more concise.
Client.where("created_at >= :start_date AND created_at <= :end_date",
{start_date: params[:start_date], end_date: params[:end_date]})
How you solve this problem will depend on what database you're using b/c as far as I'm aware there aren't any ActiveRecord helpers for this type of operation. But there are date functions that should help you figure this out
Postgres Date functions
Mysql Date functions
So for example, in Postgres you might try
extract(hour from timestamp 'a') BETWEEN (extract(hour from timestamp 'b') AND extract(hour from timestamp 'c'))

Rails & Sqlite question - comparing dates to db timestamps

I'm trying to do this: map the total sales on a day to an array of dates for highcharts (yes my project is effectively exactly the same as the railscast example).
I'm unfortunately just ending up with a lot of 0s; I believe the piece in my model:
def self.total_revenue_on(date)
where("date(created_at) = ?", date).sum(:amt)
end
is failing to match the date to the datetime written in my database, e.g. "2011-07-21 09:22:28.388944+0000". Pretty sure that's where it's failing because if I remove the timezone piece manually from my database (get rid of "+0000" and leave just "2011-07-21 09:22:28.388944") it works just fine.
I think this is really a rails/sqlite question: am I storing the timestamp improperly, or comparing improperly? Any help is greatly appreciated!
The best practice is to use to_s(:db) for referencing datetimes in a database in Rails. Try:
def self.total_revenue_on(date)
where("date(created_at) = ?", date.to_s(:db)).sum(:amt)
end
OK, I managed to solve this by using a different lookup method:
def self.total_revenue_on(date)
where("datetime >= ? and datetime < ?", date, date + 1.day).sum(:amt)
end
Still completely perplexed by the problem with the original, but this seems to be working.

How do I write an activerecord query that only looks at the time component of a datetime field?

Is it possible to do an activerecord query that only looks at the time component of a datetime field?
e.g. Battle.where('start_time < ? and start_time > ?','12:00','06:00')
to find all battles that were started between 6am and 12pm regardless of the day they occurred? In this example, start_time is defined as a datetime.
The only way to do this is using a SQL function, if you're on MySQL you could do it like this:
Battle.where( 'HOUR( start_time ) >= ? AND HOUR( start_time ) <= ?', 12, 6 )
But this is hugely inefficient and is always going to generate a full table scan and you surely don't want that.
The best solution is to add columns with the hour values alone at your battle model, index them and query directly on them like this:
Battle.where( 'start_time_hour >= ? start_time_hour <= ?', 12, 6 )
Just add an before_save callback that sets this values before saving your Battle model based on the start_time property.
EDIT
BTW, if you're using PostgreSQL, it's capable of creating an index on the result of a function, so, if you don't mind having to stick with PostgreSQL you could just use the SQL function. And before you ask, no, i don't know how it works, I only know the database allows you to do so and that this is not available on MySQL.

Resources