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

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

Related

Rails - How to use custom attribute in where?

I have Order model in which I have datetime column start and int columns arriving_dur, drop_off_dur, etc.. which are durations in seconds from start
Then in my model I have
class Order < ApplicationRecord
def finish_time
self.start + self.arriving_duration + self.drop_off_duration
end
# other def something_time ... end
end
I want to be able to do this:
Order.where(finish_time: Time.now..(Time.now+2.hours) )
But of course I can't, because there's no such column finish_time. How can I achieve such result?
I've read 4 possible solutions on SA:
eager load all orders and select it with filter - that would not work well if there were more orders
have parametrized scope for each time I need but that means soo much code duplication
have sql function for each time and bind it to model with select() - it's just pain
somehow use http://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute ? But I have no idea how to use it for my case or whether it even solves the problem I have.
Do you have any idea or some 'best practice' how to solve this?
Thanks!
You have different options to implement this behaviour.
Add an additional finish_time column and update it whenever you update/create your time values. This could be done in rails (with either before_validation or after_save callbacks) or as psql triggers.
class Order < ApplicationRecord
before_validation :update_finish_time
private
def update_finish_time
self.finish_time = start_time + arriving_duration.seconds + drop_off_duration.seconds
end
end
This is especially useful when you need finish_time in many places throughout your app. It has the downside that you need to manage that column with extra code and it stores data you actually already have. The upside is that you can easily create an index on that column should you ever have many orders and need to search on it.
An option could be to implement the finish-time update as a postgresql trigger instead of in rails. This has the benefit of being independent from your rails application (e.g. when other sources/scripts access your db too) but has the downside of splitting your business logic into many places (ruby code, postgres code).
Your second option is adding a virtual column just for your query.
def orders_within_the_next_2_hours
finishing_orders = Order.select("*, (start_time + (arriving_duration + drop_off_duration) * interval '1 second') AS finish_time")
Order.from("(#{finishing_orders.to_sql}) AS orders").where(finish_time: Time.now..(Time.now+2.hours) )
end
The code above creates the SQL query for finishing_order which is the order table with the additional finish_time column. In the second line we use that finishing_orders SQL as the FROM clause ("cleverly" aliased to orders so rails is happy). This way we can query finish_time as if it was a normal column.
The SQL is written for relatively old postgresql versions (I guess it works for 9.3+). If you use make_interval instead of multiplying with interval '1 second' the SQL might be a little more readable (but needs newer postgresql version, 9.4+ I think).

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

Rails Scoping a payroll period

I've built a Rails app that logs employee time (clockin/clockout) and calculates total hours, allows exports to CSV/PDF, search timecards based on dates, etc.
What I'm really wanting to do is to implement payroll periods via a scope of some sort of a method.
The payroll period begins on a Sunday and ends on a Saturday 14 days later. What would be the best way to write a scope like this? Also is it possible to split the weeks into two for the payroll period?
I wrote these scopes but they are flawed:
scope :payroll_week_1, -> {
start = Time.zone.now.beginning_of_week - 1.day
ending = Time.zone.now.end_of_week - 1.day
where(clock_in: start..ending)
}
scope :payroll_week_2, -> {
start = Time.zone.now.end_of_week.beginning_of_day
ending = Time.zone.now.end_of_week.end_of_day + 6.days
where(clock_in: start..ending)
}
These works if you are currently in a payroll period, but once you pass the end of the week, the scopes no longer work because I'm basing my timing off of Time.zone.now
Is there any way to actually do this? Even if I have to set some sort of static scope or value which says April 10 - 23 is payroll period 1, etc etc. I'm really not sure how to approach this problem and what might work. So far what I've written works in the current pay period but as time advances the scope drifts.
Any help would be greatly appreciated.
I think what you want is to create a scope, which can receive a start_day as a parameter:
scope :payroll_week_starting, -> (start_day) {
where(clock_in: start_day..(start_day + 1.week))
}
Then, in the future you will be able to call your scope with the first day of your pay period:
ModelName.payroll_week_starting(Date.parse('31/12/2015'))
UPDATE:
As per your comment, it seems that you're looking for a bit more information from an architectural perspective. It's pretty tough to help you without understanding your database architecture, so I'm just going to go from a high level.
Let's assume you have an Employee model and a Shift model with the clock_in and clock_out fields. You may also want a model called PayPeriod with the fields start_date and end_date.
Each Employee has_many :shifts and each PayPeriod has_many :shifts
You might add a couple of class methods on PayPeriod, so you can find and/or create the PayPeriod for any given datetime.
def self.for(time)
find_by("start_date < ? AND end_date > ?", time, time)
end
def self.create_for(time)
# yday is the number of days into the current year
period_start_yday = 14 * (time.yday / 14)
start_date = Date.new(time.year) + period_start_yday.days
next_year = Date.new(time.year) + 1.year
create(
start_date: start_date,
end_date: [start_date + 14.days, last_day_of_year].min,
)
end
def self.find_or_create_for(time)
for(time) || create_for(time)
end
The create_for logic is pretty complicated, but an example will help you understand:
Say I clocked in today May 17th, 2016, the yday for today is 138, if you use integer division (default for ruby) to divide by 14 (the length of your pay periods), you'll get 9. By multiplying that by 14 again, you'll get 126, which is the most recent yday divisible by 14. If you add that number of days to the beginning of this year, you'll get the begining of the PayPeriod. The end of the PayPeriod is 14 days after the start_date, but not rolling over to the next year.
What I would then do, is add a before_save callback to Shift to find or create the corresponding PayPeriod
before_save :associate_pay_period
def associate_pay_period
self.pay_period_id = PayPeriod.find_or_create_for(clock_in)
end
Then every PayPeriod will have a bunch of Shifts, and every Shift will belong to a PayPeriod.
If, for example, you wanted to get all of the shifts for a specific employee during a specific PayPeriod (to perhaps sum the hours worked for that PayPeriod) add a scope to Shift:
scope :for, -> (user) { where(user: user) }
And call pay_period.shifts.for(user)
UPDATE #2:
One other (much simpler) thought I had (if you don't want to create an actual PayPeriod model), would be to just add a method to the model that has clock_in (I'm going to refer to it as Shift):
def pay_period
clock_in.to_date.mjd / 14
end
Which will basically just boil down any clock_in time to an integer that represents a 14 day period. Then you can call
Shift.all.group_by { |shift| shift.pay_period }
If you need each pay_period to be contained within a single calendar year, you can do:
def pay_period
[clock_in.year, clock_in.to_date.yday / 14]
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'))

DateTime arithmetic in an ActiveRecord query (PostgreSQL)

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.

Resources